🔥1.背景小故事
小兔子兔八哥家里有超级多的萝卜坑,每个坑里都有一定的萝卜数量。兔八哥的妈妈为了考验兔八哥,要求每次给它两个坑位x和y,让它在一定时间内告诉自己从x坑到y坑一共有多少个萝卜呢?兔八哥每次都要跑到坑位x数有多少个萝卜,再跑去x+1坑位....直到y,当他兴高采烈的得到答案回家时,妈妈却告诉它——你超时啦!
为什么会超时呢?
兔八哥冥思苦想这个问题,每次自己都跑去萝卜坑一个个数,真是太笨了,为什么不拿个本子去记下来呢?兔八哥懊恼的拍了拍脑袋,可是再一想,每次妈妈给定x,y。我还是得用坑x+坑x+1+坑x+2一直到坑y。这太慢了!妥妥的O(n)的时间复杂度。
直到兔八哥的朋友小黑兔来了以后,兔八哥向他说了自己的烦恼,小黑说这还不简单?于是两人拿了本子去了萝卜坑。先用数组a记录了每个萝卜坑的数量,a[i]表示第i个坑的萝卜数量。然后小黑一阵计算以后给了兔八哥一个数组S,告诉小黑告诉兔八哥每次妈妈告诉你x和y后,用S[y]-S[x-1]就是答案啦。
兔八哥照做以后,在妈妈念出x和y后下一秒就能给出答案,妥妥的O(1)时间复杂度。而这个神秘的数组S——就是前缀和数组!
🔆 2.什么是前缀和数组?
其实通过背景小故事,就能很清楚的感受到什么是前缀和数组,以及它的使用场景。它是一种通过对原静态数组预处理得到一个数组,该数组可以用于快速求出原数组的一段区间和。
原数组a[i]的含义——第i个元素的值
a[i]的前缀和数组s[i]的含义——数组a中前i个元素的和
如该图,上面的数组为原数组,则下面的数组为改数组对应的前缀和数组。
😾3.如何预处理得到前缀和数组
首先,我们要知道,前缀和数组s的下标含义s[i]是前i个元素之和。则s[i-1]的含义是前i-1个元素之和,此时我们在s[i-1]的基础上,所以我们可以得知:
但要注意一个问题,一般我们的前缀和数组s[0]一定是等于0的,代表前0个元素之和为0。而且也是为了防止出现下标越界的问题,比如当公式内i等于0时,后面就会出现越界。
还有一个很重要的问题。
这个预处理公式并不是一成不变的,这是因为原数组的首位数有可能是下标从0开始,也有可能下标从1开始。当下标从0开始时,原数组第i个元素是a[i-1],当下标从1开始时,原数组第i个元素是a[i]。
所以在原数组下标为1的情况下,求前缀和的公式也可能是这样:
大家应该灵活变通。
🎃 4.前缀和数组的使用
我们通过预处理得到了前缀和数组,那么如何使用它呢?
我们还是通过前缀和的含义出发,假设我们要得到第x个元素到第y个元素之和。而s[y]的值是前y个元素之和,s[x-1]的含义是前n-1个元素之和,两者作差即可得到第x个元素到第y个元素之和。
公式为:
这里也体现了为什么前缀和的下标要从1开始,如果从0开始,那么在x-1时又容易产生越界问题。
🍋5.前缀和模板题练习
1)、区域和检索
题目链接:区域和检索https://leetcode.cn/problems/range-sum-query-immutable/
实打实的一维前缀和裸题,我们以arr为前缀和数组,先预处理好,然后直接在函数中使用即可,注意这里给定的原数组下标是从0开始。
class NumArray { int[] arr; public NumArray(int[] nums) { int n=nums.length; arr=new int[n+1]; for(int i=0;i<n;i++){ arr[i+1]=arr[i]+nums[i]; } } public int sumRange(int left, int right) { return arr[right+1]-arr[left]; } }
2)、连续的子数组和
题目链接:连续的子数组和https://leetcode.cn/problems/continuous-subarray-sum/
题目分析,首先我们要知道同余定理。也就是:
如果我们想找到一段子数组的和是k的倍数,也就说明我们要在前缀和数组中找到两个和为a和b的满足以上式子的关系,同时得保证两者的距离不能小于2。由于是回答是否存在,不需要找出具体的子数组,所以我们可以用set存储前缀和数组中每个元素对k取余的值。
代码转换:
class Solution { public boolean checkSubarraySum(int[] nums, int k) { int n=nums.length; if(n<2) return false; int[] arr=new int[n+1]; for(int i=0;i<n;++i){ arr[i+1]=arr[i]+nums[i]; } Set<Integer> set=new HashSet<>(); for(int i=2;i<n+1;i++){ set.add(arr[i-2]%k); if (set.contains(arr[i]%k)) return true; } return false; } }
3)、连续数组
给定一个二进制数组 nums , 找到含有相同数量的 0 和 1 的最长连续子数组,并返回该子数组的长度。
题目链接:连续数组 https://leetcode.cn/problems/contiguous-array/
这道题虽然不是前缀和裸题,却是运用了前缀和的思想,我们知道:数组中只有0和1两个数字,如果我们用变量count记录,遇到0时count--,遇到1时count++。如果一段区间0和1的个数一样,那么count在这两个位置的值就一样,我们用Map来记录,count值为key,下标为value。当遍历到某个位置时,如果前面某个位置的count值和此时位置一样,说明两点区间的0和1个数一样,更新答案的最大值。
class Solution { Map<Integer,Integer> map=new HashMap(); //记录答案 int res=0; int count=0; public int findMaxLength(int[] a) { int n=a.length; map.put(0,-1); for(int i=0;i<n;++i){ if(a[i]==0) --count; else count++; if(map.containsKey(count)){ res=Math.max(res,i-map.get(count)); }else{ map.put(count,i); } } return res; } }