1.问题描述
问题:一个有N个整数元素的一维数组(A[0],A[1],A[2],…A[n-1]),这个数组中子数组之和的最大值是多少?
该子数组是连续的。例如 数组:[1,-2,3,5,-3,2]返回8; 数组:[0,-2,3,5,-1,2]返回9。
网上有称之为最大子序列和,亦有称连续子数组最大和。个人觉得叫最大子序列和不太妥,数学上讲,子序列不一定要求连续,而这里我们的题目必然要求是连续的,如果不连续而求子序列最大和很显然就无意义了,这也是为啥又称连续子数组最大和。不过,莫要在意细节。
这题是很经典的一道面试题,也有各种解法,从算法分析上,时间复杂度也有很大差别,下面我就给出三种不同的解法。
2.解法一:暴力枚举法
此种方法最简单,我想应该也是每个人拿到题目想到的第一种解法了,学过一点编程的人都应该能编出此类程序。
记sum[i..j]为数组中第i个元素到第j个元素的和(其中0<=i
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
此种方法的时间复杂度为O(n2),显然不是一种很好的办法,也不是公司面试希望你写出这样的程序的。
3.解法二:分支界定
这里再介绍一种更高效的算法,时间复杂度为O(nlogn)。这是个分治的思想,解决复杂问题我们经常使用的一种思维方法——分而治之。
而对于此题,我们把数组A[1..n]分成两个相等大小的块:A[1..n/2]和A[n/2+1..n],最大的子数组只可能出现在三种情况:
A[1..n]的最大子数组和A[1..n/2]最大子数组相同;
A[1..n]的最大子数组和A[n/2+1..n]最大子数组相同;
A[1..n]的最大子数组跨过A[1..n/2]和A[n/2+1..n]
前两种情况的求法和整体的求法是一样的,因此递归求得。
第三种,我们可以采取的方法也比较简单,沿着第n/2向左搜索,直到左边界,找到最大的和maxleft,以及沿着第n/2+1向右搜索找到最大和maxright,那么总的最大和就是maxleft+maxright。
而数组A的最大子数组和就是这三种情况中最大的一个。
伪代码如下:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
4.解法三:动态规划(DP)
我们考虑最后一个元素arr[n-1]与最大子数组的关系,有如下三种情况:
(1)arr[n-1]单独构成最大子数组
(2)最大子数组以arr[n-1]结尾
(3)最大子数组跟arr[n-1]没关系,最大子数组在arr[0-n-2]范围内,转为考虑元素arr[n-2]
从上面我们可以看出,问题分解成了三个子问题,最大子数组就是这三个子问题的最大值,现假设:
(1) 以arr[n-1]为结尾的最大子数组和为End[n-1]
(2) 在[0-(n-1)]范围内的最大子数组和为All[n-1]
如果最大子数组跟最后一个元素无关,即最大和为All[n-2](存在范围为[0-n-2]),则解All[n-1]为三种情况的最大值,即All[n-1] = max{ arr[n-1],End[n-1],All[n-2] }。从后向前考虑,初始化的情况分别为arr[0],以arr[0]结尾,即End[0] = arr[0],最大和范围在[0,0]之内,即All[0]=arr[0]。根据上面分析,给出状态方程:
All[i] = max{ arr[i],End[i-1]+arr[i],All[i-1] }
代码如下:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
上述代码在空间上是可以优化为O(1)的
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
下面说一下由DP而导出的另一种O(N)的实现方式,该方法直观明了,个人比较喜欢,所以后续问题的求解也是基于这种实现方式来的。
仔细看上面DP方案的代码,End[i] = max{arr[i],End[i-1]+arr[i]},如果End[i-1]<0,那么End[i]=arr[i],什么意思?End[i]表示以i元素为结尾的子数组和,如果某一位置使得它小于0了,那么就自当前的arr[i]从新开始,且End[i]最初是从arr[0]开始累加的,所以这可以启示我们:我们只需从头遍历数组元素,并累加求和,如果和小于0了就自当前元素从新开始,否则就一直累加,取其中的最大值便求得解。
到这里其实就可以了,在《编程之美》中,作者故意没有按照这种推导来实现(我猜的),而是在End[i-1]<0时,让End[i]=0,从而留出了一个问题(元素全是负数怎么办),其实如果按照上面的推导直接实现的话,就不存在这个问题了;
基于上面的推导,代码如下:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
其实上面的方法虽说是从DP推导出来的,但是写完发现也是很直观的方法,求最大和,那就一直累加呗,只要大于0,就说明当前的“和”可以继续增大,如果小于0了,说明“之前的最大和”已经不可能继续增大了,就从新开始,如此这样。
5.问题扩展:返回最大子数组始末位置
这个问题是《编程之美》2.14的扩展问题,返回始末位置还是比较容易的,我们知道,每当当前子数组和的小于0时,便是新一轮子数组的开始,每当更新最大和时,便对应可能的结束下标,这个时候,只要顺便用本轮的起始和结束位置更新始末位置就可以,程序结束,最大子数组和以及其始末位置便一起被记录下来了。
C++代码如下:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 75
- 76
- 77
- 78
- 79
- 80
- 81
- 82
- 83
- 84
- 85
- 86
- 87
测试结果:
6.问题扩展:允许数组首尾相连
这个也是2.14的扩展问题,如果数组arr[0],…,arr[n-1]首尾相邻,也就是允许找到一段数字arr[i],…,arr[n-1],arr[0],…,a[j],使其和最大,该如何?
编程之美解法:这个问题的解可以分为两种情况:
1) 解没有跨越arr[n-1]到arr[0] (原问题)
2) 解跨越arr[n-1]到arr[0]
对于第一种情况按照之前的方式计算即可,对于第二种情况我们可以巧妙地 进行问题转换。我们找最大子数组的对偶问题——最小子数组,有了最小子数组的值,总值减去它不就可以了么?但是我又想,这个对偶问题只能处理这种跨界的特殊情况吗?答案是肯定的,如果最大子数组跨界,那么剩余的中间那段和就一定是最小的,而且和必然是负的;相反,如果最大子数组不跨界,那么总值减去最小子数组的值就不一定是最大子数组和了,例如例子[8,-10,60,3,-1,-6],最大子数组为[8 | 60,3,-1,-6],而最小子数组和为[-10],显然不能用总值减去最小值。
故,在允许数组跨界(首尾相邻)时,最大子数组的和为下面的最大值
Maxsum={ 原问题的最大子数组和;数组所有元素总值-最小子数组和 }。