算法每日一题 | LeetCode 438:找到字符串中所有字母异位词
📌 题目描述
给定两个字符串 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" 的异位词。
🧠 思路分析
拿到这题,第一反应是暴力——遍历 s 里每个长度为 p.length() 的子串,挨个判断是不是 p 的异位词。思路没问题,但问题在于怎么判断两个字符串是异位词?排序比较一下?那每比一次就是 ,数据量一大直接拉胯。
换个角度想:两个字符串要是异位词,说白了就是每个字符出现的次数一模一样。题目又限定了只有小写字母,那直接用个长度为 26 的 int 数组记频次就行了,两个数组一比较,完全相等就是异位词。
然后就是怎么高效遍历的问题了。因为 p 的长度是固定的(假设叫 ),所以我们在 s 上只需要看所有长度为 的连续子串。这不就是滑动窗口嘛——维护一个固定大小的窗口在 s 上滑过去就行了:
- 先统计
p里每个字符出现了多少次,存进pCount数组 - 再搞一个同样大小的
windowCount数组,用来记当前窗口里的字符频次 - 右指针
right往右走,把新字符加进窗口 - 窗口一旦超过 ,左指针
left就得跟着往右缩,把最左边的字符从窗口里踢出去 - 窗口刚好等于 的时候,拿
windowCount和pCount比一下,一样就把left记下来
拿 s = "cbaebabacd",p = "abc" 来手动走一遍():
pCount = {a:1, b:1, c:1}- 窗口滑到
[c,b,a](索引 0~2):频次是{a:1, b:1, c:1},跟pCount一样,记 0 - 滑到
[b,a,e](索引 1~3):多了个e,不对 - 继续滑...中间都不匹配
- 滑到
[b,a,c](索引 6~8):频次又是{a:1, b:1, c:1},记 6 - 最终结果
[0, 6]
🖼️ 图解与执行流程
┌─────────────────────────────────────────────────────┐
│ 字符串 s = "cbaebabacd" │
│ 目标 p = "abc" (L = 3) │
└─────────────────────────────────────────────────────┘
Step 1: 初始化频次数组
┌──────────────────────────────────────┐
│ pCount = [a:1, b:1, c:1, ...] │
│ windowCount= [0, 0, 0, 0, ...] │
└──────────────────────────────────────┘
Step 2: 窗口滑动过程
right=0 窗口: [c] → 窗口没满,继续
right=1 窗口: [c,b] → 还没满,继续
right=2 窗口: [c,b,a] → 满了!比一下 → 匹配!记 left=0
right=3 窗口: [c,b,a,e] → 超了!左边缩 → [b,a,e] → 不行
right=4 窗口: [b,a,e,b] → 超了!左边缩 → [a,e,b] → 不行
right=5 窗口: [a,e,b,a] → 超了!左边缩 → [e,b,a] → 不行
right=6 窗口: [e,b,a,b] → 超了!左边缩 → [b,a,b] → 不行
right=7 窗口: [b,a,b,a] → 超了!左边缩 → [a,b,a] → 不行
right=8 窗口: [a,b,a,c] → 超了!左边缩 → [b,a,c] → 匹配!记 left=6
right=9 窗口: [b,a,c,d] → 超了!左边缩 → [a,c,d] → 不行
最终结果: [0, 6]
💻 核心代码实现
class Solution {
public List<Integer> findAnagrams(String s, String p) {
// 结果列表
List<Integer> res = new ArrayList<>();
// p 的长度,也就是窗口的固定大小
int length = p.length();
// 滑动窗口的左右指针
int left = 0, right = 0;
// pCount 记 p 的字符频次,windowCount 记当前窗口的字符频次
int[] pCount = new int[26];
int[] windowCount = new int[26];
// 先把 p 里每个字符出现几次统计好
for (int i = 0; i < length; i++) {
char c = p.charAt(i);
pCount[c - 'a']++;
}
// 右指针从头到尾扫一遍 s
while (right < s.length()) {
// 右指针指的字符加入窗口
windowCount[s.charAt(right) - 'a']++;
right++;
// 窗口超长了,左指针往右缩
while (right - left > length) {
windowCount[s.charAt(left) - 'a']--;
left++;
}
// 窗口刚好等于 p 的长度,比一下频次
if (right - left == length) {
if (Arrays.equals(pCount, windowCount)) {
// 匹配上了,把起始位置记下来
res.add(left);
}
}
}
return res;
}
}