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

有几种方法估计一个程序的运行时间。前面的表是凭经验得到的(可以参考:<数据结构与算法分析>读书笔记--要分析的问题)

如果认为两个程序花费大致相同的时间,要确定哪个程序更快的最好方法很可能将它们编码并运行。

一般地,存在几种算法思想,而我们总愿意尽早除去那些不好的算法思想,因此,通常需要分析算法。不仅如此,进行分析的能力常常提供对于设计有效算法的洞察能力。一般说来,分析还能准确地确定瓶颈,这些地方值得仔细编码。

为了简化分析,我们将采纳如下的约定:不存在特定的时间单位。因此,我们抛弃一些前导的常数。我们还将抛弃低阶项,从而要做的就计算大O运行时间。由于大O是一个上界,因此我们必须仔细,绝不要低估程序的运行时间。实际上,分析的结果为程序在一定的时间范围内能够终止运行提供了保障。程序可能提起结束,但绝不可能错后。

 

一、一个简单的例子

package cn.simple.example;

public class SimpleExample {

    static int sum(int n) {
        
         partialSum;
        
1        partialSum = 0;
        
2       for(int i = 1; i <= n;i++) 
3            partialSum = i * i *i;
            
4           return partialSum;
        
    }

}

 

对这个程序段的分析是简单的。所有的声明均不计时间。第1行和第4行各占一个时间单元。第3行每执行一次占用4个时间单元(两次乘法,一次加法和一次赋值),而执行N次共占用4N个时间单元。第2行在初始化i、测试i<=N和对i的自增运算隐含着开销。所有这些的总开销是初始化1个单元时间,所有的测试为N+1个单元时间,而所有的自增运算为N个单元时间,共2N+2个时间单元。我们忽略调用方法和返回值的开销,得到总量是6N+4个时间单元。因此,我们说该方法是O(N)。

如果每次分析一个程序都要演示所有这些工作,那么这项任务很快就会变成不可行的负担。幸运的是,由于我们有了大O的结果,因此就存在许多可以采取的捷径,并且不影响最后的结果。例如,第3行(每次执行时)显然是O(1)语句,因此精确计算它究竟是2、3还是4个时间单元是愚蠢的。这无关紧要。第1行与for循环相比显然是不重要的,所以在这里花费时间也是不明智的。这使我们得到若干一般法则。

 

二、一般法则

法则1-for循环

一个for循环的运行时间至多是该for循环内部那些语句(包括测试)的运行时间乘以迭代的次数。

法则2-嵌套的for循环

从里向外分析,在一组嵌套循环内部的一条语句总的运行时间为该语句的运行时间乘以该组所有的for循环的大小的乘积。

例如:下列程序片段为O(N的2次方):

for(i=0;i<n;i++)
    for(j =0;j<n;j++)
        k++

法则3-顺序语句

将各个语句的运行时间求和即可(这意味着,其中的最大值就是所得的运行时间)

例如:下面的程序片段先花费O(N),接着是O(N的2次方),因此总量也是O(N的次方):

)
 a[i]=0;
for(j=0;j<n;j++)
        a[i]+=a[j]+i+j;

 

法则4-if/else语句

对于程序片段

if(condition)
    S1
else
    S2

 

一个if/else语句的运行时间从不超过判断的运行时间再加上S1和S2中运行时间长者的总的运行时间。

显然在某些情形下这么估计有些过头,但决不会估计过低。

其他的法则都是显然的,但是,分析的基本策略是从内部(或最深层部分)向外展开工作的。如果有方法调用,那么要首先分析这些调用。如果有递归过程,那么存在几种选择。若递归实际上只是被薄棉纱遮住的for循环,则分析通常是很简单的。例如,下面的方法实际上就是一个简单的循环从而其运行时间为O(N):

    long factorial( n) {
        if(n <= 1) 
            return 1;
        else
            return n*factorial(n-1);
    }
    

 

实际上这个例子对递归的使用并不好。当递归被正常使用时,将其转换成一个循环结构是相当困难的。在这种情况下,分析将涉及求解一个递推关系。为了观察到这种可能发生的情形,考虑下列程序,实际上它对递归使用的效率低得令人惊诧。

    long fib( n) {
1        if(n<=1)
2            ;
       else
3            return fib(n-1) +fib(n-2);
    }

 

初看起来,该程序似乎对递归的使用非常聪明。可是,如果将程序编码并在N值为40左右时运行,那么这个程序让人感到低得吓人。分析是十分简单的。令T(N)为调用函数fib(n)的运行时间。如果N=0或N=1,则运行时间是某个常数值,即第一行上做判断以及返回所用的时间。因为常数并不重要,所以我们可以说T(0)=T(1)=1。对于N的其他值的运行时间则相对于基准情形的运行时间来度量。若N>2,则执行该方法的时间是第1行上的常数工作加上第3行上的工作。第3行由一次加法和两次方法调用组成。由于方法调用不是简单的运算,因此必须用它们自己来分析它们。第一次方法调用是fib(n-1),从而按照T的定义它需要T(N-1)个时间单位。类似的论证指出,第二次方法调用需要T(N-2)个时间单位。此时总的时间需求为T(N-1)+T(N-2)+2,其中2指的是第1行上的工作加上第3行上的加法。于是对于N>=2,有下列关于fib(n)的运行时间公式:

T(N)=T(N-1)+T(N-2)+2

但是fib(N)=fib(N-1)+fib(N-2),因此由归纳法容易证明T(N)>=fib(N)。之前我们证明过fib(N)<(5/3)的N次方,类似的计算可以证明(对于N>4)fib(N)>=(3/2)的N次方,从而这个程序的运行时间以指数的速度增长。这大致是最坏的情况。通过保留一个简单的数组 并使用一个for循环,运行时间可以显著降低。

这个程序员之所以运行缓慢,是因为存在大量多余的工作要做,违反了之前叙述的递归的第四条主要法则(合成效益法则)。注意,在第3行上的第一次调用即fib(n-1)实际上在某处计算fib(n-2)。这个信息被抛弃而在第3行上的第二次调用时又重新计算了一遍。抛弃的信息量递归第合成起来并导致巨大的运行时间。这或许是格言,“计算任何事情不要超过一次”的最好实例,但它不应使你被吓得远离递归而不敢使用。

 

《数据结构与算法分析》这本书确实不太好读,通过将边看边用记录,总算还是注意力比较集中。但愿能够使我痛苦的能够使我变得强大。

 

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

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐


【啊哈!算法】算法3:最常用的排序——快速排序       上一节的冒泡排序可以说是我们学习第一个真正的排序算法,并且解决了桶排序浪费空间的问题,但在算法的执行效率上却牺牲了很多,它的时间复杂度达到了O(N2)。假如我们的计算机每秒钟可以运行10亿次,那么对1亿个数进行排序,桶排序则只需要0.1秒,而冒泡排序则需要1千万秒,达到115天之久,是不是很吓人。那有没有既不浪费空间又可以快一点的排序算法
匿名组 这里可能用到几个不同的分组构造。通过括号内围绕的正则表达式就可以组成第一个构造。正如稍后要介绍的一样,既然也可以命名组,大家就有考虑把这个构造作为匿名组。作为一个实例,请看看下列字符串: “08/14/57 46 02/25/59 45 06/05/85 18 03/12/88 16 09/09/90 13“ 这个字符串就是由生日和年龄组成的。如果需要匹配年两而不要生日,就可以把正则
选择排序:从数组的起始位置处开始,把第一个元素与数组中其他元素进行比较。然后,将最小的元素方式在第0个位置上,接着再从第1个位置开始再次进行排序操作。这种操作一直到除最后一个元素外的每一个元素都作为新循环的起始点操作过后才终止。 public void SelectionSort() { int min, temp;
public struct Pqitem { public int priority; public string name; } class CQueue { private ArrayList pqueue; public CQueue() { pqueue
在编写正则表达式的时候,经常会向要向正则表达式添加数量型数据,诸如”精确匹配两次”或者”匹配一次或多次”。利用数量词就可以把这些数据添加到正则表达式里面了。 数量词(+):这个数量词说明正则表达式应该匹配一个或多个紧紧接其前的字符。 string[] words = new string[] { "bad", "boy", "baad", "baaad" ,"bear", "b
来自:http://blog.csdn.net/morewindows/article/details/6678165/归并排序是建立在归并操作上的一种有效的排序算法。该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。首先考虑下如何将将二个有序数列合并。这个非常简单,只要从比较二个数列的第一个数,谁小就先取谁,取了后就在对应数列中删除这个数。然后再进行比较,如果有数列
插入排序算法有两层循环。外层循环会啄个遍历数组元素,而内存循环则会把外层循环所选择的元素与该元素在数组内的下一个元素进行比较。如果外层循环选择的元素小于内存循环选择的元素,那么瘦元素都想右移动以便为内存循环元素留出位置。 public void InsertionSort() { int inner, temp;
public int binSearch(int value) { int upperBround, lowerBound, mid; upperBround = arr.Length - 1; lowerBound = 0; while (lowerBound <= upper
虽然从表内第一个节点到最后一个节点的遍历操作是非常简单的,但是反向遍历链表却不是一件容易的事情。如果为Node类添加一个字段来存储指向前一个节点的连接,那么久会使得这个反向操作过程变得容易许多。当向链表插入节点的时候,为了吧数据复制给新的字段会需要执行更多的操作,但是当腰吧节点从表移除的时候就能看到他的改进效果了。 首先需要修改Node类来为累增加一个额外的链接。为了区别两个连接,这个把指
八、树(Tree)树,顾名思义,长得像一棵树,不过通常我们画成一棵倒过来的树,根在上,叶在下。不说那么多了,图一看就懂:当然了,引入了树之后,就不得不引入树的一些概念,这些概念我照样尽量用图,谁会记那么多文字?树这种结构还可以表示成下面这种方式,可见树用来描述包含关系是很不错的,但这种包含关系不得出现交叉重叠区域,否则就不能用树描述了,看图:面试的时候我们经常被考到的是一种叫“二叉树”的结构,二叉
Queue的实现: 就像Stack类的实现所做的一样,Queue类的实现用ArrayList简直是毋庸置疑的。对于这些数据结构类型而言,由于他们都是动态内置的结构,所以ArrayList是极好的实现选择。当需要往队列中插入数据项时,ArrayList会在表中把每一个保留的数据项向前移动一个元素。 class CQueue { private ArrayLis
来自:http://yingyingol.iteye.com/blog/13348911 快速排序介绍:快速排序是由东尼·霍尔所发展的一种排序算法。在平均状况下,排序 n 个项目要Ο(n log n)次比较。在最坏状况下则需要Ο(n2)次比较,但这种状况并不常见。事实上,快速排序通常明显比其他Ο(n log n) 算法更快,因为它的内部循环(inner loop)可以在大部分的架构上很有效率地
Stack的实现必须采用一种基本结构来保存数据。因为再新数据项进栈的时候不需要担心调整表的大小,所以选择用arrayList.using System;using System.Collections.Generic;using System.Linq;using System.Text;using System.Threading.Tasks;using System.Collecti
数组类测试环境与排序算法using System;using System.Collections.Generic;using System.Linq;using System.Text;using System.Threading.Tasks;namespace Data_structure_and_algorithm{ class CArray { pr
一、构造二叉树 二叉树查找树由节点组成,所以需要有个Node类,这个类类似于链表实现中用到的Node类。首先一起来看看Node类的代码。 public class Node { public int Data; public Node Left; public Node Right; public v
二叉树是一种特殊的树。二叉树的特点是每个结点最多有两个儿子,左边的叫做左儿子,右边的叫做右儿子,或者说每个结点最多有两棵子树。更加严格的递归定义是:二叉树要么为空,要么由根结点、左子树和右子树组成,而左子树和右子树分别是一棵二叉树。 下面这棵树就是一棵二叉树。         二叉树的使用范围最广,一棵多叉树也可以转化为二叉树,因此我们将着重讲解二叉树。二叉树中还有连两种特殊的二叉树叫做满二叉树和
上一节中我们学习了队列,它是一种先进先出的数据结构。还有一种是后进先出的数据结构它叫做栈。栈限定只能在一端进行插入和删除操作。比如说有一个小桶,小桶的直径只能放一个小球,我们现在向小桶内依次放入2号、1号、3号小球。假如你现在需要拿出2号小球,那就必须先将3号小球拿出,再拿出1号小球,最后才能将2号小球拿出来。在刚才取小球的过程中,我们最先放进去的小球最后才能拿出来,而最后放进去的小球却可以最先拿
msdn中的描述如下:(?= 子表达式)(零宽度正预测先行断言。) 仅当子表达式在此位置的右侧匹配时才继续匹配。例如,w+(?=d) 与后跟数字的单词匹配,而不与该数字匹配。此构造不会回溯。(?(零宽度正回顾后发断言。) 仅当子表达式在此位置的左侧匹配时才继续匹配。例如,(?此构造不会回溯。msdn描述的比较清楚,如:w+(?=ing) 可以匹配以ing结尾的单词(匹配结果不包括ing),(
1.引入线索二叉树 二叉树的遍历实质上是对一个非线性结构实现线性化的过程,使每一个节点(除第一个和最后一个外)在这些线性序列中有且仅有一个直接前驱和直接后继。但在二叉链表存储结构中,只能找到一个节点的左、右孩子信息,而不能直接得到节点在任一遍历序列中的前驱和后继信息。这些信息只有在遍历的动态过程中才能得到,因此,引入线索二叉树来保存这些从动态过程中得到的信息。 2.建立线索二叉树 为了保
排序与我们日常生活中息息相关,比如,我们要从电话簿中找到某个联系人首先会按照姓氏排序、买火车票会按照出发时间或者时长排序、买东西会按照销量或者好评度排序、查找文件会按照修改时间排序等等。在计算机程序设计中,排序和查找也是最基本的算法,很多其他的算法都是以排序算法为基础,在一般的数据处理或分析中,通常第一步就是进行排序,比如说二分查找,首先要对数据进行排序。在Donald Knuth 的计算机程序设