题目描述
给定两个字符串 s 和 p,找到 s 中所有 p 的 异位词 的子串,返回这些子串的起始索引。不考虑答案输出的顺序。
示例:
输入: s = "cbaebabacd", p = "abc"
输出: [0,6]
解释:
起始索引等于 0 的子串是 "cba", 它是 "abc" 的异位词。
起始索引等于 6 的子串是 "bac", 它是 "abc" 的异位词。
提示:
1 <= s.length, p.length <= 3 * 104s和p仅包含小写字母
解法
滑动窗口
var findAnagrams = function(s, p) {
const sLen = s.length, pLen = p.length;
if (sLen < pLen) {
return [];
}
const ans = [];
const count = Array(26).fill(0);
for (let i = 0; i < pLen; ++i) {
++count[s[i].charCodeAt() - 'a'.charCodeAt()];
--count[p[i].charCodeAt() - 'a'.charCodeAt()];
}
let differ = 0;
for (let j = 0; j < 26; ++j) {
if (count[j] !== 0) {
++differ;
}
}
if (differ === 0) {
ans.push(0);
}
for (let i = 0; i < sLen - pLen; ++i) {
if (count[s[i].charCodeAt() - 'a'.charCodeAt()] === 1) { // 窗口中字母 s[i] 的数量与字符串 p 中的数量从不同变得相同
--differ;
} else if (count[s[i].charCodeAt() - 'a'.charCodeAt()] === 0) { // 窗口中字母 s[i] 的数量与字符串 p 中的数量从相同变得不同
++differ;
}
--count[s[i].charCodeAt() - 'a'.charCodeAt()];
if (count[s[i + pLen].charCodeAt() - 'a'.charCodeAt()] === -1) { // 窗口中字母 s[i+pLen] 的数量与字符串 p 中的数量从不同变得相同
--differ;
} else if (count[s[i + pLen].charCodeAt() - 'a'.charCodeAt()] === 0) { // 窗口中字母 s[i+pLen] 的数量与字符串 p 中的数量从相同变得不同
++differ;
}
++count[s[i + pLen].charCodeAt() - 'a'.charCodeAt()];
if (differ === 0) {
ans.push(i + 1);
}
}
return ans;
};
整体思路:通过差值数组和滑动窗口来实现
count大小为 26 的数组 用来记录当前窗口与目标 p 之间每个字符的数量差异。
利用ASCII码差值来实现:
小写字母 'a' 到 'z' 在 ASCII 表中是连续编码的: 'a' → ASCII 值 97 ;'b' → 98 ... 'z' → 122
小写字母 - 'a' 的结果:
'a' - 'a' = 0
'b' - 'a' = 1
...
'z' - 'a' = 25
这样就将 26 个字母映射到 0~25 的整数,完美对应数组索引。
具体实现是 首先记录s的前pLen个字符,p字符串中的字符,s中的出现的字符+1,p中的字符-1。那么count的值
- 1 s字符串当前窗口有该字符,p字符串没有该字符
- -1 s字符串当前窗口没有该字符,p字符串有该字符
- 0 s当前窗口和p字符串都有该字符
diff整数,记录 count 数组中非零元素的个数(即不平衡的字符种类数)。
通过diff来记录s和p之间的差异,先循环遍历count,只要不为当前count[i]不为0,说明s和p在该字符上有差异,diff+1
然后通过一个for循环,不断的在s字符串上向右移动从i到i+pLen这样一个窗口
在移动中:
- 首先去除s[i]的存在致使的diff变化,看
count[s[i].charCodeAt() - 'a'.charCodeAt()]
- 若为1,说明它之前会造成
++diff,那么现在就要--diff - 若为0,那么删除s[i]后对应的字符数量不再一致,就要
--diff
然后count中对应的s[i]字符数量-1
- 然后在添加s[i+pLen]的加入致使的diff变化,仍然是看
count[s[i+pLen]].charCodeAt() - 'a'.charCodeAt()]的值
- 若为-1,添加s[i+pLen]后,这个字母的数量一致,
--diff - 若为1,
diff不变 - 若为0,添加s[i+pLen]后,这个字母的数量不再一致,
++diff
然后count中对应的s[i+pLen]字符数量+1
最后进行判断当前diff是否为0,若为0,证明当前的s[i+1] 到 s[i+pLen]是p的字母异位词,将开始序号i+1添加到ans中,再开启下一次循环
最终返回ans
时间,空间复杂度
好的,下面是对您提供的代码的时间复杂度和空间复杂度的纯文本分析。
代码复杂度分析
时间复杂度分析:O(N)
用 N 表示字符串 s 的长度,用 M 表示目标字符串 p 的长度。
-
初始化
count数组: 代码首先执行一个循环,遍历 pLen 次(即 M 次),在循环内部,它进行两次 O(1) 的数组访问和增减操作。因此,初始化阶段的复杂度为 O(M)。 -
初始化
differ变量: 接下来,代码遍历固定的 26 个字符位置来统计初始差异。这是一个固定的操作次数,因此复杂度为 O(26)常数次,即 O(1)。 -
滑动窗口主循环: 主逻辑在一个
for循环中完成,该循环从 i=0 运行到 sLen−pLen−1。这相当于循环了 N−M 次。- 循环内部操作: 在每次循环中,代码处理一个字符的移除(窗口左侧)和一个字符的加入(窗口右侧)。所有对
count数组的访问、对differ的增减判断,以及对if/else条件的判断,都是固定的几次操作,其复杂度为 O(1)。
- 循环内部操作: 在每次循环中,代码处理一个字符的移除(窗口左侧)和一个字符的加入(窗口右侧)。所有对
将所有步骤的复杂度相加:O(M)+O(1)+O(N−M)×O(1)。当 N 远大于 M 时,总的时间复杂度被简化为O(N),即线性时间复杂度。
空间复杂度分析:O(1)
count数组: 代码的核心数据结构是一个大小为 26 的数组 (count),用于存储 26 个小写字母的频率差异。这个大小是固定的,不随输入字符串 s 和 p 的长度 N 和 M 变化而变化。- 其他变量: 变量如
sLen、pLen、differ都是占用固定空间的基本数据类型。
因此该算法的空间复杂度为O(1)(常数空间)。