算法设计与分析:基于C++编程语言的描述
上QQ阅读APP看书,第一时间看更新

1.3.4 算法渐进复杂性

视频讲解

随着经济的发展、社会的进步、科学研究的深入,人们要求用计算机解决的问题越来越复杂,规模也越来越大。对解决这类问题的算法进行分析时,如果精打细算,即把所有的相关因素及元运算都考虑进去,那么由于问题的规模很大且结构复杂,算法分析的工作量之大、步骤之繁将令人难以承受。

为此,对于规模充分大、结构又十分复杂的这类问题的解决算法,人们提出了其复杂性分析应如何简化的问题。

视频讲解

视频讲解

视频讲解

1.算法渐进复杂性的引入

假设算法A的运行时间表达式T1n)为

T1n)=30n4+20n3+40n2+46n+100 (1-1)

算法B的运行时间表达式T2n)为

T2n)=1000n3+50n2+78n+10 (1-2)

显然,当问题的规模足够大的时候,例如n=100万,算法的运行时间将主要取决于时间表达式的第一项,其他项的执行时间只有第一项的几十万分之一,可以忽略不计。随着n的增大,第一项的常数对算法的执行时间也变得不重要了。

于是,算法A的运行时间可以记为:n)≈n4,称n4n)的阶。

同理,算法B的运行时间可以记为:n)≈n3,称n3n)的阶。

由上述分析可以得出一个结论:随着问题规模的增大,算法时间复杂性主要取决于运行时间表达式的阶。如果要比较两个算法的效率,只需比较它们的阶就可以了。

定义1 设算法的运行时间为Tn),如果存在Tn),使得

就称Tn)为算法渐进时间复杂性。

可见,问题规模充分大时,Tn)和Tn)近似相等。因此,在算法分析中,对算法时间复杂性和算法渐进时间复杂性往往不加区分,并常用后者来对一个算法时间复杂性进行衡量,从而简化了大规模问题的时间复杂性分析。

2.渐进意义下的记号

与简化的复杂性相匹配,引入了渐近意义下的记号OoΩwΘ,下面讨论OΩΘ三个记号。设Tn)、fn)和gn)是正数集上的正函数,其中n是问题规模。

(1)渐近上界记号:O(big-oh)。

定义2 若存在两个正常数cn0,使得当nn0时,都有Tn)≤cfn),则称Tn)=Ofn)),即fn)是Tn)的上界。换句话说,在n满足一定条件的范围内,函数Tn)的阶不高于函数fn)的阶。

【例1-1】 用O表示Tn)=10n+4的阶。

存在c=11,n0=4,使得当nn0都有

Tn)=10n+4≤10n+n=11n

fn)=n,可得

Tn)≤cfn

Tn)=Ofn))=On)。

应该指出,根据符号O的定义,用它评估算法的复杂性得到的只是问题规模充分大时的一个上界。这个上界的阶越低,则评估就越精确,结果就越有价值。如果有一个新的算法,其运行时间的上界低于以往解同一问题的所有其他算法的上界,就认为建立了一个解该问题所需时间的新上界。

常见的几类时间复杂性有:

O(1):常数阶时间复杂性。它的基本运算执行的次数是固定的,总的时间由一个常数来限界,此类时间复杂性的算法运行时间效率最高。

On)、On2)、On3)、……:多项式阶时间复杂性。大部分算法的时间复杂性是多项式阶的,通常称这类算法为多项式时间算法。On)称为1阶时间复杂性,On2)称为2阶时间复杂性,On3)称为3阶时间复杂性……。

O(2n)、On!)和Onn):指数阶时间复杂性。这类算法的运行效率最低,这种复杂性的算法根本不实用。如果一个算法的时间复杂性是指数阶的,通常称这个算法为指数时间算法。

Onlogn)和O(logn):对数阶时间复杂性。

以上几种复杂性的关系为

O(1)<O(logn)<On)<Onlogn)<On2)<On3)<O(2n)<On!)<Onn)其中,指数阶时间复杂性最常见的是O(2n),当n取值很大时,指数时间算法和多项式时间算法所需运行时间的差距将非常悬殊。因为,对于任意的m≥0,总可以找到n0n0>0),当nn0时,有2nnm。因此,只要有人能将现有指数时间算法中的任何一个算法化简为多项式时间算法,就取得了一个伟大的成就。

另外,按照O的定义,容易证明如下运算规则成立,这些规则对后面的算法分析是非常有用的。

Of)+Og)=O(max(fg))。

Of)+Og)=Of+g)。

OfOg)=Ofg)。

④如果gn)=Ofn)),则Of)+Og)=Of)。

OCfn))=Ofn)),其中C是一个正的常数。

f=Of)。

规则①的证明:

Fn)=Of)。按照符号O的定义,存在正常数c1n1,使得当nn1时,都有Fn)≤c1f

类似地,设Gn)=Og)。按照符号O的定义,存在正常数c2n2,使得当nn2时,都有Gn)≤c2g

c3=max{c1c2},n3=max{n1n2},hn)=max{fg},则使得当nn3时,有

Fn)≤c1fc1hn)≤c3hn

类似地,有

Gn)≤c2gc2hn)≤c3hn

因此

Of+Og)=Fn+Gn

c3hn+c3hn

=2c3hn

即,存在c=2c3n3,使得nn3时,有Of)+Og)≤chn)恒成立,因此

Of+Og)=Ohn))

=O(max(fg))

其余规则的证明与此类似,感兴趣的读者可自行进行证明。

(2)渐近下界记号:Ω(big-omega)。

定义3 若存在两个正常数cn0,使得当nn0时,都有Tn)≥cfn),则称Tn)=Ωfn)),即fn)是Tn)的下界。换句话说,在n满足一定条件的范围内,函数Tn)的阶不低于函数fn)的阶。它的概念与O的概念是相对的。

【例1-2】 用Ω表示Tn)=30n4+20n3+40n2+46n+100的阶。

存在c=30,n0=1,使得当nn0都有

Tn)≥30n4

fn)=n4,可得

Tn)≥cfn

Tn)=Ωfn))=Ωn4)。

同样,用Ω评估算法的复杂性,得到的只是该复杂性的一个下界。这个下界的阶越高,则评估就越精确,结果就越有价值。如果有一个新的算法,其运行时间的下界低于以往解同一问题的所有其他算法的下界,就认为建立了一个解该问题所需时间的新下界。

(3)渐近精确界记号:Θ(big-theta)。

定义4 若存在3个正常数c1c2n0,使得当nn0时,都有c2fn)≤Tn)≤c1fn),则称Tn)=Θfn)。Θ意味着在n满足一定条件的范围内,函数Tn)和fn)的阶相同。由此可见,Θ用来表示算法的精确阶。

【例1-3】 用Θ表示Tn)=20n2+8n+10的阶。

①存在c1=29,n0=10,使得当nn0时都有

Tn)≤20n2+8n+n=20n2+9n≤20n2+9n2=29n2

fn)=n2,可得

Tn)≤c1fn

Tn)=Ofn))=On2)。

②存在c2=20,n0=10,使得当nn0时都有

Tn)≥20n2

fn)=n2,可得

Tn)≥c2fn

Tn)=Ωfn))=Ωn2)。

③由此可见,存在c1=29、c2=20和n0=10,使得当nn0时都有

c2fn)≤Tn)≤c1fn

fn)=n2,可得Tn)=Θfn)=Θn2)。

定理1 若Tn)=amnm+am-1nm-1+…+a1n+a0ai>0,0≤im)是关于n的一个m次多项式,则Tn)=Onm),且Tn)=Ωnm),因此有Tn)=Θnm)。

证明:

①根据O的定义,取n0=1,当nn0时都有

则有Tn)≤c1nm,由此可得Tn)=Onm)。

②根据Ω的定义,取n0=1,当nn0时都有

Tn)≥amnm

c2=am

则有Tn)≥c2nm,由此可得Tn)=Ωnm)。

③根据Θ的定义,取c1c2n0,当nn0时都有

c2nmTn)≤c1nm

至此可证明Tn)=Θnm)。

3.算法的运行时间Tn)建立的依据

如1.3.2节所述可知,要想精确地表示出算法的运行时间是很困难的。考虑到算法分析的主要目的是比较求解同一个问题的不同算法的效率。因此,在算法分析中只是对算法的运行时间进行粗略估计,得出其增长趋势即可,而不必精确计算出具体的运行时间。

(1)非递归算法中Tn)建立的依据。

为了求出算法的时间复杂性,通常需要遵循以下步骤:

①选择某种能够用来衡量算法运行时间的依据。

②依照该依据求出运行时间Tn)的表达式。

③采用渐进符号表示Tn)。

④获得算法的渐进时间复杂性,进行进一步的比较和分析。

其中,步骤①是最关键的,它是其他步骤能够进行的前提。通常衡量算法运行时间的依据是基本语句,所谓基本语句是指对算法的运行时间贡献最大的原操作语句。

当算法的时间复杂性只依赖问题规模时,基本语句选择的标准是:必须能够明显地反映出该语句操作随着问题规模的增大而变化的情况,其重复执行的次数与算法的运行时间成正比,多数情况下是算法最深层循环内的语句中的原操作;对算法的运行时间贡献最大,在解决问题时占支配地位。这时,就可以采用该基本语句的执行次数来作为运行时间Tn)建立的依据,即用其执行次数对运行时间Tn)进行度量。

【例1-4】 求出一个整型数组中元素的最大值。

算法描述如下:

     int array Max(int a[],int n)
     {
       int max=a[0];
       for(int i=1;i<n;i++)
          if(a[i]>max)
                max=a[i];
       return max;
     }

在该算法中,问题规模就是数组a中的元素个数。显然,执行次数随问题规模的增大而变化,且对算法的运行时间贡献最大的语句是if(a[i]>max),因此将该语句作为基本语句。显然,每执行一次循环,该语句就执行一次,循环变量i从1变化到n-1,因而该语句共执行了n-1次,由此可得Tn)=n-1=On)。

当算法的时间复杂性既依赖问题规模又依赖输入序列时,例如插入、排序、查找等算法,如果要合理全面地对这类算法的复杂性进行分析,则要从最好、最坏和平均情况三方面进行讨论。

【例1-5】 在一个整型数组中顺序查找与给定整数值K相等的元素(假设数组中至多有一个元素的值为K)。

算法描述如下:

     int find(int a[],int n,int K)
     {
          int i;
         for(i=0;i<n;i++)
             if(a[i]==K)
                  break;
         return i;
     }

在该算法中,问题的规模由数组中的元素个数决定。显然,对算法的运行时间贡献最大的语句是if(a[i]==K),因此将该语句作为基本语句。但是该语句的执行次数不但依赖问题的规模,还依赖于输入数据的初始状态。

如果a[0]的元素为K,该语句的执行次数为1,这是最好情况,即Tminn)=O(1)。如果数组a[n-1]的元素为K,则该语句的执行次数为n,这是最坏情况,即Tmaxn)=On)。如果数组a中的元素呈等概率分布,则该语句的执行次数为,这是平均情况,即Tavgn)=

这3种情况下的时间复杂性分别从不同角度反映了算法的时间效率,各有各的用处,各有各的局限性。一般来说,最好情况不能用来衡量算法的时间复杂性,因为它发生的概率太小了。实践表明可操作性最好且最有实际价值的是最坏情况下的时间复杂性,它至少使人们知道算法的运行时间最坏能坏到什么程度。如果输入数据呈等概率分布,要以平均情况来作为运行时间的衡量。

(2)递归算法中Tn)建立的依据。

对于递归算法的时间复杂性分析方法将在1.4.4节讲述。

4.算法所占用的空间Sn)建立的依据

在渐进意义下所定义的复杂性的阶、上界与下界等概念,也同样适用于算法空间复杂性的分析。如1.3.3节所述,本书讨论算法的空间复杂性只考虑算法在运行过程中所需要的辅助空间。

【例1-6】 用插入法升序排列数组s中的n个元素。

算法描述如下:

     void insert_sort(int n,int s[])
      { int a,i,j;
            for(i=1;i<n;i++)
              {
                  a=s[i];
                  j=i-1;
                  while(j>=0&&s[j]>a)
                    {
                         s[j+1]=s[j];
                         j--;
                    }
                  s[j+1]=a;
           }
       }

在算法insert_sort中,为参数表中的形参变量ns所分配的存储空间,是属于为输入输出数据分配的空间。那么,该算法所需的辅助空间只包含为aij分配的空间,显然insert_sort算法的空间复杂性是常数阶,即Sn)=O(1)。

另外,若一个算法为递归算法,其空间复杂性是为实现递归所分配的堆栈空间的大小,具体分析方法将在1.4.4节讲述。