题目描述
找出 s 中所有是 p 的字母异位词的子串,返回它们的起始下标。异位词 = 字母种类和个数都一样、只是顺序不同。
思路解析
每个起点都切出长 k 的子串、排好序和 p 比,能做,但每个窗口都重头排序,重复劳动太多。
窗口长度固定等于 len(p)。先数出 p 的字母计数,再让窗口在 s 上滑:每右移一格,右边新进一个字符、左边挤出一个字符,计数 O(1) 更新。窗口计数和 p 计数一致,起点就是答案。计数相等就是异位词(顺序无所谓),所以根本不用排序。
准备:先数 p="ab":{a:1, b:1}。窗口固定 2 格,将从最左滑到最右,每停一处就比一次计数。
建首窗口 · 进 s[0]=a:先把前 k=2 个字符装进首窗口。第一个吃进 a,计数 {a:1},窗口还差一格、先不比。
窗口 [0,1] = "ab":第一个窗口 "ab",计数 {a:1, b:1},和 p 一模一样——是异位词!记下起点 0。ans=[0]。
右滑 → 窗口 [1,2]:窗口整体右移一格:右端吃进 s[2]=b,左端挤出 s[0]=a。这就是定长滑窗的核心——一进一出,不用重数。现在窗口是 "bb"。
窗口 [1,2] = "bb":"bb" 的计数是 {a:0, b:2},b 多了一个、a 少了一个,和 p 对不上——不是异位词,跳过不记录。这就是反例:计数不等就直接放过,窗口照样往右挤。
右滑 → 窗口 [2,3]:继续右移:右端吃进 s[3]=a,左端挤出 s[1]=b。同样只改了两个计数。窗口变成 "ba"。
窗口 [2,3] = "ba":"ba" 计数 {a:1, b:1},又和 p 相等——是异位词!记下起点 2。顺序虽和 p 不同,但计数相等就算数。ans=[0, 2]。
结束:窗口右端到头,再也滑不动了。一路收集到的起点是 [0, 2],就是答案。
凡是「固定长度子串/子数组,问满足某条件的有几处」都是这个套路:窗口右滑时同步加右、减左,用计数 O(1) 维护,绝不每个窗口重头算。
参考代码(Python)
1def findAnagrams(self, s, p):
2 from collections import Counter
3 if len(s) < len(p): return []
4 k = len(p)
5 need = Counter(p) # p 的字母计数
6 window = Counter(s[:k]) # 第一个窗口的计数
7 ans = []
8 if window == need: ans.append(0) # 首窗口先比一次
9 for r in range(k, len(s)): # 窗口整体右滑
10 window[s[r]] += 1 # 右边新进一个字符
11 l = r - k
12 window[s[l]] -= 1 # 左边挤出一个字符
13 if window[s[l]] == 0: del window[s[l]] # 清零别留 0 键
14 if window == need: ans.append(l + 1) # 计数相等 → 异位词
15 return ans复杂度分析
- 时间复杂度:O(n) —— 每个字符进窗口、出窗口各一次;比计数是常数 26 个字母
- 空间复杂度:O(字符集) —— need 和 window 各最多存 26 个小写字母的计数
套路模板
定长窗口的灵魂是「右进左出成对出现」:每右移一格,加一个新字符、必须同步减一个旧字符,窗口大小才恒为 k。
1# 固定长度 k 的窗口通用骨架
2window = Counter(s[:k])
3check(0) # 先处理首窗口
4for r in range(k, len(s)):
5 window[s[r]] += 1 # 右进
6 window[s[r-k]] -= 1 # 左出(别忘缩左!)
7 清理为 0 的键; check(r - k + 1)易错点
- 错误写法:窗口右移只加右端、忘了减左端 → 正确写法:右进 window[s[r]]+=1 的同时,左出 window[s[r-k]]-=1(窗口长度固定为 k,只进不出会越滑越长,计数全错)
- 错误写法:每个窗口切片排序再和 p 比 → 正确写法:维护一份计数,用 window == need 一次比对(排序每窗 O(k log k),整体退化到 O(nk log k);比计数才是 O(1) 一步)
- 错误写法:计数减到 0 还留着 {x:0} 这个键 → 正确写法:window[c]==0 时 del 掉它(Counter 残留 0 键会让 window == need 误判为不相等)
以上文字与上方动画完全同一思路:先看动画建立直觉 → 读文字巩固 → 关掉页面自己默写一遍代码,能写出来才算真的掌握。