LeetCode 438. 找到字符串中所有字母异位词 - 滑动窗口解法详解
一、问题理解
问题描述
给定两个字符串 s 和 p,找到 s 中所有 p 的字母异位词的起始索引,并返回这些索引的列表。
字母异位词定义:由相同字母重排列形成的字符串,包括相同的字符串。
示例:
text
输入: s = "cbaebabacd", p = "abc" 输出: [0,6] 解释: 起始索引 0 的子串 "cba" 是 "abc" 的字母异位词。 起始索引 6 的子串 "bac" 是 "abc" 的字母异位词。
二、核心思路:固定窗口滑动 + 字符频率统计
暴力解法的局限性
枚举 s 中所有长度为 len(p) 的子串,逐个检查是否为 p 的字母异位词,时间复杂度为 O(n×m),其中 n 是 s 的长度,m 是 p 的长度。
滑动窗口优化思路
-
字符频率统计:用两个长度为 26 的数组(对应 26 个小写字母)分别记录
p的字符频率和当前窗口的字符频率 -
固定窗口滑动:窗口大小固定为
p的长度,在s上滑动 -
高效比对:通过比较两个频率数组判断当前窗口是否是
p的字母异位词
三、代码逐行解析
Java 解法(修正版)
java
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
class Solution {
public List<Integer> findAnagrams(String s, String p) {
// 1. 初始化结果列表
List<Integer> ans = new ArrayList<>();
// 2. 边界处理:如果s的长度小于p的长度,不可能有异位词
if (s.length() < p.length()) {
return ans;
}
// 3. 初始化两个计数数组(26个小写字母)
int[] cntP = new int[26];
int[] cntS = new int[26];
int lenP = p.length();
int n = s.length();
// 4. 统计p的字符频率
for (int i = 0; i < lenP; i++) {
cntP[p.charAt(i) - 'a']++;
}
// 5. 滑动窗口遍历s
for (int right = 0; right < n; right++) {
// 5.1 将当前字符加入窗口(更新cntS)
cntS[s.charAt(right) - 'a']++;
// 5.2 计算当前窗口的左边界
int left = right - lenP + 1;
// 5.3 如果左边界小于0,说明窗口还没达到p的长度,继续扩展
if (left < 0) {
continue;
}
// 5.4 窗口大小达到p的长度,比较频率数组
if (Arrays.equals(cntS, cntP)) {
ans.add(left);
}
// 5.5 移除左边界字符的频率(为下一次窗口移动做准备)
cntS[s.charAt(left) - 'a']--;
}
// 6. 返回结果
return ans;
}
}
Python 解法
python
from typing import List
from collections import Counter
class Solution:
def findAnagrams(self, s: str, p: str) -> List[int]:
# 1. 边界处理
if len(s) < len(p):
return []
# 2. 初始化结果和计数
result = []
len_p = len(p)
# 3. 统计p的字符频率
p_counter = [0] * 26
for ch in p:
p_counter[ord(ch) - ord('a')] += 1
# 4. 初始化窗口计数器
window_counter = [0] * 26
# 5. 滑动窗口遍历s
for right in range(len(s)):
# 5.1 将当前字符加入窗口
window_counter[ord(s[right]) - ord('a')] += 1
# 5.2 计算左边界
left = right - len_p + 1
# 5.3 如果窗口还没达到p的长度,继续扩展
if left < 0:
continue
# 5.4 窗口大小达到p的长度,比较频率数组
if window_counter == p_counter:
result.append(left)
# 5.5 移除左边界字符的频率
window_counter[ord(s[left]) - ord('a')] -= 1
return result
Python 解法(使用Counter)
python
from typing import List
from collections import Counter
class Solution:
def findAnagrams(self, s: str, p: str) -> List[int]:
result = []
len_p = len(p)
len_s = len(s)
# 边界处理
if len_s < len_p:
return result
# 统计p的字符频率
p_count = Counter(p)
# 初始化窗口计数器
window_count = Counter()
# 滑动窗口遍历s
for right in range(len_s):
# 将当前字符加入窗口
window_count[s[right]] += 1
# 计算左边界
left = right - len_p + 1
# 如果窗口还没达到p的长度,继续扩展
if left < 0:
continue
# 窗口大小达到p的长度,比较频率
if window_count == p_count:
result.append(left)
# 移除左边界字符
window_count[s[left]] -= 1
if window_count[s[left]] == 0:
del window_count[s[left]]
return result
四、Java 与 Python 语法对比
1. 数组/列表操作
| 操作 | Java | Python |
|---|---|---|
| 创建固定大小数组 | int[] arr = new int[26]; | arr = [0] * 26 |
| 字符转索引 | c - 'a' | ord(c) - ord('a') |
| 数组比较 | Arrays.equals(arr1, arr2) | arr1 == arr2 |
2. 字符串处理
| 操作 | Java | Python |
|---|---|---|
| 字符串长度 | s.length() | len(s) |
| 获取字符 | s.charAt(index) | s[index] |
| 字符串转字符数组 | s.toCharArray() | list(s) |
3. 集合/计数器操作
| 操作 | Java | Python |
|---|---|---|
| 计数器 | 需要手动实现数组 | collections.Counter |
| 添加元素 | map.put(key, value) | counter[key] = value |
| 删除元素 | map.remove(key) | del counter[key] |
五、实例演示
以测试用例 s = "cbaebabacd", p = "abc" 为例,演示滑动窗口过程:
初始状态
-
lenP = 3,cntP = [1, 1, 1, 0, 0, ...](a:1, b:1, c:1) -
ans = []
步骤演示
| 步骤 | right | s[right] | cntS更新后 | left | 窗口内容 | 频率匹配? | 结果 | 移除字符 |
|---|---|---|---|---|---|---|---|---|
| 1 | 0 | 'c' | c:1 | -2 | 窗口不足 | 否 | [] | 无 |
| 2 | 1 | 'b' | b:1, c:1 | -1 | 窗口不足 | 否 | [] | 无 |
| 3 | 2 | 'a' | a:1, b:1, c:1 | 0 | "cba" | 是 | [0] | 移除s[0]='c' |
| 4 | 3 | 'e' | a:1, b:1, e:1 | 1 | "bae" | 否 | [0] | 移除s[1]='b' |
| 5 | 4 | 'b' | a:1, b:1, e:1 | 2 | "aeb" | 否 | [0] | 移除s[2]='a' |
| 6 | 5 | 'a' | a:1, b:1, e:1 | 3 | "eba" | 否 | [0] | 移除s[3]='e' |
| 7 | 6 | 'c' | a:1, b:1, c:1 | 4 | "bac" | 是 | [0,6] | 移除s[4]='b' |
| 8 | 7 | 'a' | a:1, c:1, a:1 | 5 | "aca" | 否 | [0,6] | 移除s[5]='a' |
| 9 | 8 | 'c' | a:1, c:1, c:1 | 6 | "cac" | 否 | [0,6] | 移除s[6]='c' |
| 10 | 9 | 'd' | a:1, c:1, d:1 | 7 | "acd" | 否 | [0,6] | 移除s[7]='a' |
最终结果:[0, 6]
六、关键细节解析
1. 为什么使用固定大小为26的数组?
-
题目假设字符串只包含小写英文字母,共26个
-
数组索引直接对应字符,访问时间复杂度O(1)
-
比使用HashMap更高效,不需要哈希计算
2. 滑动窗口的维护
-
窗口大小固定:始终为
len(p) -
扩展窗口:右指针
right向右移动,将新字符加入窗口 -
收缩窗口:当窗口达到固定大小时,左指针
left也需要向右移动(通过移除最左边的字符)
3. 边界条件处理
-
如果
s.length() < p.length(),直接返回空列表 -
当
left < 0时,窗口还未达到固定大小,跳过比较
4. 频率数组比较
-
使用
Arrays.equals(cntS, cntP)比较两个频率数组 -
时间复杂度为O(26),属于常数时间
七、复杂度分析
时间复杂度:O(n + m)
-
统计
p的频率:O(m),其中 m 是p的长度 -
滑动窗口遍历
s:O(n),其中 n 是s的长度 -
每次循环中的操作(更新计数、比较数组)都是 O(1)
空间复杂度:O(1)
-
使用两个固定大小的数组(各26个元素)
-
与输入字符串长度无关
八、优化技巧与变体
1. 使用变量记录匹配的字符数
java
class Solution {
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];
for (char c : p.toCharArray()) {
count[c - 'a']++;
}
int left = 0, right = 0;
int matchCount = 0;
while (right < s.length()) {
// 右指针扩展窗口
char rightChar = s.charAt(right);
if (count[rightChar - 'a'] > 0) {
matchCount++;
}
count[rightChar - 'a']--;
right++;
// 当窗口大小等于p的长度时
if (right - left == p.length()) {
if (matchCount == p.length()) {
result.add(left);
}
// 左指针收缩窗口
char leftChar = s.charAt(left);
if (count[leftChar - 'a'] >= 0) {
matchCount--;
}
count[leftChar - 'a']++;
left++;
}
}
return result;
}
}
2. 使用单个计数数组(差值统计)
python
class Solution:
def findAnagrams(self, s: str, p: str) -> List[int]:
result = []
if len(s) < len(p):
return result
# 初始化计数数组(统计p和当前窗口的差值)
count = [0] * 26
for ch in p:
count[ord(ch) - ord('a')] += 1
# 记录有多少个字符的计数不为0
non_zero_count = sum(1 for c in count if c != 0)
left, right = 0, 0
while right < len(s):
# 右指针扩展窗口
right_char = s[right]
idx = ord(right_char) - ord('a')
count[idx] -= 1
if count[idx] == 0:
non_zero_count -= 1
elif count[idx] == -1:
non_zero_count += 1
right += 1
# 当窗口大小等于p的长度时
if right - left == len(p):
if non_zero_count == 0:
result.append(left)
# 左指针收缩窗口
left_char = s[left]
idx = ord(left_char) - ord('a')
count[idx] += 1
if count[idx] == 0:
non_zero_count -= 1
elif count[idx] == 1:
non_zero_count += 1
left += 1
return result
九、常用函数积累
Java 常用函数
java
// 字符串操作 String s = "hello"; int len = s.length(); // 字符串长度 char c = s.charAt(0); // 获取指定位置的字符 char[] chars = s.toCharArray(); // 字符串转字符数组 // 数组操作 int[] arr = new int[26]; Arrays.fill(arr, 0); // 填充数组 Arrays.equals(arr1, arr2); // 比较两个数组 Arrays.toString(arr); // 数组转字符串 // 列表操作 List<Integer> list = new ArrayList<>(); list.add(element); // 添加元素 list.size(); // 获取列表大小
Python 常用函数
python
# 字符串操作
s = "hello"
length = len(s) # 字符串长度
char = s[0] # 获取指定位置的字符
char_list = list(s) # 字符串转列表
# 列表操作
arr = [0] * 26
arr == other_arr # 比较两个列表
str(arr) # 列表转字符串
# 内置函数
ord(c) # 字符转ASCII码
chr(code) # ASCII码转字符
sum(iterable) # 求和
# collections模块
from collections import Counter
counter = Counter("hello") # 创建计数器
counter == other_counter # 比较两个计数器
十、总结
核心要点
-
固定窗口滑动:窗口大小固定为
p的长度,在s上滑动 -
字符频率统计:用数组记录字符出现次数,判断是否为字母异位词
-
高效比对:通过比较频率数组而不是排序字符串来判断异位词
面试常见问题
-
为什么使用数组而不是HashMap?
-
字符集固定(26个小写字母)时,数组访问更快(O(1))
-
数组不需要哈希计算,更高效
-
-
如何处理大写字母或Unicode字符?
-
对于大写字母,数组大小可以扩展到52(大小写各26)
-
对于Unicode字符,可以使用HashMap
-
-
这个算法是否会漏掉某些异位词?
-
不会。滑动窗口检查了
s中所有长度为len(p)的子串
-
-
最坏情况下的时间复杂度?
-
O(n + m),其中 n 是
s的长度,m 是p的长度
-
-
如果
p很长怎么办?-
算法仍然有效,但初始统计
p的频率需要 O(m) 时间
-
扩展思考
-
类似问题:
-
最小覆盖子串(LeetCode 76)
-
字符串的排列(LeetCode 567)
-
找到字符串中所有字母异位词(本题)
-
-
变体问题:
-
允许最多k个不同字符的异位词
-
寻找最长的异位词子串
-
-
实际应用:
-
文本搜索和模式匹配
-
DNA序列分析
-
数据压缩和加密
-
掌握固定窗口滑动+字符频率统计的解法,不仅能够解决字母异位词问题,还能够解决一系列类似的字符串匹配问题,是面试中非常重要的算法技巧。

502

被折叠的 条评论
为什么被折叠?



