文章目录
一、题目描述
给定两个字符串 s 和 p,请你在 s 中找出所有 p 的字母异位词的起始索引。
字母异位词指包含相同字母、但排列顺序不同的字符串。
示例:
输入: s = "cbaebabacd", p = "abc"
输出: [0,6]
解释:
起始索引为 0 的子串是 "cba",是 "abc" 的字母异位词。
起始索引为 6 的子串是 "bac",是 "abc" 的字母异位词。
二、问题分析
本题的核心是判断字母组成是否一致。
即:
给定字符串 s,判断 s 的所有长度为 p.length() 的子串中,哪些与 p 含有相同字符频次。
关键在于:
- 判断两个字符串是否为异位词 → 比较 26 个英文字母频次是否相同。
- 遍历
s时,为了避免重复计算,可使用"滑动窗口"。
三、解法一:暴力法
思路
- 遍历
s的每一个长度为len(p)的子串; - 统计该子串中字符频率;
- 与
p的字符频率比较; - 若相同,则记录其起点索引。
流程图
是
否
开始
计算模式串 p 的字符频率
遍历 s 中每个起始位置 i
截取子串 sub = s[i..i+len(p)-1]
统计 sub 的字符频率
freq[sub] == freq[p]?
记录索引 i
继续下一轮
结束遍历
返回结果列表
时间复杂度
- 统计每个窗口频率 O(n × 26)
- 总体时间复杂度 O(n × m),m = |p|
- 空间复杂度 O(26)
暴力法在大字符串下效率较低。
四、解法二:滑动窗口法(推荐解法)
核心思路
使用 长度固定的滑动窗口 统计当前窗口中的字符频次。
只在窗口边界上进行更新操作,避免重复计算。
- 用两个数组统计字符出现次数:
need[26]:p 中每个字母出现次数window[26]:当前窗口中字母出现次数
- 用两个指针维护一个长度固定的窗口
[left, right) - 每当窗口长度等于
p.length()时,比较两数组是否相等;若相等则记录起点索引。 - 向右滑动窗口时,删除左侧字符,添加右侧字符,更新频次数组。
动态示意图
假设:
s = "cbaebabacd"
p = "abc"
| 步骤 | 窗口位置 | 窗口子串 | 是否为异位词 | 记录 |
|---|---|---|---|---|
| 1 | [0,2] | cba | ✅ | [0] |
| 2 | [1,3] | bae | ❌ | / |
| 3 | [2,4] | aeb | ❌ | / |
| 4 | [3,5] | eba | ❌ | / |
| 5 | [4,6] | bab | ❌ | / |
| 6 | [5,7] | aba | ❌ | / |
| 7 | [6,8] | bac | ✅ | [0,6] |
代码逻辑流程
是
否
是
相同
不同
开始
初始化 need[26], window[26]
计算 p 中的字母频率 need
left=0, right=0
遍历 s
将 s[right] 加入 window
right++
窗口大小 > len(p)?
移除 s[left] , left++
继续
窗口大小 == len(p)?
比较 window 与 need
记录 left 索引
继续
继续向右移动窗口
遍历结束
返回结果
五、代码实现(Java)
java
import java.util.*;
class Solution {
public List<Integer> findAnagrams(String s, String p) {
List<Integer> res = new ArrayList<>();
if (s.length() < p.length()) return res;
int[] need = new int[26];
int[] window = new int[26];
for (char c : p.toCharArray()) {
need[c - 'a']++;
}
int left = 0, right = 0;
while (right < s.length()) {
window[s.charAt(right) - 'a']++;
right++;
// 当窗口大小超过 p 的长度时,移除左侧字符
if (right - left > p.length()) {
window[s.charAt(left) - 'a']--;
left++;
}
// 当窗口大小刚好等于 p 时,比较频率表
if (right - left == p.length()) {
if (Arrays.equals(need, window)) {
res.add(left);
}
}
}
return res;
}
}
六、复杂度分析
| 项目 | 时间复杂度 | 空间复杂度 | 说明 |
|---|---|---|---|
| 暴力法 | O(n × m) | O(26) | 每次重新统计窗口频率 |
| 滑动窗口法 | O(n) | O(26) | 仅更新边界字符频率 |
七、优化思路
优化一:字符数量匹配计数器
- 维护一个变量
matchCount表示当前有多少字符频率相等。 - 当
matchCount == 26时,表示两个字符串异位词匹配。 - 减少了数组比较的 O(26) 操作。
优化二:提前跳过无效字符
- 若 s 仅包含 a-z,则无;若字符集更大,可跳过 p 中未出现的字符区域。
八、总结对比
| 解法 | 思路 | 时间复杂度 | 是否推荐 |
|---|---|---|---|
| 暴力法 | 枚举所有长度 m 的子串逐一判断 | O(n × m) | ❌ |
| 滑动窗口 | 维护频率数组,窗口移动更新 | O(n) | ✅ 推荐 |
| 滑动窗口 + 计数优化 | 增加 matchCount 降低比较代价 | O(n) | ✅✅ |
总结
本题的关键思想是:
"固定窗口大小 + 动态维护词频 + 线性滑动"
这种滑动窗口模板不仅能解决异位词匹配问题,也广泛应用于:
- 最长无重复子串;
- 固定长度子串统计;
- 子数组和问题。