算法运行时间计算

摘自《数据结构和算法分析》Java语言描述

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

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

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

 一个简单的例子:1^3+2^3+3^3+4^3+……+n^3+……

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

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

如果每次分析一个程序都要演示所有的这些工作,那么这项任务很快就会变成负担。由于我们有了大O的结果,因此就存在许多可以采取的捷径并且不影响最后的结果。例如每次都执行的partialSum += i *i * i的时间明显是类似O(1),至于精确计算它究竟是2、3、或者4等是愚蠢的,因为这无关紧要。而int partialSum和for循环显然是不重要的,所以在这里话费时间是不明智的。这使我们得到若干法则。

法则1——for循环

一个for循环的运行时间至多使该for循环内部那些语句的运行时间乘以迭代的次数。

法则2——嵌套的for循环

从里向外分析这些循环。在一组嵌套循环内部的一条语句总的运行时间为该语句的运行时间乘以该组所有的for循环的大小的乘积。例如下列程序片段为O(N^2)

for (int i = 0; i < n; i++)
    for (int j = 0; j < n; j++)
        System.out.println();

法则3——顺序语句

将各个语句的运行时间求和即可(这意味着,,其中的最大值就是所得的运行时间)例如,下面的程序片段显示花费O(N),接着使O(N^2),因此总量也是O(N^2),而忽略O(N)

for (int i = 1; i < n; i++) {
    System.out.println();
}
for (int i = 0; i < n; i++)
    for (int j = 0; j < n; j++)
        System.out.println();

法则4——if/else语句

if(condition)
    S1
else 
    S2

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

…………

其他的法则都是冥想的,但是分析的基本策略使从内部(或最深层部分)向外开展工作的。如果有方法调用,那么要首先分析这些调用。如果有递归过程,那么存在几种情况:若递归实际上只是被薄面纱遮住的for循环(很容易转换成for循环),则分析很简单。如下面递归实际上就是一个for循环:

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

这是一个特殊的例子,但当递归被正常使用时,很难将其转换成循环结构。这种情况下,分析将设计求解一个递推关系。

public static long fib(int n) {
    if (n <= 1)
        return 1;
    else
        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,则执行时间是判断时间加上else语句上的执行时间,而else语句有一次家发和两次方法调用组成。第一次方法调用是fib(n-1),及T(N-1)运行时间。以此类推可以得到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)=fib(N-1)+fib(N-2)使用归纳法可以得出fib(N)>=(3/2)^N,及可得出T(N)>=(3/2)^N,可以看出运行时间以指数的速度增长。

这个程序之所以运行缓慢,是因为存在大量多余的工作要做。在fib(n - 1) + fib(n - 2)语句中,fib(n - 1)调用的时候实际上已经计算过一次fib(n - 2),而这个信息被抛弃,之后在+之后又计算了一次fib(n - 2)。这些抛弃的信息递归地合起来就导致了巨大的运行时间。这或许是“计算任何事情不要超过一次”的最好的实例,但它不应使你被吓得远离递归而不敢使用。

 

你可能感兴趣的:(算法运行时间计算)