438. 找到字符串中所有字母异位词
两种方法:定长滑窗/不定长滑窗
方法一:定长滑窗
✅ 算法思路
目标:找出字符串 s 中所有 p 的字母异位词的起始索引。
核心思路是:滑动窗口 + 哈希表比较字符频次
步骤详解:
-
定义两个字典(
collections.Counter):-
cnt_p:统计字符串p中每个字符出现的次数。 -
cnt_s:用来统计s中滑动窗口内的字符频次。
-
-
使用滑动窗口遍历
s:-
每次向右滑动一位,记录当前字符(
c)的频率到cnt_s中。 -
left = right - len(p) + 1是当前滑动窗口的左边界。- 如果
left < 0,说明窗口长度还不够,跳过本次比较。
- 如果
-
若当前窗口内字符频率与
p的字符频率相同(cnt_s == cnt_p),说明这是一个异位词的起始位置,加入结果列表。 -
移除窗口最左边的字符计数(
cnt_s[s[left]] -= 1),为下次滑动窗口做准备。
-
python
# 请使用 Python3 提交代码!Python2 已经被淘汰了
class Solution:
def findAnagrams(self, s: str, p: str) -> List[int]:
ans = []
cnt_p = Counter(p) # 统计 p 的每种字母的出现次数
cnt_s = Counter() # 统计 s 的长为 len(p) 的子串 s' 的每种字母的出现次数
for right, c in enumerate(s):
cnt_s[c] += 1 # 右端点字母进入窗口
left = right - len(p) + 1
if left < 0: # 窗口长度不足 len(p)
continue
if cnt_s == cnt_p: # s' 和 p 的每种字母的出现次数都相同
ans.append(left) # s' 左端点下标加入答案
cnt_s[s[left]] -= 1 # 左端点字母离开窗口
return ans
在一个窗口内,如果字母出现次数相同,那么就判断是字母异位词;
⏱️ 时间复杂度分析
设:
-
n是字符串s的长度; -
m是字符串p的长度; -
a是字符集大小(最多 26 个小写字母)。
每一步操作:
-
每个字符只进入和离开窗口一次:共
O(n)。 -
每次比较
cnt_s == cnt_p:-
在 Python 中,
Counter的比较会遍历内部 key。 -
最坏情况下是
O(a),即 26 个小写字母。
-
综合时间复杂度:
- 总体为:
O(n * a),在本题中a = 26是常数,因此可以认为是O(n)。
🧠 空间复杂度分析
-
cnt_p和cnt_s都是Counter对象,最多存储 26 个字母的频次。 -
所以:空间复杂度是
O(a)=O(26)=O(1)(常数级) -
ans最多包含n - m + 1个索引,最坏为O(n)。
总空间复杂度:O(n)(主要是结果数组,计数器是常数空间)
方法二:不定长滑窗
✅ 算法思路
💡 目标:
在字符串 s 中查找所有与字符串 p 是字母异位词的子串起始索引。
📌 思路核心:
-
使用 滑动窗口 + Counter 频次计数表;
-
与前一个版本不同的是:只使用一个计数器
cnt,而非同时维护两个窗口计数器; -
在窗口中每加入一个字符,就将
cnt[c] -= 1; -
如果某个字符计数小于 0,说明此字符出现次数过多,则不断收缩左边界(恢复字符计数);
-
当窗口长度刚好等于
p长度时,表示当前窗口是合法的异位词,记录其起始位置。
python
class Solution:
def findAnagrams(self, s: str, p: str) -> List[int]:
ans = []
cnt = Counter(p) # 统计 p 的每种字母的出现次数
left = 0
for right, c in enumerate(s):
cnt[c] -= 1 # 右端点字母进入窗口
while cnt[c] < 0: # 字母 c 太多了
cnt[s[left]] += 1 # 左端点字母离开窗口
left += 1
if right - left + 1 == len(p): # s' 和 p 的每种字母的出现次数都相同
ans.append(left) # s' 左端点下标加入答案
return ans
🧠 举个例子:
python
s = "cbaebabacd", p = "abc"
-
初始化
cnt = Counter({'a':1, 'b':1, 'c':1}) -
当遍历到前三个字符
"cba"时,窗口长度等于 3,且所有cnt中字符都被消耗为 0,说明是一个异位词 → 加入答案。 -
如果某字符被多加了(
cnt[c] < 0),就从左侧不断弹出直到窗口合法。
⏱️ 时间复杂度分析
设:
-
n = len(s),字符串 s 的长度; -
m = len(p),字符串 p 的长度; -
a = 26,英文小写字母数量。
✅ 主体操作:
-
遍历一次
s,每个字符至多进入窗口一次,离开窗口一次; -
每个字符进入/退出窗口都只涉及对
cnt的简单加减操作,是O(1); -
整体的操作是 线性的扫描和窗口移动。
✅ 总时间复杂度:
O(n)
🧠 空间复杂度分析
-
cnt最多维护 26 个小写字母的计数; -
ans最多存储O(n)个索引位置。
✅ 总空间复杂度:
O(1)(cnt是常数空间)+O(n)(结果数组)
🔁 对比:
| 项目 | 本版本(单 Counter + 窗口平衡) | 前版本(双 Counter 比较) |
|---|---|---|
| 比较逻辑 | 巧妙用计数平衡维护合法窗口 | 每轮完整比较两个 Counter |
| 时间复杂度 | O(n)(无需逐字符比较) |
O(n * a)(每轮比较 26 字符) |
| 空间复杂度 | O(1) + O(n) |
O(1) + O(n) |
| 优势 | 更快,避免多余字典比较 | 结构直观,便于理解 |