找到字符串中所有字母异位词 图解题解
p 的所有异位词藏在 s 的哪些位置?一块等宽纸片从头滑到尾,边滑边比计数。
像用一块和 p 等宽的透明纸片在 s 上从左滑到右:纸片宽度固定,每往右移一格就纳入右边新字母、丢掉左边旧字母,字母计数 O(1) 更新。只要纸片里每个字母的数量和 p 的字母计数完全吻合,不管顺序怎样,起点就是一个答案。滑一遍,全部找齐。
这道题到底在问什么
- 输入
- s="cbaebabacd", p="abc"
- 输出
- [0, 6]
最优解:为什么这么做
一句话答案:LeetCode 438 找到字符串中所有字母异位词的标准解法是定长滑动窗口加字符计数:窗口大小固定为 p 的长度,每右移一格就把新字符计数加一、最左字符计数减一,窗口计数与 p 的计数完全相等时记下起点。字母表只有 26 个,每步比较是常数,整体时间 O(n)、空间 O(1)。
这道题真正在问什么
给字符串 s 和 p,找出 s 中所有「p 的字母异位词」子串的起始下标。异位词指字母种类和各自的个数都完全相同、只是排列顺序不同,比如 "cba" 是 "abc" 的异位词。题目里藏着一个决定解法形态的事实:异位词的长度必然等于 p 的长度——这直接把问题变成「检查 s 中每个长度为 len(p) 的窗口」。比如 s = "cbaebabacd"、p = "abc",答案是 [0, 6]。
为什么想到定长滑动窗口
暴力做法是对每个起点截出长 k = len(p) 的子串,排序后和排序过的 p 比对,每个起点花 O(k log k),整体 O(n · k log k),重复劳动很多。
关键观察是:判断异位词根本不需要顺序信息,只需要「每个字母出现几次」这份统计;而相邻两个窗口只差两个字符——左端少一个、右端多一个,其余 k - 1 个字符原封不动。既然统计量只有两处变化,就没理由每次推倒重算,把计数表增量地改两格就够了。这正是定长滑动窗口的用武之地。
窗口计数怎么维护,何时判定命中
先统计 p 的字符计数 need,再统计 s 前 k 个字符组成的初始窗口计数 win,两表相等则下标 0 记入答案。之后窗口每右移一格:新进的字符 s[r] 计数加一,移出的字符 s[r - k] 计数减一(减到 0 就把该键删掉,保证两张表能直接判等),然后比较 win 与 need,相等就把起点 r - k + 1 记入答案。
全程保持的不变量是:win 永远精确等于当前窗口内各字符的出现次数。因此「win == need」当且仅当窗口是 p 的异位词,判定既不漏也不多。
为什么必须比次数而不是只比种类
p 里的字母可能重复,比如 p = "aab":窗口 "abb" 字母种类和 p 一样是 {a, b},但个数对不上,不是异位词。只用集合判断会放过这类假货,必须逐字母比较出现次数。
另一个方向的错误是让窗口变长变短——本题窗口大小恒等于 k,一旦写成「凑齐了就收缩」的变长窗口套路反而复杂化。定长窗口和变长窗口(如 LeetCode 76 最小覆盖子串)是滑动窗口的两个分支,本题因为「异位词长度确定」落在定长这一支,代码也因此更简单:右端每进一个,左端必出一个。
复杂度怎么算,边界在哪里
时间 O(n):窗口右端把 s 扫一遍,每步做两次计数增减加一次计数比较;字母表固定 26 个小写字母,比较是 O(26) 的常数操作。空间 O(1):两张计数表至多各存 26 个键,不随 n 增长。
最先要防的边界是 len(s) < len(p):窗口根本摆不下,直接返回空数组,否则初始窗口的截取就会出错。另外结果记录的是起点 r - k + 1 而不是右端 r,下标换算写错一格,答案会整体偏移。
▶ 动画逐步走查(共 21 步)——想跟着动画一帧帧对照就展开
- 3窗口大小恒等于 p 的长度,下面每帧窗口右移一格、对比计数。
- 4上方是 s(长度固定)。右表是「窗口计数 / p 目标」——窗口里每个字母的个数都和 p 一致时,起点就是一个异位词。
- 5右指针扩到 0,把 'c' 纳入窗口。窗口还没满 3 个字符,继续填。
- 6右指针扩到 1,把 'b' 纳入窗口。窗口还没满 3 个字符,继续填。
- 7右指针到 2,纳入 'a',窗口刚好凑满 3 格,准备和 p 对比计数。
- 8窗口字符计数和 p 完全一致(种类+个数都相等)——下标 0 是一个异位词,记下!
- 9窗口整体右移一格:左端挤掉 'c'、右端纳入 'e'。窗口大小始终固定为 3。
- 10对比 p 的计数:'c' 个数对不上,窗口不是 p 的异位词,继续右移。
- 11窗口整体右移一格:左端挤掉 'b'、右端纳入 'b'。窗口大小始终固定为 3。
- 12对比 p 的计数:'c' 个数对不上,窗口不是 p 的异位词,继续右移。
- 13窗口整体右移一格:左端挤掉 'a'、右端纳入 'a'。窗口大小始终固定为 3。
- 14对比 p 的计数:'c' 个数对不上,窗口不是 p 的异位词,继续右移。
- 15窗口整体右移一格:左端挤掉 'e'、右端纳入 'b'。窗口大小始终固定为 3。
- 16对比 p 的计数:'b', 'c' 个数对不上,窗口不是 p 的异位词,继续右移。
- 17窗口整体右移一格:左端挤掉 'b'、右端纳入 'a'。窗口大小始终固定为 3。
- 18对比 p 的计数:'a', 'c' 个数对不上,窗口不是 p 的异位词,继续右移。
- 19窗口整体右移一格:左端挤掉 'a'、右端纳入 'c'。窗口大小始终固定为 3。
- 20窗口字符计数和 p 完全一致(种类+个数都相等)——下标 6 是一个异位词,记下!
- 21窗口整体右移一格:左端挤掉 'b'、右端纳入 'd'。窗口大小始终固定为 3。
- 22对比 p 的计数:'b' 个数对不上,窗口不是 p 的异位词,继续右移。
- 23定长窗口从左滑到右,凡是窗口计数和 p 一致的起点都记下,答案就是 [0,6]。
⚠️ 容易写错的地方
✗ 错:窗口大小不固定
✓ 对:窗口恒为 len(p),超过就移出最左
异位词长度必须等于 p
✗ 错:只比种类、不比次数
✓ 对:用计数比较,每个字母个数都要相等
p 里字母可能重复
✗ 错:每步重排序比较
✓ 对:维护计数数组增减,O(1) 更新
重排序每步 O(k log k) 会超时
完整代码(Python / C++ / Java)
Python
from collections import Counter
def findAnagrams(s, p):
k = len(p)
if len(s) < k: return []
need = Counter(p)
win = Counter(s[:k])
res = [0] if win == need else []
for r in range(k, len(s)):
win[s[r]] += 1 # 纳入新字符
win[s[r - k]] -= 1 # 移出最左字符
if win[s[r - k]] == 0:
del win[s[r - k]]
if win == need:
res.append(r - k + 1)
return resC++
vector<int> findAnagrams(string s, string p){
int k = p.size(), n = s.size();
vector<int> res;
if(n < k) return res;
vector<int> need(26,0), win(26,0);
for(char c : p) need[c-'a']++;
for(int r = 0; r < n; r++){
win[s[r]-'a']++; // 纳入
if(r >= k) win[s[r-k]-'a']--; // 移出最左
if(r >= k-1 && win == need)
res.push_back(r-k+1);
}
return res;
}Java
public List<Integer> findAnagrams(String s, String p){
List<Integer> res = new ArrayList<>();
int k = p.length(), n = s.length();
if (n < k) return res;
int[] need = new int[26], win = new int[26];
for (char c : p.toCharArray()) need[c - 'a']++;
for (int r = 0; r < n; r++) {
win[s.charAt(r) - 'a']++; // 纳入
if (r >= k) win[s.charAt(r - k) - 'a']--; // 移出最左
if (r >= k - 1 && Arrays.equals(win, need))
res.add(r - k + 1);
}
return res;
}复杂度
时间
O(n)
窗口右端扫一遍 s,每步比较 O(26) 常数
空间
O(1)
计数数组固定 26 个字母
看不够?换成动画再走一遍
上面的推演每一步都对应一帧动画。点开交互动画版,能一步步看着 找到字符串中所有字母异位词 的数据怎么变、指针怎么走,还能切 Python / Java / C++ 跟着练。
面试官可能追问
为什么用计数数组而不是每步排序?+
窗口进一个字符 +1、出一个字符 -1,O(1) 维护;排序每步 O(k log k),定长窗口下用计数最快。
怎么 O(1) 判断两个计数是否相等?+
字母集固定 26,直接比较两个长 26 的数组(Arrays.equals / vector ==),常数时间。
想听吴师兄把这道题讲给你听?
文字版和动画都随便看。开通图解算法年卡,可以听吴师兄把 找到字符串中所有字母异位词 一步步讲透(全站已上线 905 份讲透,持续扩充), 卡住的地方还有 AI 私教小欧就着动画帮你拆到懂。
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。