🧩 题目
给定两个字符串 s 和 p,找到 s 中所有 p 的 异位词 的子串,返回这些子串的起始索引。不考虑答案输出的顺序。
✅ 示例
示例 1:
输入: s = "cbaebabacd", p = "abc"
输出: [0,6]
解释:
起始索引等于 0 的子串是 "cba", 它是 "abc" 的异位词。
起始索引等于 6 的子串是 "bac", 它是 "abc" 的异位词。
示例 2:
输入: s = "abab", p = "ab"
输出: [0,1,2]
解释:
起始索引等于 0 的子串是 "ab", 它是 "ab" 的异位词。
起始索引等于 1 的子串是 "ba", 它是 "ab" 的异位词。
起始索引等于 2 的子串是 "ab", 它是 "ab" 的异位词。
提示:
1 <= s.length, p.length <= 3 * 104s和p仅包含小写字母
🛠️解题代码
/**
* @param {string} s
* @param {string} p
* @return {number[]}
*/
var findAnagrams = function(s, p) {
if (p.length > s.length) return [];
const result = [];
const count = new Array(26).fill(0);
for (const char of p) {
count[char.charCodeAt() - 'a'.charCodeAt()]++;
}
let left = 0;
for (let right = 0; right < s.length; right++) {
const currentChar = s[right];
const index = currentChar.charCodeAt() - 'a'.charCodeAt();
count[index]--;
while (count[index] < 0) {
const leftChar = s[left++];
count[leftChar.charCodeAt() - 'a'.charCodeAt()]++;
}
if (right - left + 1 === p.length) {
result.push(left);
}
}
return result;
};
⏱️ 时间复杂度:O(n) —— 很快,只遍历一遍字符串
💾 空间复杂度:O(1) —— 很省内存,只用了固定大小的数组
📌 基本思路:滑动窗口 + 字符计数
我们要找的是长度为 len(p) 的子串,而且它和 p 是异位词。为了判断这一点,我们可以使用:
- 一个固定大小的数组
count[26]来记录字母出现次数(因为只包含小写字母)。 - 使用滑动窗口技巧维护当前窗口中字符的频率。
- 当窗口大小等于
p.length时,就认为找到了一个匹配项。
🧠 详细解释代码
1. 先处理边界情况
if (p.length > s.length) return [];
如果 p 比 s 还长,那肯定不可能有异位词,直接返回空数组。
2. 初始化变量
const result = []; // 存放结果(满足条件的起始索引)
const count = new Array(26).fill(0); // 记录每个字母需要多少个(相当于 p 的字符频率)
我们创建一个长度为 26 的数组 count,对应 'a' 到 'z'。
3. 统计 p 中每个字符的频率
for (const char of p) {
count[char.charCodeAt() - 'a'.charCodeAt()]++;
}
-
charCodeAt()可以把字符变成 ASCII 码。 -
'a'.charCodeAt()是 97,所以'a'对应下标 0,'b'对应下标 1,以此类推。 -
比如
p = "abc",那么:count[0] = 1 ('a') count[1] = 1 ('b') count[2] = 1 ('c')
4. 开始滑动窗口(双指针)
let left = 0; // 左指针,表示窗口左边界
for (let right = 0; right < s.length; right++) { // right 是右指针
const currentChar = s[right]; // 当前字符
const index = currentChar.charCodeAt() - 'a'.charCodeAt(); // 找到这个字符对应的数组下标
count[index]--; // 把这个字符的需求减 1
比如窗口中有 'a',我们就把 count[0] 减 1,表示已经有一个 'a' 被用了。
5. 如果某个字符用得太多了怎么办?
while (count[index] < 0) { // 说明这个字符用得太多,要缩小窗口
const leftChar = s[left++]; // 左指针向右移
count[leftChar.charCodeAt() - 'a'.charCodeAt()]++; // 把左边移出窗口的字符加回来
}
举个例子:
- 假设
p = "aab",我们需要两个'a' - 现在窗口里有三个
'a',这时候count['a']就会变成 -1 - 我们就把窗口左边的字符移出去,直到不再多为止
6. 判断是否找到一个异位词
if (right - left + 1 === p.length) {
result.push(left);
}
当窗口长度正好等于 p.length 时,说明找到了一个符合条件的异位词,把这个窗口的起始位置 left 加入结果数组。
🧮时间复杂度和空间复杂度
✅ 这段代码的时间复杂度:O(n)
-
n是字符串s的长度。 -
我们只用了一个循环来遍历整个字符串
s,并且用双指针(left和right)控制窗口。 -
每个字符最多被处理两次:
- 一次被右指针处理(加入窗口)
- 一次被左指针处理(移出窗口)
所以整体上就是线性时间:O(n)
✅ 这段代码的空间复杂度:O(1)
- 我们使用了一个固定大小的数组
count[26]来记录字母频率,这个数组大小不会随输入变化,所以是常量级空间。 - 虽然我们还用了
result数组来存答案,但在算法题中,输出结果所占的空间通常不计入空间复杂度。
所以整体上就是常量空间:O(1)
📝 总结一下整个过程(模拟一遍)
假设 s = "abacbabc", p = "abc"
- 初始化
count数组,统计p的字符频率。 - 从左往右遍历
s,每次加入一个字符,更新count。 - 如果某个字符多了,就移动左指针,让窗口变小。
- 当窗口长度刚好等于
p.length时,说明找到了一个异位词,记录起始位置。
📚 知识点回顾
| 概念 | 说明 |
|---|---|
| 异位词 | 字符种类相同、数量相同但顺序不同 |
| 滑动窗口 | 一种常用于字符串查找的技巧,保持窗口内数据结构稳定 |
| 字符编码 | JavaScript 中可以用 charCodeAt() 获取字符的 ASCII 码 |
| 数组代替 Map | 在仅包含小写字母的情况下,数组比 Map 更快更方便 |