LeetCode 热题 100 之第9题 找到字符串中所有字母异位词(JavaScript篇)

178 阅读4分钟

传送门:438. 找到字符串中所有字母异位词 - 力扣(LeetCode)

🧩 题目

给定两个字符串 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 * 104
  • s 和 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 [];

如果 ps 还长,那肯定不可能有异位词,直接返回空数组。


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"

  1. 初始化 count 数组,统计 p 的字符频率。
  2. 从左往右遍历 s,每次加入一个字符,更新 count
  3. 如果某个字符多了,就移动左指针,让窗口变小。
  4. 当窗口长度刚好等于 p.length 时,说明找到了一个异位词,记录起始位置。

📚 知识点回顾

概念说明
异位词字符种类相同、数量相同但顺序不同
滑动窗口一种常用于字符串查找的技巧,保持窗口内数据结构稳定
字符编码JavaScript 中可以用 charCodeAt() 获取字符的 ASCII 码
数组代替 Map在仅包含小写字母的情况下,数组比 Map 更快更方便