现在我们将要叙述四个算法来求解早先提出的最大子序列和问题。

第一个算法,它只是穷举式地尝试所有的可能。for循环中的循环变量反映了Java中数组从0开始而不是从1开始这样一个事实。还有,本算法并不计算实际的子序列;实际的计算还要添加一些额外的代码。

    public static int maxSubSum1(int[] a) {

        int maxSum = 0;

        for(int i = 0;i<a.length;i++)
for(int j = i;j<a.length;j++) {
int thisSum = 0; for(int k = i;k<=j;k++)
thisSum +=a[k];
if(thisSum > maxSum)
maxSum = thisSum;
} return maxSum;
}

该算法肯定会正确运行(这用不着花太多时间去证明)。运行时间为O(N的3次方),这完全取决这两行代码:

     for(int k = i;k<=j;k++)
thisSum +=a[k];

它们由一个含于三种嵌套for循环中的O(1)语句组成。

下面这行代码循环大小为N

        for(int i = 0;i<a.length;i++)

第二个循环大小为N-i,它可能要小,但也可能是N。我们必须假设最坏的情况,而这可能会使得最终的界有些大。第三个循环的大小为j-i+1我们也要假设它的大小为N。因此总数为

O(1.N.N.N)=O(N的3次方)。

而下面这行代码,开销只是O(1)

 int maxSum = 0;

然而,下面这段代码也只不过总共开销O(N的2次方),因为它们只是两层循环内部的简单表达式

        if(thisSum > maxSum)
maxSum = thisSum;

第二个算法显然是O(N的2次方)

 public static int maxSubSum2(int [] a) {

       int maxSum = 0;

       for(int i = 0;i<a.length;i++) {

           int thisSum = 0;

           for (int j = 0; j < a.length; j++) {

               thisSum += a[j];

               if(thisSum > maxSum)
maxSum = thisSum;
} } return maxSum; }

对这个问题有一个递归和相对复杂的O(N logN)解法,我们现在就来描述它。要是真的没出现O(N)(线性的)解法,这个算法就会是体现递归威力的极好的范例。该方法采用一种对它们求解,这是“分”的部分。“治”阶段将两个子问题的解修补到一起并可能再做些少量的附加工作,最后得到整个问题的解。

在我们的例子中,最大子序列和可能在三处出现。或者整个出现在输入数据的左半部,或者整个出现在右半部,或者跨越输入数据的中部从而位于左右两半部分之中。前两种情况可以递归求解。第三种情况的最大和可以通过求出前半部分(包含前半部分最后一个元素)的最大和以及后半部分(包含后半部分第一个元素)的最大和而得到。此时将这两个和相加。作为一个例子,考虑下列输入,如图:

其中前半部分的最大子序列和为6(从元素A1到元素A3)而后半部分的最大子序列和为8(从元素A6到A7)。

前半部分包含其最后一个元素的最大和子序列和是4(从元素A1到元素A4),而后半部分 包含其第一个元素的最大和是7(从元素A5到A7)。因此,横跨这部分且通过中间的最大和为4+7=11(从元素A1到A7)。

我们看到,在形成本例中的最大和子序列的三种方式中,最好的方式是包含两部分的元素。于是,答案为11。

有必要对算法3的程序进行一些说明。递归过程调用的一般形式是传递输入的数组以及左边界和右边界,它们界定了数组要被处理的部分。单行驱动程序通过传递数组以及边界0和N-1而将该过程启动。

代码示例如下:

package cn.simple.example;

public class AlgorithmTestExample {

    public static int maxSubSum1(int[] a) {

        int maxSum = 0;

        for(int i = 0;i<a.length;i++)
for(int j = i;j<a.length;j++) {
int thisSum = 0; for(int k = i;k<=j;k++)
thisSum +=a[k];
if(thisSum > maxSum)
maxSum = thisSum;
} return maxSum;
} public static int maxSubSum2(int [] a) { int maxSum = 0; for(int i = 0;i<a.length;i++) { int thisSum = 0; for (int j = 0; j < a.length; j++) { thisSum += a[j]; if(thisSum > maxSum)
maxSum = thisSum;
} } return maxSum; } private static int maxSumRec(int [] a,int left,int right) { if(left == right) if(a[left] > 0)
return a[left];
else
return 0; int center = (left + right)/2;
int maxLeftSum = maxSumRec(a,left,center);
int maxRightSum = maxSumRec(a,center+1,right); int maxLeftBorderSum = 0,leftBorderSum = 0; for(int i = center;i>=left;i--) {
leftBorderSum +=a[i];
if(leftBorderSum > maxLeftBorderSum)
maxLeftBorderSum = leftBorderSum;
} int maxRightBorderSum = 0,rightBorderSum = 0; for(int i = center+1;i<=right;i++) {
rightBorderSum += a[i]; if(rightBorderSum > maxRightBorderSum) maxRightBorderSum = rightBorderSum;
} return max3(maxLeftSum,maxRightSum,maxLeftBorderSum + maxRightBorderSum);
} private static int max3(int maxLeftSum, int maxRightSum, int i) { int max; if(maxLeftSum > maxRightSum)
max = maxLeftSum; else
max = maxRightSum; if(i > max) max = i; return max; } public static int maxSubSum3(int [] a) { return maxSumRec(a,0,a.length-1);
} }

看这段代码,如下所示:

 if(left == right)

           if(a[left] > 0)
return a[left];
else
return 0;

如果left==right,那么只有一个元素,并且当该元素非负时它就是最大子序列。left>right的情况是不可能出现的,除非N是负数(不过,程序中小的扰动有可能致使这种混乱产生)。

下面这两个递归调用,我们可以看到,递归调用总是对小于原问题的问题进行,不过程序小的扰动有可能破坏这个特性。

       int maxLeftSum = maxSumRec(a,left,center);
int maxRightSum = maxSumRec(a,center+1,right);

我们再看这下面两段代码:

代码1

    int maxLeftBorderSum = 0,leftBorderSum = 0;

       for(int i = center;i>=left;i--) {
leftBorderSum +=a[i];
if(leftBorderSum > maxLeftBorderSum)
maxLeftBorderSum = leftBorderSum;
}

代码2

int maxRightBorderSum = 0,rightBorderSum = 0;

       for(int i = center+1;i<=right;i++) {
rightBorderSum += a[i]; if(rightBorderSum > maxRightBorderSum) maxRightBorderSum = rightBorderSum;
}

这两段代码达到中间分界处的两个最大和的和数。这两个值的和为扩展到左右两部分的最大和。例程max3返回这三个可能的最大和的最大者。

显然,算法3需要比前面两种算法更多的编程努力。然而,程序短并不总意味着程序好。正如我们在前面显示算法运行时间的表中已经看到的(可以参考这篇文章:<数据结构与算法分析>读书笔记--要分析的问题),除最小的输入量外,该算法比前两个算法明显要快。

算法4:

    public static int maxSubSum4(int [] a) {
int maxSum = 0,thisSum = 0; for(int j = 0;j<a.length;j++) {
thisSum +=a[j]; if(thisSum > maxSum) maxSum = thisSum;
else if(thisSum < 0)
thisSum = 0; } return maxSum;
}

不难理解为什么时间的界是正确,但是要明白为什么算法是正确可行的却需要多加思考。为了分析原因,注意,像算法1和算法2一样,j代表当前序列的终点,而i代表当前序列的起点。碰巧的是,如果我们不需要知道具体最佳的子序列在哪里,那么i的使用可以从程序上被优化,因此在设计算法的时候假设i是需要的,而且我们想要改进算法2.一个结论是,如果a[i]是负的,那么它不可能代表最优序列的起点,因为任何包含a[i]的作为起点的子序列都可以通过用a[i+1)作起点而得到改进。类似地,任何负的子序列不可能是最优子序列的前缀。如果在内循环中检测到从a[i]到a[j]的子序列是负的,那么可以推进i。关键的结论是,我们不仅能够把i推进到i+1,而且实际上还可以把它一直推进到j+1。为了看清楚这一点,令p为i+1和j之间的任一下标。开始于下标p的任意子序列都不大于在下标i开始并包含从a[i]到a[p-1)的子序列的对应的子序列,因为后面这个子序列不是负的(j是使得从下标i开始其值成为负值的序列的第一个下标)。因此,把i推进到j+1是没有风险的:我们一个最优解也不会错过。

这个算法是许多聪明算法的典型:运行时间是明显的。但正确性则不那么容易看出来。对于这些算法,正式的正确性证明(比上面的分析更正式)几乎总是被需要的;然而,即使到那时,许多人仍然不信服。此外,许多这类算法需要更有技巧的编程,这导致更长的开发过程。不过当这些算法正常工作时,它们运行得很快,而我们将它们和一个低效的蛮力算法通过小规模的输入进行比较可以测试大部分的程序原理。

该算法的一个附带的优点是,它只对数据进行一次扫描,一旦a[i]被读入并被处理,它就不再需要被记忆。因此,如果数组在磁盘上或通过互联网传送,那么它就可以被按顺序读入,在主存中不必存储数组的任何部分。不仅如此,在任意时刻,算法都能对它已经读入的数据给出子序列问题的正确答案。具有这种特性的算法叫做联机算法。仅需要常量空间并以线性时间运行的联机算法几乎是完美的算法。

代码示例地址为:https://github.com/youcong1996/The-Data-structures-and-algorithms/tree/master/algorithm_analysis

<数据结构与算法分析>读书笔记--最大子序列和问题的求解的更多相关文章

  1. <数据结构与算法分析>读书笔记--运行时间计算

    有几种方法估计一个程序的运行时间.前面的表是凭经验得到的(可以参考:<数据结构与算法分析>读书笔记--要分析的问题) 如果认为两个程序花费大致相同的时间,要确定哪个程序更快的最好方法很可能 ...

  2. <数据结构与算法分析>读书笔记--函数对象

    关于函数对象,百度百科对它是这样定义的: 重载函数调用操作符的类,其对象常称为函数对象(function object),即它们是行为类似函数的对象.又称仿函数. 听起来确实很难懂,通过搜索我找到一篇 ...

  3. <数据结构与算法分析>读书笔记--利用Java5泛型实现泛型构件

    一.简单的泛型类和接口 当指定一个泛型类时,类的声明则包括一个或多个类型参数,这些参数被放入在类名后面的一对尖括号内. 示例一: package cn.generic.example; public ...

  4. <数据结构与算法分析>读书笔记--数学知识复习

    数学知识复习是<数据结构与算法分析>的第一章引论的第二小节,之所以放在后面,是因为我对数学确实有些恐惧感.不过再怎么恐惧也是要面对的. 一.指数 基本公式: 二.对数 在计算机科学中除非有 ...

  5. <数据结构与算法分析>读书笔记--运行时间中的对数及其分析结果的准确性

    分析算法最混乱的方面大概集中在对数上面.我们已经看到,某些分治算法将以O(N log N)时间运行.此外,对数最常出现的规律可概括为下列一般法则: 如果一个算法用常数时间(O(1))将问题的大小削减为 ...

  6. <数据结构与算法分析>读书笔记--要分析的问题

    通常,要分析的最重要的资源就是运行时间.有几个因素影响着程序的运行时间.有些因素(如使用编译器和计算机)显然超出了任何理论模型的范畴,因此,虽然它们是重要的,但是我们在这里还是不能考虑它们.剩下的主要 ...

  7. <数据结构与算法分析>读书笔记--实现泛型构件pre-Java5

    面向对象的一个重要目标是对代码重用的支持.支持这个目标的一个重要的机制就是泛型机制:如果除去对象的基本类型外,实现的方法是相同的,那么我们就可以用泛型实现来描述这种基本的功能. 1.使用Object表 ...

  8. <数据结构与算法分析>读书笔记--模型

    为了在正式的构架中分析算法,我们需要一个计算模型.我们的模型基本上是一台标准的计算机,在机器中指令被顺序地执行.该模型有一个标准的简单指令系统,如加法.乘法.比较和赋值等.但不同于实际计算机情况的是, ...

  9. <数据结构与算法分析>读书笔记--递归

    一.什么是递归 程序调用自身的编程技巧称为递归( recursion).递归做为一种算法在程序设计语言中广泛应用. 一个过程或函数在其定义或说明中有直接或间接调用自身的一种方法,它通常把一个大型复杂的 ...

随机推荐

  1. [android] 数据的异步加载和图片保存

    把从网络获取的图片数据保存在SD卡上, 先把权限都加上 网络权限 android.permission.INTERNET SD卡读写权限 android.permission.MOUNT_UNMOUN ...

  2. JavaSE Lambda表达式(JDK1.8新特性)

    在前面有一篇写到了Lambda表达式,现在可以给你们介绍什么是Lambda表达式 现在有很多老程序员都不喜欢这个函数式编程思想 主要就一点 : 老程序员习惯了 面向过程 写程序,而Lambda表达式是 ...

  3. java连接OPC之——Windows7 With SP1 网络OPC的DCOM配置

    由于 OPC(OLE for Process Control)建立在 Microsoft 的 COM(COmponent Model)基础 上,并且 OPC 的远程通讯依赖 Microsoft 的 D ...

  4. canvas-2lineCap.html

    <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8&quo ...

  5. 【读书笔记】iOS-viewWillAppear:和viewDidLoad:

    viewDidLoad:是视图第一次载入到内存中后调用的,viewWillApear:则是在每次视图显示到屏幕上之前调用. 参考资料:<iOS编程指南>

  6. web全栈架构师[笔记] — 03 html5新特性

    HTML5新特性 一.geolocation PC端 精度比较低 通过IP库定位 移动端 通过GPS window.navigator.geolocation 单次 getCurrentPositio ...

  7. 泛化之美--C++11可变模版参数的妙用

    1概述 C++11的新特性--可变模版参数(variadic templates)是C++11新增的最强大的特性之一,它对参数进行了高度泛化,它能表示0到任意个数.任意类型的参数.相比C++98/03 ...

  8. gitlab 和 github 配置 SSH Keys

    gitlab 文档上给了很好的配置的例子:https://gitlab.com/help/ssh/README#locating-an-existing-ssh-key-pair 针对mac 下的使用 ...

  9. PJ初赛复习日记

    PA姑娘的PJ初赛复习日记 by Pleiades_Antares PJ初赛考试马上就要开始了(今年应该是10.13吧?),作为蒟蒻的我们怎么能不复习呢? 众所周知,复习方法有很多很多种-- 比如 ( ...

  10. safari 与 chrome 的小区别大BUG

    safari 与 chrome 的小区别大BUG 时间:2016-11-01 17:33:19 作者:zhongxia 原文地址:https://github.com/zhongxia245/blog ...