问题描述
给定两个字符串 s
和 p
,要求找到 s
中所有是 p
的**字母异位词(Anagram)**的子串的起始索引。
字母异位词是指由相同字母重新排列形成的字符串(包含相同的字母且每个字母出现的次数相同)。
例如:
- 输入:
s = "cbaebabacd"
,p = "abc"
输出:[0, 6]
解释:s
中从索引0
开始的"cba"
和索引6
开始的"bac"
均为"abc"
的异位词。
核心思路:滑动窗口 + 字符计数数组
1. 字符计数数组
- 核心思想:通过固定长度的数组(长度26,对应26个小写字母)记录字符串中每个字符的出现次数。
- 比较机制:若两个字符串的字符计数数组相同,则它们是字母异位词。
2. 滑动窗口
- 窗口初始化:在
s
中初始化一个长度为p.length()
的窗口。 - 窗口滑动:每次向右移动窗口,移除左边界字符的计数,增加右边界字符的计数。
- 实时比较:每次滑动后检查当前窗口的计数数组是否与
p
的计数数组一致。
算法步骤
- 边界处理:若
s
的长度小于p
或p
为空,直接返回空列表。 - 初始化计数数组:
pCount
:统计p
中每个字符的出现次数。sCount
:统计s
中初始窗口(前p.length()
个字符)的出现次数。
- 初始窗口检查:若初始窗口的计数与
p
一致,记录索引0
。 - 滑动窗口遍历:
- 窗口每次右移一位,更新左边界字符的计数(减少)和右边界字符的计数(增加)。
- 每次更新后检查计数数组是否匹配,若匹配则记录当前窗口的起始索引。
完整代码实现
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
public class Solution {
public List<Integer> findAnagrams(String s, String p) {
List<Integer> result = new ArrayList<>();
int m = p.length();
int n = s.length();
// 边界条件处理
if (m == 0 || n < m) {
return result;
}
int[] pCount = new int[26]; // 记录p的字符计数
int[] sCount = new int[26]; // 记录当前窗口的字符计数
// 初始化p和s的初始窗口的字符计数
for (int i = 0; i < m; i++) {
pCount[p.charAt(i) - 'a']++;
sCount[s.charAt(i) - 'a']++;
}
// 检查初始窗口是否匹配
if (Arrays.equals(pCount, sCount)) {
result.add(0);
}
// 滑动窗口:每次移动一位,更新sCount并检查
for (int i = 1; i <= n - m; i++) {
// 移除左边界字符的计数
int leftChar = s.charAt(i - 1);
sCount[leftChar - 'a']--;
// 添加右边界字符的计数
int rightChar = s.charAt(i + m - 1);
sCount[rightChar - 'a']++;
// 检查当前窗口是否匹配
if (Arrays.equals(pCount, sCount)) {
result.add(i);
}
}
return result;
}
}
复杂度分析
- 时间复杂度:
O(n)
,其中n
是字符串s
的长度。
滑动窗口遍历s
需要O(n)
,每次窗口操作(更新计数和比较)的时间为常数。 - 空间复杂度:
O(1)
,使用固定长度的两个长度为26的数组。
关键点总结
- 字符计数数组:利用数组索引映射字符,快速统计字符出现次数。
- 滑动窗口优化:避免每次重新计算整个子串的计数,通过动态更新窗口边界字符的计数保证高效性。
- 边界处理:注意字符串长度不足时的直接返回逻辑。
类似问题
- LeetCode 567. 字符串的排列:判断
s2
是否包含s1
的排列。 - LeetCode 76. 最小覆盖子串:寻找覆盖目标字符的最短子串。