一道 LeetCode 题,彻底搞懂「滑动窗口」到底在滑什么
关键词:滑动窗口 / HashSet / 不变量 / 时间复杂度 O(n)
很多人刷过这道题,但真正理解滑动窗口的人并不多。
今天我想用一篇笔记,把这道经典题拆清楚。
一、题目回顾
无重复字符的最长子串
给定一个字符串 s,请你找出其中不含有重复字符的 最长子串 的长度。
示例:
输入: "abcabcbb"
输出: 3
解释: 因为无重复字符的最长子串是 "abc"
二、为什么暴力解法不行?
最直观的思路是:
- 枚举所有子串
- 检查是否有重复字符
- 取最长的
时间复杂度?
- 子串个数:O(n²)
- 每个子串检查重复:O(n)
👉 总复杂度:O(n³)
字符串稍微一长,直接超时。
三、核心思想:滑动窗口是什么?
一句话解释:
滑动窗口 = 用两个指针维护一个连续区间,并在区间内维护某种约束条件
在本题中,这个约束条件是:
窗口内的字符必须全部唯一
四、为什么用 Set?
因为我们需要的是:
- 快速判断某个字符是否已经出现过
- 快速插入 / 删除
👉 这正是 HashSet 擅长的事情:
Set<Character> occ = new HashSet<>();
你可以把它理解为:
occ记录的是 当前窗口内已经出现过的字符
五、双指针设计
我们使用两个指针:
left:窗口左边界right:窗口右边界
窗口表示的区间是:
[left, right]
六、算法流程
整体思路只有三步:
-
右指针不断向右扩展窗口
-
如果新字符导致重复:
- 不断移动左指针
- 同时从 Set 中移除字符
-
每次窗口合法时,更新最大长度
七、完整代码实现
class Solution {
public int lengthOfLongestSubstring(String s) {
Set<Character> occ = new HashSet<>();
int left = 0;
int ans = 0;
for (int right = 0; right < s.length(); right++) {
char c = s.charAt(right);
// 如果窗口中已经有该字符,不断收缩左边界
while (occ.contains(c)) {
occ.remove(s.charAt(left));
left++;
}
// 扩展窗口
occ.add(c);
// 更新答案
ans = Math.max(ans, right - left + 1);
}
return ans;
}
}
八、为什么这里必须用 while,而不是 if?
这是这道题最容易被忽略、但最核心的点。
❌ 错误理解
“出现重复了,左指针动一下就好了”
✅ 正确理解
必须一直移动左指针,直到窗口重新满足「无重复字符」这个约束
换句话说:
- 重复字符可能在窗口中出现多次
- 一次
if不一定能修复窗口 - 必须用
while持续修复
九、用一个反例说明问题
假设字符串是:
"abba"
当右指针指向第二个 'b' 时:
-
窗口中已经包含
'b' -
如果只用
if:- 只移除一次左边字符
- 窗口里仍然有重复的
'b'
👉 窗口状态仍然非法,结果必然出错
十、滑动窗口的“不变量”
整个算法始终维护一个不变量:
在任何一次计算答案时,窗口
[left, right]内都不存在重复字符
而 while 的作用正是:
当不变量被破坏时,持续修复,直到不变量重新成立
十一、时间与空间复杂度分析
时间复杂度:O(n)
right指针最多走 n 次left指针最多走 n 次- 每个字符最多被加入和移除一次
👉 虽然有 while,但整体仍是线性复杂度
空间复杂度:O(字符集大小)
- 最多存储窗口中的字符
- ASCII 情况下可视为常数级
十二、总结一句话
滑动窗口不是在寻找某一个“完美窗口”,而是在不断维护约束条件下,动态演化出所有合法窗口,并从中取最优解。
十三、写在最后
这道题真正的价值不在于代码本身,而在于它揭示了:
- 如何用 Set 维护窗口状态
- 如何用 while 维护算法不变量
- 为什么滑动窗口能把 O(n²) 优化到 O(n)
理解了这一题,你会发现很多字符串 / 数组问题,本质都在滑同一个窗口。