最大子数组和
就快要秋招了,又该准备刷题了。其实一直都很反感刷题这件事,大一的时候就刷OJ,找实习的时候刷LeetCode,找工作的时候也刷,考研的时候又刷一回,现在又得再刷一回。刷题这事吧,挺花时间,但是收益并不高,这几天刷题,过几天去忙点其他事,基本就忘得差不多了。很无奈,你不刷,企业就觉得你基础不行,coding不行。想想后面还得再回去看八股文,头都大了,计算机专业课,可以说也是反反复复学了好多遍吧,也是学了忘,忘了学,但是实际用上的知识,不多。
这是上个月的事情了。信软的妹妹给我发来一道题,LeetCode的第53题,最大子数组和。这个题目要求在一个给定的整数数组中,找到一个连续子区间,使得子区间中元素的和最大。
既然是妹妹发的,那肯定不能做不出来,一时间胜负欲拉满。我当时看完题目的时候,坚信用双指针能解出来。理由是,要寻找的子区间是一个连续的区间,两个指针之间也是连续的子区间,如果能够通过移动两个指针,使它们逐步向目标子数组靠近,不就可以解决了吗?
一开始是想着能不能只经过一次遍历,就能找到最大子数组和呢?先初始化左右指针,初始时刻都指向数组首元素,结合滑动窗口的思想,右指针不断右移,扩大窗口的大小,左指针也向右移动,但是不超过右指针,维护一个求和的变量,右指针每移动一次,就把指向的元素加进来,左指针每移动一次,就把指向的元素减去,这样,求和变量就能记录窗口中元素的和,只需要找到目标窗口就可以了,也就是什么时候右指针该移动,什么时候左指针该移动的事了。
1 | class Solution { |
这是一开始的想法,右指针一路狂奔,在狂奔的过程中记录最大和,然后左指针再追赶右指针,进一步缩小子区间的长度。这个思路看起来好像没啥问题,提交的时候信心满满,结果也就A了一部分的测试样例而已。
问题在于max
被初始化为nums[0]
了,题目中给定的整数数组是允许出现负数的,但当数组元素全是负数且按照递增顺序排列时,如[-2, -1]
,上面的代码就不work了,因为往右走元素和会越来越小,max
一直是nums[0]
的值。
接下来就开始走歪路了,想着把数组元素全是负数(或者0)且按照递增顺序排列的这种情况做特殊处理:
1 | int k = 1; |
再一提交,很好,现在数组元素全是负数(或者0)且按照递增顺序排列的这种情况解决了,但是,如果数组不是有序的,比如,[-2, -3, -1]
就不在上述代码的处理范围之内了,前面的[-2, -3]
仍然阻碍了max
变量的更新。那这样的话,直接不管顺序了,如果数组全是负数,找最大值就行了:
1 | int max = nums[0]; |
处理完数组元素全是负数的情况,又出现了左侧负数和太大导致max
依旧不能正常更新的情况,如数组[0, -3, 1, 1]
,最大和子数组是[1, 1]
,但是右指针根本就没有移动过。处理这种情况的思路简单又粗暴,把数组逆向再处理一遍就好了,然后再取两个方向找到的子数组和的最大值:
1 | class Solution { |
做到这里,我已经不管一次遍历就能解决的事情了,只想赶紧A完给妹妹看。这一次最大元素和的子数组不在最左边,也不在最右边了,是夹在数组中间了,比如数组[0,-3,-2,-3,-2,2,-3,0,1,-1]
,其最大和子数组是[2]
,双向处理的方式也失效了。
这下想不出其他方法了,做到这里花了快一个小时了,只能先吃个饭,顺便想想有没有其他控制指针移动的条件。后来实在是想不出来了,去看了一下评论区,发现有一个很优雅的答案,很符合我对一次遍历数组就能解决问题的完美期待。
1 | class Solution { |
但是这段代码读起来晦涩难懂,取最大值可以理解,但是为什么sum <= 0
的时候要重新置零呢?这跟找最大子数组和有什么关系呢?
这段代码中,其实是维护了一个滑动窗口,并不断更新窗口中元素的最大和。当窗口内的元素和小于或者等于零时,丢弃该窗口。显然,该窗口中的元素是原数组的一个连续子区间,sum
变量记录了窗口起始元素到窗口结束元素之间所有元素的累计和,res
变量则记录了从窗口起始元素开始,到窗口结束元素之间所有元素的最大累计和。注意到,sum
的值是可能小于res
的,比如,在求累积和的过程中又加入了一个负数。
这个算法可以处理上面提到的数组元素全是负数的情况吗?答案是肯定的,证明如下:
证:给定一个序列sum
的值总是小于或等于零,该算法退化为找最大值的算法,证毕!
max
的值记录的是从窗口起始元素开始,到窗口结束元素之间所有元素的最大累计和,当sum <= 0
时,算法很自信地把窗口给丢弃了,我的疑问是,有没有可能窗口中存在一个连续的子区间,其元素和比res
还要大?答案是不可能,证明如下:
证:给一个序列res
更大的连续子数组和。
另外的一个问题是,窗口中的后半段的连续元素与数组后续的元素组成的连续子数组,其元素和有没有可能比res
还要大?只要存在这样的连续子数组,那么当前窗口就不能随意丢弃。答案是可能存在,但当前窗口仍然可以丢弃。证明如下:
证:窗口中任取一元素sum
的值。由于sum
的值最后小于或等于零,则数组后续的元素至少需要提供与res
相等的连续子数组和,才有可能对res
进行更新,此时,将窗口内的元素丢弃,留下数组后续的元素组成的连续子数组,其元素和将不少于res
,因此,当前窗口可以直接丢弃。
这道题整整折腾了好几个小时才想明白,一共提交了N才成功A过去:
虽然代码不是自己想的,但也是自己想办法证明的,觉得还是挺值得记录下来的,也证明我是真的有在刷题的,虽然秋招笔试面试的时候可能一道题也做不出来。