滑动窗口人人会写,但多数人写的其实是 O(2n)。差的那一倍,面试官一眼就能看出来。
📋 题目
原题:给定一个字符串s,找出其中不含有重复字符的最长子串的长度。
| 项目 | 说明 |
|---|---|
| 输入 | s = “abcabcbb” |
| 输出 | 3(最长子串 “abc”) |
| 约束 | 0 ≤ s.length ≤ 5×10⁴,字符集为 ASCII 128 |
💡 先问一个问题
我跟不少面试官聊过,统一的反馈是:这道题拉出来一问,80% 的人思路都能说对——两个指针框个窗口,没重复就右扩,有了就左缩。
但一到写代码,事就不一样了。超过一半的人会写出一个 O(2n) 的版本,然后自信地在注释里标上 O(n)。
他自己看不出来。但你如果是面试官,你能。
滑动窗口的坑,就藏在 2n 到 n 的这一步。
🤖 第一版:AI 的直觉写法(也是你的直觉)
我问 ChatGPT:「用 Python 写一个求最长无重复子串的函数。」
它给出的第一版代码是这样:
deflengthOfLongestSubstring(s:str)->int:char_set=set()left=0max_len=0forrightinrange(len(s)):whiles[right]inchar_set:char_set.remove(s[left])left+=1char_set.add(s[right])max_len=max(max_len,right-left+1)returnmax_len看起来很干净对吧?时间复杂度标注 O(n),面试官往往也给过。
问题在哪?
看内层那个while。字符串是"aaaaa"的时候,每处理一个a,left 都得一步步往前挪,每个字符从 set 里 remove 掉一次。一次 add,一次 remove,每个字符被操作了两次。
大 O 上还是 O(n),但常数因子翻倍。
🧠 AI 在对话中是怎么慢慢改对的
光给结论没意思,看对话过程——这才是面试时你应该主动展示的东西。
我问它:「能不能别一步步挪了,直接跳到重复位置?」
deflengthOfLongestSubstring(s:str)->int:char_index={}left=0max_len=0forright,chinenumerate(s):# left 直接跳到重复字符的下一个位置ifchinchar_indexandchar_index[ch]>=left:left=char_index[ch]+1char_index[ch]=right max_len=max(max_len,right-left+1)returnmax_len就改了三处:
set→dict,存的是字符的最近位置while整个干掉,换成一次哈希查找- left 不再一步一步挪,直接崩到
index + 1
这才是货真价实的 O(n),每个字符只摸一次。
char_index[ch] >= left这个判断,是区分「背模板」和「真理解」的试金石。
测一下"abba":处理到第二个a时,哈希表里存着第一个a的位置 0。但 left 已经走到 2 了——这时候还敢跳回 0,窗口直接倒退,答案就错了。
漏掉这个>= left的人,基本就是记住了「用 dict 存位置」,没想过 left 已经越过了旧位置该怎么办。
☕ Java 实现(思路完全一致,给 CSDN 的 Java 开发者)
publicintlengthOfLongestSubstring(Strings){Map<Character,Integer>map=newHashMap<>();intleft=0,maxLen=0;for(intright=0;right<s.length();right++){charc=s.charAt(right);if(map.containsKey(c)&&map.get(c)>=left){left=map.get(c)+1;}map.put(c,right);maxLen=Math.max(maxLen,right-left+1);}returnmaxLen;}为什么要加 Java?CSDN 第一大用户群体是 Java 开发者。Python 能看懂不代表面试能用 Java 写出来。两版代码摆在一起,读者自己对比,比你讲十句话都有用。
这张图比文字描述直观得多——每一步 left 是怎么跳的,哈希表怎么更新的,一目了然。
🔍 滑动窗口模式拆解
LeetCode 把滑动窗口归到「连续子串/子数组」的万能套路里。套路是好东西,但背套路的人有一个通病——模板会写,一变形就懵。
滑动窗口的本质就一个东西:维护一个「窗口内无重复」的不变量。
- 加进来一个字符 → 不变量可能被破坏
- 不变量被破坏 → 收缩窗口直到恢复
用 set 一步步缩是滑动窗口,用 dict 直接跳也是滑动窗口。模板不重要,你脑子里的那个「窗口内无重复」才重要。
同类题变体一览:
| 题目 | 窗口收缩条件 | 关键技巧 |
|---|---|---|
| 无重复最长子串 | 出现重复字符 | dict 存位置直接跳 |
| 最小覆盖子串 | 窗口内包含所有目标字符 | 双哈希表计数 |
| 长度最小的子数组 | 窗口和 ≥ target | 累加和 + while 收缩 |
🏗️ 真实产品场景
我知道有人觉得这种题面试造火箭,工作拧螺丝。说几个真实用到的场景:
1. 日志去重窗口
监控系统的日志流水线,每秒可能吐出上百条重复的错误日志。产品需求是「在最近 N 秒内,同一条错误日志只触发一次告警」。
你把错误消息 hash 一下当字符串,维护一个时间窗口,不重复的消息才放行——内核逻辑跟今天这题一模一样。区别只是窗口收缩条件从「出现重复字符」变成了「时间戳过期」,但代码骨架完全复用。
2. Netflix 播放断点检测
流媒体服务的播放数据里,同一个 session 的 seek 事件可能连续多次跳到同一秒(用户疯狂拖进度条)。后端要找出「没有重复跳转位置的最长连续播放段」来判定有效观看时长。
字符串换成播放位置的时间数组,滑动窗口往里一套,就是这道题。
3. 消息队列去重
Kafka 消费者本地缓存最近 1000 条消息 ID,用滑动窗口确保同一个 ID 在窗口内只处理一次。窗口滑出旧消息、滑入新消息,窗口内无重复 ID——这个约束和今天的题完全同构。
顺便说一句,Redpanda 和 Apache Pulsar 的 dedup 实现里,底层用的就是类似的滑动窗口逻辑。不是玩具,是真跑在生产环境里的。
✅ 面试官的点评
我模拟过这道题的面试,以下是一个有经验的面试官真实的打分心理:
写到什么程度能过:
- set + while 版本,能说清楚为什么不退化成 O(n²) → 及格。注意,面试官在这个阶段真正想听的是「每个字符被 add 一次、remove 一次,加起来 O(2n) = O(n)」,听到这句话他就放心了。
- 主动提到 dict 可以优化掉 while → 良好。这时候面试官会追问「那为什么第一版不直接用 dict」,你如果能答出「set 版本更直观,先保证正确性再优化」,直接加分。
char_index[ch] >= left写对了,拿"abba"测一遍不出错 → 优秀。到这个程度,面试官已经在想 offer 的事了。- 顺嘴提一句「ASCII 128 可以直接用
int[128]代替哈希表,空间 O(1)」→ 满分。这属于降维打击——大部分候选人根本想不到。
翻车姿势一览:
"abba"测试用例直接挂——十有八九是漏了>= left。面试官看到这,大概率已经在心里给你打了个问号。- set 版本里 while 半天不动——
remove完忘了left += 1,死循环。这种低级错误发生一次,后面的表现再好都很难拉回来。 - 空字符串直接炸——
s[0]都不看一眼。没有边界检查的行为,是面试官最忌讳的。
📊 同类题推荐
- LeetCode 76 - 最小覆盖子串(Hard):滑动窗口 + 双哈希表,窗口收缩条件更复杂
- LeetCode 209 - 长度最小的子数组(Medium):纯数字版滑动窗口,入门必做
- LeetCode 159 - 至多包含两个不同字符的最长子串(Medium):今天这题的「允许 K 个不同字符」扩展版
来源说明:
- ✅ 已验证:LeetCode 官方题解 + ChatGPT/Claude 多轮实测
- 📄 参考:“Sliding Window Technique” — 算法导论模式归纳