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

第一个算法,它只是穷举式地尝试所有的可能。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. Redis Eval Script

    简介 从Redis 2.6 版本开始,内嵌支持 Lua 环境.通过使用EVAL或EVALSHA命令可以使用 Lua 解释器来执行脚本. EVAL和EVALSHA的使用是差不多的(下面有讲区别). EV ...

  2. Asp.Net MVC学习总结之过滤器详解(转载)

    来源:http://www.php.cn/csharp-article-359736.html   一.过滤器简介 1.1.理解什么是过滤器 1.过滤器(Filters)就是向请求处理管道中注入额外的 ...

  3. Java基础——Ajax(一)

    学习之前举一个简单的小栗子,让我们简单了解一下Ajax的应用与好处.一般网站都会有自己的数据库,用来单独存储用户的个人信息,平时我们在注册账号的时候会遇到信息输入的页面,假设第一个输入信息是用户名的填 ...

  4. 【Redis】4、Redis学习资料

    Redis 集群规范 http://www.redis.cn/topics/cluster-spec.html Redis 集群教程 http://www.redis.cn/topics/cluste ...

  5. JavaScript一团乱,这是好事

    译者按: JavaScript从简单变复杂了,作者从另一个角度看待这个问题. 原文: JavaScript’s a mess – and that’s a good thing 译者: Fundebu ...

  6. js 中导出excel 较长数字串会变成科学计数法

    在做项目中,碰到如题的问题.比如要将居民的信息导出到excel中,居民的身份证号码因为长度过长(大于10位),excel会自动的将过长的数字串转换成 科学计数法.现在网上找到解决方案之一: (在数字串 ...

  7. 透明度 rgba 和 opacity 的区别

    rgba: 使用方式:rgba(255, 255, 255, .5) 最后一个参数表示透明度取值范围 0 ~1    只作用于元素的颜色或其背景色. opacity :  使用方式:opacity : ...

  8. html学习笔记——ife task0001

    花了两三天大概看完html和css基本用法,但到自己布局的时候还是很懵不知道从哪里入手啊,就找了个简单的任务(ife2015 spring)试一下. 之前不涉及到布局的跳过,从涉及到position和 ...

  9. Linux 目录结构学习与简析 Part2

    linux目录结构学习与简析 by:授客 QQ:1033553122 ---------------接Part 1-------------- #1.查看CPU信息 #cat /proc/cpuinf ...

  10. AJAX的优点 个人理解记录

    1:对网站性能的提高.例如我只需要刷新页面中购物车的数据,使用ajax时不需要请求整个页面的数据,对于客户端和服务器的压力都会降低, 减少了ISP的负担,服务器的空间和带宽压力都会降低. 2:用户体验 ...