C++ 力扣 438.找到字符串中所有字母异位词 题解 优选算法 滑动窗口 每日一题
文章目录
- 一、题目描述
- 二、为什么这道题值得我们弄懂?
- 三、题目拆解:抓住关键约束
- 四、思路演进:从暴力到滑动窗口
- 五、算法实现:固定窗口的滑动与频率更新
- 实现代码
- 代码细节拆解:
- 时间复杂度与空间复杂度
- 六、实现过程中的坑点总结
- 七、下题预告
这是封面原图,还有AI生成的动图,嘿嘿:


一、题目描述
题目链接:找到字符串中所有字母异位词
题目描述:
示例 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 * 10^4
s 和 p 仅包含小写字母
二、为什么这道题值得我们弄懂?
作为滑动窗口在字符串匹配场景的经典例题,LeetCode 第 438 题的价值体现在:
- 帮你掌握“固定窗口长度”的滑动窗口用法——不同于“水果成篮”中动态调整窗口大小,本题窗口长度由
p
的长度固定,是滑动窗口的另一类典型应用; - 带你理解“字符频率匹配”的核心逻辑——字母异位词的本质是“字符种类和数量完全相同”,如何通过统计频率高效判断匹配;
- 让你学会“窗口滑动时的频率更新技巧”——如何避免重复计算,仅通过增减边界字符的频率实现高效维护。
学会这道题,能为解决“字符串子串匹配”“字符频率相关”的问题打下基础,比如后续要讲的“串联所有单词的子串”,核心思路也与此题相通。
三、题目拆解:抓住关键约束
结合题目要求和示例,核心要素如下:
- 输入是两个字符串
s
(待搜索的主串)和p
(目标子串),长度均可达 3×10⁴(需考虑效率)。 - 核心约束是“子串需为
p
的字母异位词”,即:- 子串长度必须与
p
相同(因字母数量相同); - 子串中每种字符的出现次数与
p
完全一致。
- 子串长度必须与
- 目标是找到
s
中所有满足条件的子串的起始索引,并返回这些索引的列表。
关键点提炼:
- 窗口长度固定:子串长度必等于
p.size()
,因此滑动窗口的大小是固定的(p.size()
); - 匹配核心是频率:无需关注字符顺序,只需统计窗口内字符频率与
p
的字符频率是否完全一致; - 效率要求高:需避免暴力枚举(枚举所有长度为
p.size()
的子串,再逐个统计频率),时间复杂度需控制在 O(n) 级别(n 为s
的长度); - 边界处理重要:窗口滑动时,需准确更新进入窗口和离开窗口的字符频率,避免统计错误。
四、思路演进:从暴力到滑动窗口
1. 暴力解法
最直观的思路是:枚举 s
中所有长度为 p.size()
的子串,对每个子串统计字符频率,再与 p
的字符频率对比,若一致则记录起始索引。
-
枚举方式:
起始索引i
从 0 到s.size()-p.size()
,对每个i
,取子串s[i:i+p.size()]
,用数组或哈希表统计其字符频率,再与p
的频率数组对比。 -
为什么不可行:
时间复杂度高。假设p
的长度为 m,s
的长度为 n,则需枚举n-m+1
个子串,每个子串统计频率需 O(m) 时间,对比频率也需 O(26) 时间(因只有小写字母),总时间复杂度为 O((n-m+1)×m),当 m 接近 n 时(如 m=1.5×10⁴,n=3×10⁴),会产生约 2×10⁸ 次操作,容易超时。
结论:必须用滑动窗口优化,通过“固定窗口+动态更新频率”减少重复计算,将时间复杂度降至 O(n)。
2. 滑动窗口的思路:固定窗口+频率匹配
滑动窗口的核心是:维护一个长度为 p.size()
的固定窗口,在 s
中从左到右滑动,通过动态更新窗口内的字符频率,实时判断窗口是否与 p
的频率匹配。
步骤拆解:
- 预处理
p
的频率:用一个数组hash1
统计p
中每个字符的出现次数(因只有 26 个小写字母,数组大小设为 26 即可)。 - 初始化窗口:用另一个数组
hash2
统计s
中初始窗口(前p.size()
个字符)的频率,同时用一个变量count
记录“窗口中满足‘频率≤p中对应字符频率’的字符数量”(用于快速判断匹配)。 - 滑动窗口:
- 向右移动窗口时,右侧新进入窗口的字符(
in
):更新hash2[in]
,若更新后hash2[in]
仍≤hash1[in]
,则count
加 1(说明该字符的频率仍在合理范围内); - 左侧离开窗口的字符(
out
):更新hash2[out]
,若更新前hash2[out]
≤hash1[out]
,则count
减 1(说明该字符的频率已超出合理范围);
- 向右移动窗口时,右侧新进入窗口的字符(
- 判断匹配:若
count
等于p.size()
,说明窗口内所有字符的频率均与p
一致(即窗口是p
的字母异位词),记录当前窗口的起始索引。
关键问题:为什么用 count
而不是直接对比两个频率数组?
直接对比 hash1
和 hash2
数组(每次滑动后遍历 26 个元素)也能判断匹配,但时间复杂度会增加 O(26)×(n-m+1)。而 count
的作用是“实时记录有效字符数”:当 count
等于 p.size()
时,意味着窗口内恰好有 p.size()
个字符,且每个字符的频率都未超过 p
中的频率(因只有满足 hash2[ch]≤hash1[ch]
时才会累计 count
),结合窗口长度与 p
相同,此时必然是字母异位词。用 count
可将判断环节的时间复杂度从 O(26) 降至 O(1)。
五、算法实现:固定窗口的滑动与频率更新
实现代码
class Solution {
public:vector<int> findAnagrams(string s, string p) {vector<int> ret; // 存储结果:所有满足条件的子串起始索引int m = p.size(), n = s.size();// 若s长度小于p,直接返回空(不可能有符合条件的子串)if (n < m) return ret;// hash1统计p中每个字符的频率(下标0-25对应a-z)int hash1[26] = {0};for (auto ch : p) hash1[ch - 'a']++;// hash2统计当前窗口中每个字符的频率int hash2[26] = {0};// left:窗口左边界;right:窗口右边界;count:窗口中"频率≤hash1对应频率"的字符总数for (int left = 0, right = 0, count = 0; right < n; right++) {// 右侧字符进入窗口char in = s[right];hash2[in - 'a']++;// 若进入后该字符频率仍≤p中的频率,说明是有效字符,count+1if (hash2[in - 'a'] <= hash1[in - 'a']) count++;// 当窗口长度超过p的长度时,左侧字符离开窗口if (right - left + 1 > m) {char out = s[left++]; // left右移前记录离开的字符// 若离开前该字符频率≤p中的频率,说明减少后有效字符数减少,count-1if (hash2[out - 'a'] <= hash1[out - 'a']) count--;hash2[out - 'a']--; // 更新频率(注意先判断再减,否则判断条件错误)}// 当count等于p的长度时,窗口是p的字母异位词,记录起始索引leftif (count == m) ret.push_back(left);}return ret;}
};
代码细节拆解:
-
频率数组初始化:
hash1
和hash2
均为大小 26 的数组(对应 26 个小写字母),初始化为 0。hash1
先遍历p
填充,记录p
中每个字符的出现次数。 -
窗口初始化与扩展:
右指针right
从 0 开始移动,每次将s[right]
加入窗口(hash2[in-'a']++
)。若加入后该字符的频率未超过p
中的频率(hash2[in-'a']≤hash1[in-'a']
),则count
加 1——count
累计的是“窗口中符合频率要求的字符总数”。 -
窗口收缩(固定长度):
当窗口长度(right-left+1
)超过p.size()
时,左指针left
右移,同时将s[left]
移出窗口。移出前需判断:若移出前该字符的频率≤p
中的频率,则count
减 1(因该字符不再属于窗口,有效字符数减少),之后再更新hash2[out-'a']--
。 -
匹配判断与结果记录:
当count
等于p.size()
时,说明窗口内所有字符的频率均与p
一致(且窗口长度恰好为p.size()
),因此当前窗口是p
的字母异位词,记录起始索引left
。
时间复杂度与空间复杂度
时间复杂度
该算法的时间复杂度为 O(m + n),其中 m 是字符串 p
的长度,n 是字符串 s
的长度。
具体来说:
- 预处理阶段,遍历
p
以统计字符频率,这一步的时间开销为 O(m); - 滑动窗口过程中,
right
指针从s
的起始位置移动到末尾,每个字符仅被访问一次,left
指针也仅单向移动,不会重复遍历,这部分的时间开销为 O(n); - 其余辅助操作(如更新频率数组、维护
count
变量等)均为常数级别的操作,时间开销可忽略。
当 m ≤ n 时,时间复杂度可简化为 O(n)。
空间复杂度
该算法的空间复杂度为 O(1)(常数级)。
具体来说:
- 用于统计字符频率的两个数组
hash1
和hash2
,大小均固定为 26(对应 26 个小写字母),与输入字符串s
和p
的长度无关; - 其他变量(如
left
、right
、count
等)均为单个整数,占用的空间为常数级; - 结果数组
ret
用于存储输出结果,属于必要的输出空间,通常不计入算法的额外空间复杂度。
因此,算法额外使用的空间为固定常数,空间复杂度为 O(1)。
六、实现过程中的坑点总结
-
窗口收缩时的顺序错误
- 错误:先执行
hash2[out-'a']--
,再判断是否需要减少count
。
例如:out
字符在hash2
中的频率为 2,hash1
中为 2。若先减为 1,再判断“1≤2”,会错误地认为需要减count
,但实际移出前频率是 2(符合条件),应该减count
——但顺序颠倒后逻辑就错了。 - 解决:先判断“移出前的频率是否≤hash1”,再更新
hash2
的频率。即先执行if (hash2[out-'a'] <= hash1[out-'a']) count--;
,再执行hash2[out-'a']--;
。
- 错误:先执行
-
忽略“s长度小于p”的边界情况
- 错误:未判断
n < m
直接开始滑动,可能导致窗口长度始终无法达到m
,或数组访问越界。 - 解决:开头加判断
if (n < m) return ret;
,直接返回空结果。
- 错误:未判断
-
count的含义理解偏差
- 错误:认为
count
是“窗口中与p
共有的字符种类数”,而非“符合频率的字符总数”。
例如:p
是 “aab”(hash1[a]=2, hash1[b]=1
),窗口是 “aab”(hash2[a]=2, hash2[b]=1
),此时count
应等于 3(2个a+1个b,共3个字符均符合频率),而非 2(字符种类数)。 - 解决:明确
count
统计的是“单个字符的数量”,而非“种类数”,只有当count
等于p.size()
时,才说明所有字符都匹配。
- 错误:认为
-
窗口长度判断错误
- 错误:用
right - left > m
判断窗口是否超长(正确应为right - left + 1 > m
)。
窗口长度是right - left + 1
(闭区间 [left, right] 的元素个数),例如 left=0,right=2 时,长度是 3,而非 2。 - 解决:严格用
right - left + 1 > m
作为窗口超长的判断条件。
- 错误:用
七、下题预告
明天将讲解 30. 串联所有单词的子串,这是滑动窗口在“多单词匹配”场景的进阶应用。
提前思考方向:
- 题目中“串联所有单词的子串”的本质是什么?如何将其转化为可通过滑动窗口解决的问题?
- 若单词长度固定,子串的长度是否固定?窗口大小如何设定?
- 如何统计窗口内的单词频率,与目标单词列表的频率进行匹配?(与本题的字符频率匹配有何异同?)
如果觉得这篇解析有帮助,不妨:
🌟 点个赞,让更多人看到这份清晰思路
⭐ 收个藏,下次复习时能快速找回灵感
👀 加个关注,明天见!