跳到主要内容

9 篇博文 含有标签「滑动窗口」

查看所有标签

· 阅读需 4 分钟

1、题干

给你一个下标从 0 开始的整数数组 nums 和一个整数 threshold

请你从 nums 的子数组中找出以下标 l 开头、下标 r 结尾 (0 <= l <= r < nums.length) 且满足以下条件的 最长子数组

  • nums[l] % 2 == 0
  • 对于范围 [l, r - 1] 内的所有下标 inums[i] % 2 != nums[i + 1] % 2
  • 对于范围 [l, r] 内的所有下标 inums[i] <= threshold

以整数形式返回满足题目要求的最长子数组的长度。

注意:子数组 是数组中的一个连续非空元素序列。

 

示例 1:

输入:nums = [3,2,5,4], threshold = 5
输出:3
解释:在这个示例中,我们选择从 l = 1 开始、到 r = 3 结束的子数组 => [2,5,4] ,满足上述条件。
因此,答案就是这个子数组的长度 3 。可以证明 3 是满足题目要求的最大长度。

示例 2:

输入:nums = [1,2], threshold = 2
输出:1
解释:
在这个示例中,我们选择从 l = 1 开始、到 r = 1 结束的子数组 => [2] 。
该子数组满足上述全部条件。可以证明 1 是满足题目要求的最大长度。

示例 3:

输入:nums = [2,3,4,5], threshold = 4
输出:3
解释:
在这个示例中,我们选择从 l = 0 开始、到 r = 2 结束的子数组 => [2,3,4] 。
该子数组满足上述全部条件。
因此,答案就是这个子数组的长度 3 。可以证明 3 是满足题目要求的最大长度。

 

提示:

  • 1 <= nums.length <= 100
  • 1 <= nums[i] <= 100
  • 1 <= threshold <= 100

2、思路

双指针,暴力枚举

3、代码

  • 双指针,时间复杂度 O(n)O(n)
function longestAlternatingSubarray(nums: number[], threshold: number): number {
let ans = 0;

for (let i = 0; i < nums.length; i++) {
if (nums[i] % 2 !== 0 || nums[i] > threshold) continue;

let j = i + 1;
while (j < nums.length && nums[j] <= threshold && nums[j] % 2 !== nums[j - 1] % 2) {
j++;
}

ans = Math.max(ans, j - i);
i = j - 1;
}

return ans;
};
  • 暴力枚举,时间复杂度 O(n2)O(n^2)
function longestAlternatingSubarray(nums: number[], threshold: number): number {
let ans = 0;

for (let i = 0; i < nums.length; i++) {
if (nums[i] % 2 !== 0) continue;

for (let j = i; j < nums.length && nums[j] <= threshold; j++) {
ans = Math.max(ans, j - i + 1);
if (nums[j] % 2 === nums[j + 1] % 2) break;
}
}

return ans;
};

· 阅读需 4 分钟

1、题干

给你一个长度为 n 下标从 0 开始的字符串 blocks ,blocks[i] 要么是 'W' 要么是 'B' ,表示第 i 块的颜色。字符 'W' 和 'B' 分别表示白色和黑色。

给你一个整数 k ,表示想要 连续 黑色块的数目。

每一次操作中,你可以选择一个白色块将它 涂成 黑色块。

请你返回至少出现 一次 连续 k 个黑色块的 最少 操作次数。

 

示例 1:

输入:blocks = "WBBWWBBWBW", k = 7
输出:3
解释:
一种得到 7 个连续黑色块的方法是把第 0 ,3 和 4 个块涂成黑色。
得到 blocks = "BBBBBBBWBW" 。
可以证明无法用少于 3 次操作得到 7 个连续的黑块。
所以我们返回 3 。

示例 2:

输入:blocks = "WBWBBBW", k = 2
输出:0
解释:
不需要任何操作,因为已经有 2 个连续的黑块。
所以我们返回 0 。

 

提示:

  • n == blocks.length
  • 1 <= n <= 100
  • blocks[i] 要么是 'W' ,要么是 'B'
  • 1 <= k <= n

2、思路1

双层遍历,每次取 k 个色块,对白色块计数,取最少白色块数

3、代码

function minimumRecolors(blocks: string, k: number): number {
let ans = k;
for (let i = 0; i < blocks.length; i++) {
for (let j = i, w = 0; j - i < k && j < blocks.length; j++) {
if (blocks[j] === 'W') w++;
if (j - i + 1 === k) ans = Math.min(w, ans);
}
}
return ans;
};

4、复杂度

  • 时间复杂度:O(nk)O(nk)
  • 空间复杂度:O(1)O(1)

5、执行结果

image.png

6、思路2

滑动窗口求解,窗口固定长度为 k,对窗口内白色块计数,取最少白色块数

7、代码

function minimumRecolors(blocks: string, k: number): number {
let w = 0;
for (let i = 0; i < k; i++) {
if (blocks[i] === 'W') w++;
}

let ans = w;
for (let i = k; i < blocks.length; i++) {
if (blocks[i - k] === 'W') w--;
if (blocks[i] === 'W') w++;
ans = Math.min(w, ans);
}

return ans;
};

也可以这样

function minimumRecolors(blocks: string, k: number): number {
let ans = k, w = 0;
for (let i = 0, j = i; i < blocks.length; i++) {
for (; j < i + k && j < blocks.length; j++) {
if (blocks[j] === 'W') w++;
}
if (j - i === k) ans = Math.min(w, ans);
if (blocks[i] === 'W') w--;
}
return ans;
};

8、复杂度

  • 时间复杂度:O(n)O(n)
  • 空间复杂度:O(1)O(1)

9、执行结果

image.png

· 阅读需 3 分钟

1、题干

给你一个整数数组 nums 和一个整数 x 。每一次操作时,你应当移除数组 nums 最左边或最右边的元素,然后从 x 中减去该元素的值。请注意,需要 修改 数组以供接下来的操作使用。

如果可以将 x 恰好 减到 0 ,返回 最小操作数 ;否则,返回 -1

 

示例 1:

输入:nums = [1,1,4,2,3], x = 5
输出:2
解释:最佳解决方案是移除后两个元素,将 x 减到 0 。

示例 2:

输入:nums = [5,6,7,8,9], x = 4
输出:-1

示例 3:

输入:nums = [3,2,20,1,1,3], x = 10
输出:5
解释:最佳解决方案是移除后三个元素和前两个元素(总共 5 次操作),将 x 减到 0 。

 

提示:

  • 1 <= nums.length <= 105
  • 1 <= nums[i] <= 104
  • 1 <= x <= 109

2、思路

  • 数组总和 sum 必须大于 x
  • x 转换为找 sum-x,会简单些
  • 滑动窗口找总和为 sum-x 的区间 (l,r](l, r],区间求和使用前缀和减少时间复杂度

3、代码

function minOperations(nums: number[], x: number): number {
let sum = 0, preSums = [];
for (const n of nums) {
sum += n;
preSums.push(sum);
}

if (sum <= x) return sum < x ? -1 : nums.length;

x = sum - x;
let ans = Infinity, l = -1, r = 0;
while (l <= r) {
const y = preSums[r] - (preSums[l] ?? 0);
if (y === x) ans = Math.min(nums.length - r + l, ans);
y <= x ? r++ : l++;
}

return ans < Infinity ? ans : -1;
};

4、复杂度

  • 时间复杂度:O(n)O(n)
  • 空间复杂度:O(n)O(n)

5、执行结果

image.png

· 阅读需 3 分钟

1、题干

当一个字符串 s 包含的每一种字母的大写和小写形式 同时 出现在 s 中,就称这个字符串 s 是 美好 字符串。比方说,"abABB" 是美好字符串,因为 'A' 和 'a' 同时出现了,且 'B' 和 'b' 也同时出现了。然而,"abA" 不是美好字符串因为 'b' 出现了,而 'B' 没有出现。

给你一个字符串 s ,请你返回 s 最长的 美好子字符串 。如果有多个答案,请你返回 最早 出现的一个。如果不存在美好子字符串,请你返回一个空字符串。

 

示例 1:

输入:s = "YazaAay"
输出:"aAa"
解释:"aAa" 是一个美好字符串,因为这个子串中仅含一种字母,其小写形式 'a' 和大写形式 'A' 也同时出现了。
"aAa" 是最长的美好子字符串。

示例 2:

输入:s = "Bb"
输出:"Bb"
解释:"Bb" 是美好字符串,因为 'B' 和 'b' 都出现了。整个字符串也是原字符串的子字符串。

示例 3:

输入:s = "c"
输出:""
解释:没有美好子字符串。

示例 4:

输入:s = "dDzeE"
输出:"dD"
解释:"dD" 和 "eE" 都是最长美好子字符串。
由于有多个美好子字符串,返回 "dD" ,因为它出现得最早。

 

提示:

  • 1 <= s.length <= 100
  • s 只包含大写和小写英文字母。

2、解题思路

由题意可知美好子串中不会单独存在大写或小写字母,因此可以在单独的大写或小写字母处断开字符串,对拆分出来的左右子串进行递归查找,直到子串成为美好子串。

3、代码

var longestNiceSubstring = function (s) {
for (let i = 0; i < s.length; i++) {
const c = s[i].charCodeAt(0) < 91 ? s[i].toLowerCase() : s[i].toUpperCase();
if (s.includes(c)) continue;
const s1 = longestNiceSubstring(s.slice(0, i));
const s2 = longestNiceSubstring(s.slice(i + 1));
return s1.length >= s2.length ? s1 : s2;
}
return s;
};

4、执行结果

image.png

· 阅读需 4 分钟

1、题干

给你一个整数数组 nums 和一个整数 k ,判断数组中是否存在两个 不同的索引 i 和 j ,满足 nums[i] == nums[j]abs(i - j) <= k 。如果存在,返回 true ;否则,返回 false

 

示例 1:

输入:nums = [1,2,3,1], k = 3
输出:true

示例 2:

输入:nums = [1,0,1,1], k = 1
输出:true

示例 3:

输入:nums = [1,2,3,1,2,3], k = 2
输出:false

 

 

提示:

  • 1 <= nums.length <= 105
  • -109 <= nums[i] <= 109
  • 0 <= k <= 105

2、解法1-哈希映射

遍历数组所有元素,并将当前数字和索引作为键值存入哈希映射 map 中,如果当前数字 nums[i] 已存在于 map 中且其索引与当前索引的差值不大于 k 则返回 true,遍历结束仍未找到符合条件的数字则返回 false


3、复杂度

  • 时间复杂度:O(n)O(n)
  • 空间复杂度:O(n)O(n)

4、代码

var containsNearbyDuplicate = function (nums, k) {
const map = new Map();
for (let i = 0; i < nums.length; i++) {
if (map.has(nums[i]) && i - map.get(nums[i]) <= k) return true;
map.set(nums[i], i);
}
return false;
};
class Solution:
def containsNearbyDuplicate(self, nums: List[int], k: int) -> bool:
map = {}
for i in range(len(nums)):
if(nums[i] in map and i - map[nums[i]] <= k):
return True
map[nums[i]] = i
return False

5、解法2-滑动窗口+哈希集合

遍历数组所有元素,使用哈希集合 set 作为滑动窗口容器存储数组中连续的 k 个元素,索引 i 大于 k 之后需要剔除滑动窗口容器的首个元素 nums[i - k - 1] 以保证其长度不超过 k,如果当前元素已存在于哈希集合则说明二者索引差值不大于 k 应返回 true,遍历结束仍未找到符合条件的数字则返回 false

解法1的优势在于更容易想到和理解。解法2的优势在于只需要存储值而不需要存储键值对,因此空间占用会更少;另外当 knums.length 小的情况下,解法2的空间复杂度会更小且不受 nums.length 变大而影响。


6、复杂度

  • 时间复杂度:O(n)O(n)
  • 空间复杂度:O(k)O(k)

7、代码

var containsNearbyDuplicate = function (nums, k) {
const set = new Set();
for (let i = 0; i < nums.length; i++) {
if (i > k) set.delete(nums[i - k - 1]);
if (set.has(nums[i])) return true;
set.add(nums[i]);
}
return false;
};
class Solution:
def containsNearbyDuplicate(self, nums: List[int], k: int) -> bool:
s = set()
for i in range(len(nums)):
if(i > k):
s.remove(nums[i - k - 1])
if(nums[i] in s):
return True
s.add(nums[i])
return False

· 阅读需 3 分钟

1、题干

给你一个整数数组 nums 和两个整数 kt 。请你判断是否存在 两个不同下标 ij,使得 abs(nums[i] - nums[j]) <= t ,同时又满足 abs(i - j) <= k

如果存在则返回 true,不存在返回 false

 

示例 1:

输入:nums = [1,2,3,1], k = 3, t = 0
输出:true

示例 2:

输入:nums = [1,0,1,1], k = 1, t = 2
输出:true

示例 3:

输入:nums = [1,5,9,1,5,9], k = 2, t = 3
输出:false

 

提示:

  • 0 <= nums.length <= 2 * 104
  • -231 <= nums[i] <= 231 - 1
  • 0 <= k <= 104
  • 0 <= t <= 231 - 1

 

注意:本题与主站 220 题相同: https://leetcode-cn.com/problems/contains-duplicate-iii/

2、解法1-暴力解法

双层遍历,外层从头到尾遍历,内层遍历外层元素之后的 k 个元素,如果找到两个元素差值的绝对值小于等于 t 则结果为 true

注意:内层只需要遍历后 k 个元素,因为前 k 个元素在外层遍历时已经判断过

3、复杂度

  • 时间复杂度:O(nk)O(nk)
  • 空间复杂度:O(1)O(1)

4、代码

var containsNearbyAlmostDuplicate = function (nums, k, t) {
for (let i = 0; i < nums.length; i++) {
for (let j = i + 1; j <= i + k && j < nums.length; j++) {
if (Math.abs(nums[j] - nums[i]) <= t) return true;
}
}
return false;
};

5、执行结果

执行用时: 500 ms 内存消耗: 39.1 MB

6、解法2-桶排序

遍历过程中使用桶排序维护 k 个元素的有序集合,桶的大小设置为 t + 1 以保证同一个桶内出现两个元素 n1n2 时,必定有 abs(n1 - n2) <= t 即结果为true,另外如果相邻的桶内有数字且满足与当前数字的差值的绝对值小于等于 t 则结果为 true

7、复杂度

  • 时间复杂度:O(n)O(n)
  • 空间复杂度:O(k)O(k)

8、代码

var containsNearbyAlmostDuplicate = function (nums, k, t) {
const buckets = new Map();
const genIdx = (n) => ((n + 2147483648) / (t + 1)) >> 0;

for (let i = 0; i < nums.length; i++) {
if (i > k) buckets.delete(genIdx(nums[i - k - 1]));
const idx = genIdx(nums[i]);
if (buckets.has(idx)) return true;
if (buckets.has(idx - 1) && nums[i] - buckets.get(idx - 1) <= t) return true;
if (buckets.has(idx + 1) && buckets.get(idx + 1) - nums[i] <= t) return true;
buckets.set(idx, nums[i]);
}

return false;
};

9、执行结果

image.png

· 阅读需 3 分钟

1、题干

给你一个整数数组 nums 和两个整数 kt 。请你判断是否存在 两个不同下标 ij,使得 abs(nums[i] - nums[j]) <= t ,同时又满足 abs(i - j) <= k

如果存在则返回 true,不存在返回 false

 

示例 1:

输入:nums = [1,2,3,1], k = 3, t = 0
输出:true

示例 2:

输入:nums = [1,0,1,1], k = 1, t = 2
输出:true

示例 3:

输入:nums = [1,5,9,1,5,9], k = 2, t = 3
输出:false

 

提示:

  • 0 <= nums.length <= 2 * 104
  • -231 <= nums[i] <= 231 - 1
  • 0 <= k <= 104
  • 0 <= t <= 231 - 1

 

注意:本题与主站 220 题相同: https://leetcode-cn.com/problems/contains-duplicate-iii/

2、解法1-暴力解法

双层遍历,外层从头到尾遍历,内层遍历外层元素之后的 k 个元素,如果找到两个元素差值的绝对值小于等于 t 则结果为 true

注意:内层只需要遍历后 k 个元素,因为前 k 个元素在外层遍历时已经判断过

3、复杂度

  • 时间复杂度:O(nk)O(nk)
  • 空间复杂度:O(1)O(1)

4、代码

var containsNearbyAlmostDuplicate = function (nums, k, t) {
for (let i = 0; i < nums.length; i++) {
for (let j = i + 1; j <= i + k && j < nums.length; j++) {
if (Math.abs(nums[j] - nums[i]) <= t) return true;
}
}
return false;
};

5、执行结果

执行用时: 500 ms 内存消耗: 39.1 MB

6、解法2-桶排序

遍历过程中使用桶排序维护 k 个元素的有序集合,桶的大小设置为 t + 1 以保证同一个桶内出现两个元素 n1n2 时,必定有 abs(n1 - n2) <= t 即结果为true,另外如果相邻的桶内有数字且满足与当前数字的差值的绝对值小于等于 t 则结果为 true

7、复杂度

  • 时间复杂度:O(n)O(n)
  • 空间复杂度:O(k)O(k)

8、代码

var containsNearbyAlmostDuplicate = function (nums, k, t) {
const buckets = new Map();
const genIdx = (n) => ((n + 2147483648) / (t + 1)) >> 0;

for (let i = 0; i < nums.length; i++) {
if (i > k) buckets.delete(genIdx(nums[i - k - 1]));
const idx = genIdx(nums[i]);
if (buckets.has(idx)) return true;
if (buckets.has(idx - 1) && nums[i] - buckets.get(idx - 1) <= t) return true;
if (buckets.has(idx + 1) && buckets.get(idx + 1) - nums[i] <= t) return true;
buckets.set(idx, nums[i]);
}

return false;
};

9、执行结果

image.png

· 阅读需 3 分钟

1、题干

给定一个二叉搜索树的 根节点 root 和一个整数 k , 请判断该二叉搜索树中是否存在两个节点它们的值之和等于 k 。假设二叉搜索树中节点的值均唯一。

 

示例 1:

输入: root = [8,6,10,5,7,9,11], k = 12
输出: true
解释: 节点 5 和节点 7 之和等于 12

示例 2:

输入: root = [8,6,10,5,7,9,11], k = 22
输出: false
解释: 不存在两个节点值之和为 22 的节点

 

提示:

  • 二叉树的节点个数的范围是  [1, 104].
  • -104 <= Node.val <= 104
  • root 为二叉搜索树
  • -105 <= k <= 105

 

注意:本题与主站 653 题相同: https://leetcode-cn.com/problems/two-sum-iv-input-is-a-bst/

2、解法1

DFS遍历+哈希查找:DFS遍历所有节点node,在哈希表set中查找是否存在k - node.val,若存在则返回true否则继续遍历直至结束则返回false

3、代码

var findTarget = function (root, k) {
const set = new Set();
function dfs(node) {
if (!node) return false;
if (set.has(k - node.val)) return true;
set.add(node.val);
return dfs(node.left) || dfs(node.right);
}
return dfs(root);
};

4、复杂度

时间复杂度:O(n)O(n)

空间复杂度:O(n)O(n)

5、执行结果

1.png


6、解法2

DFS遍历+DFS二分查找:DFS遍历所有节点node,利用BST特性进行DFS二分查找是否存在k - node.val,若存在则返回true否则继续遍历直至结束则返回false

这里二分搜索的时间复杂度受到二叉树平衡性的影响,最坏情况下二叉树可能退化成链表,导致搜索时间复杂度为O(n)O(n),总体时间复杂度为O(n2)O(n^2)

7、代码

var findTarget = function (root, k) {
function find(node, num) {
if (!node) return false;
if (num === node.val) return true;
return num > node.val ? find(node.right, num) : find(node.left, num);
}

function findSum(node, num) {
if (!node) return false;
return (num !== 2 * node.val && find(root, num - node.val)) || findSum(node.right, num) || findSum(node.left, num);
}

return findSum(root, k);
};

8、复杂度

时间复杂度:平衡二叉搜索树的情况下为O(nlogn)O(nlogn) ,非平衡二叉搜索树的情况下超过O(nlogn)O(nlogn),最差为O(n2)O(n^2)

空间复杂度:O(1)O(1)

9、执行结果

1.png

· 阅读需 5 分钟

1、题干

给你一个字符串 s ,考虑其所有 重复子串 :即 s 的(连续)子串,在 s 中出现 2 次或更多次。这些出现之间可能存在重叠。

返回 任意一个 可能具有最长长度的重复子串。如果 s 不含重复子串,那么答案为 ""

 

示例 1:

输入:s = "banana"
输出:"ana"

示例 2:

输入:s = "abcd"
输出:""

 

提示:

  • 2 <= s.length <= 3 * 104
  • s 由小写英文字母组成

卷不动了,滑动窗口交了

2、执行结果

1.png

3、解题思路

使用滑动窗口处理,如果当前窗口字符串长度大于已找到的最长子串长度,且在原字符串的末尾能找到另一个窗口字符串,则当前窗口字符串可以更新为最长子串;重复该动作直至窗口闭合。

  • 最长重复子串记为maxStr,窗口左右边界分别记为ij,窗口内的字符串记为curStr
  • 当窗口curStr的长度大于maxStr或者右边界j越界时,窗口左边界右移i++,并移除窗口curStr起始位置的字母
  • 当窗口curStr的长度不大于maxStr且右边界j未越界时
    • 在窗口curStr末尾追加右边界之后的字母s[j],并将窗口右边界右移j++
    • 若窗口curStr的长度大于maxStr且在字符串s尾部找到另一个curStr,则更新maxStr
  • 最后返回maxStr

4、代码

var longestDupSubstring = function (s) {
let maxStr = '',curStr = '';
for (let i = 0, j = 0; i < s.length; i++) {
curStr = curStr.replace(s[i - 1], '');
while (curStr.length <= maxStr.length && j < s.length) {
curStr += s[j], j++;
if (curStr.length > maxStr.length && s.lastIndexOf(curStr) > i) maxStr = curStr;
}
}
return maxStr;
};

5、优化

评论区有同学表示JavaC++用这个思路会超时,可能有几个方面原因:

  • 1、可能Node.js的下限比较低(受限于语言执行效率),所以Node.js能过其他不行
  • 2、字符串操作方法replace+=Java这里应该可以用StringBuilder优化,C++不太清楚能否优化
  • 3、字符串索引方法lastIndexOf没有限制边界,最坏情况下会完整遍历原字符串,但是窗口扫过的字符串实际是不需要遍历的;改成下面这样限制查找起点能快700ms~1000ms左右
 if (curStr.length > maxStr.length && s.indexOf(curStr, i + 1) > -1) maxStr = curStr;

还可以考虑手写索引方法,另外引入哈希表做缓存避免重复遍历;尝试了下,用例64(30000个v的字符串)超时,这个时候简单的缓存策略是完全失效的。

附上优化后的完整代码,只需修改 lastIndexOf 那一行

var longestDupSubstring = function (s) {
let maxStr = '', curStr = '';
for (let i = 0, j = 0; i < s.length; i++) {
curStr = curStr.replace(s[i - 1], '');
while (curStr.length <= maxStr.length && j < s.length) {
curStr += s[j], j++;
if (curStr.length > maxStr.length && s.indexOf(curStr, i + 1) > -1) maxStr = curStr;
}
}
return maxStr;
};

整体调整,更简洁的版本:

var longestDupSubstring = function (s) {
let maxStr = '';
for (let i = 0, j = 0; i < s.length; i++) {
while (j - i <= maxStr.length && j < s.length && ++j) {
if (j - i <= maxStr.length) continue;
const curStr = s.slice(i, j);
if (s.indexOf(curStr, i + 1) > -1) maxStr = curStr;
}
}
return maxStr;
};