前缀和(Prefix Sum)
定义:前缀和是一种重要的预处理,能大大降低查询的时间复杂度。结合Hash缓存,能够进一步优化提升算法执行效率
prefixSum [0] = 0 【备注:此处不要定义prefix[0]=nums[0],这种定义违背前缀和的意义】
prefixSum [1] = prefixSum[0] + nums[0] = 0 + num[0]
…
prefixSum [i] = prefixSum [i-1] + nums [i]。
由此推导出两个变换公式:
(1)nums[某一项] = 两个相邻 前缀和 之差:
nums[x] = prefixSum[x] - prefixSum[x - 1]
(2)从left到right的元素和等于prefixSum[right+1] –prefixSum[left];
例题:
给你一个整数数组 nums
和一个整数 k
,请你统计并返回 该数组中和为 k
的连续子数组的个数 。
代码
public static int subarraySum(int[] nums, int k) {
// 计算前缀和
int[] sum = new int[nums.length+1];
sum[0] = 0;
for (int i = 0; i < nums.length; i++) {
sum[i+1] = sum[i] + nums[i];
}
// 遍历数组,sum[j] - sum[i]就是i-j个元素的和,避免多次累加
int count = 0;
for (int i = 0; i < sum.length; i++) {
for (int j = i+1; j < sum.length; j++) {
if (sum[j] - sum[i] == k) {
// 注意此处循环不能跳出,例如 [1,-1,0],前两个为零,前三个相加也为零
count++;
}
}
}
return count;
}
class Solution:
def subarraySum(self, nums: List[int], k: int) -> int:
sumArry = [0]
for i in nums:
sumArry.append(sumArry[-1] + i)
res = 0
for i in range(len(sumArry)):
for j in range(i + 1, len(sumArry)):
if sumArry[j] - sumArry[i] == k:
res += 1
return res
题目
给你一个整数数组 nums 和一个整数 k ,编写一个函数来判断该数组是否含有同时满足下述条件的连续子数组:子数组大小 至少为 2 ,且子数组元素总和为 k 的倍数。
如果存在,返回 true ;否则,返回 false 。如果存在一个整数 n ,令整数 x 符合 x = n * k ,则称 x 是 k 的一个倍数。0 始终视为 k 的一个倍数分析:
题目要求的是能够整除k 的连续子数组的和,因此可以考虑使用前缀和来求解 。
代码
public boolean checkSubarraySum1(int[] nums, int k) { // 长度小于2,直接返回false if(nums.length<2) return false; // 求前缀和数组 int[] ints = new int[nums.length+1]; ints[0]=0; for (int i = 0; i < nums.length; i++) { ints[i+1]=ints[i]+nums[i]; } // 遍历前缀和数组,遇到满足要求的,返回true for (int i = 0; i < ints.length; i++) { for (int j = i; j < ints.length; j++) { int x = ints[j]- ints[i]; if (j-i+1 >2 && (x)%k == 0){ return true; } } } return false; }
很遗憾,这个解法在LeetCode上超时了,需要优化 0_o''
算法优化:
同余定理:
如果两个数被同一个数整除后,余数相同,那么这两个数的差值可以被该数整除
推理公式:
(sum[i] - sum[j]) % k == 0
sum[i] % k == sum[j] % k
演变:
如果数组前(i-1)个数字的和整除k后余数为r,那么前r个数字的和整除k后余数为(r+nums[i])%k;
代码
public boolean checkSubarraySum1(int[] nums, int k) { // 定义一个map,key存放前i个数组对K的余数,value存放上一个值为key的坐标 Map<Integer,Integer> map = new HashMap<>(); int remain = 0; for (int i = 0; i < nums.length; i++) { // 使用同余定理转换的式子 remain = (remain + nums[i]) % k; // 如果remain为0,说明前i 个数可以整除K // 只要i>=1,就可以返回true if (remain == 0 && i>=1) { return true; } // 如果不存在这个余数,那么就存起来 // value为第一次出现这个余数的坐标 if (!map.containsKey(remain)) { map.put(remain, i); } // 如果已经存在这个余数了,那么可以用同余定理进行判断 // 拿到上一次出现这个余数的坐标 // 如果当前坐标减去之前的坐标大于等于2,那么说明有子数组可以整除K if (map.containsKey(remain) && (i - map.get(remain)) >=2) { return true; } } return false; }
代码2
public boolean checkSubarraySum2(int[] nums, int k) { // 定义一个map,保存已经存在的前缀和,key是前缀和,value是第一次出现这个前缀和的坐标 Map<Integer, Integer> map = new HashMap<>(); // 定义左边界,即数组第0位开始之前的前缀和,值为0,坐标定义为-1; map.put(0, -1); // 定义x就是前缀和 int x = 0; // 遍历数组 for (int i = 0; i < nums.length; i++) { // 求前缀和的公式 x = (x + nums[i]) % k; // 已经出现过这个前缀和了,说明存在子数组的和刚好整除k if (map.containsKey(x)) { // 如果长度大于2,就返回true if ((i - map.get(x)) >= 2) { return true; } } else { // 不存在这个前缀和,就把这个前缀和放到map中 map.put(x, i); } } // 其余情况返回false。 return false; }
def checkSubarraySum1(self, nums: List[int], k: int) -> bool: cur_dict = {0: -1} x = 0 for index, value in enumerate(nums): x = (x + value) % k if x not in cur_dict.keys(): cur_dict[x] = index else: if index - cur_dict.get(x) >= 2: return True return False