hot100-子串-JS

一、560.和为k的子串

560. 和为 K 的子数组

提示

给你一个整数数组 nums 和一个整数 k ,请你统计并返回 该数组中和为 k的子数组的个数

子数组是数组中元素的连续非空序列。

示例 1:

复制代码
输入:nums = [1,1,1], k = 2
输出:2

示例 2:

复制代码
输入:nums = [1,2,3], k = 3
输出:2

提示:

  • 1 <= nums.length <= 2 * 104
  • -1000 <= nums[i] <= 1000
  • -107 <= k <= 107

1.暴力枚举法

暴力枚举的基本思路是通过两层循环来枚举所有可能的子数组,然后计算每个子数组的和,判断其是否等于目标值 k,如果相等则将计数器加 1。

javascript 复制代码
/**
 * @param {number[]} nums
 * @param {number} k
 * @return {number}
 */
var subarraySum = function (nums, k) {
    let count = 0;
    for (let i = 0; i < nums.length; i++) {
        let sum = 0;
        for (let j = i; j < nums.length; j++) {
            sum += nums[j];
            if (sum === k) {
                count++
            }
        }
    }
    return count;
};

2.使用前缀和与哈希表(推荐)

问题分析

给定一个整数数组 nums 和一个整数 k,要找出数组中和为 k 的子数组(连续非空元素序列)的个数。例如,对于数组 [1, 1, 1]k = 2,子数组 [1, 1] 满足和为 2,所以结果是 2

前缀和的引入

前缀和是指从数组开头到当前位置的所有元素的和。假设我们有一个数组 nums = [a0, a1, a2, ..., an],那么:

  • 前缀和 prefixSum[0] = a0
  • 前缀和 prefixSum[1] = a0 + a1
  • 前缀和 prefixSum[2] = a0 + a1 + a2
  • 以此类推,prefixSum[i] = a0 + a1 + ... + ai

对于计算子数组和,有一个重要的性质:如果我们要找从索引 j 到索引 ij <= i)的子数组和,那么这个子数组和 subarraySum(j, i) 就等于 prefixSum[i] - prefixSum[j - 1](当 j > 0 时),如果 j = 0,则 subarraySum(j, i) = prefixSum[i]

算法思路推导

我们的目标是找到满足 subarraySum(j, i) = k 的子数组个数。根据上面的前缀和性质,subarraySum(j, i) = prefixSum[i] - prefixSum[j - 1] = k,可以变形为 prefixSum[j - 1] = prefixSum[i] - k

这意味着,如果我们知道所有前缀和的值以及它们出现的次数,那么对于当前计算得到的前缀和 prefixSum[i],我们只需要检查之前是否出现过 prefixSum[i] - k 这个前缀和。如果出现过,那么就说明存在一个子数组的和为 k,并且出现次数就是 prefixSum[i] - k 出现的次数。

哈希表的作用

为了高效地存储和查询前缀和及其出现的次数,我们使用哈希表(在 JavaScript 中是 Map 对象)。具体步骤如下:

  1. 初始化​:

    • 创建一个空的 Map 对象 map,用于存储前缀和及其出现的次数。
    • 初始化 map.set(0, 1),这是因为前缀和为 0 的情况是存在的(对应空数组,空数组的和为 0),出现次数为 1
    • 初始化变量 sum = 0 来记录当前的前缀和,ans = 0 来记录和为 k 的子数组的个数。
  2. 遍历数组​:

    • 对于数组中的每个元素 num,将其加到当前前缀和 sum 中,即 sum += num
    • 检查 map 中是否存在 sum - k。如果存在,说明存在一个子数组的和为 k,将 map.get(sum - k)的值加到 ans 中。这是因为 sum - (sum - k) = kmap.get(sum - k) 表示之前出现过 sum - k 这个前缀和的次数,也就意味着有这么多组子数组的和为 k
    • 将当前前缀和 sum 及其出现的次数存入 map 中。如果 sum 已经存在于 map 中,将其出现次数加 1;否则,将其出现次数设为 1
  3. 返回结果​:

    • 遍历结束后,ans 中存储的就是和为 k 的子数组的个数,将其返回。

示例分析

以输入 nums = [1, 1, 1]k = 2 为例:

  • 初始化:map = {0: 1}sum = 0ans = 0
  • 第一次循环:
    • num = 1sum = 0 + 1 = 1
    • 检查 map 中是否有 1 - 2 = -1,没有。
    • map.set(1, 1)(现在 map = {0: 1, 1: 1})。
    • ans 不变,仍为 0
  • 第二次循环:
    • num = 1sum = 1 + 1 = 2
    • 检查 map 中是否有 2 - 2 = 0,有,ans = ans + map.get(0) = 0 + 1 = 1
    • map.set(2, 1)(现在 map = {0: 1, 1: 1, 2: 1})。
  • 第三次循环:
    • num = 1sum = 2 + 1 = 3
    • 检查 map 中是否有 3 - 2 = 1,有,ans = ans + map.get(1) = 1 + 1 = 2
    • map.set(3, 1)(现在 map = {0: 1, 1: 1, 2: 1, 3: 1})。
  • 最终返回 ans = 2,符合预期。

通过这样的方式,使用前缀和与哈希表的方法能够高效地解决"和为 K 的子数组"问题。希望以上解释能帮助你理解这个算法的原理和实现过程。

代码实现

javascript 复制代码
/**
 * @param {number[]} nums
 * @param {number} k
 * @return {number}
 */
var subarraySum = function (nums, k) {
    let map=new Map();
    // 初始设置为0的元素有1个
    map.set(0,1);
    let sum=0;
    let ans=0;
    for(let num of nums){
        // 计算元素的前缀和
        sum+=num;
        // 计数,统计前缀和为某个数共有多少个
        map.set(sum,(map.has(sum)||0)+1)
        // 判断map中是否有 sum-k 如果存在,说明存在一个子数组的和为 k
        if(map.has(sum-k)){
            // 将 map.get(sum - k)的值加到 ans 中
            ans+=map.get(sum-k);
        }
    }
    return ans;
};

二、239.滑动窗口最大值

239. 滑动窗口最大值

给你一个整数数组 nums,有一个大小为 k的滑动窗口从数组的最左侧移动到数组的最右侧。你只可以看到在滑动窗口内的 k 个数字。滑动窗口每次只向右移动一位。

返回 滑动窗口中的最大值

示例 1:

复制代码
输入: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

示例 2:

复制代码
输入:nums = [1], k = 1
输出:[1]

提示:

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

1.暴力法(超出时间限制)

O(nk)会很慢

javascript 复制代码
/**
 * @param {number[]} nums
 * @param {number} k
 * @return {number[]}
 */
function maxSlidingWindow(nums, k) {
    let result=[]
    for (let i = 0; i < nums.length - k + 1; i++) {
        let max = -Infinity;
        for (let j = i; j < i + k; j++) {
            max = Math.max(nums[j], max)
        }
        result.push(max)
    }
    return result
}

2.优化解法(双端队列,时间复杂度 O(n))​

算法思路

  1. 初始化数据结构​:

    • q 数组模拟单调栈,它的作用是存储数组 nums 中元素的下标,并且保证栈内元素对应的 nums 值从栈底到栈顶是单调递减的。这样,栈顶元素对应的 nums 值始终是当前窗口内的最大值(或者候选最大值)。
    • ans 数组用于存储每个滑动窗口的最大值,最终作为函数的返回结果。
  2. 遍历数组过程​:

    • 维护单调性 :在每次将新元素 nums[i] 考虑加入窗口时,通过 while 循环检查栈顶元素。如果当前元素 nums[i] 大于等于栈顶元素对应的 nums 值,就不断弹出栈顶元素。这是因为栈顶元素不可能是当前窗口内的最大值了,通过这样的操作保证了栈的单调性。
    • 加入新元素 :将当前元素的下标 i 压入栈 q 中,以便后续判断该元素是否在窗口内以及作为可能的最大值候选。
    • 检查窗口范围 :检查栈顶元素的下标是否已经不在当前窗口范围内(即小于等于 i - k)。如果是,说明该元素已经随着窗口的滑动移出了当前窗口,需要将其从栈顶弹出,以确保栈中元素对应的下标都在当前窗口内。
    • 记录最大值 :当窗口已经形成(即 i >= k - 1,因为前 k - 1 个元素构不成完整窗口)时,栈顶元素对应的 nums 值就是当前窗口的最大值,将其加入到 ans 数组中。
  3. 返回结果 ​:遍历完整个数组 nums 后,ans 数组中已经存储了每个滑动窗口的最大值,将其返回。

示例演示

给定的示例 nums = [1, 3, -1, -3, 5, 3, 6, 7]k = 3 为例,详细演示如何计算滑动窗口最大值的:

初始化

  • 初始化 q 为一个空数组,用于模拟单调栈,存储数组元素的下标;初始化 ans 为一个空数组,用于存储每个滑动窗口的最大值。

  • i 初始化为 0,开始遍历数组 nums

第一次循环 (i = 0)

  • nums[0] = 1,此时 q 为空,将 i = 0 压入 q,即 q = [0]

  • 因为 q 只有一个元素,它的下标 0 满足 0 >= 0 - 3(此时窗口还未完全形成,这里条件看似不成立但后续会处理),不执行 q.shift()

  • 由于 i = 0 < 3 - 1,窗口还未形成,不执行 ans.push(nums[q[0]])

第二次循环 (i = 1)

  • nums[1] = 3,因为 3 >= nums[q[q.length - 1]](即 3 >= nums[0]nums[0] = 1),执行 q.pop(),此时 q = [1]

  • i = 1 压入 qq = [1]

  • 因为 q[0] = 11 < 1 - 3 不成立,不执行 q.shift()

  • 由于 i = 1 < 3 - 1,窗口还未形成,不执行 ans.push(nums[q[0]])

第三次循环 (i = 2)

  • nums[2] = -1,因为 -1 < nums[q[q.length - 1]](即 -1 < nums[1]nums[1] = 3),不执行 q.pop()

  • i = 2 压入 qq = [1, 2]

  • 因为 q[0] = 11 < 2 - 3 不成立,不执行 q.shift()

  • 由于 i = 2 >= 3 - 1,窗口已经形成,执行 ans.push(nums[q[0]]),即 ans = [3]

第四次循环 (i = 3)

  • nums[3] = -3,因为 -3 < nums[q[q.length - 1]](即 -3 < nums[2]nums[2] = 3),不执行 q.pop()

  • i = 3 压入 qq = [1, 2, 3]

  • 因为 q[0] = 11 < 3 - 3 不成立,不执行 q.shift()

  • 由于 i = 3 >= 3 - 1,窗口已经形成,执行 ans.push(nums[q[0]]),即 ans = [3, 3]

第五次循环 (i = 4)

  • nums[4] = 5,因为 5 >= nums[q[q.length - 1]](即 5 >= nums[3]nums[3] = -3),执行 q.pop(),此时 q = [1, 2]

  • 因为 5 >= nums[q[q.length - 1]](即 5 >= nums[2]nums[2] = 3),再执行 q.pop(),此时 q = [1]

  • i = 4 压入 qq = [1]

  • 因为 q[0] = 11 < 4 - 3 不成立,不执行 q.shift()

  • 由于 i = 4 >= 3 - 1,窗口已经形成,执行 ans.push(nums[q[0]]),即 ans = [3, 3, 5]

第六次循环 (i = 5)

  • nums[5] = 3,因为 3 >= nums[q[q.length - 1]](即 3 >= nums[1]nums[1] = 1),执行 q.pop(),此时 q = [5]

  • i = 5 压入 qq = [5]

  • 因为 q[0] = 55 < 5 - 3 不成立,不执行 q.shift()

  • 由于 i = 5 >= 3 - 1,窗口已经形成,执行 ans.push(nums[q[0]]),即 ans = [3, 3, 5, 5]

第七次循环 (i = 6)

  • nums[6] = 6,因为 6 >= nums[q[q.length - 1]](即 6 >= nums[5]nums[5] = 3),执行 q.pop(),此时 q = [6]

  • i = 6 压入 qq = [6]

  • 因为 q[0] = 66 < 6 - 3 不成立,不执行 q.shift()

  • 由于 i = 6 >= 3 - 1,窗口已经形成,执行 ans.push(nums[q[0]]),即 ans = [3, 3, 5, 5, 6]

第八次循环 (i = 7)

  • nums[7] = 7,因为 7 >= nums[q[q.length - 1]](即 7 >= nums[6]nums[6] = 6),执行 q.pop(),此时 q = [7]

  • i = 7 压入 qq = [7]

  • 因为 q[0] = 77 < 7 - 3 不成立,不执行 q.shift()

  • 由于 i = 7 >= 3 - 1,窗口已经形成,执行 ans.push(nums[q[0]]),即 ans = [3, 3, 5, 5, 6, 7]

循环结束

遍历完整个数组 nums 后,ans 数组中存储了每个滑动窗口的最大值,最终返回 ans = [3, 3, 5, 5, 6, 7]

代码实现

javascript 复制代码
var maxSlidingWindow = function(nums, k) {
    let q = []; // 用于模拟单调栈,存储数组元素的下标
    let ans = []; // 用于存储每个滑动窗口的最大值
    for (let i = 0; i < nums.length; i++) {
        // 当栈不为空,并且当前元素大于等于栈顶元素对应的nums值时
        while (q.length > 0 && nums[i] >= nums[q[q.length - 1]]) {
            q.pop(); // 弹出栈顶元素,因为它不可能是当前窗口内的最大值了
        }
        q.push(i); // 将当前元素的下标压入栈中
        // 如果栈顶元素的下标已经不在当前窗口范围内(即小于等于i - k)
        if (q[0] <= i - k) {
            q.shift(); // 弹出栈顶元素,因为它已经不在当前窗口内了
        }
        // 当窗口已经形成(即i >= k - 1,因为前k - 1个元素构不成完整窗口)
        if (i >= k - 1) {
            ans.push(nums[q[0]]); // 将栈顶元素对应的nums值(也就是当前窗口的最大值)加入到结果数组ans中
        }
    }
    return ans;
};
相关推荐
Jerry1 小时前
LeetCode 459. 重复的子字符串
算法
成都渲染101云渲染66662 小时前
如何在3ds Max中实现更快、更高质量的渲染
前端·javascript·人工智能
海石4 小时前
1500分的题目,确实有实力,不过还是我略胜一筹
算法·leetcode
海石4 小时前
【记忆化搜索】条条大路通AC,走好适合你的那一条,走到后再考虑走得快
算法·leetcode
SRET6 小时前
Mineradio 沙盒隔离安装完全指南 | 一键安全运行 Electron 应用!!!
javascript·经验分享·安全·electron·aigc·音视频·ai编程
全栈前端老曹6 小时前
【MongoDB】安全与权限管理 —— 用户认证、角色权限、SSL 加密
前端·javascript·数据库·安全·mongodb·nosql·ssl
Jerry6 小时前
LeetCode 151. 反转字符串中的单词
算法
gugucoding7 小时前
31. 【C语言】堆栈与队列的实现
c语言·开发语言·数据结构·链表
ChaoZiLL8 小时前
我的数据结构3——链表(link list)
数据结构·链表
a1117768 小时前
LM 算法迭代过程动画演示(SLAM)
算法