LeetCode 438.找到字符串中所有字母异位词
题目描述
给定两个字符串 s 和 p,找到 s 中所有 p 的字母异位词的子串,返回这些子串的起始索引。不考虑答案输出的顺序。
字母异位词指由相同字母重排列形成的字符串(包括相同的字符串)。
示例:
输入: s = "cbaebabacd", p = "abc"
输出: [0,6]
解释:
起始索引等于 0 的子串是 "cba", 它是 "abc" 的异位词。
起始索引等于 6 的子串是 "bac", 它是 "abc" 的异位词。
思路分析
我们需要在 s 中找到所有长度为 len(p) 且字符出现次数与 p 完全相同的连续子串。一种直观的想法是固定窗口大小,每次统计窗口内字符频次并与 p 比较,但这样每次比较都需要 O(26) 或 O(字符集大小) 的时间。更高效的方法是使用滑动窗口 + 欠账/平衡标记,将比较的复杂度降为 O(1)。
核心思想
- 用一个哈希表
cnt记录p中每个字符的"需求"次数(初始为p中各字符的出现次数)。 - 维护一个窗口
[j, i],每次将右边界字符s[i]纳入窗口,相当于"消耗"了一个该字符,所以cnt[s[i]]--。 - 如果某个字符在
cnt中的值变为 0,说明当前窗口中该字符的数量已经与p中一致(不多不少),我们用一个变量tar记录当前已经达到平衡的字符种类数。 - 当窗口长度超过
len(p)时,需要移动左边界j,将左边字符"归还"到cnt中(即cnt[s[j]]++)。如果移出前该字符的计数为 0(意味着它原本在窗口内是平衡的),那么移出后会破坏平衡,所以先tar--。 - 在每一步,如果
tar == p中不同字符的总数tot,说明当前窗口内所有字符都已平衡,且窗口长度必定为len(p),此时窗口就是一个合法的字母异位词,记录左边界j。
这种方法只需遍历一次 s,每次操作 O(1),总时间复杂度 O(n)。
代码实现(含详细注释)
cpp
class Solution {
public:
vector<int> findAnagrams(string s, string p) {
unordered_map<char, int> cnt; // 记录每个字符的需求量(初始为p中的次数)
for (auto c : p) cnt[c]++; // 统计p中每个字符的出现次数
vector<int> res;
int tot = cnt.size(); // p中不同字符的种类数
// i为窗口右边界,j为左边界,tar为当前已满足需求的字符种类数
for (int i = 0, j = 0, tar = 0; i < s.size(); i++) {
// 1. 将右边界字符s[i]纳入窗口,需求量减1
if (--cnt[s[i]] == 0) tar++; // 若减1后变为0,说明该字符数量已匹配,tar++
// 2. 如果窗口长度超过p的长度,收缩左边界
while (i - j + 1 > p.size()) {
if (cnt[s[j]] == 0) tar--; // 若左边界字符原本是平衡的,移出后破坏平衡
cnt[s[j++]]++; // 将左边界字符"归还"需求量,并移动左指针
}
// 3. 检查是否所有字符都已平衡
if (tar == tot) res.push_back(j); // 找到一个异位词,记录起始索引
}
return res;
}
};
关键变量解释
| 变量 | 含义 |
|---|---|
cnt |
哈希表,初始值为 p 中每个字符的计数。滑动过程中,cnt[c] 表示当前窗口还需要多少个字符 c 才能与 p 完全匹配(负数表示窗口内多出了该字符)。 |
tot |
p 中不同字符的种类数,即最终需要满足的平衡种类数。 |
tar |
当前窗口内已经达到平衡的字符种类数。当某个字符 c 的 cnt[c] == 0 时,表示该字符在窗口内与 p 中数量相等,tar 加 1。 |
i |
窗口右边界(当前遍历到的位置)。 |
j |
窗口左边界。 |
举例演示
以 s = "cbaebabacd", p = "abc" 为例:
- 初始
cnt = {'a':1, 'b':1, 'c':1},tot = 3,tar = 0。 - 遍历过程如下表:
| i | s[i] | 操作后 cnt 变化 | 窗口 | tar | 是否记录 |
|---|---|---|---|---|---|
| 0 | 'c' | cnt['c']: 1→0 |
"c" | 1 | 否 |
| 1 | 'b' | cnt['b']: 1→0 |
"cb" | 2 | 否 |
| 2 | 'a' | cnt['a']: 1→0 |
"cba" | 3 | 是(索引0) |
| 3 | 'e' | cnt['e']: 0→-1 |
窗口变"cbae"→收缩左边界 'c' 后 "bae" | 收缩后 tar 变为2 | 否 |
| 4 | 'b' | cnt['b']: 0→-1 |
收缩左边界 'b' 后 "ae"→再扩为 "aeb" | 收缩后 tar 变为1,再扩 tar 不变 | 否 |
| 5 | 'a' | cnt['a']: 0→-1 |
"aeba"→收缩后 "eba" | ... | ... |
| 6 | 'b' | ... | 最终在 i=6 时窗口 "bac" 平衡 | tar=3 | 是(索引6) |
最终结果 [0,6]。
复杂度分析
- 时间复杂度 :O(n),其中 n 是字符串
s的长度。每个字符最多被左右指针各访问一次,哈希表操作 O(1)。 - 空间复杂度:O(1)(哈希表大小不超过字符集大小,本题为 26 个小写字母,可视为常数)。
总结
该解法利用滑动窗口与计数匹配的思想,避免了每次重新统计窗口字符频次,而是通过维护一个"需求差值"和"平衡种类数"来实时判断窗口是否满足要求,是一种非常优雅且高效的算法。理解 tar 和 cnt 的变化是掌握此题的关键。