数组篇 - 前缀和+哈希表 - leetcode 523. 连续的子数组和 + leetcode 560. 和为 k 的子数组 + 525.连续数组(medium)

✨前缀和数组的应用场景

前缀和数组经常用来求解「子数组之和」相关的问题

💡思考一个很简单的场景:对于原数组 nums 的任意一个子数组,如何在 O(1) 的时间内得到其元素和?

👉用数组 nums 的前缀和数组 preSums 就很方便算出来了

586966c51b29fa1f5dfef692ccdce7ad.png

适配上图的公式:sum(nums[p+1...q]) = preSums[q] - preSums[p]  (p < q)

 

💡 进一步思考:公式中的 p 最小是多少?

答案:最小是 -1,你可能会疑问索引 -1 不是越界了嘛

这是因为这样才能通过公式算出 nums[0...x] 的子数组和,我们不妨把 preSums 想象成在它的最左侧有一个索引为-1,值为 0 的一个元素,这样一来, nums[0...x] = preSums[x] - PreSums[-1] = preSums[x] 就也能通过公式算出来了

 

 

 

523.连续的子数组和

1. 题目描述

跳转至 leetcode 作答界面

给你一个整数数组 nums 和一个整数 k ,编写一个函数来判断该数组是否含有同时满足下述条件的连续子数组:

  • 子数组大小 至少为 2 ,且
  • 子数组元素总和为 k 的倍数。

如果存在,返回 true ;否则,返回 false 。

如果存在一个整数 n ,令整数 x 符合 x = n * k ,则称 x 是 k 的一个倍数。0 始终视为 k 的一个倍数。

 

示例 1:

输入:nums = [23,2,4,6,7], k = 6

输出:true

解释:[2,4] 是一个大小为 2 的子数组,并且和为 6 。

 

示例 2:

输入:nums = [23,2,6,4,7], k = 6

输出:true

解释:[23, 2, 6, 4, 7] 是大小为 5 的子数组,并且和为 42 。 42 是 6 的倍数,因为 42 = 7 * 6 且 7 是一个整数。

 

示例 3:

输入:nums = [23,2,6,4,7], k = 13

输出:false

 

提示:

  • 1 <= nums.length <= 10^5
  • 0 <= nums[i] <= 10^9
  • 0 <= sum(nums[i]) <= 2^31 - 1
  • 1 <= k <= 2^31 - 1

 

签名函数

def checkSubarraySum(self, nums: List[int], k: int) -> bool:

 

2. 思路/题解

提示:前缀和数组 preSums 通常搭配哈希表一起使用

根据本题要求

  • 如果 preSums[q]−preSums[p]k 的倍数,且 q−p ≥ 2 ,则可以返回 True

 

那如何进行判断preSums[q]−preSums[p] 是否为 k 的倍数这一过程 ?

先验知识:当 preSums[q]−preSums[p] 为 k 的倍数时,也就是 (preSums[q] - preSums[p]) % k = 0 时,preSums[q] % k = preSums[p] % k【preSums[q] 和 preSums[q] 除以 k 的余数相同】

👉利用先验知识」中 preSums[q] % k = preSums[p] % k 这个条件,这道题就可以使用哈希表存储 preSums[i] % k 的每个余数第一次出现的下标

 

重点步骤

  • 定义哈希表为 valToIndex , 先将 key=0, val=-1 存入 valToIndex 中,这样如果遇到 preSums[i] 本身就是 K 的倍数的情况,就不会被忽略了 

前面也说了,可以将 preSums 想象成在它的最左侧有一个索引为 -1,值为 0 的元素,题目说 0 始终为 k 的一个倍数,所以 preSums[-1] % k = 0,理应放入 valToIndex 中

  • 然后遍历 preSums 中的每个数,当遍历到 preSums[i] 时
    • 如果 preSums[i] % k 在 valToIndex 中已存在,则取出该余数在哈希表中对应的下标 preIndex( preSums[i] % k == preSums[preIndex] % k ),所以 nums 从下标 preIndex + 1 到下标 i 的子数组的元素和一定为 k 的倍数,如果 i - preIndex >=2,则找到了一个大小至少为 2 且元素和为 k 的倍数的子数组,返回 true 就好了
    • 如果 preSums[i] % k 在哈希表 valToIndex 中不存在,则将当前余数和当前下标 i 的键值对存入哈希表中,继续后面的遍历

后续思考点:由于哈希表存储的是每个余数第一次出现的下标,因此当遇到重复的余数时,根据当前下标和哈希表中存储的下标计算得到的子数组长度是以当前下标结尾的所有子数组满足元素和为 k 的倍数的子数组长度中的最大值。本题只要满足子数组最大长度 >= 2,即存在符合要求的子数组

 

3. 具体代码

class Solution:
    def checkSubarraySum(self, nums: List[int], k: int) -> bool:
        n = len(nums)
        # 构造前缀和数组
        preSumArr = [0] * n
        preSumArr[0] = nums[0]
        for i in range(1,n):
            preSumArr[i] = preSumArr[i-1] + nums[i]

        # 遍历前缀和数组,构造哈希表
        # key: preSumArr 中元素 %k 得到的余数 , val: 首次出现 key 这个余数所对应的 preSumArr 元素的下标
        valToIndex = {}
        valToIndex[0] = -1 # 初始化最早出现 0 这个余数的下标,设为 -1,但凡 preSumArr[i] %k = 0 说明 nums[0...i] 这个子数组和即为 k 的倍数
        for i in range(n):
            val = preSumArr[i] % k 
            if val not in valToIndex:
                valToIndex[val] = i
            else:
                if i - valToIndex[val] >= 2:
                    return True

        return False

 

 

560.和为 K 的子数组

 

1. 题目描述

跳转至 leetcode 作答界面

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

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

示例 1:

输入:nums = [1,1,1], k = 2

输出:2

示例 2:

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

输出:2

 

提示:

  • 1 <= nums.length <= 2 * 10^4
  • -1000 <= nums[i] <= 1000
  • -10^7 <= k <= 10^7

 

函数签名:

def subarraySum(self, nums: List[int], k: int) -> int:

 

 

2. 思路/题解

2.1 基本方法

有了上一题的经验,对子数组的和为 k 这几个字就有敏感度了,知道要用前缀和数组去做,通过读题,稍作思考一下,应该就会有如下的思路:

def subarraySum(self, nums: List[int], k: int) -> int:
    n = len(nums)
    # 构建前缀和数组 preSum
    build(preSums[0...n+1])  # preSums[i] 代表数组 nums 从下标 0 到 i-1 的元素和

	# 存储结果的变量
    res = 0

    # 对 preSum 遍历,每次 preSum[left] 为基准,看 preSum[left+1:n+1] 中是否有元素 preSum[j] 与 preSum[left] 的差为 k,
    # 有的话就说明 nums[left...j-1] 子数组之和为 k,然后更新结果
    for left in range(len(nums)):
        for right in range(left, len(nums)):
            if preSum[right + 1] - preSum[left] == k:
                res += 1
    return res
            

但这个思路的时间复杂度为 O(N²),十分低效

 

此时,可以想到,而前缀和数组往往可以和哈希表搭配着一起使用,来优化效率...

 

2.2 哈希表优化

看下面这个例子...

假设输入为:nums=[1, -1, 0] , k = 0

那我们的预期输出应该为:3

因为找到满足元素之和等于 0 的子数组有:[1, -1, 0] , [1, -1], [0]

在思考该如何利用哈希表之前,我们可以先看看前缀和数组 preSums 是什么样子的

对于 nums:[1, -1, 0],  preSum : [0, 1, 0, 0]  (此处的 preSums[i] 是 nums[-1...i-1] 的子数组和,i >= 0, 假设 nums[-1] 为 0)

下面画一张图来说明怎么从 preSum 中找出所有满足元素之和为 0 的子数组156a31c613e94f92ade52f504fbfed83.png

看上面这张图,以遍历到了 preSums[3],也就是最后一个 0 为例 ,那此时要做的事,无非就是在 preSums 中找这个 0 前面还有几个 0,官方点说就是 preSums[3] 的前面有没有 preSums[i] , 使 preSums[3] - preSums[i] = 0,发现 preSums[0] 和 preSums[2] 这两个都满足,于是 res += 2

那我们又是怎么知道前面满足条件的有多少个呢?上面这个 2 怎么得到的呢?

如果在 hashmap 中存放了前缀和数组中每个不同的值出现的次数,那我们在 preSums 中遍历到 preSums[i] 时,就能在 hashmap 中查到之前遍历过的 preSums 元素中,值为 preSums[i] - k 的元素出现过几次,然后把这个次数加到结果变量,再给 hashmap 中  preSums[i] 这个 key 的 value 上加上 1 即可

 

 

3. 具体代码 (hashmap 优化后)

class Solution:
    def subarraySum(self, nums: List[int], k: int) -> int:
        # 看到子数组之和,肯定是利用前缀和数组
        n , res = len(nums), 0
        preSums = [0] * (n + 1) 
        count = {0:1}

        for i in range(1, n+1):
            preSums[i] = preSums[i-1] + nums[i-1]
            need = preSums[i] - k
            
            if need in count:
                res += count[need]
                
            count[preSums[i]] = count.get(preSums[i], 0) + 1

        return res

 

 

525.连续数组

1. 题目描述

给定一个二进制数组 nums , 找到含有相同数量的 0 和 1 的最长连续子数组,并返回该子数组的长度。

 

示例 1:

输入: nums = [0,1]
输出: 2
说明: [0, 1] 是具有相同数量 0 和 1 的最长连续子数组。

示例 2:

输入: nums = [0,1,0]
输出: 2
说明: [0, 1] (或 [1, 0]) 是具有相同数量0和1的最长连续子数组。

 

提示:

  • 1 <= nums.length <= 10^5
  • nums[i] 不是 0 就是 1

 

2. 思路/题解

这道题的本质和 560 一样,因为题目可以转化为「将原数组中的所有 0 转换成 -1 后,求和为 0 的最长子数组」,但是要注意的是,这里哈希表存储的内容和 523 几乎一样, 这道题哈希表存的是 preSums[i] 第一次出现的下标

求解示意图

0adaa90ce10c4b17b6586a9f866ac6fc.png

重点步骤

  • 构造前缀和数组 preSums,长度为 n+1,第一个位置存 0,从第 2 个位置开始存 nums 的前缀和
  • 如果 preSums[right] - preSums[left] == 0 , 即 preSums[right] = preSums[left], 那就说明 nums[left...right-1] 的子数组之和为 0 ,子数组长度为 right - left , 然后需要的话就更新结果

 

 

3. 具体代码

class Solution:
    def findMaxLength(self, nums: List[int]) -> int:
        # 将 0 视为 -1,那本题就是找和为 0 的最长子数组
        # 构造前缀和数组
        n = len(nums)
        preSums = [0] * (n+1)
        for i in range(1, n+1):
            preSums[i] = preSums[i-1] + (-1 if nums[i-1] == 0 else 1)

        res = 0
        valToIndex = {0: 0}

        for i in range(1, n+1):
            if preSums[i] in valToIndex:
                tmplen = i - valToIndex[preSums[i]]
                res = max(tmplen, res)
            else:
                valToIndex[preSums[i]] = i

        return res

 

 

 

 

 

 

 

 

 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值