田有朋 2020-04-17
为了编写出一个“好的”的程序,必须分析待处理的对象的特性以及各处理对象之间存在的关系,这就是“数据结构”这门学科形成和发展的背景。编写出一个“好的”的程序离不开算法,算法的时间复杂度反映了程序执行时间随输入规模n增长而增长的量级,在很大程度上能很好反映出算法、尤其是扎根于算法的程序的优劣。因此,作为程序员,掌握基本的算法时间复杂度分析方法是很有必要的。
一个用高级语言编写的程序在计算机上运行时所消耗的时间取决于下列因素:
(1) 算法采用的策略、方法;
(2) 编译产生的代码质量;
(3) 问题的输入规模;
(4) 机器执行指令的速度。
一个算法是由控制结构(顺序、分支和循环3种)和原操作(指固有数据类型的操作)构成的,则算法时间取决于两者的综合效果。为了便于比较同一个问题的不同算法,通常的做法是,从算法中选取一种对于所研究的问题(或算法类型)来说是基本操作的原操作,以该基本操作的重复执行的次数作为算法的时间量度。
(1)时间频度 一个算法执行所耗费的时间,从理论上是不能算出来的,必须上机运行测试才能知道。但我们不可能也没有必要对每个算法都上机测试,只需知道哪个算法花费的时间多,哪个算法花费的时间少就可以了。并且一个算法花费的时间与算法中语句的执行次数成正比例,哪个算法中语句执行次数多,它花费时间就多。一个算法中的语句执行次数称为语句频度或时间频度,记为T(n) ,n称为问题的规模。
(2)时间复杂度 在上述时间频度中,当n不断变化时, T(n)也会不断变化。但有时我们想知道它变化时呈现什么规律。为此,我们引入时间复杂度概念:
如果存在正常数c和n0,使得当n>=n0时,T(n) <=cf(n),则记T(n) =O(f(n)),读作f(n)的大O。称O(f(n)) 为算法的渐进时间复杂度,简称时间复杂度。这种记法称为大O标记法,函数T(n)是集合O(f(n))的一个元素。在时间频度不相同时,时间复杂度有可能相同。例如,O(2n^2+n +1) = O (7n^2 + n) = O ( n^2 ) ,通常用O(n^2)表示就可以了。
下面给出两个基本的运算法则:如果T1(n) =O(f(n))且T2(n) =O(g(n)),那么
(a)求和法则T1(n)+T2(n)= O(f(n)+ g(n));
(b) 乘法法则T1(n)*T2(n)= O(f(n)* g(n))。
按数量级递增排列,常见的算法时间复杂度由小到大依次为:
Ο(1)<Ο(logn)<Ο(n)<Ο(nlogn)<Ο(n^2)<Ο(n^3)<…<Ο(2^n)<Ο(n!)
一般情况下,对一个问题(或一类算法)只需选择一种基本操作来讨论算法的时间复杂度即可,有时也需要同时考虑几种基本操作,甚至可以对不同的操作赋予不同的权值,以反映执行不同操作所需的相对时间,这种做法便于综合比较解决同一问题的两种完全不同的算法。
求解算法时间复杂度的具体步骤是:
⑴ 找出算法中的基本语句;
算法中执行次数最多的那条语句就是基本语句,通常是最内层循环的循环体。
⑵ 计算时间频度的数量级;
只需计算基本语句执行次数的数量级,这就意味着只要保证基本语句执行次数的函数中的最高次幂正确即可,可以忽略所有低次幂和最高次幂的系数。这样能够简化算法分析,并且使注意力集中在最重要的一点上:增长率。
⑶ 用大Ο标记法表示算法的时间性能。
将基本语句执行次数的数量级放入大Ο记号中。
如果算法中包含嵌套循环,则基本语句通常是最内层的循环体,如果算法中包含并列的循环,则将并列循环的时间复杂度相加。
一般来说,只要算法中不存在循环语句,其时间复杂度就是Ο(1)。其中Ο(logn)、Ο(n)、 Ο(nlogn)、Ο(n^2)和Ο(n^3)称为多项式时间,而Ο(2^n)和Ο(n!)称为指数时间。计算机科学家普遍认为前者是有效算法,把这类问题称为P(Polynomial,多项式)类问题,而把后者(即指数时间复杂度的算法)称为NP(Non-Deterministic Polynomial, 非确定多项式)问题。
在计算算法时间复杂度时,有以下几个简单的程序分析法则:
(1)对于一些简单的输入输出语句或赋值语句,近似认为需要O(1)时间。
(2)对于顺序结构,需要依次执行一系列语句所用的时间可采用大O下求和法则。
(3)对于选择结构,如if语句,它的主要时间耗费是在执行then字句或else字句所用的时间,需注意的是检验条件也需要O(1)时间。
(4)对于循环结构,循环语句的运行时间主要体现在多次迭代中执行循环体以及检验循环条件的时间耗费,一般可用大O下"乘法法则"。
(5)对于复杂的算法,可以将它分成几个容易估算的部分,然后利用求和法则和乘法法则技术整个算法的时间复杂度。
下面分别对几个常见的时间复杂度进行示例说明:
(1)常数阶O(1)
Temp=i; i=j; j=temp;
以上三条单个语句的频度均为1,该程序段的执行时间是一个与问题规模n无关的常数。算法的时间复杂度为常数阶,记作T(n)=O(1)。
注意:如果算法的执行时间不随着问题规模n的增加而增长,即使算法中有上千条语句,其执行时间也不过是一个较大的常数。此类算法的时间复杂度是O(1)。
(2)线性阶O(n)
a=0; b=1; ① for (i=1;i<=n;i++) { ② s=a+b; ③ b=a; ④ a=s; ⑤ }
解: 语句1的频度为2,语句2的频度为 n,语句3、4和5的频度为 n-1,因而有T(n)=2+n+3(n-1)=4n-1=O(n)。
(3)平方阶O(n^2)
for (i=1;i<n;i++) { y=y+1; ① for (j=0;j<=(2*n);j++) x++; ② }
解: 语句1的频度是n-1,语句2的频度是(n-1)*(2n+1)=2n2-n-1;故
T(n)=2n2-n-1+(n-1)=2n2-2;
因此,该程序的时间复杂度T(n)=O(n2)。
一般情况下,对步进循环语句只需考虑循环体中语句的执行次数,忽略该语句中步长加1、终值判别、控制转移等成分。当有若干个循环语句时,算法的时间复杂度是由嵌套层数最多的循环语句中最内层语句的频度f(n)决定的。
(4)对数阶O(logn)
i=1; while (i<=n) i=i*2;
此算法的时间复杂度是 O(logn),它是怎么算出来的呢?其实很简单:上述的代码可以理解成 i乘以多少个 2 以后才能大于等于 n,假设个数是 x,则 2^x = n,可以求得x = log2n,所以这个循环的时间复杂度就是 O(logn)。
知识扩展:对数阶中底数是多少?底数可以是2,e或者10等任意常数(记作c),但我们统一说 logn,也就是忽略对底数的描述,因为O(logcn)=logc10*O(log10n)。
(5)、立方阶O(n3)
for(i=0;i<n;i++){ for(j=0;j<i;j++){ for(k=0;k<j;k++) x=x+2; } }
时间复杂度为O(n^3),此处省略证明过程。
算法时间复杂度分析是一个很重要的问题,任何一个程序员都应该熟练掌握其概念和基本方法,而且要善于从数学层面上探寻其本质,才能准确理解其内涵。
https://www.cnblogs.com/ricklz/p/9002796.html
数据结构与算法分析Java语言描述 第三版