最大子数组和算法的思考

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

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

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

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

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

结束语:

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

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

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

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值