前端刷题路-Day92:和为K的子数组(题号560)

333 阅读5分钟

这是我参与8月更文挑战的第26天,活动详情查看:8月更文挑战

和为K的子数组(题号560)

题目

给定一个整数数组和一个整数 k,你需要找到该数组中和为 k 的连续的子数组的个数。

示例1:

输入:nums = [1,1,1], k = 2
输出: 2 , [1,1] 与 [1,1] 为两种不同的情况。

说明:

  1. 数组的长度为 [1, 20,000]
  2. 数组中元素的范围是 [-1000, 1000] ,且整数 k 的范围是 [-1e7, 1e7]

链接

leetcode-cn.com/problems/su…

解释

这题啊,这题是经典哈希表小题目。

虽说是哈希表题目,但正常人看到这题的第一想法就是暴力求解,不管那么多,写出来就行了,优化什么的后续再说。

回到题目本身,这题的重点就是连续的子数组的个数,这个连续给出来后就十分简单了。

要想求整个数组中连续的子数组和为k的总个数,那必须要遍历数组中的每个元素,求出每个位置出现和为k区间的个数,没到一个新的位置都需要做相应的处理。

那要做什么处理呢?首先,因为是连续的,所以需要倒推来进行操作,从后往前进行和的累计,如果遇到k了,那就在count上加一,注意,此时不能停止累加,因为数组中元素有可能是负值,也就是说继续累加有可能出现第二个k,如果上一个数字是0也有可能出现多个k的情况。

暴力法的介绍就到这里了,之后跑了一下,直接超时,以为自己的思路哪里有问题,这一步暂且不表。事后看了下官方答案,发现他也用的这种方法,并且是推荐的第一种。笔者寻思这思路也没啥不一样啊,改了半天不好使,那不如直接复制下官方答案跑一手,结果您猜怎么着?**的也超时了!这谁能想得到,真是活久见。

既然这个方法也超时了,那还有没有别的办法呢?显然是有的,那就是我们的哈希表。

这方法笔者也没想出来了,最后也是看的官方答案,这里就按照官方解释的思路来介绍了。

先定义一个数组pre,别问我为啥不叫sum,因为官方就是这么叫的。这个数组用来存什么呢?用来存每个位置到数组开头元素的和,那么现在pre[i]就代表这数组从0到i中所有数字的和。

那根据题意,需要找出[i, j]区间内数字和为k的所有可能性,换成上面的公式就是:

pre[i] - pre[j - 1] === k

交换下位置可以得到这个公式:

pre[j - 1] === pre[i] - k

那么此时只需要考虑整个数组中有多少个和为pre[i] - kpre[j]即可,先搞他一个Map来存储,这里为什么不用数组呢?就是因为有重复和结果的出现,如果用正常的DP数组来存储,必然会出现重复值的情况,这就不好累计了,如果使用Map,可以在原值上增加个数,这就简单不少了。

回到这个Map,这个Map用和作为key,出现次数作为value,只要在循环数组的过程中不断更新Map,累计目标值出现的次数,最后即可拿到我们想要的结果。

那在循环内部具体要做什么操作呢?

首先需要判断Map中有没有pre[i] - k的值,如果有,在count上进行累计操作

接下来在Map上设置当前pre的值,如果当前位置到数组头部的元素和已经存在了,那就自增一,否则初始化赋值为1即可。

到这也就完事了,其实感觉这题看看就得了,前端面试基本上遇不到。

自己的答案(暴力)

var subarraySum = function(nums, k) {
  let count = 0
  for (let i = 0; i < nums.length; i++) {
    let sum = 0
    for (let j = i; j > -1; j--) {
      sum += nums[j]
      if (sum === k) {
        count++
      }
    }    
  }
  return count
};

经典小暴力,最后的结果也是经典小超时,倒在了最后一个测试用例上,木得办法,官方答案也是倒在了最后一个测试用例上。

更好的方法(哈希表)

哈希表的思路在上面说过了,现在来看看具体实现👇:

var subarraySum = function(nums, k) {
  const map = new Map()
  map.set(0, 1)
  let count = 0
  let pre = 0
  for (const num of nums) {
    pre += num
    if (map.has(pre - k)) {
      count += map.get(pre - k)
    }
    map.set(pre, (map.has(pre) ? map.get(pre) : 0)+ 1)
  }
  return count
};

这里要注意的有两点:

  1. 由于是从左往右遍历,pre就不用存成一个数组了,用一个值来累计即可

  2. 关于map.set(0, 1)

    这块内容记得疼悦悦宝贝哦~老哥说的很好:

    关于mp.put(0, 1); 这一行的作用就是为了应对 nums[0] +nums[1] + ... + nums[i] == k 的情况的, 也就是从下标 0 累加到下标 i, 举个例子说明, 如数组 [1, 2, 3, 6], 那么这个数组的累加和数组为 [1, 3, 6, 12] 如果 k = 6, 假如map中没有预先 put 一个 (0, 1) , 如果此时我们来到了累加和为 6 的位置, 这时map中的情况是 (1, 1), (3, 1), 而 mp.containsKey(pre - k) , 这时 pre - k 也就是 6 - 6 = 0, 因为 map 中没有 (0, 1) 所以 count 的值没有加一, 其实这个时候我们就是忽略了从下标 0 累加到下标 i 等于 k 的情况, 我们仅仅是统计了从下标大于 0 到某个位置等于 k 的所有答案

    想看更多内容的同学可以点击这里,老哥说的很详细,这里不多赘述

理解完这两点后这种解法就没什么困难的了,内部逻辑并不复杂,只是某些细小的点可能不好太GET,慢慢来嘛~



PS:想查看往期文章和题目可以点击下面的链接:

这里是按照日期分类的👇

前端刷题路-目录(日期分类)

经过有些朋友的提醒,感觉也应该按照题型分类
这里是按照题型分类的👇

前端刷题路-目录(题型分类)