【Hot100|9-LeetCode 438. 找到字符串中所有字母异位词】
这段代码是解决 LeetCode 438. 找到字符串中所有字母异位词 问题的固定大小滑动窗口 + 数组计数解法。核心目标是在字符串s中找到所有是字符串p的字母异位词的子串,并返回这些子串的起始索引。字母异位词指字符种类和数量完全相同,但排列顺序不同的字符串。该解法时间复杂度为 O (m + n)(m 为p长度,n 为s长度),空间复杂度为 O (1)(因仅用两个固定大小的 26 位数组)。
注:代码存在一处小笔误(未定义变量n),需补充int n = s.length();才能正常运行,后续解析会修正该问题。
一、问题理解
问题要求
给定两个字符串s和p,找到s中所有p的字母异位词的起始索引。
- 示例:输入
s = "cbaebabacd",p = "abc",输出[0,6]。因为s中索引 0 开始的"cba"和索引 6 开始的"bac"都是"abc"的字母异位词。
核心解题逻辑
字母异位词的核心特征是字符种类和对应数量完全一致,因此可通过统计字符频率判断两个字符串是否为异位词。结合固定大小的滑动窗口,在s中滑动出与p长度相同的子串,逐一比对频率,高效找到目标子串。
二、核心思路
- 字符频率统计:用两个长度为 26 的数组(对应 26 个小写英文字母)
cntP和cntS,分别记录p的字符频率和s当前滑动窗口内的字符频率。 - 固定窗口滑动:窗口大小固定为
p的长度lenP。右指针遍历s时,不断将当前字符加入窗口并更新cntS;当窗口长度达到lenP后,每次右移都移除窗口最左侧的字符频率,保持窗口大小不变。 - 频率比对:当窗口长度等于
lenP时,对比cntS和cntP。若两者相等,当前窗口对应的子串是p的异位词,记录窗口起始索引。
三、代码逐行解析(含修正)
先补充笔误修正后的完整代码,再逐行讲解:
java
运行
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;class Solution {public List<Integer> findAnagrams(String s, String p) {List<Integer> ans = new ArrayList<>();// 初始化两个计数数组,统计26个小写字母的出现次数int[] cntP = new int[26];int[] cntS = new int[26];int lenP = p.length();int n = s.length(); // 补充笔误,定义s的长度// 步骤1:统计字符串p的字符频率for (char c : p.toCharArray()) {// 字符c - 'a' 映射为0-25的索引(如'a'→0,'b'→1)cntP[c - 'a']++;}// 步骤2:右指针遍历s,维护固定大小的滑动窗口for (int right = 0; right < n; right++) {// 当前字符加入窗口,更新s的窗口频率数组cntS[s.charAt(right) - 'a']++;// 计算当前窗口的左边界:窗口大小固定为lenP,左边界 = 右边界 - lenP + 1int left = right - lenP + 1;// 窗口未达到p的长度(左边界为负),跳过后续比对if (left < 0) {continue;}// 步骤3:窗口大小达标,比对两个频率数组if (Arrays.equals(cntS, cntP)) {// 频率一致,记录当前窗口起始索引leftans.add(left);}// 步骤4:窗口右移前,移除左边界字符的频率(避免影响下一个窗口)cntS[s.charAt(left) - 'a']--;}return ans;}
}
关键代码拆解
初始化与
p的频率统计cntP和cntS数组:因题目默认字符串为小写字母,用长度 26 的数组足够,通过c - 'a'将字符映射为 0 - 25 的索引,实现 O (1) 的频率更新。- 遍历
p的字符,填充cntP,完成对p的字符频率记录。
右指针遍历与窗口维护
right从 0 开始遍历s,每遍历一个字符就将其频率加入cntS,相当于窗口向右扩展。- 计算
left:left = right - lenP + 1,这是固定窗口的核心,确保窗口长度始终等于lenP。例如lenP=3,right=2时,left=0,窗口为[0,2],长度 3。
窗口有效性判断与结果记录
- 当
left < 0时,说明窗口还没达到p的长度(如right=1、lenP=3时,left=-1),无需比对频率,直接跳过。 Arrays.equals(cntS, cntP):比对两个频率数组,若相等则当前窗口是p的异位词,将left加入结果列表。
- 当
窗口右移的预处理比对完成后,将
cntS中left对应字符的频率减 1。这是因为下一轮right右移时,窗口会抛弃当前左边界的字符,确保下一个窗口的频率统计准确。
四、实例演示(直观理解过程)
以测试用例s = "cbaebabacd",p = "abc"为例(lenP=3,cntP中a:1、b:1、c:1,其余为 0):
| 步骤 | right | 遍历字符 | cntS 更新后 | left | 窗口内容 | 频率是否匹配 | 结果列表 | cntS 左边界减 1(操作) |
|---|---|---|---|---|---|---|---|---|
| 1 | 0 | 'c' | c:1 | -2 | 无(窗口不足) | 否 | [] | 无 |
| 2 | 1 | 'b' | c:1、b:1 | -1 | 无(窗口不足) | 否 | [] | 无 |
| 3 | 2 | 'a' | c:1、b:1、a:1 | 0 | [0,2]:"cba" | 是(匹配 cntP) | [0] | 减 'a'→a:0 |
| 4 | 3 | 'e' | c:1、b:1、e:1 | 1 | [1,3]:"bae" | 否 | [0] | 减 'b'→b:0 |
| 5 | 4 | 'b' | c:1、b:1、e:1 | 2 | [2,4]:"aeb" | 否 | [0] | 减 'a'→a:0 |
| 6 | 5 | 'a' | a:1、b:1、e:1 | 3 | [3,5]:"eba" | 否 | [0] | 减 'e'→e:0 |
| 7 | 6 | 'c' | a:1、b:1、c:1 | 4 | [4,6]:"bac" | 是 | [0,6] | 减 'b'→b:0 |
| ... | ... | ... | ... | ... | ... | ... | ... | ... |
最终结果列表为[0,6],与预期一致。
五、关键细节与复杂度分析
1. 关键细节
- 为什么用数组而非 HashMap:26 个小写字母的场景下,数组比 HashMap 更高效,无需哈希计算和处理哈希冲突,且
Arrays.equals比对数组的时间为 O (26),属于常数时间。 - 固定窗口的优势:相比可变滑动窗口,固定窗口无需复杂的左指针收缩逻辑,仅需在每次窗口达标后移除左边界字符频率,逻辑更简洁。
- 边界处理:
left < 0的判断避免了窗口未达标时的无效比对,确保代码鲁棒性。
2. 复杂度分析
- 时间复杂度:O (m + n)。遍历
p统计频率耗时 O (m);遍历s耗时 O (n),每次遍历中的频率更新和数组比对均为 O (1)(数组比对固定 26 次),整体时间与字符串长度线性相关。 - 空间复杂度:O (1)。两个计数数组均为固定长度 26,与输入字符串长度无关,无额外空间开销。
六、总结
该解法的核心是 **“固定滑动窗口 + 字符频率统计”**,精准利用字母异位词的频率特征,通过高效的数组计数和窗口维护,实现线性时间求解。这种思路不仅适用于本题,还可迁移到类似的字符串匹配问题(如判断子串是否包含目标字符集、统计符合字符频率要求的子串数量等),是处理字符串频率类问题的经典范式。
