先给出实现程序,如下:
1: int max_sub_sum2(const int v[],int n)
2: {
3: int max=0;
4: int currentSum=0;
5: int i=0;
6:
7: for(i=0;i<n;i++)
8: {
9: currentSum+=v[i];
10: if(currentSum>max)
11: {
12: max=currentSum;
13: }
14: else if(currentSum<0)
15: {
16: currentSum=0;
17: }
18: }
19:
20: return max;
21: }
扫描算法是Jon Bentley在编程珠玑里给出的名字,程序看起来非常简洁,不过简介并非意味着简单,至少在当时,问题的发现与提出者Ulf Grenander只想到了O(n2)的算法,而之后的Michael Shamos给出了分治算法,把复杂度降低到O(NlogN),当Bentley以及其它数学家都以为Shamos的算法最好的时候,卡梅隆大学的统计学家Jay Kadane发现了本文算法。
在这里我们至少能获得关于算法学习的一个重要启示:任何一个看似简单的算法都来之不易,只有经过广泛的研究和实践,你才能熟练地运用算法设计技术。这也是Bentley在编程珠玑八章深入阅读一节种所给出的建议。
该算法的复杂度很明显是O(N),甚至无需证明。
但是为理解该算法,我们仍有必要简单分析证明一下该算法的正确性。
现在命题为:如上图所示序列A1~AN,最大子序列Ai-Aj,则该最大子序列一定可以由上述算法求得。
证明:
A[i~j]为最大子序列,则意味着A[i-1]<=0与A[j+1]>=02,论点(1)
且更可推导出A[1]+A[2]+…+A[i]<=0与A[j+1]+A[j+2]+…+A[N]<=0; 论点(2)
论点(2)亦可简单地由反证法推得,如二者任意之一若大于零,则很明显A[i~j]不是最大子序列,这与命题所给条件矛盾。
由论点(2)也可推导出A[i-1]与A[1~i-2]之间的任意子集之和皆<=0,也即|A[i-1]|>A[1]+…+A[i-2],论点(3)
而论点(2)(3)的情况恰可由程序14~17行规避。
而程序10~13行计算的又是论点(2)(3)情况之后的子序列的最大值。max用来保存该最大值,currentSum则用来判断是否会出现下一个符合(2)(3)的位置出现,如出现,则该值被清零,并从新位置寻找下一个可能会超过max值的位置,以此归纳,可值命题得证,也即max就是最大子序列的值。