438. 找到字符串中所有字母异位词

中等 含交互动画

学完这道你应该能: 说清它为什么用「定长」,而不是死记步骤; 合上页面,自己默写出核心代码; 用 30 秒把思路讲清楚。

AI 私教 · 就着本题动画讲
卡住了不用硬扛,按 8 步把这题真正走通。
从读题、试解、找法到手写代码,边问边打勾,适合第一次系统学算法的同学。

题目描述

找出 s 中所有是 p 的字母异位词的子串,返回它们的起始下标。异位词 = 字母种类和个数都一样、只是顺序不同。

s = "abba"
p = "ab"
输出 = [0, 2]

思路解析

每个起点都切出长 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)

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。

Python
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 误判为不相等)

以上文字与上方动画完全同一思路:先看动画建立直觉 → 读文字巩固 → 关掉页面自己默写一遍代码,能写出来才算真的掌握

下一题 →56. 合并区间 ← 返回题库