最大子数组和算法的思考

来源:互联网 发布:阿里云客服电话 编辑:程序博客网 时间:2024/06/06 09:10

如果之前没有看过最大子数组和的解法思想,这一问题很能体现算法的设计能力。当然,算法是两面性的,越简单的效率越低,效率高的算法往往易错和更难理解。本文简单针对此例说说对算法设计的一些感悟

首先是问题定义给定一个一维数组,其中包含一些负数,求其中任意连续子数组之和的最大值

首先是最基本最简单的思想:求和嘛,就是把所有的和都求出来,然后比较取其中的最大值就好了

maxsofar = 0for i = [0,n)  for j = [i,n)    sum = 0    for k = [i,j]      sum += x[k]      maxsofar = max(maxsofar,sum)
跑完这个三重循环,maxsofar的值便是所求最大值。显然这一算法的效率非常低(O(n³)),而且因为x[i...j+1]的和实际上是x[i...j]+x[j+1],而前者已经在上次计算中算得了,所以很容易想到避免重复计算的优化,将算法的时间复杂度降低一阶,如下

maxsofar = 0for i = [0,n)  sum = 0  for j = [i,n)    sum += x[j]    maxsofar = max(maxsofar,sum)

或者用另外一种方式,使用一个临时数组保存计算出的x[0...i]的和,这样任意的子数组x[i...j]的和都可以用x[0...j]-x[0...i-1]表示,依次比较即可(算法略),O(n²)

也许到这一步,很多人都已经满足了,或者说,想不出更好的优化方式了。其实不然,算法优化总是有无尽的神秘,总会有更优的方式(应用一些高级算法思想)

当规模比较大时,一般用到的就是分治法,即将问题分成近似相等的两部分,分开解决,最后合并解的结果

于是,我们可以把这个数组从中间分成两个部分,设为a和b,然后,分别计算a中的最大值和b中的最大值,最后结果再取一次最大值,是这样吗?

想想,如果这个最大数组c恰好在a,b之间,即左半部分在a右边界,右半部分在b左边界呢?

so,现在只要找到计算数组c的方法,以及定义边界取值情况(问题是基于左右下标的),算法就可以写出来了。计算数组c的思想在前一算法中已有体现,从中间元素开始,分别向左,向右找到最大的子序列和。然后求它们的和。因为这个子序列是“特殊的”一端固定,所以只需要O(n)时间计算得出

算法设计如下,使用递归方式:

float maxsum(l,r)  if(l>r) return 0  if(l=r) return max(0,x[l])  m = (l+r) /2  lmax = sum = 0  for(i=m;i>=l;i--)    sum += x[i]    lmax = max(lmax,sum)  rmax = sum = 0    for(i=m;i<=r;i++)    sum += x[i]    rmax = max(rmax,sum)  return max(lmax+rmax,maxsum(l,m),maxsum(m+1,r))

这样只要调用一次maxsum(0,n-1)就可以得到结果。也许有人会疑问这样的算法复杂度是不是反而增加了?T(n) = 2T(n/2) + O(n) = O(nlogn),一般来说,只要额外开销不大,分治法都可以达到这样的效率。这种方式虽然好,但是在边界细节上不处理正确,程序就会跑错

is that enough good?

其实算法设计也有一定的规律,也就是越通用的算法,应用于特定问题往往效率一般。而针对问题专门设计的算法,虽然丧失了通用性,却能得到很好的效果。来看下面的分析:

这个问题中定义子数组必然是连续的,那么我们用一个长度可变的“扫描器”,把这个数组从头到尾扫描一遍,是不是一定可以扫描到这个最大子数组呢?

显然是可以的。问题的关键就是“扫描器”的长度如何伸缩,是否舍弃了不必要的检测。 打个有趣的比方,有一只虫子,当它吃了正数就会变长,吃了负数就会缩短,于是我们可以让它爬过这个数组,同时保持纪录它成长过程中的最大值,当它爬到终点,不管最后实际长度是怎样的,我们总会纪录到这个最大值。

算法的核心在于负数的处理。试想,当它吃了一个小负数后又吃了一个较大的正数,总长度增加了,需要继续保持纪录。而一旦它遇到很大的负数,该怎么做?

虫子是不会变成负长度的,而我们也没必要留着这个负值,因为后面无论遇到什么样的数,都会“拖低”最大值的计算。所以,果断放弃,让虫子重生为0吧!

这样,我们就有了特有的线性算法:

maxsofar = 0maxendinghere = 0for i = [0,n)  maxendinghere = max(maxendinghere+x[i],0)  maxsofar = max(maxsofar,maxendinghere)
maxendinghere即所说虫子的长度,舍弃负值为新的开始。最终只需要线性扫描一遍即完成算法

(关于算法复杂度所带来的影响,本文不涉及讨论)

结束语:

任何事物都有两面性,算法更是如此。简单易懂的算法容易修改维护,但是执行效率低。常规高级算法(姑且这样叫分治,回溯等思想把)有很多需注意的细节之处,需要多多实践运用才能驾轻就熟,高效运行。而特定算法往往需要非常透彻的分析+经验铺垫后的灵光一现,达到非常简单而意想不到的效果

有时候把算法和生活中的相关例子联系起来想,也是蛮有趣的^_^

(阅读编程珠玑column 8所感,代码均来自原书,思想自创)

原创粉丝点击