




已阅读5页,还剩3页未读, 继续免费阅读
版权说明:本文档由用户提供并上传,收益归属内容提供方,若内容存在侵权,请进行举报或认领
文档简介
摘要本文分析并演示最大子序列和问题的几种算法,它们都能解决问题,但是时间复杂度却大相径庭,最后将逐步降低至线性。算法子序列和问题的引入给定(可能有负数)整数序列A1, A2, A3., An, 求这个序列中子序列和的最大值。(为方便起见,如果所有整数均为负数,则最大子序列和为0)。例如:输入整数序列: -2, 11, 8, -4, -1, 16, 5, 0,则输出答案为35,即从A2A6。这个问题之所以有吸引力,主要是因为存在求解它的很多算法,而这些算法的性能差异又很大。这些算法,对于少量的输入差别都不大,几个算法都能在瞬间完成,这时若花费大量的努力去设计聪明的算法恐怕就不太值得了;但是如果对于大量的输入,想要更快的获取处理结果,那么设计精良的算法显得很有必要。切入正题下面先提供一个设计最不耗时间的算法,此算法很容易设计,也很容易理解,但对于大量的输入而言,效率太低:算法一:publicstaticintmaxSubsequenceSum(inta)intmaxSum=0;for(inti=0;ia.length;i+)/i为子序列的左边界for(intj=i;ja.length;j+)/j为子序列的右边界intthisSum=0;for(intk=0;kmaxSum)maxSum=thisSum;returnmaxSum;上述设计很容易理解,它只是穷举各种可能的结果,最后得出最大的子序列和。毫无疑问,这个算法能够正确的得出和,但是如果还要得出是哪个子序列,那么这个算法还需要添加一些额外的代码。现在来分析以下这个算法的时间复杂度。运行时间的多少,完全取决于第6、7行,它们由一个含有三重嵌套for循环中的O(1)语句组成:第3行上的循环大小为N,第4行循环大小为N-i,它可能很小,但也可能是N。我们在判断时间复杂度的时候必须取最坏的情况。第6行循环大小为j-i+1,我们也要假设它的大小为N。因此总数为O(1*N*N*N)=O(N3)。第2行的总开销为O(1),第8、9行的总开销为O(N2),因为它们只是两层循环内部的简单表达式。我们可以通过拆除一个for循环来避免3次的运行时间。不过这不总是可能的,在这种情况下,算法中出现大量不必要的计算。纠正这种低效率的改进算法可以通过观察Sum(AiAj) = Aj + Sum(AiAj-1)而看出,因此算法1中第6、7行上的计算过分的耗费了。下面是在算法一的基础上改进的一种算法:算法二:publicstaticintmaxSubsequenceSum(inta)intmaxSum=0;for(inti=0;ia.length;i+)intthisSum=0;for(intj=i;jmaxSum)maxSum=thisSum;returnmaxSum;对于此算法,时间复杂度显然是O(N2),对它的分析甚至比前面的分析还要简单,就是直接使用穷举法把序列中i后面的每个值相加,如果发现有比maxSum大的,则更新maxSum的值。对于这个问题,有一个递归和相对复杂的O(NlogN)解法,我们现在就来描述它。要是真的没有出现O(N)(线性的)解法,这个算法就会是体现递归为例的极好的范例了。该方法采用一种“分治”策略。其想法就是吧问题分成两个大致相等的子问题,然后递归地对它们求解,这是“分”的阶段。“治”阶段就是将两个子问题的解修补到一起并可能再做些少量的附加工作,最后得到整个问题的解。在我们的例子中,最大子序列的和只可能出现在3个地方:1.出现在输入数据的左半部分2.3.出现在输入数据的右半部分4.5.跨越输入数据的中部而位于左右两个部分6.前两种情况可以递归求解,第三种情况的最大和可以通过求出前半部分(包含前半部分的最后一个元素)的最大和以及后半部分(包括后半部分的第一个元素)的最大和,再将二者相加得到。作为例子,考虑以下输入:-前半部分后半部分-2,11,8,-4,-1,16,5,0-其中,前半部分的最大子序列和为19(A2A3),而后半部分的最大子序列和为21(A6A7)。前半部分包含其最后一个元素的最大和是15(A2A4),后半部分包含第一个元素的最大和是20(A5A7)。因此,跨越这两部分的这个子序列才是拥有最大和的子序列,和为15+20=35(A2A7)。于是出现了下面这种算法:算法三:publicstaticintmaxSubsequenceSum(inta,intleft,intright)if(left=right)/Basecaseif(aleft0)returnaleft;elsereturn0;/保证最小值为0intcenter=(left+right)/2;intmaxLeftSum=maxSubsequenceSum(a,left,center);/递归调用,求左部分的最大和intmaxRightSum=maxSubsequenceSum(a,center+1,right);/递归调用,求右部分的最大和intleftBorderSum=0,maxLeftBorderSum=0;/定义左边界子序列的和for(inti=center;i=left;i-)leftBorderSum+=ai;if(leftBorderSummaxLeftBorderSum)maxLeftBorderSum=leftBorderSum;intrightBorderSum=0,maxRightBorderSum=0;/定义左边界子序列的和for(inti=center+1;imaxRightBorderSum)maxRightBorderSum=rightBorderSum;/选出这三者中的最大值并返回(max3(inta,intb,intc)的实现没有给出)returnmax3(maxLeftSum,maxRightSum,maxLeftBorderSum+maxRightBorderSum);有必要对算法三的程序进行一些说明。递归过程调用的一般形式是传递输入的数组和左右边界,它们界定了数组要被处理的部分。第28行处理基准情况,让递归调用有退出的机会。如果left=right,那么只有一个元素,并且当该元素非负时,它就是最大子序列。第11、12行执行两个递归调用。我们可以看到,递归调用总是对小于原问题的问题进行,不过程序中的小扰动有可能破坏这个特性。1420行,2228行分界处左右两边的最大子序列和,这两个值的和就有可能是整个序列中的最大子序列和。第31行调用max3方法,求出这三种情况下的最大值,该值即为整个序列的最大子序列和。显然,算法三需要比设计前两种算法付出更多的编程努力,看上去前面两种算法的代码量要比算法三少许多,然而,程序短并不意味着程序好。测试表明,除较小的输入量外,算法三比前两个算法明显要快。现在来分析以下算法三的时间复杂度。令T(N)是求解大小为N的最大子序列和问题所花费的时间。如果N=1,则算法3执行程序第28行花费某个常数时间,我们称之为一个时间单位。于是T(1)=1,否则,程序必须执行两个递归调用,即在1428行之间的两个for循环以及几个小的簿记量,如10、14行。这两个for循环总共接触到A1An中的每个元素,而在循环内部的工作量是常量,于是1428行花费的时间为O(N)。从210、14、22和31行上的程序的工作量是常量,从而与O(N)相比可以忽略。其余就剩下1112行上运行的工作。这两行求解大小为N/2的子序列问题(假设N为偶数)。因此,这两行每行花费T(N/2)个时间单元,共花费2T(N/2)个时间单元。因此,算法三花费的总时间为2T(N/2)+O(N)。于是我们得到方程组:T(1)=1T(N)=2T(N/2)+O(N)为了简化计算,我们可以用N代替上面方程中的O(N)项;由于T(N)最终还是要用大O表示,因此这么做并不影响答案。现在,如果T(N) = 2T(N/2) + N,且T(1) = 1,那么T(2) = 4 = 2*2;T(4) = 12 = 4*3;T(8) = 32 = 8*4;T(16) = 80 = 16*5。用数学归纳法可以证明若N=2k,那么T(N) = 2k * (k+1) = N * (k+1) = N(logN + 1) = NlogN + N = O(NlogN)。即算法三的时间复杂度为O(NlogN),这明显小于算法二的复杂度O(N2),因此算法三会更快的得出结果。这个分析假设N是偶数,否则N/2就不确定了。通过该分析的递归性质可知,实际上只有当N是2的幂时结果才是合理的,否则我们最终要得到大小不是偶数的子问题,方程就是无效的了。当N不是2的幂时,我们多少需要更加复杂一些的分析,但是大O的结果还是不变的。更优秀的算法虽然算法三已经足够优秀,将时间复杂度由O(N2)降低为O(NlogN),但是,这并不是最优秀的,下面介绍针对这个问题更优秀的解法。算法四:publicstaticintmaxSubsequenceSum(inta)intmaxSum=0,thisSum=0;for(inti=0;imaxSum)maxSum=thisSum;elseif(thisSum0)thisSum=0;returnmaxSum;很显然,此算法的时间复杂度为O(N),这小于算法三中的时间复杂度O(NlogN),因此,此算法比算法三更快!方法固然已给出,但是要明白为什么此方法能用,还需多加思考。在算法一和算法二中,i代表子序列的起点,j代表子序列的终点。碰巧的是,我们不需要知道具体最佳的子序列在哪里,那么i的使用可以从程序上被优化,因此在设计算法的时候假设i是必需的,而且我们想改进算法二。一个结论是:如果ai是负数,那么它不可能代表最优序列的起点,因为任何包含ai的作为起点的子序列都可以通过使用ai+1作为起点得到改进。类似的,任何负的子序列也不可能是最优子序列的前缀(原理相同)。如果在内循环中检测到从ai到aj的子序列的和是负数,那么可以向后推进i。关键的结论是:我们不仅能够把i推进到 i+1,而且实际上我们还可以把它一直推进到 j+1。为了看清楚这一点,我们令 p 为 i+1 和 j 之间的任意一下标。开始于下标 p 的任意子序列都不大于在下标i开始并包含从 ai 到 ap-1 的子序列的对应的子序列,因为后面这个子序列不是负的(即j是使得从下标 i 开始,其值成为负值的序列的第一个下标)。因此,把 i 推进到 j+1 是没有风险的:我们一个最优解也不会错过。这个算法是许多聪明算法的典型:运行时间是明显的,但正确性却不那么容易就能看出来。对于这些算法,正式的正确性证明(比上面分析更加正式)几乎总是需要的;然而,及时到那时,许多人仍然还是不信服。此外,许多这类算法需要更有技巧的编程,这导致更长的开发过程。不过,当这些算法正常工作时,它们运行得很快!而我
温馨提示
- 1. 本站所有资源如无特殊说明,都需要本地电脑安装OFFICE2007和PDF阅读器。图纸软件为CAD,CAXA,PROE,UG,SolidWorks等.压缩文件请下载最新的WinRAR软件解压。
- 2. 本站的文档不包含任何第三方提供的附件图纸等,如果需要附件,请联系上传者。文件的所有权益归上传用户所有。
- 3. 本站RAR压缩包中若带图纸,网页内容里面会有图纸预览,若没有图纸预览就没有图纸。
- 4. 未经权益所有人同意不得将文件中的内容挪作商业或盈利用途。
- 5. 人人文库网仅提供信息存储空间,仅对用户上传内容的表现方式做保护处理,对用户上传分享的文档内容本身不做任何修改或编辑,并不能对任何下载内容负责。
- 6. 下载文件中如有侵权或不适当内容,请与我们联系,我们立即纠正。
- 7. 本站不保证下载资源的准确性、安全性和完整性, 同时也不承担用户因使用这些下载资源对自己和他人造成任何形式的伤害或损失。
评论
0/150
提交评论