评估算法及算法的时间复杂度

文章导读

【对于一个给定的算法,通常要评估其正确性和运行效率的高低。算法的正确性评估不在本文范围之内,本文主要讨论从算法的时间复杂度特性去评估算法的优劣。】

 

程序是用来解决问题的,是由多个步骤或过程组成的,这些步骤和过程就是解决问题的算法。

解决一个问题有多种方法,也就有多种算法。每一种算法都可以达到解决问题的目的,但花费的成本和时间不尽相同,从节约成本和时间的角度考虑,需要找出最优算法。

那么,如何衡量一个算法的好坏呢?

显然,选用的算法应该是正确的(算法的正确性不在此论述)。除此之外,通常有三个方面的考虑:

(1)算法在执行过程中所消耗的时间;

(2)算法在执行过程中所占资源的大小,例如,占用内存空间的大小;

(3)算法的易理解性、易实现性和易验证性等等。

衡量一个算法的好坏,可以通过前面提出的三个方面进行综合评估。从多个候选算法中找出运行时间短、资源占用少、易理解、易实现的算法。然而,现实情况却不尽人意。往往是,一个看起来很简便的算法,其运行时间要比一个形式上复杂的算法慢的多;而一个运行时间较短的算法往往占用较多的资源。

因此,在不同情况下需要选择不同的算法。在实时系统中,对系统响应时间要求高,则尽量选用执行时间少的算法;当数据处理量大,而存储空间较少时,则尽量选用节省空间的算法。本文主要讨论算法的时间特性,并给出算法在时间复杂度上的度量指标。

一个算法在执行过程中所消耗的时间取决于下面的因素:

(1)算法所需数据输入的时间;

(2)算法编译为可执行程序的时间;

(3)计算机执行每条指令所需的时间;

(4)算法语句重复执行的次数。

其中(1)依赖于输入设备的性能,若是脱机输入,则输入数据的时间可以忽略不计。(2)(3)取决于计算机本身执行的速度和编译程序的性能。因此,习惯上将算法语句重复执行的次数作为算法的时间量度。

例如:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

// x=x+1执行一次

    // 执行一次

    public void add(int x) {

        x = x + 1;

    }

  

    // x=x+1执行n次

    // 执行n次

    public void madd(int x, int n) {

        for (int i = 0; i < n; i++)

            x = x + 1;

    }

  

    // x=x+1执行n*n次

    // 执行n*n次

    public void loopadd(int x, int n) {

        for (int i = 0; i < n; i++)

            for (int j = 0; j < n; j++)

                x = x + 1;

    }

add函数仅包含一条语句,执行次数为1次;madd函数包含n次的单重for循环,每次循环都执行x=x+1语句,因此执行次数为n次;loopadd函数包含n次的双重循环,在第二层循环执行x=x+1语句,因此执行次数为n*n次,即n^2次。

上面的例子并没有把循环本身执行的次数算进去,下面给出2个执行次数计算较为精确的例子。

《评估算法及算法的时间复杂度》

 

函数SUM执行次数为2n+3次。

再看下面矩阵相加的例子。

《评估算法及算法的时间复杂度》

 

矩阵相加的执行次数为2n^2+2n+2。

一般情况下,n为问题规模(大小)的量度,如数组的长度、矩阵的阶、图中的顶点数等等。

对于前面add函数来说,问题规模量度为常数(1);对于数组排序问题来说,问题规模量度为输入数组的长度(记为n);对于n阶矩阵相加来说,问题规模量度为矩阵阶数的平方(记为n^2)。

为了给出算法通用的时间量度,用数学概念来描述算法的执行次数,可以把一个算法中语句的执行次数称为语句频度或时间频度,记为T(n)。当问题规模n不断变化时,时间频度T(n)也会不断变化,我们需要评估当n不断变化时,时间频度T(n)的变化规律。

若有某个辅助函数f(n),当n趋向于无穷大时,如果T(n)/ f(n)的极限为不等于零的常数,则认为T(n)与f(n)是同量级的函数,记作:

T(n) =O(f(n))

O(f(n))称为算法的渐进时间复杂度,简称时间复杂度。

渐进时间复杂度表示的意义是:

(1)在较复杂的算法中,进行精确分析是非常复杂的;

(2)一般来说,我们并不关心T(n)的精确度量,而只是关心其量级。

T (n) = O(f (n)) 表示存在一个常数C,当n趋于正无穷大时,总有:

T (n) ≤ C * f(n)

上面公式的意思是T(n)在n趋于正无穷大时跟f(n)基本接近,因此完全可以用f(n)来表示T(n)。

O(f (n))通常取执行次数中最高次方或最大指数部分的项。例如:

阵列元素相加为2n+3 = O(n)

矩阵相加为2n^2+2n+1 = O(n^2)

矩阵相乘为2n^3+4n^2+2n+2 = O(n^3)

在各种不同的算法中,若算法语句的执行次数为常数,则算法的时间复杂度为O(1),按数量级递增排列,常见的时间复杂度量有:

(1)O(1):常量阶,运行时间为常量

(2)O(logn):对数阶,如二分搜索算法

(3)O(n):线性阶,如n个数内找最大值

(4)O(nlogn):对数阶,如快速排序算法

(5)O(n^2):平方阶,如选择排序,冒泡排序

(6)O(n^3):立方阶,如两个n阶矩阵的乘法运算

(7)O(2^n):指数阶,如n个元素集合的所有子集的算法

(8)O(n!):阶乘阶,如n个元素全部排列的算法

下图给出了随着n的变化,不同量级的时间复杂度变化曲线。

《评估算法及算法的时间复杂度》

 

图1 时间复杂度变化曲线图

评估算法时间复杂度的具体步骤是:

(1)找出算法中重复执行次数最多的语句的频度来估算算法的时间复杂度;

(2)保留算法的最高次幂,忽略所有低次幂和高次幂的系数;

(3)将算法执行次数的数量级放入大Ο记号中。

例如,下列三个简单的程序段:

1

2

3

4

5

(a)x =  x + 1;

(b)for(i = 0;i<n;i++)  x=x+1;

(c)for(i = 0;i<n;i++)

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

          x=x+1;

在程序段(a)中,语句x=x+1不在任何一个循环体内,则它的时间频度为1,其执行时间是个常量;而(b)中,同一语句被重复执行n次,其时间频度为n;显然在(c)中,该语句的频度为n^2。由此,这三个程序段的时间复杂度为O(1)、O(n)、O(n^2)。分别为常量、线性阶和平方阶。

对于较为复杂的算法,可以将它们分隔成容易估算的几个部分,然后利用O的求和原则得到整个算法的时间复杂度。例如,若算法的两个部分的时间复杂度分别为T1(n)=O(f(n))和T2(n)=O(g(n)),则总的时间复杂度为:

T(n)= T1(n)+ T2(n)=O(max(f(n), g(n)))

然而,很多算法的运行时间不仅依赖于问题的规模,也与处理的数据集有关。例如,有的排序算法对某些原始数据(如自小至大有序),则其时间复杂度为O(n),而对另一些数据可达O(n^2)。因此,在估算算法的时间复杂度时,均以数据集中最坏的情况来估算。

文章小结

评估算法时间复杂度的要点是:如果一个算法的执行次数是 T(n),那么只保留最高次项,同时忽略最高项的系数后得到函数 f(n),此时算法的时间复杂度就是 O(f(n))。

    原文作者:langhonglin
    原文地址: https://blog.csdn.net/langhonglin/article/details/82253610
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞