【哈希表】LeetCode 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
【分析】
方法一:暴力解法
该题目是要找到元素和等于k的所有连续子数组的个数。那么怎么求一个数组的连续子数组呢。可以使用两层循环来实现。
n = len(nums) subnums = [] for i in range(n): for j in range(i, n): subnums.append(nums[i:j+1])
既然这样,稍作修改,就可以求元素和为k的连续子数组的个数了。
class Solution: def subarraySum(self, nums: List[int], k: int) -> int: n = len(nums) # 求原始数组长度 count = 0 # 初始化子数组个数 for i in range(n): for j in range(i, n): if sum(nums[i: j+1]) == k: count += 1 return count
提交代码,提示超出时间限制。
很明显,这么做的时间复杂度是O(n3)。因为除了两层for循环,还有一层求和。
其实,可以对上面方法做一些优化。因为我们求 nums[i: j + 1] 的下一次nums[i : j + 1 + 1],其实只比前一次多了一个元素nums[j + 1]。那么我们就没必要每次都从i开始算了。但是这么做时间复杂度依然有O(n2),依然超时。
class Solution: def subarraySum(self, nums: List[int], k: int) -> int: n = len(nums) # 求原始数组长度 count = 0 # 初始化子数组个数 for i in range(n): sum = 0 for j in range(i, n): sum += nums[j] if sum == k: count += 1 return count
前缀和
什么是前缀和:前缀和是指一个数组的某下标之前的所有数组元素的和(包含其自身)。
通常,会在前缀和首位放一个0,比如数组[1,2,3],其前缀和是[0,1,3,6]。
前缀和通常可以帮助我们快速计算某个区间内的和。比如我们要计算i, j之间的和,那么就是nums[i] + nums[i+1] + ... + nums[j],可以看作是:
nums[0] + nums[1] + ... + nums[i-1] + nums[i] + nums[i+1] + ... + nums[j] -
nums[0] + nums[1] + ... + nums[i-1]
这个式子也是preSum[j] - preSum[i-1]。
那么,我们先遍历一次数组,求出前缀和数组。之后这个数组可以代替我们最开始暴力解法的sum函数。但是很遗憾,这种做法复杂度依然还是O(n2)。
class Solution: def subarraySum(self, nums: List[int], k: int) -> int: # 要求的连续子数组 count = 0 n = len(nums) preSum = [0] # 求前缀和数组 tmp = 0 for i in range(n): tmp += nums[i] preSum.append(tmp) # 求和为k的连续子数组,求i到j之间的和 for i in range(1, n+1): for j in range(i, n+1): if preSum[j] - preSum[i-1] == k: # preSum[j] - preSum[i-1]代表着在nums数组中,前j个数之和减去前i-1个数之和 count += 1 return count
进一步优化的话,我们可以边计算前缀和,边统计。遍历过程中,我们统计历史中每一个前缀和出现的次数,然后计算到位置i(含i)的前缀和preSum减去目标k在历史上出现过几次,假如出现过m次,代表第i位以前不含(不含i)有m个连续子数组的和为presum - k,这m个和为presum - k的连续子数组,每一个都可以和presum组合成为presum - (presum - k)= k
上述示意图,红色的是当前遍历到的前缀和presum,加入它之前有两个前缀和等于presume - k(蓝色区域范围),那么很明显就会有两个连续子数组的元素和为k,对应橙色区域范围。
class Solution: def subarraySum(self, nums: List[int], k: int) -> int: # 要求的连续子数组 count = 0 n = len(nums) preSums = collections.defaultdict(int) preSums[0] = 1 presum = 0 for i in range(n): presum += nums[i] # if preSums[presum - k] != 0 count += preSums[presum - k] # 利用defaultdict的特性,当presum-k不存在时,返回的是0。这样避免了判断 preSums[presum] += 1 return count