Leetcode438. 找到字符串中所有字母异位词
Leetcode438. 找到字符串中所有字母异位词
- 题目详情
- 问题分析
- 核心思路:滑动窗口
- 方法一:固定窗口比较法
- 复杂度分析:
- 方法二:计数差分法(优化版)
- 优化原理
- 方法三:滑动窗口模板(通用解法)
- 关键点与技巧
- 性能对比
- 总结
题目详情
Leetcode438. 找到字符串中所有字母异位词
问题分析
字母异位词的关键特征是:
字符组成完全相同(字符种类及每个字符的出现次数相同)
字符顺序可以不同
因此,判断两个字符串是否为字母异位词,可以转化为判断它们字符频率分布是否相同。
核心思路:滑动窗口
由于我们要在字符串 s 中寻找与 p 长度相同的异位词子串,可以使用固定大小的滑动窗口来遍历 s,窗口大小即为 p 的长度。然后比较窗口内字符的频率分布与 p 的频率分布是否一致。
方法一:固定窗口比较法
最直接的思路是每次移动窗口后,都比较窗口内字符频率与 p 的字符频率。
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 变量来优化判断过程,避免每次都比较整个数组。
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 变量来跟踪已匹配的字符种类数。
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() 时直接返回空列表
性能对比

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