前言
LeetCode 438 要求从字符串 s 中找出所有与字符串 p 互为字母异位词的子串,并返回这些子串的起始下标。
这道题可以使用固定长度滑动窗口解决:字母异位词的长度一定相同,因此只需遍历 s 中所有长度等于 p.length() 的子串。对于每个窗口,将字符排序后与排序后的 p 比较;如果两个字符数组完全相同,当前窗口就是 p 的字母异位词。
本文严格围绕"固定长度滑动窗口 + 排序比较"的解法展开。
一、题目说明
给定字符串 s 和 p,找到 s 中所有 p 的字母异位词子串,返回这些子串的起始索引。
字母异位词由相同字母重新排列形成。例如,abc、cba 和 bac 互为字母异位词;abc 和 abb 则不是,因为字符出现次数不同。
输入:s = "cbaebabacd", p = "abc"
输出:[0, 6]
从下标 0 开始的子串是 cba,从下标 6 开始的子串是 bac。二者排序后都是 abc,因此返回 [0, 6]。
二、核心思路
设 p 的长度为 m。如果一个子串是 p 的字母异位词,它的长度也必须是 m。因此,可以在 s 中维护一个长度固定为 m 的窗口:
-
将
p转成字符数组并提前排序; -
使用
left和right表示窗口左右边界; -
截取当前窗口,将其转成字符数组并排序;
-
使用
Arrays.equals()比较两个有序数组; -
如果相同,记录窗口起点
left; -
同时将
left和right向右移动一位。
窗口只改变位置,不改变长度。
三、为什么排序后可以判断异位词
两个字符串互为字母异位词,需要同时满足:长度相同,并且每种字符的出现次数相同。当两个字符串的字符及数量完全一致时,按照相同规则排序后,得到的字符序列必然一致。
abc 排序后:abc
cba 排序后:abc
bac 排序后:abc
abb 排序后:abb
因此,只需判断两个排序后的字符数组是否完全相等。
四、滑动窗口如何定义
代码使用两个指针定义窗口:
int left = 0, right = p.length() - 1;
区间 [left, right] 的长度是 right - left + 1。代入初始值可得:
p.length() - 1 - 0 + 1 = p.length()
所以初始窗口长度正好等于 p 的长度。每次检查结束后执行:
left++;
right++;
左右边界同时增加,窗口整体右移一位,长度保持不变。
五、示例演算
以 s = "cbaebabacd"、p = "abc" 为例,p 排序后仍为 abc。
left |
当前窗口 | 排序结果 | 是否匹配 | 操作 |
|---|---|---|---|---|
| 0 | cba |
abc |
是 | 记录 0 |
| 1 | bae |
abe |
否 | 不记录 |
| 2 | aeb |
abe |
否 | 不记录 |
| 3 | eba |
abe |
否 | 不记录 |
| 4 | bab |
abb |
否 | 不记录 |
| 5 | aba |
aab |
否 | 不记录 |
| 6 | bac |
abc |
是 | 记录 6 |
| 7 | acd |
acd |
否 | 不记录 |
最终得到 [0, 6]。
六、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> res = new ArrayList<>();
if (p.length() > s.length()) {
return res;
}
char[] chs = p.toCharArray();
Arrays.sort(chs);
// 定义滑动窗口,其长度与 p 的长度相同
int left = 0, right = p.length() - 1;
while (right < s.length()) {
// 截取当前滑动窗口
String win = s.substring(left, right + 1);
char[] winArr = win.toCharArray();
Arrays.sort(winArr);
// 判断当前窗口是否为 p 的异位词
if (Arrays.equals(chs, winArr)) {
res.add(left);
}
// 窗口整体向右滑动一位
left++;
right++;
}
return res;
}
}
七、代码逐段解析
1. 处理 p 比 s 长的情况
if (p.length() > s.length()) {
return res;
}
如果 p 比 s 还长,s 中不可能存在长度相等的候选子串,可以直接返回空集合。
2. 提前排序 p
char[] chs = p.toCharArray();
Arrays.sort(chs);
p 在遍历中不会变化,因此只需要排序一次,后续所有窗口都与这个数组比较。
3. 截取并排序当前窗口
String win = s.substring(left, right + 1);
char[] winArr = win.toCharArray();
Arrays.sort(winArr);
substring(beginIndex, endIndex) 采用左闭右开区间:包含 beginIndex,不包含 endIndex。由于 right 是需要包含的右边界,所以第二个参数必须写成 right + 1。
4. 判断并记录答案
if (Arrays.equals(chs, winArr)) {
res.add(left);
}
两个有序字符数组完全相同,说明当前窗口和 p 的字符组成一致,记录窗口起始位置即可。
5. 移动窗口
left++;
right++;
两个指针同时右移,保证窗口长度始终等于 p.length()。
八、复杂度分析
设 s 的长度为 n,p 的长度为 m。
对 p 排序需要 O(m log m)。字符串 s 中共有 n - m + 1 个候选窗口。对于每个窗口:
-
截取子串并转换为字符数组需要
O(m); -
对窗口排序需要
O(m log m); -
比较两个数组需要
O(m)。
总时间复杂度为:
O(m log m + (n - m + 1) × m log m)
通常简写为 O(n × m log m)。每次需要创建长度为 m 的子串和字符数组,所以额外空间复杂度为 O(m)。