LeetCode438 找到字符串中所有字母异位词
题目
解题:滑动窗口 + 双指针
LeetCode76 最小覆盖子串 同样使用滑动窗口 + 双指针来解题。下面一版的代码也与其类似,两道问题的不同之处在于:76题返回的最短字符串里面包含 p 的异位词,长度可以比 p 长,可以包含 p 以外的字母等,只要从中选取的一组字符可以构成 p 的异位词就成。
438题找的是异位词,长度和每个字符的个数都要相同。
sFreq 只记录在 p 中出现的字符的频数。 从提交结果来看,用数组来实现 sFreq 效率更高。
用 sFreq.clear(); / sFreq.fill(0); 的写法不能用一个哈希表来代替 sFreq 和 pFreq,因为在清除时,会把 p 中字符频数的信息一起抹去。
// javascript
var findAnagrams = function(s, p) {
const sFreq = new Map(), pFreq = new Map();
const sLen = s.length, pLen = p.length;
// pFreq 记录 p 所有字符频数
for (const pEle of p) {
pFreq.set(pEle, (pFreq.get(pEle) || 0) + 1);
}
const res = [];
// 左右指针 left 和 right, matchSuccess 记录有多少个字符频数 >= pFreq
let left = 0, right = 0, matchSuccess = 0;
while (right < sLen) {
if (pFreq.has(s[right]) === true) {
sFreq.set(s[right], (sFreq.get(s[right]) || 0) + 1);
if (sFreq.get(s[right]) === pFreq.get(s[right])) {
matchSuccess += 1;
}
} else {
// [left, right]里如果包含不在 p 中的字符,那必定不是 p 的异位词
// left 指向 s[right] 的后一位,并且将 matchSuccess 和 sFreq 记录清除
// 新一轮的扫描时 left = right,并且该值还未处理,相当于窗口里没有字符
left = right + 1;
matchSuccess = 0;
sFreq.clear();
}
// s 涵盖 p 所有字符且 left <= right 时:开始收缩窗口
while (matchSuccess === pFreq.size && left <= right) {
// s 涵盖 p 所有字符且 [left, right] 长度与 p 一样:找到异位词
if (right - left + 1 === pLen) {
res.push(left);
}
// 下一步要移除 left,如果 pFreq.has(s[left]) 是 true,sFreq 对应的字符频数要减少
if (pFreq.has(s[left]) === true) {
sFreq.set(s[left], sFreq.get(s[left]) - 1);
if (sFreq.get(s[left]) < pFreq.get(s[left])) {
matchSuccess -= 1;
}
}
// 移动窗口的左边界
left++;
}
// 移动窗口的右边界
right++;
}
return res;
};
上面的写法是等到 s[left, right] 包含了 p 的所有字符后再移动左指针来收缩窗口,看能不能找到一个 s[left, right] 包含了 p 的所有字符并且长度正好等于 pLen。
另一种写法是保证在 s[left, right] 中,sFreq 的每个频数都 <= pFreq:一旦发现例外就移动左指针,直到满足条件为止。
// javascript
var findAnagrams = function(s, p) {
const sFreq = new Map(), pFreq = new Map();
const sLen = s.length, pLen = p.length;
for (const pEle of p) {
pFreq.set(pEle, (pFreq.get(pEle) || 0) + 1);
}
const res = [];
let left = 0, right = 0;
while (right < sLen) {
if (pFreq.has(s[right]) === true) {
sFreq.set(s[right], (sFreq.get(s[right]) || 0) + 1);
// sFreq 中 s[right] 的频数多于 pFreq,移动左指针收缩窗口直到相等
while (sFreq.get(s[right]) > pFreq.get(s[right])) {
if (pFreq.has(s[left]) === true) {
sFreq.set(s[left], sFreq.get(s[left]) - 1);
}
left++;
}
} else {
// 跳过没有在 p 中出现的字符,清空 sFreq
left = right + 1;
sFreq.clear();
}
// s[left, right] 中所有字符频数 <= p,但是长度等于 p:真相是异位词找到了!
if (right - left + 1 === pLen) {
res.push(left);
}
right++;
}
return res;
};
题目里表明了 s 和 p 仅包含小写字母,可以用数组来代替哈希表:
// javascript
var findAnagrams = function(s, p) {
const sLen = s.length, pLen = p.length;
const sFreq = new Array(26).fill(0), pFreq = new Array(26).fill(0);
const codeA = "a".charCodeAt();
for (let i = 0; i < pLen; i++) pFreq[p.charCodeAt(i) - codeA]++;
const res = [];
let left = 0, right = 0;
while (right < sLen) {
const idxRight = s.charCodeAt(right) - codeA;
if (pFreq[idxRight] > 0) { // > 0 代表该字符在 p 中出现过
sFreq[idxRight]++;
while (sFreq[idxRight] > pFreq[idxRight]) {
const idxLeft = s.charCodeAt(left) - codeA;
if (pFreq[idxLeft] > 0) {
sFreq[idxLeft]--;
}
left++;
}
} else {
left = right + 1;
sFreq.fill(0);
}
if (right - left + 1 === pLen) {
res.push(left);
}
right++;
}
return res;
};
假设 sFreq 记录所有在 s 中出现的字符的频数:即如果 s 中有 p 中没有出现过的字符,也进行记录。 这会导致该字符的频数大于 p 中该字符的频数(p 中没出现过,频数是 0)。触发了 left 指针移动到 right + 1 的位置,并且 sFreq 也同步被清空了:不用再写 else 来单独处理,两种情况可以合并处理。
// javascript
var findAnagrams = function(s, p) {
const sLen = s.length, pLen = p.length;
const sFreq = new Array(26).fill(0), pFreq = new Array(26).fill(0);
const codeA = "a".charCodeAt();
for (let i = 0; i < pLen; i++) pFreq[p.charCodeAt(i) - codeA]++;
const res = [];
let left = 0, right = 0;
while (right < sLen) {
const idxRight = s.charCodeAt(right) - codeA;
sFreq[idxRight]++;
while (sFreq[idxRight] > pFreq[idxRight]) {
const idxLeft = s.charCodeAt(left) - codeA;
sFreq[idxLeft]--;
left++;
}
if (right - left + 1 === pLen) {
res.push(left);
}
right++;
}
return res;
};
时间复杂度:最坏情况下左右指针对 s s s 的每个元素各遍历一遍, t t t 的每个元素也会被遍历一遍, O ( ∣ s ∣ + ∣ t ∣ ) O(∣s∣+∣t∣) O(∣s∣+∣t∣);
空间复杂度:这里用了两张哈希表 / 数组作为辅助空间,每张哈希表最多不会存放超过字符集大小的键值对,因为仅包含小写字母,字符集大小 C C C 为 26,空间复杂度为 O ( 1 ) O(1) O(1)。