算法困惑问答 · LC 3 无重复字符的最长子串
滑动窗口什么时候该移动左指针?
直接答案
只在窗口「不合法」的时候动左指针:右指针每纳入一个新元素,先检查窗口是否还满足题目条件(无重复字符、和不超标……),不满足就一直右移左指针、同步撤销窗口状态,直到恢复合法为止。左指针只前进、永不回退——这一条守住了,整个算法才是 O(n)。
收缩条件就是窗口定义的镜像
写滑窗前先用一句话写下「窗口合法」的定义,收缩条件就是它的否定。求最长无重复子串:合法 = 窗口内无重复字符,所以「新字符进来后出现重复」就收缩,收到重复消失。求和 ≥ target 的最短子数组(LC209)方向相反:和够了反而要拼命收缩,因为求的是「最短」,每次收缩前都记录一次答案。
也就是说:求最长,在「合法」状态下扩张、记答案,不合法才收缩;求最短,在「达标」状态下收缩、记答案。先分清题目要的是哪种,再写循环。
左指针为什么绝不回退
以 LC3 为例:出现重复字符时,把 l 一格格右移并把移出的字符从计数中删掉,直到重复消失。有人会想「每遇到重复就把 l 重置回 0 重新数」——这一下就退化成 O(n²),而且没必要:以旧位置开头的更长窗口已经被证明不合法,回头是白走。
l 和 r 各自最多走 n 步,每个字符最多进窗口一次、出窗口一次,总操作 2n 次,这就是 O(n) 的全部来源。优化版还可以用哈希记「字符上次出现的位置」,让 l 直接跳到 last[c] + 1——注意要先判 last[c] ≥ l,重复字符若已在窗口外就不用理它。
代码关键行(Python)
def lengthOfLongestSubstring(s):
window, l, best = set(), 0, 0
for r, c in enumerate(s):
while c in window: # 不合法:出现重复
window.remove(s[l]) # 撤销左端字符
l += 1 # 左指针只前进
window.add(c)
best = max(best, r - l + 1)
return best`while c in window` 就是收缩条件——窗口定义(无重复)的否定。收缩时必须同步撤销状态(remove),否则窗口内容和指针位置对不上。
常见追问
定长窗口也要判断收缩吗?
不用判断。长度固定为 k 的窗口每步「右进一格、左出一格」机械滑动,如找字母异位词(LC438);只有不定长窗口才有「何时收缩」的问题。
为什么我的滑窗答案总是偏大?
十有八九是收缩时只动了指针没撤销状态(计数没减、和没扣),窗口状态里残留着已移出的元素。指针移动和状态撤销必须成对出现。
把这道题彻底吃透
LC 3「无重复字符的最长子串」有逐步交互动画和完整图文题解,配着本页的结论看,一遍就通。