题目描述
给定两个字符串 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"的异位词。
提示:
1 <= s.length,p.length <= 3 * 10^4s和p仅包含小写字母
固定窗口滑动 + 计数数组
- 初始化处理:
- 若
s长度小于p,直接返回空列表。 - 创建两个长度为
26的数组pCount和sCount,分别统计p的字符频次和s中前p.length()个字符的频次。
- 首次匹配检查:
- 比较
pCount和sCount,若相等说明起始索引0是异位词,加入结果列表。
- 滑动窗口:
- 窗口长度固定为
p.length(),每次右移一位:- 移除窗口最左侧字符(对应频次减
1)。 - 加入窗口右侧新字符(对应频次加
1)。
- 移除窗口最左侧字符(对应频次减
- 每次窗口移动后,比较
pCount和sCount,若相等则记录当前窗口起始索引(i + 1)。
1 | public List<Integer> findAnagrams(String s, String p) { |
复杂度分析
- 时间复杂度:
O(26n),其中n是s的长度。因为每次窗口移动后,我们都要比较两个长度为26的数组(Arrays.equals内部会循环26次)。 - 空间复杂度:
O(1),固定使用两个长度为26的数组(常数空间)。
不固定窗口滑动 + 计数数组
以下参考大佬的解法:438. 找到字符串中所有字母异位词 | 题解 | 灵茶山艾府
- 频次数组预处理:
- 统计
p的字符频次到数组pCount。
- 双指针滑动窗口:
- 使用双指针
left和right,right向右移动,将遇到的字符在pCount中减1(相当于进入窗口)。 - 如果某个字符在
pCount中的值小于0,说明当前窗口中这个字符的数量超过了p中该字符的数量,或者p中根本没有这个字符。那么就需要移动left指针,将left指向的字符在pCount中加1(相当于移出窗口),直到pCount[c]不再小于0(即调整到当前字符数量正常)。 - 当窗口长度(
right - left + 1)等于p的长度时,说明我们找到了一个异位词子串,将left加入结果列表。
注意:这种方法中,pCount 数组被复用,我们通过加减操作来维护窗口内字符的计数。当窗口长度等于 p 的长度时,由于我们保证了窗口内每个字符的出现次数都不超过 p 中的出现次数(通过 while 循环调整),并且窗口长度恰好等于 p 的长度,那么窗口内的字符串必然是 p 的一个异位词。因为如果窗口内某个字符的出现次数大于 p 中的出现次数,我们会通过 left 右移来减少它,直到它等于 p 中的出现次数(即不再为负)。而当窗口长度等于 p 的长度时,每个字符的出现次数恰好等于 p 中的出现次数(因为如果少了,那么 pCount 中对应的值应该是正数,但我们的操作中,进入窗口减 1,移出窗口加 1,并且我们保证了没有负值,所以每个字符都不多不少)。
1 | public List<Integer> findAnagrams(String s, String p) { |
复杂度分析
- 时间复杂度:
O(m + n),其中m是s的长度,n是p的长度。虽然写了个二重循环,但是内层循环中对left加一的总执行次数不会超过m次,所以滑窗的时间复杂度为O(m)。 - 空间复杂度:
O(1),使用固定大小的数组(26个整数)。