📌 题目链接:(leetcode.cn/problems/fi…)
🔍 难度:中等 | 🏷️ 标签:字符串、哈希表、滑动窗口、双指针
⏱️ 目标时间复杂度:O(n)
💾 空间复杂度:O(1)(常数空间,因为字符集固定为26个小写字母)
🧠 题目分析
✅ 给定两个字符串
s和p,要求找出s中所有p的异位词(anagram)的起始索引。🔤 什么是异位词?
指两个字符串由相同的字母组成,但顺序可以不同。例如"abc"和"bca"是异位词。
🎯 关键点:
- 异位词长度必须相等。
- 字符频次相同即可,顺序无关。
- 要求返回的是所有满足条件的子串的起始位置。
❗ 注意:
s可能比p短,此时直接返回空。- 不考虑输出顺序。
- 全部是小写英文字母 → 可用数组代替哈希表优化空间和速度。
🔍 核心算法及代码讲解
✅ 核心思想:滑动窗口 + 字符频次统计
我们使用一个长度为 len(p) 的滑动窗口在 s 上移动,每次判断当前窗口内的字符频次是否与 p 完全一致。
💡 为什么用滑动窗口? 因为我们要找的是“长度固定”的子串(等于
p的长度),且只关心字符出现次数,不关心顺序。这正是滑动窗口的经典应用场景!
🛠️ 数据结构选择:
- 使用
vector<int> count(26)来记录每个字母的频次差(或绝对值)。- 由于只有小写字母,所以可以用下标
c - 'a'映射到数组索引。
🧪 方法一:基础滑动窗口(推荐初学者掌握)
class Solution {
public:
vector<int> findAnagrams(string s, string p) {
int sLen = s.size(), pLen = p.size();
if (sLen < pLen) {
return vector<int>(); // s太短,不可能有异位词
}
vector<int> ans;
vector<int> sCount(26); // 当前窗口中各字符的数量
vector<int> pCount(26); // p中各字符的数量
// 初始化:先处理第一个窗口 [0, pLen-1]
for (int i = 0; i < pLen; ++i) {
++sCount[s[i] - 'a']; // 加入窗口中的字符
++pCount[p[i] - 'a']; // 记录p中字符数量
}
// 第一个窗口是否匹配?
if (sCount == pCount) {
ans.emplace_back(0);
}
// 滑动窗口:从 i=0 开始,每次右移一位
for (int i = 0; i < sLen - pLen; ++i) {
// 移除左边界的字符(即将离开窗口)
--sCount[s[i] - 'a'];
// 添加右边界的字符(进入窗口)
++sCount[s[i + pLen] - 'a'];
// 判断当前窗口是否与p的字符分布一致
if (sCount == pCount) {
ans.emplace_back(i + 1); // 新窗口起点
}
}
return ans;
}
};
✅ 逐行解析:
vector<int> sCount(26), pCount(26);
- 两个大小为26的数组,分别存储
s的当前窗口和p的字符频次。- 下标
0~25对应'a' ~ 'z'。for (int i = 0; i < pLen; ++i) { ++sCount[s[i] - 'a']; ++pCount[p[i] - 'a']; }
- 初始化第一个窗口
[0, pLen-1],同时统计p的字符频次。if (sCount == pCount)
- C++ 中
vector<int>支持==操作符,逐元素比较,非常高效。--sCount[s[i] - 'a']; ++sCount[s[i + pLen] - 'a'];
- 左边界
s[i]出窗,右边界s[i+pLen]入窗,更新频次。if (sCount == pCount)
- 每次滑动后检查是否匹配。
🚀 方法二:优化版滑动窗口(面试加分项!)
💡 优化思路:不再每次都做
sCount == pCount的完整比较(O(26)),而是维护一个变量differ表示有多少个字符的频次不一致。
class Solution {
public:
vector<int> findAnagrams(string s, string p) {
int sLen = s.size(), pLen = p.size();
if (sLen < pLen) {
return vector<int>();
}
vector<int> ans;
vector<int> count(26); // count[i] = s中字符 - p中字符 的频次差
int differ = 0; // 当前有多少个字符的频次不一致
// 初始化:计算初始窗口的频次差
for (int i = 0; i < pLen; ++i) {
++count[s[i] - 'a'];
--count[p[i] - 'a'];
}
// 统计有多少个字符频次不一致
for (int j = 0; j < 26; ++j) {
if (count[j] != 0) {
++differ;
}
}
// 初始窗口是否匹配?
if (differ == 0) {
ans.emplace_back(0);
}
// 滑动窗口
for (int i = 0; i < sLen - pLen; ++i) {
// 处理左边界字符 s[i] 离开窗口
if (count[s[i] - 'a'] == 1) { // 从"不同"变为"相同"
--differ;
} else if (count[s[i] - 'a'] == 0) { // 从"相同"变为"不同"
++differ;
}
--count[s[i] - 'a'];
// 处理右边界字符 s[i + pLen] 进入窗口
if (count[s[i + pLen] - 'a'] == -1) { // 从"不同"变为"相同"
--differ;
} else if (count[s[i + pLen] - 'a'] == 0) { // 从"相同"变为"不同"
++differ;
}
++count[s[i + pLen] - 'a'];
// 如果没有字符频次不一致,则是异位词
if (differ == 0) {
ans.emplace_back(i + 1);
}
}
return ans;
}
};
✅ 关键逻辑详解:
count[c] > 0:表示s中该字符多于p
count[c] < 0:表示s中该字符少于p
count[c] == 0:刚好相等当某个字符的频次变化导致其从非零变为零时,
differ--从零变为非零时,
differ++🧠 例子: 假设
count['a'] = 1(s中多一个'a'),现在把一个'a'移出窗口:
count['a']变成 0 → 从“不同”变“相同” →differ--同理,加入一个'b',原来
count['b'] = -1,现在变成 0 →differ--
✅ 优点:
- 时间复杂度更优:O(n + m),避免了每次 O(Σ) 的比较。
- 面试中展示你对“状态维护”的理解,体现工程思维!
🧩 解题思路(分步拆解)
-
预处理边界情况
- 若
s.length() < p.length(),直接返回空。
- 若
-
初始化频次数组
- 创建两个长度为26的数组:
sCount,pCount或单个count数组(带差值)。
- 创建两个长度为26的数组:
-
初始化第一个窗口
- 将
s[0:pLen]和p的字符频次统计出来。
- 将
-
判断第一个窗口是否匹配
- 若频次完全一致,记录索引
0。
- 若频次完全一致,记录索引
-
滑动窗口
- 循环遍历
i = 0到sLen - pLen - 1:- 移除
s[i](左边界) - 添加
s[i + pLen](右边界) - 更新频次并判断是否匹配
- 移除
- 循环遍历
-
收集结果
- 匹配则将
i+1加入答案列表。
- 匹配则将
📊 算法分析
| 项目 | 方法一(基础) | 方法二(优化) |
|---|---|---|
| 时间复杂度 | O(m + (n-m) × Σ) | O(n + m + Σ) |
| 空间复杂度 | O(Σ) | O(Σ) |
| Σ(字符集大小) | 26(小写字母) | 26 |
| 是否适合面试 | ✅ 适合入门 | ✅✅ 推荐进阶 |
📌 解释:
m = p.length(),n = s.length()- 方法一每次比较
sCount == pCount花费 O(26)- 方法二通过
differ变量将判断降到 O(1)
💡 面试提示:
- 主考官可能先让你写方法一,再问:“能否优化?”
- 此时你可以提出方法二,并解释
differ的设计思想。- 展现你对“减少重复比较”的敏感度。
🧪 代码(完整可运行)
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
// 方法一:基础滑动窗口(清晰易懂)
class Solution {
public:
vector<int> findAnagrams(string s, string p) {
int sLen = s.size(), pLen = p.size();
if (sLen < pLen) {
return vector<int>();
}
vector<int> ans;
vector<int> sCount(26);
vector<int> pCount(26);
// 初始化窗口和p的频次
for (int i = 0; i < pLen; ++i) {
++sCount[s[i] - 'a'];
++pCount[p[i] - 'a'];
}
if (sCount == pCount) {
ans.emplace_back(0);
}
// 滑动窗口
for (int i = 0; i < sLen - pLen; ++i) {
--sCount[s[i] - 'a'];
++sCount[s[i + pLen] - 'a'];
if (sCount == pCount) {
ans.emplace_back(i + 1);
}
}
return ans;
}
};
// 测试
signed main(){
ios::sync_with_stdio(false);
cin.tie(nullptr);
cout.tie(nullptr);
Solution sol;
// 测试用例1
string s1 = "cbaebabacd", p1 = "abc";
auto result1 = sol.findAnagrams(s1, p1);
cout << "Test 1: ";
for (auto x : result1) cout << x << " "; // 输出: 0 6
cout << endl;
// 测试用例2
string s2 = "abab", p2 = "ab";
auto result2 = sol.findAnagrams(s2, p2);
cout << "Test 2: ";
for (auto x : result2) cout << x << " "; // 输出: 0 1 2
cout << endl;
return 0;
}
✅ 测试结果:
Test 1: 0 6 Test 2: 0 1 2
✅ 验证正确性:
"cba"→"abc"的异位词 ✔️"bac"→"abc"的异位词 ✔️"ab","ba","ab"都是"ab"的异位词 ✔️
🌟 本期完结,下期见!🔥
👉 点赞收藏加关注,新文更新不迷路。关注专栏【算法】LeetCode Hot100刷题日记,持续为你拆解每一道热题的底层逻辑与面试技巧!
💬 欢迎留言交流你的解法或疑问!一起进步,冲向 Offer!💪
📣 下一期预告:LeetCode 热题 100 第44题 —— 和为 K 的子数组(中等)
🔹 题目:给定一个整数数组
nums和整数k,返回数组中和为k的连续子数组的个数。🔹 核心思路:使用前缀和 + 哈希表,记录每个前缀和出现的次数,从而快速判断是否存在和为
k的子数组。🔹 考点:前缀和、哈希表、动态规划思想、面试高频率题。
🔹 难度:中等,但属于“经典模板题”,建议背熟!
💡 提示:不要暴力枚举所有子数组(O(n²)),要用哈希表优化到 O(n)!
📌 记住:当你在刷题时,不要只看答案,要像写这篇文章一样,深入思考每一步背后的原理、优化空间和面试价值。这才是真正提升算法能力的方式!
✅ 本篇总结:
- 学会了如何用滑动窗口解决“固定长度子串匹配”问题。
- 掌握了字符频次统计的两种实现方式。
- 理解了如何通过状态维护(如
differ)来优化比较操作。- 面试中可灵活选择方法一(清晰)或方法二(高效)。
📌 建议练习:
- 567. 字符串的排列(类似题)
- 76. 最小覆盖子串(扩展:变长滑动窗口)
🚀 坚持每天一题,算法不再难!
下期见!👋