算法学习专栏,本篇为第一章前缀和系列算法的学习。算法学习需要投入大量的时间和精力,不断的练习。本人建议从前缀和开始学习,理由是前缀和算法不需要其他算法或者数据结构为基础,适用于算法初学者(熟悉一门语言的编写。)通过学习,可以体会到算法之美(难度较低,收益较高同时非常实用的算法,直接甩掉其他竞争者 狗头。)~~
什么是前缀和算法
什么是前缀和算法呢??为了引出这个算法,首先我们来看一题使用前缀和算法的经典案例。
上链接~303. 区域和检索 - 数组不可变 - 力扣(LeetCode)
题目页面组成部分
首先我们不看题目怎么写,我们来分析一下leetcode这个题目页面的组成部分。我把它分成了四部分
- 包含题目名字、题目难度、题目是否完成(绿勾代表完成)、题目点赞数(点赞数越多说明题目越有价值) 、收藏按钮、分享按钮,相关企业(会员功能,代表企业面试出现过)
- 题目描述和介绍题目。(初学者可能看的一脸懵逼,难以集中注意力,需要强迫自己看完。不过有时看完也一脸懵,所以需要第三部分来辅助自己理解。)
- 题目示例。给出输入、输出示例用于解释题目同时验证自己对题目的理解是否正确。如果按照「输入」按照自己的理解进行题目的「输出」解答与示例的输出不一致,则说明存在题目理解的偏差,需要重新理解题目。一般官方可能也知道自己写的太抽象了,所以可能存在「解释」我们就需要通过「解释」去明白这样的「输入」为何会「输出」这样的结果,从而达到自己对题目的理解正确。
- 提示。这里的提示并不是大家一般印象中的提示,例如提示你使用什么算法、数据结构。而是对第二部分出现的对象的长度、调用频率的声明。不过对于算法练习老手而言,这些范围其实就蕴藏了提示!!提示你使用什么算法、什么数据结构!!(非常重要!!!)
如何通过「提示」来选择对应的算法
一般来说这种笔试题的时间限制是1秒或2秒。按照1秒来算,我们代码的操作次数需要控制在107~108最佳。
我们来分析一下当前这个题目可以使用什么算法。能通过该题目的总体时间复杂度是:sumRange的时间复杂度*sumRange调用次数<=10^8
- sumRange调用次数为最多调用10^4
- 通过该题目的时间复杂度最大为:108/104=10^4
- sumRange主要是用来计算数组nums,而数组nums.length<=10^4
- 通过该题目的时间复杂度最大为:10^4。即O(N)。
通过以上简单的分析我们得出,通过该题目的最大时间复杂度为O(N),所以我们可以直接遍历数组来通过该题目。
以上分析较为粗略主要是通过提示来倒退通过题目的最大时间复杂度,涉及到了时间复杂度概念,如果点赞破20我会出一篇算法时间复杂度的分析。
题目分析
接着我们来理解一下该题目的意思:给定一个数组,通过sumRange(int left,int right)来计算在nums数组中下标从 [left,right](闭区间)的元素和。
我们通过第二部分「示例1」来理解这个题目:
首先通过NumArray初始化一个数组:[-2,0,3,-5,2,-1]
接着调用sumRange():left=0、right=2。即计算nums中下标从[0,2]
中元素的和。为-2+0+3=1
同样后面的
- sumRange(2,5) = 3±5+2-1=-1
- sumRange(0,5) = -2+0+3±5+2+1=-3
相信通过以上例子,你已经完全理解了该题目的意思。同时通过上面对该题目的分析,我们可以直接通过下标从[left,right]
遍历计算得出题目所需的元素和sum。
class NumArray {
private int[] array;
public NumArray(int[] nums) {
array = nums;
}
public int sumRange(int left, int right) {
int sum = 0;
for(int i = left;i<=right;i++){
sum += array[i];
}
return sum;
}
}
正如我们所预料的这种O(N)的解法是能正常通过该题目的,并且也耗时50ms击败了20.49的用户。(一般题目不会限制内存的使用情况,同时基本上也很难写出极为消耗内存的代码。同时在当今这个时代内存的大量消耗对于代码执行时间的提升已经是微不足道的代价。所以有许多算法都是「空间换时间」纯属个人见解~)
以上的成绩对于第一次通过题目还是新手的我们也是不错的成绩了~ 第一天一上来就已经击败了1/5的对手,如此天赋何惧算法??
当然作为一个真正的高手,我们岂能停滞不前,固步自封?通过前面的分析我们发现我们最大能通过的时间耗时是104,此番通过还是存在侥幸。如果sumRange()的调用次数超过104,或者nums.lenght的长度大于10^4。那我们这种方法就饮恨于此了。
sumRange的调用次数我们不能优化(有条件的联系下leetcode客服少调用几次),所以我们要优化此方法的运行时间复杂度。
怎么优化呢??说了这么多,终于到我们本文的主角出场了—— 前缀和算法。
前缀和算法:通过名字我们能得到两个信息
- 前缀
- 和
前缀是什么?例如存在一个字符“abcdefg”。以下几种字符,那个是该字符的前缀?
- ab ✓
- abc ✓
- acd ×
1和2属于是“abcdefg”前缀字符,而3是不属于。前缀字符就是该字符下标从[0,i]
所截取的字符。必须要从0开始出发且连续。
对比到数组中,数组的前缀数组就是从下标[0,i]
所截取的子数组(子数组是下标连续的,子序列是下标可以不连续的。子数组<=子序列)。而前缀和就是对应前缀数组的所有元素和。
我们定义一个前缀和数组preSum[]
- 数组长度为nums.length+1。
- preSum[i]:定义为nums下标在
[0,i-1]
的元素和。
我们对该题目的用例进行创建一个前缀和数组:
int[] preSum;
public NumArray(int[] nums) {
preSum = new int[nums.length+1];
//从下标i=1开始,[1,nums.length]
for(int i = 1;i<=nums.length;i++){
//累加
preSum[i] = preSum[i-1] + nums[i-1];
}
}
得到这个前缀和数组有什么用呢??接下来就是前缀和数组最擅长的地方了,计算区间和。计算nums数组中的[left,right]
的元素和。只需要以下简单的一步。
- preSum[right+1] - preSum[left]
发生了什么???? 不要震惊~这就是算法的魅力。我们先来使用前缀和通过该题目,看看能不能行?
class NumArray {
int[] preSum;
public NumArray(int[] nums) {
preSum = new int[nums.length+1];
//从下标i=1开始,[1,nums.length]
for(int i = 1;i<=nums.length;i++){
//累加
preSum[i] = preSum[i-1] + nums[i-1];
}
}
public int sumRange(int left, int right) {
return preSum[right+1] - preSum[left];
}
}
没有任何悬念,轻松通过,并且时间耗时达到7ms
比之前快了整整7倍!!!,更是超过了 100% 的选手~ 少侠你的天赋之强无以言表~
哈哈打住~ 验证了我们代码的正确性,同时展现了它的强大之处~~~ 那么为什么这样计算区间和只要使用这个公式呢?其实也很简单,从我们的前缀和的定义可知。
- preSum[left]:代表nums下标从[0,left-1]的元素和。
nums[0]+nums[1]+nums[2]+...+nums[left-1]
- preSum[right+1]:代表nums下标从[0,right]的元素和。
nums[0]+nums[1]+nums[2]+...+nums[lfet-1]+nums[left]+nums[left+1]+...+nums[right]
两者相减,相同的抵消可以得出
nums[left]+nums[left+1]+...nums[right]
正好是nums下标从[left,right]
的元素和
我们通过图来分析下sumRange(2,5)怎么通过前缀和数组pre[5+1]-pre[2]
得出。
时间空间复杂度
- 构建前缀和的时间复杂度是O(N)(只需要运行一次),求区间和的时间复杂度是O(1)。空间复杂度为O(N)(N为nums.length)。
- 前面的方法求区间和的时间复杂度为O(N),空间复杂度为O(1)(只需要几个变量)
虽然前缀和的所需要的空间复杂度为O(N),但是正如前面所说这点空间消耗换与换来的性能提升来比是微不足道的。所以前缀和算法nb!!!
例题
难度都比较低,再厉害的高手也需要「筑基」看看你能做出来几道题吧~~
总结
- 前缀和算法适用于求「区间和」不过是适用于数组元素不会发生改变的数组。那么会发生改变的呢??该怎么办呢?(挖坑1)
- 前缀和属于「空间换时间」的算法或者也可以说不是算法,而是一种数组的「预处理」。
- 前缀和长度通常为原数组长度+1,从下标1开始构建。(那么能不能从下标0开始构建了?可以的话为什么不从0开始构建呢?)(挖坑2)
- 前缀和计算区间和的公式为:
preSum[right+1] - preSum[left]
。 - 这种前缀和操作的数组是一维数组,所以又称为「一维数组前缀和」(那么是不是存在二维数组前缀和呢???)(挖坑3)
额,不知不觉就挖了3个坑?不是我不想一次写完,而是字数已经够多6k+,太多了怕没人看啊~~ 狗头。
你:是不是想多水几篇???
哈哈,时间有限,麻烦大家多多点赞,关注。想先填哪个坑的可以在评论区留言,下期很快就来了~~