云计算百科
云计算领域专业知识百科平台

LeetCode 438:找到字符串中所有字母异位词(滑动窗口) —— 题解

  👋 欢迎阅读

一.题目

438. 找到字符串中所有字母异位词 – 力扣(LeetCode)

🎯 欢迎来到「找到字符串中所有字母异位词」题解之旅! 本文将带你从"在长串中滑动窗口寻找异位词"这一直观场景出发,深入理解滑动窗口 + 计数数组的巧妙运用,并掌握如何用有效计数 count 判断命中来定位全部异位词起点。

在开始之前,建议你先:

  • 了解题目背景:这是 LeetCode 438 题,给定字符串 s 和 p,找出 s 中所有 p 的字母异位词子串起始下标。本质上,异位词即各字母出现次数相同,问题转化为定长窗口内的计数比对。

  • 明确学习目标:掌握滑动窗口 + 双计数数组技术,理解有效计数 count 的维护原理,并熟练处理窗口超长时的出窗口逻辑。

  • 准备好环境:建议在本地 IDE 或 LeetCode 在线编辑器中打开代码,边看边运行,亲手验证示例(如 s = "cbaebabacd", p = "abc" 输出 [0, 6])。

本文将从问题转化、进窗口、出窗口、更新结果到代码实现,层层递进。即使你对滑动窗口还不熟悉,我们也会从"让窗口与 p 等长,边走边核对字母账本"这一直觉出发,让你轻松抓住核心思想——窗口定长滑动,计数对齐即命中。现在,让我们一起滑动窗口,找出所有字母异位词的起点吧! 📍🔍

二.做题思路

一、问题分析(前置分析)

  • 题目要求:在 s 中找出所有与 p 互为字母异位词的连续子串,返回其起始下标。
  • 关键约束:子串长度必须等于 p 的长度(定长窗口);仅含小写字母(可用 26 长度计数数组);异位词只看字符频次相等,与顺序无关。
  • 核心思路:维护长度恒为 n1 的滑动窗口,用有效计数 count 把 O(26) 的频次比对压缩为 O(1) 的整数判断。

二、算法策略(滑动窗口 + 计数数组)

核心步骤:

  • 统计 p 中各字符频次到 hash1[26]。
  • 初始化窗口:left = 0、right = 0、count = 0,hash2[26] 记录窗口内频次。
  • 进窗口:right 指向的字符加入 hash2;若加入后 hash2[ch] <= hash1[ch],说明该字符仍在配额内,count++。
  • 出窗口:当窗口长度 right – left + 1 > n1 时,移除 left 指向的字符:若移除前 hash2[ch] <= hash1[ch],说明它曾计入配额,count–,再 hash2[ch]–、left++。
  • 更新结果:若 count == n1,说明窗口内频次与 p 完全一致,记录 left。
  • 示例执行过程(s = "cbaebabacd",p = "abc",n1 = 3):

    步骤变量变化操作结果
    预处理 hash1[a]=1, b=1, c=1 统计 p 的频次 计数表就绪
    right=0 hash2[c]=1, count=1 进 'c',配额内 未命中
    right=1 hash2[b]=1, count=2 进 'b',配额内 未命中
    right=2 hash2[a]=1, count=3 进 'a',配额内,窗口=3 命中,记录 left=0
    right=3 count: 3→2, left=1 进 'e' 不计;出 'c' 减 count 未命中
    right=8 count: 2→3, left=6 进 'c' 计 1;出 'a' 不减 命中,记录 left=6
    right=9 count: 3→2, left=7 进 'd' 不计;出 'b' 减 count 未命中

    最终返回 [0, 6],与题目示例一致。

    三、正确性说明(简单版本)

    • 窗口长度恒为 n1:每次右指针前进后,只要长度超过 n1 就立刻收缩左端,保证被判断的窗口始终与 p 等长,不漏检也不重检。
    • count 语义可靠:count 表示窗口中"有效字符"总数——即每个字符在不超过 p 需求配额内的累计数量。只有当 count == n1 时,窗口内频次才恰好全部等于 p 的需求,此时窗口必然是异位词;反之亦然,不会错解。
    • 每个起点都被覆盖:left 从 0 一路右移到 n2 – n1,所有可能的定长子串恰好被窗口完整覆盖一次,不会漏解。
    • 出窗口判定安全:先依据移除前的频次判断是否 count–,再真正减频次,顺序保证 count 始终准确。

    四、实现细节(边界防护)

    • 初始化:hash1[26] = {0}、hash2[26] = {0}、left = 0、right = 0、count = 0、n1 = p.size()、n2 = s.size()。
    • 边界防护:若 n1 > n2,s 中不可能存在长度 n1 的异位词子串,直接返回空;right 遍历到 n2 即停;字符统一用 ch – 'a' 映射到 0~25。
    • 复杂度:时间 O(n2)(每个字符进、出窗口各一次,count 判断 O(1)),空间 O(1)(两个固定长度 26 的数组)。
    • 关键判断:if (hash2[in] <= hash1[in]) count++(进窗口配额判断)、if (right – left + 1 > n1)(窗口收缩判断)、if (count == n1)(命中判断)。

    五、返回值(目标映射)

    • 返回 v:所有满足条件的子串起始下标。由于 left 从左向右移动,结果自然升序排列,正好对应题目要求的"返回所有起始索引"。

    三.代码

    class Solution
    {
    public:
    vector<int> findAnagrams(string s, string p)
    {
    vector<int> v; // 结果数组:记录所有异位词子串的起始下标
    int n1 = p.size(); // p 的长度,即窗口的固定长度
    int n2 = s.size();

    // 边界防护:s 比 p 短,不可能存在异位词子串,直接返回空
    if (n1 > n2)
    {
    return v;
    }

    int hash1[26] = { 0 }; // p 的频次表:记录 p 中每个字母的出现次数
    // 1. 预处理:统计 p 中各字符频次
    for (auto ch : p)
    {
    hash1[ch – 'a']++; // 'a' 的 ASCII 码是 97,统一映射到 0~25
    }

    int hash2[26] = { 0 }; // 窗口频次表:记录当前窗口内各字符出现次数

    // 2. 滑动窗口:进窗口 -> 判断/出窗口 -> 更新结果
    for (int left = 0, right = 0, count = 0; right < n2; right++)
    {
    // ———- 进窗口 ———-
    int in = s[right] – 'a'; // 进入窗口的字符
    hash2[in]++; // 窗口频次 +1
    if (hash2[in] <= hash1[in])
    {
    count++; // 该字符仍在 p 的“配额”内,有效计数 +1
    }

    // ———- 判断 / 出窗口 ———-
    if (right – left + 1 > n1) // 窗口长度超出 p 的长度,必须收缩
    {
    int out = s[left] – 'a'; // 即将离开窗口的字符
    if (hash2[out] <= hash1[out])
    {
    count–; // 移除前该字符曾计入配额,有效计数 -1
    }
    hash2[out]–; // 窗口频次 -1
    left++; // 左指针右移,窗口恢复为 n1 长度
    }

    // ———- 更新结果 ———-
    if (count == n1) // 有效计数等于 p 长度,说明频次完全匹配
    {
    v.push_back(left); // 记录当前窗口起点
    }
    }
    return v; // 3. 返回所有异位词起始下标
    }
    };

    四、易错点分析

    难点1:进窗口时"先加频次,再判断配额"的顺序

    hash2[in]++;
    if (hash2[in] <= hash1[in])
    {
    count++;
    }

    count 统计的是窗口中未超出 p 配额的字符个数。进窗口时判断依据必须是加入后的最新频次,所以必须先 hash2[in]++ 再比较;若顺序颠倒,就会用旧频次误判,导致 count 偏小、漏解。


    难点2:出窗口时 count– 的判断基于"移除前"的频次

    int out = s[left] – 'a';
    if (hash2[out] <= hash1[out])
    {
    count–;
    }
    hash2[out]–;

    这里判断的是"该字符被移除前是否处于配额内"。如果先 hash2[out]– 再判断,频次已减少,结果可能失真(例如原频次恰好超配额 1,先减后判变成"在配额内",导致 count 漏减)。判断与修改的先后顺序是本题最容易写错的地方。


    难点3:count 不是"窗口字符总数",而是"有效字符总数"

    if (count == n1)
    {
    v.push_back(left);
    }

    窗口里可能混入 p 中没有的字符(如 'e'、'd'),它们不计入 count;同一字符超出配额的重复出现也不计入。只有 count == n1 时,窗口内频次才与 p 完全一致。把 count 误当成窗口长度是常见的理解误区。

    难点4:窗口收缩条件 > 与 >= 的一字之差

    if (right – left + 1 > n1)
    {

    left++;
    }

    窗口长度必须恰好等于 n1:短了会漏检,长了会重复。用 > 保证每轮至多收缩一次,窗口稳定在 n1;若误写成 >=,窗口会被压到 n1-1,导致永远无法命中,返回空数组。

    五、流程图

     🎯 闭幕

    🎉 恭喜你完成了「找到字符串中所有字母异位词」问题的学习!

    为了巩固知识并进一步拓展,建议你:

    🚀 动手实践 在 LeetCode 上提交代码,尝试不同的测试用例。

    💡 深入思考

    • 本题使用 固定长度的滑动窗口(窗口大小固定为 p.length()),并通过 数组哈希表 统计窗口内字符频次与 p 的频次进行比较。请问 为什么窗口长度固定为 p 的长度?如果窗口长度不固定,还能用这种方式吗?

    • 代码中通过 count 变量记录 有效字符数(即窗口内频次不超过 p 对应频次的字符个数),从而避免每次完整比较两个哈希表。为什么 count == n1 就能说明窗口是异位词? 这背后的等价条件是什么?

    • 当窗口长度超过 n1 时,代码先判断 hash2[out] <= hash1[out] 再 count–,然后再 hash2[out]–。为什么先判断再减,而不是先减再判断? 顺序颠倒会有什么问题?

    • 本题字符集限定为 小写英文字母,所以用 int hash[26] 足够。如果字符串包含 Unicode 字符(如中文),应如何改造代码?

    • 滑动窗口的 “进窗口 → 判断/出窗口 → 更新结果” 三段式结构是本题模板的典型写法。如果将出窗口放在 更新结果之后(即先判断再收缩),会有什么影响?

    📚 延伸挑战

    • 如果题目要求返回 所有异位词子串本身(而不是起始下标),你的代码应做哪些调整?

    • 如果 p 中可能包含 重复字符(例如 p = "aa"),当前的 count 计数逻辑是否仍然正确?请结合示例 s = "baa" 验证。

    如果你觉得本文对你有所帮助,欢迎:

    👍 点赞 / 收藏 👤 关注作者,获取更多题解 💬 留言交流你的疑问或优化思路


    📌 深入思考答案

    • 窗口长度固定为 n1,因为异位词要求 长度相同且字符频次相同,长度不同则不可能匹配,因此固定窗口长度是自然的约束。

    • count == n1 等价于完全匹配,因为 count 记录的是窗口内所有字符的频次都不超过 p 中对应字符频次的数量,且窗口长度等于 n1,这意味着窗口内每个字符的频次恰好等于 p 中对应字符的频次,即完全匹配。

    • 必须先判断再减,因为判断的是 移除前 该字符是否在 p 的配额内,若先减再判断,hash2[out] 已变化,判断结果会错误,导致 count 计数不准。

    • 若字符集为 Unicode,改用 unordered_map<char, int> 替代固定数组,动态统计频次,适应任意字符。

    • 若先更新结果再收缩,会导致窗口长度大于 n1 时仍可能被判断为有效,破坏固定窗口的语义,必须先在更新结果前确保窗口长度合法。

    🔍 延伸挑战答案

    • 挑战1:只需在 v.push_back(left) 的同时,用 s.substr(left, n1) 取出子串并存入结果数组即可。

    • 挑战2:count 逻辑仍然正确。以 s="baa", p="aa" 为例:初始窗口 "ba",count 中 b 频次 1 > hash1['b']=0,不计入;a 频次 1 ≤ hash1['a']=2,计入,count=1,不满足 count==2。右移后窗口 "aa",两个 a 均计入,count=2,匹配,逻辑无误。

    祝你在 算法之路 上越走越稳,早日攻克每一道难题!下次见 🚀✨

    赞(0)
    未经允许不得转载:网硕互联帮助中心 » LeetCode 438:找到字符串中所有字母异位词(滑动窗口) —— 题解
    分享到: 更多 (0)

    评论 抢沙发

    评论前必须登录!