滑动窗口 + 哈希表:高效查找字符串中的所有字母异位词

54 阅读3分钟

在 LeetCode 上刷题时,经常会遇到一类经典问题:在主串中找出所有与目标串互为字母异位词(Anagram)的子串。这类题目看似简单,但若采用暴力解法,很容易陷入性能瓶颈。今天我想和大家分享一种更优雅、高效的解法——滑动窗口 + 哈希表(计数数组)

什么是字母异位词?

首先明确一下概念:两个字符串互为字母异位词,意味着它们包含完全相同的字符,且每个字符出现的次数也完全相同,只是顺序不同。例如 "abc""bca" 就是一对字母异位词。

因此,判断两个字符串是否为字母异位词的关键,并不在于字符的顺序,而在于字符频次的一致性

暴力解法为何不可取?

最初我想到的思路是:

  • 在字符串 s 中,每次取出长度等于 p 的子串;
  • 对该子串进行 split → sort → join,得到一个标准化的字符串;
  • 与同样处理过的 p 进行比较;
  • 如果相等,就记录当前起始索引。

这种方法逻辑清晰,但时间复杂度非常高

滑动窗口 + 计数数组:优化的核心思想

既然字母异位词只关心字符频次,那我们完全可以用一个长度为 26 的数组来统计每个字母出现的次数(假设只包含小写字母)。这样,判断两个字符串是否为异位词,就变成了比较两个计数数组是否完全相等

而“滑动窗口”技巧,则能让我们在移动窗口时,只更新进出窗口的字符计数,避免重复遍历整个子串

具体实现步骤

以下是我在 LeetCode 上通过的 JavaScript 代码,配合详细注释:

var findAnagrams = function(s, p) {
  // 如果 p 比 s 还长,肯定找不到,直接返回空数组
  if (p.length > s.length) return [];

  // 创建两个长度为 26 的计数数组 初始时用0填充
  const need = new Array(26).fill(0);   // 记录 p 中每个字母需要多少个
  const window = new Array(26).fill(0); // 记录当前滑动窗口中每个字母有多少个

  // 辅助函数:将字符 'a'~'z' 转换为 0~25 的索引
  function charToIndex(char) {
    return char.charCodeAt(0) - 'a'.charCodeAt(0);
  }

  // 第一步:统计 p 中每个字符的出现次数
  for (let char of p) {
    need[charToIndex(char)]++;
  }

  // 第二步:初始化滑动窗口(前 p.length 个字符)
  for (let i = 0; i < p.length; i++) {
    window[charToIndex(s[i])]++;
  }

  const result = [];

  // 第三步:检查初始窗口是否匹配
  if (arraysEqual(window, need)) {
    result.push(0);
  }

  // 第四步:滑动窗口向右移动
  for (let i = p.length; i < s.length; i++) {
    // 左边字符滑出窗口
    const leftChar = s[i - p.length];
    window[charToIndex(leftChar)]--;

    // 右边字符滑入窗口
    const rightChar = s[i];
    window[charToIndex(rightChar)]++;

    // 检查当前窗口是否与 p 构成异位词
    if (arraysEqual(window, need)) {
      result.push(i - p.length + 1);
    }
  }

  return result;
};

// 辅助函数:判断两个长度为 26 的数组是否完全相等
function arraysEqual(a, b) {
  for (let i = 0; i < 26; i++) {
    if (a[i] !== b[i]) return false;
  }
  return true;
}

算法优势分析

  • 时间复杂度:O(n),其中 n 是字符串 s 的长度。每个字符最多被访问两次(进窗口和出窗口),计数数组比较固定为 26 次。
  • 空间复杂度:O(1),因为计数数组大小固定为 26,不随输入规模变化。

相比暴力解法,效率提升非常明显,尤其在处理长字符串时优势巨大。

总结

这道题很好地展示了滑动窗口哈希思想(计数数组) 的结合威力。它提醒我们:在处理字符串匹配、子串查找等问题时,不要只盯着字符本身,更要关注其内在的统计特征

如果你也在刷 LeetCode,不妨多思考:能否用“状态压缩”或“频次统计”代替逐字比较?很多时候,换个角度,就能从 O(n²) 优化到 O(n)。

希望这篇分享对你有帮助!欢迎在评论区交流你的解题思路~