一、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
到索引 i
(j <= 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
对象)。具体步骤如下:
-
初始化:
- 创建一个空的
Map
对象map
,用于存储前缀和及其出现的次数。 - 初始化
map.set(0, 1)
,这是因为前缀和为0
的情况是存在的(对应空数组,空数组的和为0
),出现次数为1
。 - 初始化变量
sum = 0
来记录当前的前缀和,ans = 0
来记录和为k
的子数组的个数。
- 创建一个空的
-
遍历数组:
- 对于数组中的每个元素
num
,将其加到当前前缀和sum
中,即sum += num
。 - 检查
map
中是否存在sum - k
。如果存在,说明存在一个子数组的和为k
,将map.get(sum - k)
的值加到ans
中。这是因为sum - (sum - k) = k
,map.get(sum - k)
表示之前出现过sum - k
这个前缀和的次数,也就意味着有这么多组子数组的和为k
。 - 将当前前缀和
sum
及其出现的次数存入map
中。如果sum
已经存在于map
中,将其出现次数加1
;否则,将其出现次数设为1
。
- 对于数组中的每个元素
-
返回结果:
- 遍历结束后,
ans
中存储的就是和为k
的子数组的个数,将其返回。
- 遍历结束后,
示例分析
以输入 nums = [1, 1, 1]
,k = 2
为例:
- 初始化:
map = {0: 1}
,sum = 0
,ans = 0
。 - 第一次循环:
num = 1
,sum = 0 + 1 = 1
。- 检查
map
中是否有1 - 2 = -1
,没有。 map.set(1, 1)
(现在map = {0: 1, 1: 1}
)。ans
不变,仍为0
。
- 第二次循环:
num = 1
,sum = 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 = 1
,sum = 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.滑动窗口最大值
给你一个整数数组
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))
算法思路
-
初始化数据结构:
q
数组模拟单调栈,它的作用是存储数组nums
中元素的下标,并且保证栈内元素对应的nums
值从栈底到栈顶是单调递减的。这样,栈顶元素对应的nums
值始终是当前窗口内的最大值(或者候选最大值)。ans
数组用于存储每个滑动窗口的最大值,最终作为函数的返回结果。
-
遍历数组过程:
- 维护单调性 :在每次将新元素
nums[i]
考虑加入窗口时,通过while
循环检查栈顶元素。如果当前元素nums[i]
大于等于栈顶元素对应的nums
值,就不断弹出栈顶元素。这是因为栈顶元素不可能是当前窗口内的最大值了,通过这样的操作保证了栈的单调性。 - 加入新元素 :将当前元素的下标
i
压入栈q
中,以便后续判断该元素是否在窗口内以及作为可能的最大值候选。 - 检查窗口范围 :检查栈顶元素的下标是否已经不在当前窗口范围内(即小于等于
i - k
)。如果是,说明该元素已经随着窗口的滑动移出了当前窗口,需要将其从栈顶弹出,以确保栈中元素对应的下标都在当前窗口内。 - 记录最大值 :当窗口已经形成(即
i >= k - 1
,因为前k - 1
个元素构不成完整窗口)时,栈顶元素对应的nums
值就是当前窗口的最大值,将其加入到ans
数组中。
- 维护单调性 :在每次将新元素
-
返回结果 :遍历完整个数组
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
压入q
,q = [1]
。 -
因为
q[0] = 1
,1 < 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
压入q
,q = [1, 2]
。 -
因为
q[0] = 1
,1 < 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
压入q
,q = [1, 2, 3]
。 -
因为
q[0] = 1
,1 < 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
压入q
,q = [1]
。 -
因为
q[0] = 1
,1 < 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
压入q
,q = [5]
。 -
因为
q[0] = 5
,5 < 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
压入q
,q = [6]
。 -
因为
q[0] = 6
,6 < 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
压入q
,q = [7]
。 -
因为
q[0] = 7
,7 < 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;
};