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

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)。

核心思想

  1. 用一个哈希表 cnt 记录 p 中每个字符的"需求"次数(初始为 p 中各字符的出现次数)。
  2. 维护一个窗口 [j, i],每次将右边界字符 s[i] 纳入窗口,相当于"消耗"了一个该字符,所以 cnt[s[i]]--。
  3. 如果某个字符在 cnt 中的值变为 0,说明当前窗口中该字符的数量已经与 p 中一致(不多不少),我们用一个变量 tar 记录当前已经达到平衡的字符种类数。
  4. 当窗口长度超过 len(p) 时,需要移动左边界 j,将左边字符"归还"到 cnt 中(即 cnt[s[j]]++)。如果移出前该字符的计数为 0(意味着它原本在窗口内是平衡的),那么移出后会破坏平衡,所以先 tar--。
  5. 在每一步,如果 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 si 操作后 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 的变化是掌握此题的关键。

相关推荐
UIU1146 小时前
P1233 [ICPC 2001 Taejon R] 木棍加工
数据结构·c++·算法·动态规划
论文复现现场7 小时前
WRF、FEA 能用 L40S 加速吗?求解器检查、断点恢复与长期运行
java·数据库·算法·wrf·有限元分析·gpu计算·断点续算
ZhengEnCi7 小时前
用 AI 写代码为什么拼到最后还是拼人:算力瓶颈与领域专家提示词的本质差异
算法·架构
秋天的怀念_David8 小时前
第二十一夜 · 数字黑洞与西西弗斯的惩罚
java·程序人生·算法
碧海银沙音频科技研究院9 小时前
在PC电脑上用Whisper→Marian算法实现离线语音翻译功能
算法·whisper
niyongsheng9 小时前
3D 装箱算法工程化:数据结构、放置启发式与支撑规则
数据结构·算法·rust
j7~12 小时前
【穷举、深搜与回溯算法】一棵决策树讲透全排列与子集
数据结构·c++·算法·leetcode·回溯·递归
傅科摆 _ py13 小时前
DP,贪心,堆和栈,二分力扣复习
开发语言·python·算法
Shaoshing13 小时前
LeetCode100 42.接雨水
java·算法·leetcode
域限奇点访客13 小时前
用AI生成问卷的网站怎么选?看题目录入结构、题型映射和生成后的校验
算法