Leetcode438. 找到字符串中所有字母异位词
题目详情
问题分析
字母异位词的关键特征是:
字符组成完全相同(字符种类及每个字符的出现次数相同)
字符顺序可以不同
因此,判断两个字符串是否为字母异位词,可以转化为判断它们字符频率分布是否相同。
核心思路:滑动窗口
由于我们要在字符串 s 中寻找与 p 长度相同的异位词子串,可以使用固定大小的滑动窗口来遍历 s,窗口大小即为 p 的长度。然后比较窗口内字符的频率分布与 p 的频率分布是否一致。
方法一:固定窗口比较法
最直接的思路是每次移动窗口后,都比较窗口内字符频率与 p 的字符频率。
java
public List<Integer> findAnagrams(String s, String p) {
List<Integer> result = new ArrayList<>();
if (s.length() < p.length()) return result;
int[] pCount = new int[26];
int[] sCount = new int[26];
// 统计p的字符频率,并初始化第一个窗口
for (int i = 0; i < p.length(); i++) {
pCount[p.charAt(i) - 'a']++;
sCount[s.charAt(i) - 'a']++;
}
// 检查第一个窗口
if (Arrays.equals(pCount, sCount)) {
result.add(0);
}
// 滑动窗口:每次向右移动一位
for (int i = 0; i < s.length() - p.length(); i++) {
// 移除左边界字符
sCount[s.charAt(i) - 'a']--;
// 添加右边界字符
sCount[s.charAt(i + p.length()) - 'a']++;
// 检查当前窗口
if (Arrays.equals(pCount, sCount)) {
result.add(i + 1);
}
}
return result;
}
复杂度分析:
时间复杂度:O(n × 26),其中 n 是字符串 s 的长度,每次比较数组需要 O(26) 时间
空间复杂度:O(26),使用固定大小的数组
方法二:计数差分法(优化版)
我们可以通过维护一个 count 变量来优化判断过程,避免每次都比较整个数组。
java
public List<Integer> findAnagrams(String s, String p) {
List<Integer> result = new ArrayList<>();
if (s.length() < p.length()) return result;
int[] count = new int[26];
// 初始化p的字符统计
for (char c : p.toCharArray()) {
count[c - 'a']++;
}
int left = 0, right = 0;
int needToMatch = p.length();
while (right < s.length()) {
// 右指针字符进入窗口
char rightChar = s.charAt(right);
if (count[rightChar - 'a'] > 0) {
needToMatch--;
}
count[rightChar - 'a']--;
right++;
// 当窗口大小等于p长度时,检查结果
if (right - left == p.length()) {
if (needToMatch == 0) {
result.add(left);
}
// 左指针字符移出窗口
char leftChar = s.charAt(left);
if (count[leftChar - 'a'] >= 0) {
needToMatch++;
}
count[leftChar - 'a']++;
left++;
}
}
return result;
}
优化原理
count 数组记录当前窗口中字符与 p 中字符的差值
needToMatch 跟踪还需匹配的字符总数
当 needToMatch == 0 时,说明窗口内字符与 p 完全匹配
方法三:滑动窗口模板(通用解法)
这是一种更通用的滑动窗口解法,使用 valid 变量来跟踪已匹配的字符种类数。
java
public List<Integer> findAnagrams(String s, String p) {
List<Integer> result = new ArrayList<>();
Map<Character, Integer> need = new HashMap<>();
Map<Character, Integer> window = new HashMap<>();
// 初始化need映射
for (char c : p.toCharArray()) {
need.put(c, need.getOrDefault(c, 0) + 1);
}
int left = 0, right = 0;
int valid = 0;
while (right < s.length()) {
// 扩大右边界
char rightChar = s.charAt(right);
right++;
if (need.containsKey(rightChar)) {
window.put(rightChar, window.getOrDefault(rightChar, 0) + 1);
if (window.get(rightChar).equals(need.get(rightChar))) {
valid++;
}
}
// 当窗口大小达到p长度时,收缩左边界
while (right - left >= p.length()) {
// 检查是否找到异位词
if (valid == need.size()) {
result.add(left);
}
char leftChar = s.charAt(left);
left++;
if (need.containsKey(leftChar)) {
if (window.get(leftChar).equals(need.get(leftChar))) {
valid--;
}
window.put(leftChar, window.get(leftChar) - 1);
}
}
}
return result;
}
关键点与技巧
窗口大小固定:窗口大小始终为 p.length(),每次移动只需考虑进出的两个字符
字符频率统计:使用长度为26的数组统计小写字母频率,比HashMap更高效
优化判断:通过 count 或 valid 变量避免全数组比较,提升效率
边界处理:当 s.length() < p.length() 时直接返回空列表
性能对比

总结
滑动窗口是解决子串搜索问题的利器。对于字母异位词问题,关键是抓住字符频率一致性这一特征,通过维护窗口内字符频率来高效判断。
对于此题,推荐使用计数差分法,它在实现复杂度和性能之间取得了良好平衡。理解并掌握这种滑动窗口的优化技巧,对解决类似的字符串搜索问题大有裨益。