一、题目描述
给定两个字符串 s
和 p
,找到 s
****中所有 p
****的 异位词 的子串,返回这些子串的起始索引。不考虑答案输出的顺序。
示例 1:
makefile
输入: s = "cbaebabacd", p = "abc"
输出: [0,6]
解释:
起始索引等于 0 的子串是 "cba", 它是 "abc" 的异位词。
起始索引等于 6 的子串是 "bac", 它是 "abc" 的异位词。
示例 2:
makefile
输入: s = "abab", p = "ab"
输出: [0,1,2]
解释:
起始索引等于 0 的子串是 "ab", 它是 "ab" 的异位词。
起始索引等于 1 的子串是 "ba", 它是 "ab" 的异位词。
起始索引等于 2 的子串是 "ab", 它是 "ab" 的异位词。
提示:
1 <= s.length, p.length <= 3 * 104
s
和p
仅包含小写字母
二、题解
反面题解(本人的)
js
var findAnagrams = function(s, p) {
const result = [];
if (s.length < p.length) return result;
const primes = [2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67, 71, 73, 79, 83, 89, 97, 101];
const charToPrime = new Map();
for (let i = 0; i < 26; i++) {
charToPrime.set(String.fromCharCode('a'.charCodeAt(0) + i), primes[i]);
}
let target = 1;
for (const char of p) {
target *= charToPrime.get(char);
}
let left = 0, right = p.length - 1;
let current = 1;
for (let i = left; i <= right; i++) {
current *= charToPrime.get(s[i]);
}
while (right < s.length) {
if (current === target) {
result.push(left);
}
// 这里应该先除然后加完再成,不然不对
current /= charToPrime.get(s[left]);
left++;
right++;
if (right < s.length) {
current *= charToPrime.get(s[right]);
}
}
return result;
};
问题
- 数值溢出: 即使你使用了相对较小的质数,当字符串
p
稍长时,target
和current
的值很容易变得非常大,超出 JavaScript Number 类型的安全整数范围 (Number.MAX_SAFE_INTEGER),从而导致精度丢失和比较错误。
正解(滑动窗口+出现频次)
js
var findAnagrams = function(s, p) {
const result = [];
if (s.length < p.length) return result;
const pLen = p.length;
const sLen = s.length;
// 初始化频率数组(26个小写字母)
const pCount = new Array(26).fill(0);
const sCount = new Array(26).fill(0);
// 统计 p 的字符频率
for (let i = 0; i < pLen; i++) {
pCount[p.charCodeAt(i) - 'a'.charCodeAt(0)]++;
}
// 初始化滑动窗口(窗口大小为 pLen)
for (let i = 0; i < pLen; i++) {
sCount[s.charCodeAt(i) - 'a'.charCodeAt(0)]++;
}
// 比较初始窗口是否匹配
if (arraysEqual(pCount, sCount)) {
result.push(0);
}
// 滑动窗口:每次右移一位,更新频率数组
for (let i = pLen; i < sLen; i++) {
// 移除左边界的字符
sCount[s.charCodeAt(i - pLen) - 'a'.charCodeAt(0)]--;
// 添加右边界的字符
sCount[s.charCodeAt(i) - 'a'.charCodeAt(0)]++;
// 检查当前窗口是否匹配
if (arraysEqual(pCount, sCount)) {
result.push(i - pLen + 1);
}
}
return result;
};
// 辅助函数:比较两个频率数组是否相等
function arraysEqual(a, b) {
for (let i = 0; i < a.length; i++) {
if (a[i] !== b[i]) return false;
}
return true;
}
核心思想
-
频率统计:
- 它使用两个数组
pCount
和sCount
来存储字符串p
和滑动窗口中字符的频率。pCount[i]
表示字符'a' + i
在字符串p
中出现的次数。sCount
存储相同的信息,但针对的是在字符串s
中的滑动窗口。
- 它使用两个数组
-
滑动窗口:
- 它维护一个长度等于
p
的窗口,在字符串s
上滑动。 - 窗口的初始位置是
s
的前pLen
个字符。 - 每次滑动,窗口向右移动一个字符。
- 在移动后,函数更新
sCount
以反映窗口中字符频率的变化。 具体来说,它从sCount
中移除窗口左边界的字符,并添加窗口右边界的字符。
- 它维护一个长度等于
-
判断异位词:
- 在每次窗口滑动后,函数比较
pCount
和sCount
。 如果这两个数组相等,这意味着窗口中的字符是p
的一个异位词,因此将窗口的起始位置添加到result
数组中。arraysEqual
函数用于比较两个频率数组。
- 在每次窗口滑动后,函数比较
详细解释
-
var findAnagrams = function(s, p) {
:定义函数,接收两个字符串s和p作为输入。 -
const result = [];
:初始化结果数组,用于存放异位词起始下标。 -
if (s.length < p.length) return result;
:如果s比p短,直接返回空数组(不可能有异位词)。 -
const pCount = new Array(26).fill(0);
:创建数组pCount,统计p中每个字母出现的次数。 -
const sCount = new Array(26).fill(0);
:创建数组sCount,统计滑动窗口中每个字母出现的次数。 -
for (let i = 0; i < p.length; i++) { ... }
: 循环遍历字符串p
。*
pCount[p.charCodeAt(i) - 'a'.charCodeAt(0)]++;
: 统计p
中每个字符出现的次数,并存储在pCount
数组中。*
sCount[s.charCodeAt(i) - 'a'.charCodeAt(0)]++;
: 初始化滑动窗口,统计第一个窗口(长度和p相同)中每个字符出现的次数,存储在sCount
中。 -
for (let i = 0; i <= s.length - p.length; i++) { ... }
: 滑动窗口循环遍历字符串s
。s.length - p.length
计算了需要滑动的最大次数, 因为超过这个次数后, 剩余字符串的长度将小于p
的长度, 无法形成异位词. -
if (areArraysEqual(sCount, pCount)) { result.push(i); }
: 比较sCount
和pCount
, 如果二者相同则表示当前滑动窗口内的子字符串是p
的一个异位词, 将窗口起始位置i
添加到result
-
if (i < s.length - p.length) { ... }
: 在更新滑动窗口字符统计前,检查窗口是否可以滑动(避免数组越界)sCount[s.charCodeAt(i) - 'a'.charCodeAt(0)]--;
: 移除窗口起始位置的字符统计sCount[s.charCodeAt(i + p.length) - 'a'.charCodeAt(0)]++;
: 添加新进入窗口的字符统计
-
return result;
:返回结果列表。 -
function areArraysEqual(arr1, arr2) { ... }
: 辅助函数,用于比较两个数组是否相等. 遍历数组,如果对应索引的值不相等,说明数组不相等.
三、结语
再见!