定义
滑动窗口是双指针的高级用法,通常用來将嵌套的循环问题,转换为单循环问题來降低时间复杂度。
类型分为以下
- 固定窗口大小
- 窗口大小不固定,求解最大的满足条件的窗口
- 窗口大小不固定,求解最小的满足条件的窗口
伪代码框架如下:
int left = 0, right = 0;
while (right < s.size()) {
window.add(s[right]);
right++;
while (valid) {
window.remove(s[left]);
left++;
}
}
滑动窗口常见题
滑动窗口最大值
给定一个数组 nums,有一个大小为 k 的滑动窗口从数组的最左侧移动到数组的最右侧。你只可以看到在滑动窗口内的 k 个数字。滑动窗口每次只向右移动一位。 返回滑动窗口中的最大值
输入: nums = [1,3,-1,-3,5,3,6,7], 和 k = 3
输出: [3,3,5,5,6,7]
解释:
滑动窗口的位置 最大值
--------------- -----
[1 3 -1] -3 5 3 6 7 3
1 [3 -1 -3] 5 3 6 7 3
1 3 [-1 -3 5] 3 6 7 5
1 3 -1 [-3 5 3] 6 7 5
1 3 -1 -3 [5 3 6] 7 6
1 3 -1 -3 5 [3 6 7] 7
来源:leetcode-cn.com/problems/sl…
var maxSlidingWindow = function(nums, k) {
let n = nums.length;
class slideWindow {
constructor() {
this.data = []
}
push(val) {
while(this.data.length > 0 && this.data[this.data.length - 1] < val) {
this.data.pop()
}
this.data.push(val)
}
pop(val) {
if(this.data.length > 0 && this.data[0] === val) {
this.data.shift()
}
}
max() {
return this.data[0]
}
}
let res = [];
let window = new slideWindow();
for (let i = 0; i < n; i++) {
if(i < k - 1) {
window.push(nums[i])
} else {
window.push(nums[i]);
res.push(window.max());
window.pop(nums[i - k + 1])
}
}
return res;
};
无重复字符的最长子串
给定一个字符串,请你找出其中不含有重复字符的 最长子串 的长度。
输入: "abcabcbb"
输出: 3
解释: 因为无重复字符的最长子串是 "abc",所以其长度为 3。
來源:leetcode-cn.com/problems/lo…
var lengthOfLongestSubstring = function(s) {
let window = {}, l = 0, r = 0, count = 0;
while(r < s.length) {
let c = s[r];
r++;
window[c] ? window[c]++ : window[c] = 1;
while(window[c] > 1) {
let d = s[l];
l++;
window[d]--;
}
count = Math.max(count, r - l);
}
return count;
};
最小覆盖子串
给你一个字符串 S、一个字符串 T 。请你设计一种算法,可以在 O(n) 的时间复杂度内,从字符串 S 里面找出:包含 T 所有字符的最小子串。
输入:S = "ADOBECODEBANC", T = "ABC"
输出:"BANC"
来源:leetcode-cn.com/problems/mi…
var minWindow = function(s, t) {
let window = {}, need = {}, l = 0, r = 0, count = 0, start = -1, minLen = Infinity;
for (let j = 0; j < t.length; j++) {
let tmp = t[j];
need[tmp] ? need[tmp]++ : need[tmp] = 1;
}
let tkeylen = Object.keys(need).length;
while(r < s.length) {
let c = s[r];
r++;
if(need[c]) {
window[c] ? window[c]++ : window[c] = 1;
if(window[c] === need[c]) {
count++
}
}
while(count === tkeylen) {
if(r - l < minLen) {
start = l;
minLen = r - l;
}
let d = s[l];
l++;
if(need[d]){
if(window[d] === need[d]) {
count--;
}
window[d]--
}
}
}
return minLen === Infinity ? "" : s.substr(start, minLen);
};
找到字符串中所有字母异位词
给定一个字符串 s 和一个非空字符串 p,找到 s 中所有是 p 的字母异位词的子串,返回这些子串的起始索引。
输入:
s: "cbaebabacd" p: "abc"
输出:
[0, 6]
解释:
起始索引等于 0 的子串是 "cba", 它是 "abc" 的字母异位词。
起始索引等于 6 的子串是 "bac", 它是 "abc" 的字母异位词。
来源:leetcode-cn.com/problems/fi…
var findAnagrams = function(s, p) {
let window = {}, need = {}, l = 0, r = 0, count = 0, res = [];
for (let j = 0; j < p.length; j++) {
let tmp = p[j];
need[tmp] ? need[tmp]++ : need[tmp] = 1;
}
let tkeylen = Object.keys(need).length;
while(r < s.length) {
let c = s[r];
r++;
if(need[c]) {
window[c] ? window[c]++ : window[c] = 1;
if(window[c] === need[c]) {
count++
}
}
while(r - l >= p.length){
if(count === tkeylen) {
res.push(l)
}
let d = s[l];
l++;
if(need[d]) {
if(window[d] === need[d]) {
count--
}
window[d]--
}
}
}
return res;
};
水果成篮
在一排树中,第 i 棵树产生 tree[i] 型的水果。你可以从你选择的任何树开始,然后重复执行以下步骤:
- 把这棵树上的水果放进你的篮子里。如果你做不到,就停下来
- 移动到当前树右侧的下一棵树。如果右边没有树,就停下来。
请注意,在选择一颗树后,你没有任何选择:你必须执行步骤 1,然后执行步骤 2,然后返回步骤 1,然后执行步骤 2,依此类推,直至停止。
你有两个篮子,每个篮子可以携带任何数量的水果,但你希望每个篮子只携带一种类型的水果。用这个程序你能收集的水果树的最大总量是多少?
输入:[0,1,2,2]
输出:3
解释:我们可以收集 [1,2,2]
如果我们从第一棵树开始,我们将只能收集到 [0, 1]。
來源:leetcode-cn.com/problems/fr…
var totalFruit = function(tree) {
let l = 0, r = 0 , count = 0, window = {}, res = 0;
while(r < tree.length) {
let c = tree[r];
if(window[c]) {
window[c]++;
} else {
window[c] = 1;
count++
}
while(count > 2) {
let d = tree[l];
l++;
window[d]--;
if(!window[d]) {
count--
}
}
res = Math.max(res, r - l + 1);
r++;
}
return res;
};
和相同的二元子数组
在由若干 0 和 1 组成的数组 A 中,有多少个和为 S 的非空子数组。
输入:A = [1,0,1,0,1], S = 2
输出:4
解释:
如下面黑体所示,有 4 个满足题目要求的子数组:
[1,0,1,0,1]
[1,0,1,0,1]
[1,0,1,0,1]
[1,0,1,0,1]
來源:leetcode-cn.com/problems/bi…
var numSubarraysWithSum = function(A, S) {
let l = r = 0, sum = 0, count = 0;
while(r < A.length) {
let c = A[r];
sum += c;
while(sum > S) {
let d = A[l];
sum -= d;
l++;
}
if(sum === S) {
let tmp = sum, j = l;
while(j <= r && tmp === S) {
count++;
tmp -= A[j];
j++
}
}
r++;
}
return count;
};
K 个不同整数的子数组
给定一个正整数数组 A,如果 A 的某个子数组中不同整数的个数恰好为 K,则称 A 的这个连续、不一定独立的子数组为好子数组。
输入:A = [1,2,1,2,3], K = 2
输出:7
解释:恰好由 2 个不同整数组成的子数组:[1,2], [2,1], [1,2], [2,3], [1,2,1], [2,1,2], [1,2,1,2].
來源:leetcode-cn.com/problems/su…
var subarraysWithKDistinct = function(A, K) {
let len = A.length, l = 0, r = 0, c = 0, res = 0;
let need = new Array(len + 1).fill(0)
for (; r < len; ++r) {
if(need[A[r]]++ == 0) ++c;
while( c > K ) {
if(--need[A[l]] === 0) --c;
++l;
}
let t = l;
if(c === K) {
while(c === K) {
if(--need[A[t]] === 0) --c;
++t;
++res;
}
for (let j = l; j < t; ++j) {
if(need[A[j]]++ === 0) ++c;
}
}
}
return res;
};
替换子串得到平衡字符串
有一个只含有 'Q', 'W', 'E', 'R' 四种字符,且长度为 n 的字符串。 假如在该字符串中,这四个字符都恰好出现 n/4 次,那么它就是一个「平衡字符串」。 给你一个这样的字符串 s,请通过「替换一个子串」的方式,使原字符串 s 变成一个「平衡字符串」。 你可以用和「待替换子串」长度相同的 任何 其他字符串来完成替换。 请返回待替换子串的最小可能长度。 如果原字符串自身就是一个平衡字符串,则返回 0。
输入:s = "QQWE"
输出:1
解释:我们需要把一个 'Q' 替换成 'R',这样得到的 "RQWE" (或 "QRWE") 是平衡的。
來源:leetcode-cn.com/problems/re…
var balancedString = function(s) {
let window = {}, need = {}, l = 0, r = 0, count = 0;
let times = s.length / 4;
for (let i of s) {
need[i] ? need[i]++ : need[i] = 1;
}
for (let key of Object.keys(need)) {
if(need[key] > times) {
need[key] = need[key] - times;
} else {
delete need[key]
}
}
let keylens = Object.keys(need).length;
if(keylens === 0) return 0;
let minlen = Infinity;
while( r < s.length) {
let c = s[r];
r++;
if(need[c]) {
window[c] ? window[c]++ : window[c] = 1;
if(window[c] === need[c]) {
count++
}
}
while(count === keylens) {
minlen = Math.min(minlen, r - l);
let d = s[l];
l++;
if(need[d]) {
window[d]--;
if(window[d] < need[d]) {
count--
}
}
}
}
return minlen;
};
串联所有单词的子串
给定一个字符串 s 和一些长度相同的单词 words。找出 s 中恰好可以由 words 中所有单词串联形成的子串的起始位置。 注意子串要与 words 中的单词完全匹配,中间不能有其他字符,但不需要考虑 words 中单词串联的顺序。
输入:
s = "barfoothefoobarman",
words = ["foo","bar"]
输出:[0,9]
解释:
从索引 0 和 9 开始的子串分别是 "barfoo" 和 "foobar" 。
输出的顺序不重要, [9,0] 也是有效答案。
來源:leetcode-cn.com/problems/su…
var findSubstring = function(s, words) {
let left = 0, right = 0, len = words.length;
if(len === 0) return [];
let res = [], gaplen = words[0].length;
let need = {}, window = {}, count = 0;
words.forEach(item => {
need[item] ? need[item]++ : need[item] = 1;
})
console.log(need)
let keylen = Object.keys(need).length;
for (let i = 0; i < gaplen; i++) {
right = left = i;
count = 0;
while(right <= s.length - gaplen) {
let c = s.substring(right, right + gaplen);
right += gaplen;
if(need[c]) {
window[c] ? window[c]++ : window[c] = 1;
if(window[c] === need[c]) {
count++;
}
}
while(left < right && count === keylen) {
if(Math.floor((right - left) / gaplen) === len) {
res.push(left)
}
let d = s.substring(left, left + gaplen);
left += gaplen;
window[d]--;
if(need[d] && window[d] < need[d]) {
count--;
}
}
}
window = {}
}
return res;
};
最短超串
假设你有两个数组,一个长一个短,短的元素均不相同。找到长数组中包含短数组所有的元素的最短子数组,其出现顺序无关紧要。 返回最短子数组的左端点和右端点,如有多个满足条件的子数组,返回左端点最小的一个。若不存在,返回空数组。
输入:
big = [7,5,9,0,2,1,3,5,7,9,1,1,5,8,8,9,7]
small = [1,5,9]
输出: [7,10]
來源:leetcode-cn.com/problems/sh…
var shortestSeq = function(big, small) {
let window = {}, need = {}, l = -1, r = -1, count = 0;
small.forEach(item => need[item] ? need[item]++ : need[item] = 1)
let length = big.length + 1;
let ansl = -1, ansr = -1;
while(r < big.length) {
r++;
let c = big[r];
if(need[c]) {
window[c] ? window[c]++ : window[c] = 1;
if(window[c] === 1) {
++count;
}
}
while(count === small.length && big.length - l > small.length) {
l++;
let d = big[l];
if (need[c]) {
if(--window[d] == 0) {
count--;
if(length > r - l + 1) {
ansl = l;
ansr = r;
length = r - l + 1;
}
}
}
}
}
return ansl === -1 && ansr === -1 ? [] : [ansl, ansr];
};
字符串的排列
给定两个字符串 s1 和 s2,写一个函数来判断 s2 是否包含 s1 的排列。换句话说,第一个字符串的排列之一是第二个字符串的子串。
输入: s1 = "ab" s2 = "eidbaooo"
输出: True
解释: s2 包含 s1 的排列之一 ("ba").
來源:leetcode-cn.com/problems/pe…
var checkInclusion = function(s1, s2) {
let window = {}, need = {}, l = 0, r = 0, count = 0;
for (let j = 0; j < s1.length; j++) {
let tmp = s1[j];
need[tmp] ? need[tmp]++ : need[tmp] = 1;
}
let tkeylen = Object.keys(need).length;
while(r < s2.length) {
let c = s2[r];
r++;
if(need[c]){
window[c] ? window[c]++ : window[c] = 1;
if(window[c] === need[c]) {
count++
}
}
while(r - l >= s1.length) {
if(count == tkeylen) {
return true
}
let d = s2[l];
l++;
if(need[d]) {
if(window[d] === need[d]) {
count--;
}
window[d]--
}
}
}
return false;
};
爱生气的书店老板
今天,书店老板有一家店打算试营业 customers.length 分钟。每分钟都有一些顾客(customers[i])会进入书店,所有这些顾客都会在那一分钟结束后离开。 在某些时候,书店老板会生气。 如果书店老板在第 i 分钟生气,那么 grumpy[i] = 1,否则 grumpy[i] = 0。 当书店老板生气时,那一分钟的顾客就会不满意,不生气则他们是满意的。 书店老板知道一个秘密技巧,能抑制自己的情绪,可以让自己连续 X 分钟不生气,但却只能使用一次。 请你返回这一天营业下来,最多有多少客户能够感到满意的数量。
输入:customers = [1,0,1,2,1,1,7,5], grumpy = [0,1,0,1,0,1,0,1], X = 3
输出:16
解释:
书店老板在最后 3 分钟保持冷静。
感到满意的最大客户数量 = 1 + 1 + 1 + 1 + 7 + 5 = 16.
來源:leetcode-cn.com/problems/gr…
var maxSatisfied = function(customers, grumpy, X) {
if(grumpy.length < 0) return 0;
let len = grumpy.length, l = 0, r = 0, count = 0, max = 0, res = 0;
while (r < len) {
if(grumpy[r] === 1) {
count += customers[r]
}
while(r - l + 1 > X) {
if(grumpy[l] === 1) {
count -= customers[l]
}
l++;
}
r++;
max = Math.max(count, max)
}
for (let i = 0; i < len; i++) {
res += (grumpy[i] === 0 ? customers[i] : 0);
}
return res + max;
};
尽可能使字符串相等
给你两个长度相同的字符串,s 和 t。 将 s 中的第 i 个字符变到 t 中的第 i 个字符需要 |s[i] - t[i]| 的开销(开销可能为 0),也就是两个字符的 ASCII 码值的差的绝对值。 用于变更字符串的最大预算是 maxCost。在转化字符串时,总开销应当小于等于该预算,这也意味着字符串的转化可能是不完全的。 如果你可以将 s 的子字符串转化为它在 t 中对应的子字符串,则返回可以转化的最大长度。 如果 s 中没有子字符串可以转化成 t 中对应的子字符串,则返回 0。
输入:s = "abcd", t = "bcdf", cost = 3
输出:3
解释:s 中的 "abc" 可以变为 "bcd"。开销为 3,所以最大长度为 3。
來源:leetcode-cn.com/problems/ge…
var equalSubstring = function(s, t, maxCost) {
let l = 0;
let r = 0;
let maxlen = 0;
let tmp = 0;
while(r < s.length) {
tmp = Math.abs(s[r].charCodeAt() - t[r].charCodeAt());
if(maxCost - tmp < 0) {
l++;
maxCost += Math.abs(s[l - 1].charCodeAt() - t[l - 1].charCodeAt())
}
maxCost -= tmp;
maxlen = Math.max(maxlen, r - l + 1);
r++;
}
return maxlen;
};
单字符重复子串的最大长度
如果字符串中的所有字符都相同,那么这个字符串是单字符重复的字符串。 给你一个字符串 text,你只能交换其中两个字符一次或者什么都不做,然后得到一些单字符重复的子串。返回其中最长的子串的长度。
输入:text = "aaabaaa"
输出:6
来源:leetcode-cn.com/problems/sw…
var maxRepOpt1 = function(text) {
let res = 0, n = text.length, l = 0, r = 0, mf = 0, mc = text[0];
let count = {}; let ds = {};
for (let r = 0; r < n; r++) {
let c = text[r];
count[c] = count[c] + 1 || 1;
}
let keylen = Object.keys(count).length;
for (let r = 0; r < n; ++r) {
let c = text[r];
if(mf < ds[c]) {
mf = ds[c];
mc = c;
}
if(r - l + 1 - mf > 1) {
let d = text[l];
ds[d]--;
l++
}
res = Math.max(res, r - l + 1)
}
return Math.min(res, count[mc])
};
和为s的连续正数序列
输入一个正整数 target ,输出所有和为 target 的连续正整数序列(至少含有两个数)。 序列内的数字由小到大排列,不同序列按照首个数字从小到大排列。
输入:target = 15
输出:[[1,2,3,4,5],[4,5,6],[7,8]]
来源:leetcode-cn.com/problems/he…
var findContinuousSequence = function(target) {
let list = [];
let left = 1;
let right = 1;
let sum = 0;
while(left < target/2) {
if(sum < target) {
sum += right;
right++;
} else if (sum > target) {
sum -= left;
left++;
} else {
let arr = [];
for (let i = left; i < right; i++) {
arr.push(i);
}
list.push(arr);
sum -= left;
left++;
}
}
return list;
};