算法的时间复杂度

[转自:http://blog.sciencenet.cn/blog-340399-861142.html ]

其实说那么多,只要记住一句话: 109 的数据可以在一秒内跑出(可以由此来判断自己的算法是否科学)

简介:算法时间复杂度的研究,以所谓的多项式时间做为最低复杂度,由此认定只有多项式类型的算法程序执行最快,实在有些不靠谱。实际上计算机程序执行时间可以用其编译之后的机器语言程序来计算。因为每一机器指令(汇编指令)的指令周期都是确定的,故计算程序执行的时间,可先计算出每条机器指令重复执行次数,然后与指令周期相乘求和,最终获得准确时间。本文给出了的算法程序执行中指令重复执行次数的二元函数基本计算公式,依据该计算公式可以实际计算出程序运行的时间消耗,又提出了程序设计方法转换为一元函数之后,如何判定在同样的循环重复数之下,判定不同算法指令重复执行次数增加的快慢,进而判定各种算法程序执行时间的变化。

  1. 背景

为了研究算法程序完成任务运行时间长短,人们引入了算法时间复杂度的概念。

在维基百科中,算法时间复杂度被定义为:在计算机科学中,算法的时间复杂度是一个函数,它定量描述了该算法的运行时间。这是一个关于代表算法输入值的字符串的长度的函数。时间复杂度常用大O符号表述,不包括这个函数的低阶项和首项系数。

在百度文库中有计算方法解释:一般情况下,算法的基本操作重复执行的次数是模块n的某一个函数f(n),因此,算法的时间复杂度记做:T(n)=O(f(n))。若 T(n)/f(n) 求极限可得到一常数c,则时间复杂度T(n) = O(f(n))。

不论维基百科还是百度的解释,都称算法时间复杂度是程序运行时间的定量描述。实际上,我们见到的大O符号表述只能算定性描述,根本谈不上定量。

为了能够确实对算法程序运行时间进行定量分析,本文从单处理器计算机指令设计与程序执行的原理出发,依据程序设计的基本结构,提出了一种比较切合实际的程序执行时间计算方法。结果会见到有限时间的程序运行计算,都可以表达成一种二元的多项式形式。

  1. 指令重复执行次数

我们知道,不论任何形式的计算机设计语言程序都要转化成机器语言程序才能够执行。因此,用与机器语言一一对应的汇编语言来讨论程序执行时间才应该是最准确的。不过由于各种语言程序在执行时间分析上具有一定的一致性,故而作定性分析时,也可以替代汇编语言。

不论何种计算机程序设计语言,就程序设计的基本结构来说都是一致的。程序的基本结构分为:顺序、分支、循环和子程序调用。任何计算机程序的执行都可以说是这几种程序结构的重复,所以计算程序执行过程所消耗的时间,都离不开对程序基本结构的分析。

程序执行的过程是由每个指令执行的过程组成的,因而程序执行的时间就是指令执行时间的累加。在汇编程序语言中,每条指令执行的时间消耗是确定的。特别是那些指令周期相等的计算机系统设计,用单一指令执行的时间乘以全部指令重复执行的次数,就能够计算出程序执行所需要的时间。如果计算机的指令周期不相同,则可以用每条指令的指令周期做为系数,进行所谓的加权求和来计算程序执行的时间。因此,算法程序执行时间的消耗,最关键的是计算出每条指令重复执行的次数。

  1. 指令重复执行次数公式

我们将汇编程序不在执行状态的每条指令叫“写出的指令”,计算指令重复执行次数,就与这种写出的指令有关。

指令重复执行次数是由程序的基本结构所决定的。在顺序程序结构中写出的指令重复执行次数是1。而在循环结构中,写出的指令重复执行次数与循环的次数一致。程序执行中消耗时间最长的就是循环程序结构。直观地说,循环的次数越多,处在循环体内的写出指令重复执行次数越多,因而累计耗时就越长。在此种观点之下,整个程序就可以看成是多层循环结构,可以按照多层循环结构来计算程序执行时间。当然,子程序可以单独计算子程序执行的时间。由于分支程序结构的程序分支选择总是惟一的,故时间计算基本上等同于顺序结构。

我们将顺序与分支都认定为0层循环,那么程序运行的耗时计算,可以建立在多层循环结构的耗时计算上,也就是循环结构的写出指令重复执行次数计算上。如果用函数来描述,那么一个程序的指令重复执行次数T应为层内循环次数n与循环层数k的二元函数,即T=f(n,k)。为说明问题简单通俗,我们通过c程序的例子加以解释。

例1,假设k=n,那么幂nn结构的n重循环用c语言描述如下。

for( i1 =1; i0 <=n; i1 ++){

for( i2 =1; i1 <=n; i2 ++){

……

for( in =1; in <=n; in ++){

no1 += 1;

no2 += no1 ;

}}…}

for语句条件表达式的初值语句在每层只执行一次,而定界和步长语句都要执行n次。显然最内层每个语句重复循环次数是nn,向外各层依次为 nn1 nn2 、…、 n2 、n。那么总的语句重复执行次数为1+3n+ 3n2 + 3n3 +…+ 3nn2 + 3nn1 + 4nn

对于一个程序,在顺序结构中,指令重复执行次数是1;在单层n次循环结构中,指令重复执行的次数是n;在k层n次循环当中写出的指令,其重复执行的次数是 nk 。于是各层指令重复执行的次数可用公式

f(n,k) = a0 + a1n + a2n2 + ... + ak1nk1 + aknk 来计算,其中 ai 是各循环层的写出指令数,i=0,1,2,…,k。

如果将公式(1)的k认为是常量,那么(1)式不就是一个多项式吗?我们能说这种形式的算法复杂度程序执行最快吗?

  1. 计算类型实例

公式(1)中,若幂指数最高固定为常数k,则得到k次多项式;若幂底数固定为常数k,则得到指数多项式;若1层循环次数从1开始,逐层循环次数加1递增,则得到阶乘多项式。如此变化循环层数或循环次数,则可以得到各种重复执行计算的实际公式。为了说明这方面的演变,我们再举两个实例。

例2,将(1)式的幂底数(循环次数)设定为常数2,则能够得到指数型指令重复执行的多项式表达式 a0 + a12 + a222 +…+ an12n1 + an2n

例3,设定幂底数是常数k,下面的循环结构可以实现nlogkn型的指令重复执行次数。

for( i=0; i < <script type="math/tex" id="MathJax-Element-3932"><</script>n; i++){

for( j=k; j<=n; j*=k){

no1 += 1;

no2 += no1 ;

}}

由j*=k知j的值循环变化序列为k, k2 , k3 ,…, kt =n,于是内层循环次数t= logk n。

由于这个程序的外层循环次数是n,内层循环次数是logkn,因而程序指令重复执行次数总共是1+2n+n+4n logk n=1+3n+4n logk n。

  1. 程序执行时间比较

关于算法程序时间复杂度,一种流行的观点认为多项式类型的算法复杂度较低,而且认为多项式类型算法程序运行耗时最少。其实在大多数情况下这是一种错觉。

从公式(1)我们知道,决定算法程序执行时间长短的有两个因素,一个是多项式的幂指数k,另一个是多项式的幂底数n。当幂指数k较大的时候,虽然它是一个常数,我们也不能够认为多项式时间类型算法程序运行时间消耗会最少。特别在n < <script type="math/tex" id="MathJax-Element-3942"><</script>k时,从(1)式立即知道, nk 多项式型算法程序时间的消耗会大于幂 nn 型算法程序的耗时。这说明在较大的循环层数的范围内,不能够就认为多项式时间具有较低的复杂度,当然也不能够认为这种情况的算法程序耗时最短。我们不妨仅就一项来进行比较。例如设n=5,k=7,那么 nk = 57 =78125;而 nn = 55 =3125。由此可见用多项式时间复杂度来说明算法程序耗时长短是不太靠谱的事情。有人也许会说,n趋于无穷才对。想象一下,n趋于无穷大对程序执行的时间计算的意义有多大?

  1. 算法指令增速

在前面我们已经指出,程序执行时间要用指令重复执行次数为计算单位。并给出了二元多项式型计算公式。这自然会引出什么样的算法程序耗时较多,什么样的算法程序耗时较少的问题。由于同样的问题或任务可以采用不同形式的算法解决,故而研究不同算法程序执行耗时的多少具有现实意义。

6.1 指令增速

将(1)这个公式进行一元函数变换,即或者固定比较次数,或者固定循环层数,可以得到各种与实际问题有关的一元函数。例如我们用c表示常数,n表示变量,(1)式可以演化出 nc cn nn 、n logc n等多种类型程序指令重复执行次数的计算公式。不同的类型公式直接与程序执行的耗时有关,因而我们有兴趣去研究,那些能够判定出采用何种类型算法更能够耗时较少的方法。

为了统一且不失一般性,我们让循环各层参与执行的指令都尽可能的少些,比如各层执行的指令只有一个。那么由(1)式可以得到下面一些类型的指令重复次数的一元函数表达式。这个函数我们用F(n)表示。

F(n)= 1c + 2c + 3c +…+ nc (2)

F(n)= 20 + 21 + 22 +…+ 2n1 + 2n (3)

F(n)=1!+2!+3!+…+(n-1)!+n! (4)

F(n)= lg1+2lg2+3lg3+…+(n-1)lg(n-1)+nlgn (5)

F(n)= 11 + 22 + 33 +…+ (n1)n1 + nn (6)

这些表达式中的变量n或者表示循环中指令重复执行次数,或者表示程序循环结构的层数,也可以具有两者同步的属性。为了研究各种算法程序执行过程中F(n)的增长快慢,我们引入F(n)的指令增长速度概念。

定义1:F(n)中变量n增加一个单位的指令重复次数增长量,称为指令增速。

由定义可知指令增速是与n有关的函数。将定义1用表达式写出,并用υ(n)来记,则有:υ(n)=F(n) - F(n-1)

于是,表达式(2)的指令n点增速υ(n)= nc

表达式(3)的指令增速υ(n)= 2n

表达式(4)的指令增速υ(n)=n!;

表达式(5)的指令增速υ(n)= nlgn;

表达式(6)的指令增速υ(n)= nn

例,n=10,求上列各算法指令增速。

表达式(2)的指令增速是υ(n)= nc = 10c .

表达式(3)的指令增速是υ(n)= 2n = 210 =1024.

表达式(4)的指令增速是υ(n)=n!=10!=3628800.

表达式(5)的指令增速是υ(n)= nlgn=10 lg10=10.

表达式(6)的指令增速是υ(n)= nn = 1010 =10000000000.

指令增速不是表示算法程序执行时间缩短的指标,而是在算法中指令重复执行的次数增加的快慢指标。如果指令增速较大,则说明在某种程序设计之下指令重复执行的次数增加很快。例如在n=9到n=10的变化中,上面各种算法程序指令增速最快的是幂 nn 型,指令增速最慢的是nlgn型。所谓的多项式型由指数常量确定,c=1时指令增速最慢,若是c的值超过10,那么在这个地方,会成为指令增速最快的算法形式。因而我们说所谓的O( nk )表示的多项式时间程序执行最快是不靠谱的。

公式(1)的每一种确定的变化形式都代表一种算法。指令增速无疑能够表示在确定的循环次数或循环层数处指令重复执行次数的增长快慢。这自然可以大致描述n值附近的程序执行耗时的变化。例如n=10,我们立即可以计算出由n=9变到n=10的指令增速,从而判定出那种算法在此处耗时增加快慢。

6.2 指令相对变化率

依据定义1我们能够计算出指令增速表达式,但是由于在n的值变动的情况下,我们仍然不能直接比较出各种形式的表达式指令增速的快慢,为了能够在统一的范围内进行比较,我们引入单位指令重复执行次数下的指令增速,即指令相对变化率的概念。

定义2:指令增速与指令重复执行次数的比,称为指令相对变化率。

用μ表示指令相对变化率,即有μ=υ(n)/ F(n)。由指令相对变化率的大小就可以知道,不同类型算法的指令增速对程序执行时间长短的影响程度。

这样,表达式(2)的指令相对变化率

μ=υ(n)/ F(n)= nc /( 1c + 2c + 3c +…+ nc );

表达式(3)的指令相对变化率

μ=υ(n)/ F(n)= 2n /( 20 + 21 + 22 +…+ 2n1 + 2n );

表达式(4)的指令相对变化率

μ=υ(n)/ F(n)= n! /(1!+2!+3!+…+(n-1)!+n!);

表达式(5)的指令相对变化率

μ=υ(n)/ F(n)= nlgn /( lg1+2lg2+3lg3+…+(n-1)lg(n-1)+nlgn);

表达式(6)的指令相对变化率

μ=υ(n)/ F(n)= nn /( 11 + 22 + 33 +…+ (n1)n1 + nn )。

在这些公式中只要确定n值,就可以求出指令相对变化率。

6.3 算法指令增速

做为一种算法,一般都要研究公式(1)中k或n的全局变化情况。这种变化会随着k与n的值超大变化带来诸多的未知状况,为此对k或n向着无穷大方向的变化,就体现了与算法相关的指令执行重复数的整体变化规律。对于一元函数的情况,我们引入算法指令增速的概念。

定义3:指令相对变化率的极限称为算法指令增速。

我们希望在n较大的时候,通过算法一元函数全局的变化,来寻找算法类型对指令执行重复数的增长规律。如此我们对上面的几种类型指令相对变化率求极限。

表达式(2)的算法指令增速为

nc /( 1c + 2c + 3c +…+ n1c + nc )=0;

表达式(3)的算法指令增速为 lim 2n /( 20 + 21 + 22 +…+ 2n1 + 2n =1/( 2n + 2n+1 +…+ 21 +1)=1/2;这是一个等比序列,如果幂底数是常数c,那么结果将是(c-1)/c。

表达式(4)的算法指令增速为 lim n! /(1!+2!+3!+…+(n-1)!+n!)=1

表达式(5)的算法指令增速为 lim nlgn /( lg1+2lg2+3lg3+…+(n-1)lg(n-1)+nlgn)

这个极限是多少?我没能求出,依指令增速判断应该是0。请有兴趣的读者帮我补充一下。

表达式(6)的算法指令增速为 lim nn /( 11 + 22 + 33 +…+ (n1)n1 + nn )=1

(上面各极限n->∞)

由指令增速与指令重复执行次数的定义可知,μ=υ(n)/ F(n)是一个0与1之间的数。因而0是最小的算法指令增速,1是最大的算法指令增速。由此知从算法全局来看,多项式型算法的指令增速最小,阶乘型或幂指型增速最快,而指数型介于它们之间。

需要说明,算法指令增速是对算法全局的一种属性描述,它并不能够代表算法程序执行的实际时间。这是因为不论何种算法,只要n趋向无穷,那么算法程序执行就不会有完结的时候,因而也就无所谓程序执行时间快慢问题了。

算法指令增速很像算法时间复杂度,但算法指令增速是真实依据时间计算获得的概念,就此一点就说明比没有确切定义的算法时间复杂度更加实用。

  1. 结论

计算程序执行时间,要通过程序写出的指令重复执行次数进行,指令重复执行次数的多少,取决于程序循环结构的循环次数和循环层数这两个变量。通俗地讲,循环层数和循环次数越大,程序运行得出结果所消耗的时间越长。

任何在计算机上完成任务的程序运行,都需要在有限时间内解决问题,不然我们编写的程序就失去了意义。因而研究程序运行时间的有限性,远比研究程序执行时间的无限性更为重要。但是为了定性地比较算法的效率,我们引入了算法指令增速的概念,作者相信,依据算法指令增速的概念可以推进算法的深入研究,也或许对P与NP的讨论会有所帮助。

【学了多年信竞却对时间复杂度不明所以的窝,真是弱爆辣!//忧愁】

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值