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) |
优势 | 更快,避免多余字典比较 | 结构直观,便于理解 |