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

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

题目描述

给定两个字符串 sp,找到 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 当前窗口内已经达到平衡的字符种类数。当某个字符 ccnt[c] == 0 时,表示该字符在窗口内与 p 中数量相等,tar 加 1。
i 窗口右边界(当前遍历到的位置)。
j 窗口左边界。

举例演示

s = "cbaebabacd", p = "abc" 为例:

  • 初始 cnt = {'a':1, 'b':1, 'c':1}tot = 3tar = 0
  • 遍历过程如下表:
i s[i] 操作后 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 个小写字母,可视为常数)。

总结

该解法利用滑动窗口与计数匹配的思想,避免了每次重新统计窗口字符频次,而是通过维护一个"需求差值"和"平衡种类数"来实时判断窗口是否满足要求,是一种非常优雅且高效的算法。理解 tarcnt 的变化是掌握此题的关键。

相关推荐
故事和你912 小时前
sdut-程序设计基础Ⅰ-期末测试(重现)
大数据·开发语言·数据结构·c++·算法·蓝桥杯·图论
努力学算法的蒟蒻2 小时前
day114(3.16)——leetcode面试经典150
算法·leetcode·职场和发展
ysa0510302 小时前
贪心【逆向dp】
数据结构·c++·笔记·算法
夜月yeyue2 小时前
Linux 邻接(Neighbor)子系统架构与 NUD 状态机
linux·运维·服务器·嵌入式硬件·算法·系统架构
ArturiaZ2 小时前
【day55】
数据结构·c++·算法
仰泳的熊猫2 小时前
题目2279:蓝桥杯2018年第九届真题-日志统计
数据结构·c++·算法·蓝桥杯
一叶落4382 小时前
LeetCode 11:盛最多水的容器(C语言实现)
c语言·数据结构·算法·leetcode
Emilin Amy2 小时前
一台具备了“观察力”的下肢康复外骨骼机器人
算法·机器人
I_LPL2 小时前
day53 代码随想录算法训练营 图论专题7
算法·图论