1. 题目描述
中等
给定两个字符串 s
和 p
,找到 s
中所有 p
的 异位词 的子串,返回这些子串的起始索引。不考虑答案输出的顺序。
示例 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 * 104
s
和p
仅包含小写字母
2. 题解
class Solution {
public List<Integer> findAnagrams(String s, String p) {
List<Integer> ans = new ArrayList<>();
int [] cntP = new int[26];
int [] cntS = new int[26];
for (char c : p.toCharArray()) {
cntP[c - 'a'] ++;
}
for (int right = 0; right < s.length(); right++) {
cntS[s.charAt(right) - 'a']++;
int left = right - p.length() + 1;
if (left < 0) {
continue;
}
if (Arrays.equals(cntS, cntP)) {
ans.add(left);
}
cntS[s.charAt(left) - 'a']--;
}
return ans;
}
}
3. 思路以及过程详解
这段代码使用滑动窗口 + 字符频率数组来高效地在字符串 s
中查找所有 p
的异位词。核心思路是:通过维护一个固定大小的滑动窗口,并比较窗口内字符频率与 p
的字符频率是否相同,来判断窗口内的子串是否是 p
的异位词。
a. 核心原理:字符频率数组与滑动窗口
- 字符频率数组:
用两个长度为 26 的数组cntP
和cntS
分别记录p
和当前滑动窗口内的字符频率(如cntP[0]
表示'a'
的出现次数)。 - 滑动窗口:
窗口大小固定为p.length()
,每次右移一位。窗口右边界right
从 0 开始递增,左边界left = right - p.length() + 1
,确保窗口长度始终等于p
的长度。
b. 代码详细解析
ⅰ. 1. 初始化频率数组
java
运行
int[] cntP = new int[26];
int[] cntS = new int[26];
// 统计p的字符频率
for (char c : p.toCharArray()) {
cntP[c - 'a']++;
}
cntP
记录p
中每个字符的出现次数(如p="abc"
,则cntP = [1, 1, 1, 0, ...]
)。
ⅱ. 2. 滑动窗口遍历 s
java
运行
for (int right = 0; right < s.length(); right++) {
// 1. 右边界元素加入窗口
cntS[s.charAt(right) - 'a']++;
// 2. 计算左边界位置
int left = right - p.length() + 1;
// 3. 左边界合法性检查(窗口长度不能超过p的长度)
if (left < 0) {
continue;
}
// 4. 比较窗口与p的字符频率
if (Arrays.equals(cntS, cntP)) {
ans.add(left); // 频率相同,记录左边界
}
// 5. 窗口左边界右移前,移除左边界元素的频率
cntS[s.charAt(left) - 'a']--;
}
- 关键步骤:
-
- 扩大窗口:将右边界元素的频率加入
cntS
。 - 计算左边界:确保窗口长度固定为
p.length()
。 - 边界检查:若窗口长度不足
p.length()
,跳过后续逻辑。 - 频率比较:若
cntS
与cntP
完全相同,说明窗口内子串是p
的异位词。 - 收缩窗口:移除左边界元素的频率,为下一次窗口右移做准备。
- 扩大窗口:将右边界元素的频率加入
c. 示例演示(以 s="cbaebabacd", p="abc"
为例)
ⅰ. 1. 初始化 cntP
p="abc"
→cntP = [1, 1, 1, 0, ..., 0]
(即a=1, b=1, c=1
)。
ⅱ. 2. 滑动窗口遍历
- right=0:
-
- 窗口元素:
s[0]='c'
→cntS = [0, 0, 1, 0, ...]
。 - 左边界
left = 0 - 3 + 1 = -2
(跳过,窗口长度不足)。
- 窗口元素:
- right=1:
-
- 窗口元素:
s[1]='b'
→cntS = [0, 1, 1, 0, ...]
。 - 左边界
left = 1 - 3 + 1 = -1
(跳过)。
- 窗口元素:
- right=2:
-
- 窗口元素:
s[2]='a'
→cntS = [1, 1, 1, 0, ...]
。 - 左边界
left = 2 - 3 + 1 = 0
(窗口长度 = 3)。 - 比较
cntS
与cntP
→ 完全相同 → 记录left=0
。 - 移除左边界元素
s[0]='c'
→cntS = [1, 1, 0, 0, ...]
。
- 窗口元素:
- right=3:
-
- 窗口元素:
s[3]='e'
→cntS = [1, 1, 0, 0, 1, 0, ...]
。 - 左边界
left = 3 - 3 + 1 = 1
。 - 比较
cntS
与cntP
→ 不同 → 不记录。 - 移除左边界元素
s[1]='b'
→cntS = [1, 0, 0, 0, 1, 0, ...]
。
- 窗口元素:
- ... 后续步骤类似...
- right=6:
-
- 窗口元素:
s[6]='b'
→cntS = [1, 1, 1, 0, ...]
。 - 左边界
left = 6 - 3 + 1 = 4
。 - 比较
cntS
与cntP
→ 相同 → 记录left=6
。
- 窗口元素:
ⅲ. 3. 最终结果
- 符合条件的起始索引:
[0, 6]
(对应子串"cba"
和"bac"
)。
d. 复杂度分析
- 时间复杂度:O (n),其中 n 是
s
的长度。每个字符仅被处理一次。 - 空间复杂度:O (1),因为频率数组固定为 26 个元素(与输入长度无关)。
e. 总结
该算法通过维护固定大小的滑动窗口和字符频率数组,高效地在 s
中查找 p
的异位词。关键在于窗口滑动时,动态更新频率数组并比较,确保每次比较的时间复杂度为 O (1)。
4. 关键详解
这里是以右边指针为主,如果右边没有匹配上,则左边一定不匹配。
相当于是【一个字母一个字母地】去匹配,而不是每次都是【整个字符串】进行匹配。
因此如果右边每次都匹配上了,则直接判断长度就可以。
所以while的意思是当右边匹配不上了,就当前匹配上的全部废弃,左右两个指针都移到【当前窗口的右边一个位置】