【LeetCode 热题 100】(三)滑动窗口
1. 无重复字符的最长字串
class Solution {public int lengthOfLongestSubstring(String s) {int length = s.length();// 1. 定义全局最大长度int ans = 0;// 2. 构建一个滑动窗口HashSet<Character> hashSet = new HashSet<>();int ret = -1;for (int i = 0; i < length; i++) {if(i!=0){hashSet.remove(s.charAt(i-1));}while ((ret+1) < length && !(hashSet.contains(s.charAt(ret+1)))){hashSet.add(s.charAt(ret + 1));ret = ret + 1;}ans = Math.max(ret-i+1, ans);}return ans;}
}
解题思路:滑动窗口法(双指针优化)
关键思想
使用滑动窗口技术维护一个不含重复字符的子串窗口,通过双指针动态调整窗口边界,高效求解最长无重复子串长度。
核心步骤
-
初始化组件
left
指针(代码中的i
):标记窗口起始位置(初始0)right
指针(代码中的ret
):标记窗口结束位置(初始-1,表示空窗口)HashSet
:存储当前窗口内字符(用于O(1)时间重复判断)maxLength
(代码中的ans
):记录全局最大长度(初始0)
-
滑动窗口操作
-
具体操作解析
- 左指针移动(每次循环左移1位):
- 当
i > 0
时,从集合移除i-1
位置的字符 - 相当于窗口左边界右移,缩小窗口
- 当
- 右指针扩展:
- 若
right+1
位置字符不在集合中且未越界:- 添加该字符到集合
- 右指针
right
右移(扩大窗口)
- 重复直到遇见重复字符或字符串末尾
- 若
- 长度更新:
- 计算当前窗口长度:
right - i + 1
- 更新全局最大值:
maxLength = max(当前长度, maxLength)
- 计算当前窗口长度:
- 左指针移动(每次循环左移1位):
时间复杂度
- O(n):左右指针各遍历字符串一次
- 左指针
i
严格右移n
次 - 右指针
right
只增不减,最多移动n
次
- 左指针
- 每个字符最多被加入/移除集合1次
空间复杂度
- O(min(n, 字符集大小))
- 英文字符集:O(26)
- 全字符集:O(128) 或 O(256)
算法优势
- 单次遍历:通过右指针不回退,避免重复检查
- 实时更新:每次窗口变动后立即更新最大值
- 边界处理:天然支持空串(
length=0
)、单字符串等边界情况
示例演算(s=“pwwkew”)
左指针i | 移除字符 | 右指针扩展 | 当前窗口 | 长度 | 最大值 |
---|---|---|---|---|---|
0 | - | p→w→停(w重复) | “pw” | 2 | 2 |
1 | p | w(重复)→停止 | “w” | 1 | 2 |
2 | w | w→k→e→停(w重复) | “wke” | 3 | 3 |
3 | w | k(已在)→停止 | “ke” | 2 | 3 |
4 | k | e→w→停止 | “ew” | 2 | 3 |
5 | e | w(重复)→停止 | “w” | 1 | 3 |
最终返回最大值 3(对应子串 "wke"
)
该实现完美体现了滑动窗口思想:通过动态调整窗口边界,高效维护解空间,在O(n)时间内解决经典子串问题。
2. 找到字符串中所有的字母异位词
class Solution {public List<Integer> findAnagrams(String s, String p) {int s_len = s.length();int p_len = p.length();LinkedList<Integer> list = new LinkedList<>();if(s_len < p_len){return list;}int[] sCount = new int[26];int[] pCount = new int[26];for (int i = 0; i < p_len; i++) {int s_index = s.charAt(i) - 'a';sCount[s_index] = sCount[s_index] + 1;int p_index = p.charAt(i) - 'a';pCount[p_index] = pCount[p_index] + 1;}if(Arrays.equals(sCount,pCount)){list.add(0);}for (int i = 0; i < s_len - p_len; i++) {int s_index_1 = s.charAt(i) - 'a';sCount[s_index_1] = sCount[s_index_1] - 1;int s_index_2 = s.charAt(i + p_len) - 'a';sCount[s_index_2] = sCount[s_index_2] + 1;if(Arrays.equals(sCount,pCount)){list.add(i+1);}}return list;}
}
解题思路:固定长度滑动窗口(字母异位词问题)
问题分析
给定字符串 s
和模式串 p
,需要在 s
中找到所有是 p
的字母异位词(anagram)的子串起始索引。字母异位词指字母相同但排列不同的字符串(如 “ab” 和 “ba”)。
核心算法:频率计数+滑动窗口
-
边界检查:
- 如果
s
长度小于p
长度,直接返回空结果(不可能存在异位词)
- 如果
-
频率数组初始化:
- 创建两个长度26的数组(对应26个小写字母):
pCount
:存储p
的字符频率sCount
:存储s
的滑动窗口字符频率
int[] sCount = new int[26]; int[] pCount = new int[26];
- 创建两个长度26的数组(对应26个小写字母):
-
初始化窗口(0位置):
- 同时统计
p
和s
前p_len
个字符的频率
for (int i = 0; i < p_len; i++) {sCount[s.charAt(i) - 'a']++; // 统计s前p_len个字符pCount[p.charAt(i) - 'a']++; // 统计p的所有字符 }
- 检查初始窗口是否匹配:
if (Arrays.equals(sCount, pCount)) list.add(0);
- 同时统计
-
滑动窗口(核心逻辑):
- 窗口从位置0开始向右滑动:
for (int i = 0; i < s_len - p_len; i++) {// 1. 移除左边界的字符sCount[s.charAt(i) - 'a']--;// 2. 添加右边界的字符sCount[s.charAt(i + p_len) - 'a']++;// 3. 检查新窗口是否匹配if (Arrays.equals(sCount, pCount)) {list.add(i + 1); // 记录起始位置} }
- 窗口移动演示:
初始窗口:s = [a b a b], p = "ab" 窗口0: "ab" -> sCount={a:1,b:1} -> 匹配 -> 记录0i=0时:移除s[0]='a':sCount={a:0,b:1}添加s[2]='a':sCount={a:1,b:1}新窗口"ba" -> 匹配 -> 记录1i=1时:移除s[1]='b':sCount={a:1,b:0}添加s[3]='b':sCount={a:1,b:1}新窗口"ab" -> 匹配 -> 记录2
- 窗口从位置0开始向右滑动:
关键特性
-
固定窗口大小:
- 始终保持窗口长度 =
p.length()
- 通过
i
控制窗口起始位置
- 始终保持窗口长度 =
-
高效频率更新:
- O(1) 时间移除左边界字符
- O(1) 时间添加右边界字符
- O(26) 时间比较频率数组(常数时间)
-
时间复杂度:
- O(n) :遍历字符串一次(n = s.length())
- 比暴力解法(O(n²))更高效
示例演示(s=“abab”, p=“ab”)
窗口起始位置 | 窗口内容 | sCount [a,b] | 是否匹配 | 结果列表 |
---|---|---|---|---|
0 | “ab” | [1,1] | 是 | [0] |
1 | “ba” | [1,1] | 是 | [0,1] |
2 | “ab” | [1,1] | 是 | [0,1,2] |
最终输出:[0,1,2](三个异位词:“ab”、“ba”、“ab”)
算法总结
- 适用场景:寻找固定长度的字母异位词
- 核心技巧:
- 频率数组代替哈希表(小写字母场景)
- 滑动窗口避免重复计算
- 优势:
- 时间复杂度 O(n)
- 空间复杂度 O(1)(固定26长度数组)
- 扩展性:
- 可处理包含大写/特殊字符的场景(扩大数组长度)
- 可扩展为找最小覆盖子串等问题