前言
通常,对于一个给定的算法,我们要做两项分析。第一是从数学上证明算法的正确性,这一步主要用到形式化证明的方法及相关推理模式,如循环不变式、数学归纳法等。而在证明算法是正确的基础上,第二就是分析算法的时间复杂度。算法的时间复杂度反映了程序执行时间随输入规模增长而增长的量级,在很大程度上能很好反映出算法的优劣与否。因此,作为程序员,掌握基本的算法时间复杂度分析方法是很有必要的。
但是很多朋友并不能清晰的理解这一概念,究其原因,主要是因为没有从数学层面上理解其本质,而是习惯于从直观理解。下面,我们就一步步走近算法时间复杂度的数学本质。
算法时间复杂度的数学意义
从数学上定义,给定算法A,如果存在函数F(n),当n=k时,F(k)表示算法A在输入规模为k的情况下的运行时间,则称F(n)为算法A的时间复杂度。
另一种定义:
一般情况下,算法中基本操作重复执行的次数是问题规模n的某个函数,用T(n)表示,若有某个辅助函数f(n),使得当n趋近于无穷大时,T(n)/f(n)的极限值为不等于零的常数,则称f(n)是T(n)的同数量级函数。记作T(n)=O(f(n)),称O(f(n))为算法的渐进时间复杂度(O是数量级的符号 ),简称时间复杂度。
这里我们首先要明确输入规模的概念。关于输入规模,不是很好下定义,非严格的讲,输入规模是指算法A所接受输入的自然独立体的大小。例如,对于排序算法来说,输入规模一般就是待排序元素的个数,而对于求两个同型方阵乘积的算法,输入规模可以看作是单个方阵的维数。为了简单起见,在下面的讨论中,我们总是假设算法的输入规模是用大于零的整数表示的,即n=1,2,3,……,k,……
我们还知道,对于同一个算法,每次执行的时间不仅取决于输入规模,还取决于输入的特性和具体的硬件环境在某次执行时的状态。所以想要得到一个统一精确的F(n)是不可能的。为了解决这个问题,我们做一下两个说明:
1.忽略硬件及环境因素,假设每次执行时硬件条件和环境条件是完全一致的。 2.对于输入特性的差异,我们将从数学上进行精确分析并带入函数解析式。算法时间复杂度分析示例
为了便于朋友们理解,我将不会采用教科书上惯用的快速排序、合并排序等经典示例进行分析,而是使用一个十分简单的算法作为示例。我们先来定义问题。
问题定义:
输入——此问题输入为一个有序序列,其元素个数为n,n为大于零的整数。序列中的元素为从1到n这n个整数,但其顺序为完全随机。 输出——元素n所在的位置。(第一个元素位置为1)这个问题非常简单,下面直接给出其解决算法之一(伪代码):
LocationN(A){ for(int i=1;i<=n;i++)-----------------------t1 { if(A[i] == n) ----------------------------t2 { return i; }------------------------t3 }}
我们来看看这个算法。其中t1、t2和t3分别表示此行代码执行一次需要的时间。
首先,输入规模n是影响算法执行时间的因素之一。在n固定的情况下,不同的输入序列也会影响其执行时间。最好情况下,n就排在序列的第一个位置,那么此时的运行时间为“t1+t2+t3”。最坏情况下,n排在序列最后一位,则运行时间为“nt1+nt2+t3=(t1+t2)*n+t3”。可以看到,最好情况下运行时间是一个常数,而最坏情况下运行时间是输入规模的线性函数。那么,平均情况如何呢?
问题定义说输入序列完全随机,即n出现在1...n这n个位置上是等可能的,即概率均为1/n。而平均情况下的执行次数即为执行次数的数学期望,其解为:
E= p(n=1)*1+p(n=2)*2+...+p(n=n)*n= (1/n)*(1+2+...+n)= (1/n)*((n/2)*(1+n))= (n+1)/2
即在平均情况下for循环要执行(n+1)/2次,则平均运行时间为“(t1+t2)*(n+1)/2+t3”。
由此我们得出分析结论:t1+t2+t3 <= F(n) <= (t1+t2)n+t3,在平均情况下F(n) = (t1+t2)(n+1)/2+t3算法的渐近时间复杂度
以上分析,我们对算法的时间复杂度F(n)进行了精确分析。但是,很多时候,我们不需要进行如此精确的分析,原因有下: 1.在较复杂的算法中,进行精确分析是非常复杂的。 2.实际上,大多数时候我们并不关心F(n)的精确度量,而只是关心其量级。
基于此,提出渐近时间复杂度的概念。在正式给出渐近时间复杂度之前,要先给出几个数学定义:
定义一:Θ(g(n))={f(n) | 如果存在正常数c1、c2和正整数n0,使得当n>=n0时,0<=f(n)<=c2g(n)恒成立}定义二:Ο(g(n))={f(n) | 如果存在正常数c和正整数n0,使得当n>=n0时,0<=f(n)<=cg(n)恒成立}定义三:Ω(g(n))={f(n) | 如果存在正常数c和正整数n0,使得当n>=n0时,0<=cg(n)<=f(n)恒成立}
可以看到,三个定义其实都定义了一个函数集合,只不过集合中的函数需要满足的条件不同。有了以上定义,就可以定义渐近时间复杂度了。
不过这里还有个问题:F(n)不是确定的,他是在一个范围内变动的,那么我们关心哪个F(n)呢?一般我们在分析算法时,使用最坏情况下的F(n)来评价算法效率,原因有如下两点:
1.如果知道了最坏情况,我们就可以保证算法在任何时候都不能比这个情况更坏了。 2.很多时候,算法运行发生最坏情况的概率还是很大的,如查找问题中待查元素不存在的情况。且在很多时候,平均情况的渐近时间复杂度和最坏情况的渐近时间复杂度是一个量级的。于是给出如下定义:设F(n)为算法A在最坏情况下F(n),则如果F(n)属于Θ(g(n)),则说算法A的渐近时间复杂度为g(n),且g(n)为F(n)的渐近确界。
还是以上面的例子为例,则在上面定义中F(n) = (t1+t2)*n+t3。则F(n)的渐近确界为n,其证明如下:
证明:设c1=t1+t2,c2=t1+t2+t3,n0=2又因为 t1,t2,t3均大于0则,当n>n0时,0<=F(n)<=c2n 即 0<(t1+t2)*n<=(t1+t2)*n+t3<=(t1+t2+t3)*n恒成立。所以 F(n)属于Θ(n)所以 n是F(n)的渐近确界证毕
在实际应用中,我们一般都是使用渐近时间复杂度代替实际时间复杂度来进行算法效率分析。一般认为,一个渐近复杂度为n的算法要优于渐近复杂度为n^2的算法。
注意,这并不是说渐近复杂度为n的算法在任何情况下都一定更高效,而是说在输入规模足够大后(大于临界条件n0),则前一个算法的最坏情况总是好于后一个算法的最坏情况。事实证明,在实践中这种分析是合理且有效的。
类似的,还可以给出算法时间复杂度的上确界和下确界 :
设F(n)为算法A在最坏情况下F(n),则如果F(n)属于Ο(g(n)),则说算法A的渐近时间复杂度上限为g(n),且g(n)为F(n)的渐近上确界。设F(n)为算法A在最坏情况下F(n),则如果F(n)属于Ω(g(n)),则说算法A的渐近时间复杂度下限为g(n),且g(n)为F(n)的渐近下确界。
这里一定要注意,由于我们是以F(n)最坏情况分析的,所以,我们可以100%保证在输入规模超过临界条件n0时,算法的运行时间一定不会高于渐近上确界,但是并不能100%保证算法运行时间不会低于渐近下确界,而只能100%保证算法的最坏运行时间不会低于渐近下确界。
相关链接
算法的时间复杂度和空间复杂度总结: