最大子数组和算法的思考
来源:互联网 发布:阿里云客服电话 编辑:程序博客网 时间: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所感,代码均来自原书,思想自创)
- 最大子数组和算法的思考
- 算法导论第三版4.1最大和子数组思考
- 算法-子数组的最大和
- 子数组的最大和[算法]
- 求子数组的最大和【算法】
- 子数组的最大和[算法]
- 子数组的最大和[算法]
- 算法----最大子数组和
- 【算法】子数组的最大累加和/子矩阵的最大累加和问题
- 连续子数组最大和或最大子段和的求解算法及其正确性
- 算法讨论(二)---求子数组的最大和
- 算法题13 求子数组的最大和
- 求数组子序列最大和的算法
- 求数组最大子段和的常用算法
- 【算法总结-DP】求子数组的最大和
- 求子数组的最大和:算法求验证
- 每天一算法(求子数组的最大和)
- 算法题:连续子数组的最大和
- ExtJs 3 ExtJs4 分页(pagingtoolbar)带参数(条件)查询解决方法
- 机器学习中的算法(1)-决策树模型组合之随机森林与GBDT
- ExtJs ComboBox 在IE 下 自动完成功能无效的解决方案
- 交叉编译工具链的构建原理
- Cocos2d-x 2.0.4 如何制作一个横版格斗过关游戏(2)
- 最大子数组和算法的思考
- Java Socket实战之一 单线程通信
- VFS 代码分析(open/read/write)
- 电脑硬件知识大全
- 深入分析 Linux 内核链表
- Ubuntu下开启SSH服务
- 推荐系统学习之mahout 学习评分计算
- LeetCode之Palindrome Partitioning II
- 预编译头技术