13、【算法】算法复杂度分析

一、算法的时间复杂度分析

1、时间复杂度的定义

    在进行算法分析时,算法中基本操作语句重复执行的次数是问题规模n的某个函数,用T(n)表示,若有某个辅助函数f(n),使得当n趋近于无穷大时,T(n)/f(n)的极限值为不等于零的常数,则称f(n)是T(n)的同数量级函数,记作T(n)=O(f(n)),它称为算法的渐进时间复杂度,简称为时间复杂度

    T (n) = Ο(f (n)) 表示存在一个常数C,使得在当n趋于正无穷时总有 T (n) ≤ C * f(n)。简单来说,就是T(n)在n趋于正无穷时最大也就跟f(n)差不多大。也就是说当n趋于正无穷时T (n)的上界是C * f(n)。其虽然对f(n)没有规定,但是一般都是取尽可能简单的函数。例如,O(2n2+n +1) = O (3n2+n+3) = O (7n2 + n) = O ( n2 ) ,一般都只用O(n2)表示就可以了。注意到大O符号里隐藏着一个常数C,所以f(n)里一般不加系数。如果把T(n)当做一棵树,那么O(f(n))所表达的就是树干,只关心其中的主干,其他的细枝末节全都抛弃不管。

    这样用大写O()来体现算法时间复杂度的记法,我们称之为大0表示法。大O表示法O(f(n)中的f(n)的值可以为1、n、logn、n²等,因此我们可以将O(1)、O(n)、O(logn)、O(n²)分别可以称为常数阶、线性阶、对数阶和平方阶。

    算法复杂度可以从最理想情况、平均情况和最坏情况三个角度来评估,由于平均情况大多和最坏情况持平,而且评估最坏情况也可以避免后顾之忧,因此一般情况下,我们设计算法时都要直接估算最坏情况的复杂度。

2、求解算法时间复杂度的具体步骤(大O阶的推导)

(1)找出算法中的基本语句
  算法中执行次数最多的那条语句就是基本语句,通常是最内层循环的循环体。
  
(2)计算基本语句的执行次数的数量级
  只需计算基本语句执行次数的数量级,这就意味着只要保证基本语句执行次数的函数中的最高次幂正确即可,可以忽略所有低次幂和最高次幂的系数。这样能够简化算法分析,并且使注意力集中在最重要的一点上:增长率。
  
(3)用大Ο记号表示算法的时间性能
  将基本语句执行次数的数量级放入大Ο记号中。

    如果算法中包含嵌套的循环,则基本语句通常是最内层的循环体,如果算法中包含并列的循环,则将并列循环的时间复杂度相加。例如:

for (i=1; i<=n; i++)
	x++;
for (i=1; i<=n; i++)
	for (j=1; j<=n; j++)
		x++;

    第一个for循环的时间复杂度为Ο(n),第二个for循环的时间复杂度为Ο(n2),则整个算法的时间复杂度为Ο(n+n2)=Ο(n2)。

    Ο(1)表示基本语句的执行次数是一个常数,一般来说,只要算法中不存在循环语句,其时间复杂度就是Ο(1)。其中Ο(log2n)、Ο(n)、 Ο(nlog2n)、Ο(n2)和Ο(n3)称为多项式时间,而Ο(2n)和Ο(n!)称为指数时间。计算机科学家普遍认为前者(即多项式时间复杂度的算法)是有效算法,把这类问题称为P(Polynomial,多项式)类问题,而把后者(即指数时间复杂度的算法)称为NP(Non-Deterministic Polynomial, 非确定多项式)问题。

3、简单的程序分析法则

    (1)对于一些简单的输入输出语句或赋值语句,近似认为需要O(1)时间

    (2)对于顺序结构,需要依次执行一系列语句所用的时间可采用大O下"求和法则"

    求和法则:是指若算法的2个部分时间复杂度分别为 T1(n)=O(f(n))和 T2(n)=O(g(n)),则 T1(n)+T2(n)=O(max(f(n), g(n)))

特别地,若T1(m)=O(f(m)), T2(n)=O(g(n)),则 T1(m)+T2(n)=O(f(m) + g(n))

    (3)对于选择结构,如if语句,它的主要时间耗费是在执行then字句或else字句所用的时间,需注意的是检验条件也需要O(1)时间

    (4)对于循环结构,循环语句的运行时间主要体现在多次迭代中执行循环体以及检验循环条件的时间耗费,一般可用大O下"乘法法则"

    乘法法则: 是指若算法的2个部分时间复杂度分别为 T1(n)=O(f(n))和 T2(n)=O(g(n)),则 T1*T2=O(f(n)*g(n))

    (5)对于复杂的算法,可以将它分成几个容易估算的部分,然后利用求和法则和乘法法则技术整个算法的时间复杂度

    另外还有以下2个运算法则:(1) 若g(n)=O(f(n)),则O(f(n))+ O(g(n))= O(f(n));(2) O(Cf(n)) = O(f(n)),其中C是一个正常数

4、复杂度分析示例

常数阶

  int sum = 0, n = 100;       /*执行一次*/
  sum = (1 + n) * n / 2;      /*执行一次*/
  printf("%d",sum);           /*执行一次*/

    上面算法的运行的次数的函数为f(n)=3,根据推导大O阶的规则1,我们需要将常数3改为1,则这个算法的时间复杂度为O(1)
    这种与问题的大小无关(n的多少),执行时间恒定的算法,我们称之为具有O(1)的时间复杂度,又叫常数阶。对于分支结构而言,无论是真,还是假,执行的次数都是恒定的,不会随着n 的变大而发生变化,所以单纯的分支结构(不包含在循环结构中),其时间复杂度也是0(1)。

线性阶
    线性阶的循环结构会复杂很多。要确定某个算法的阶次,我们常常需要确定某个特定语句或某个语句集运行的次数。因此,我们要分析算法的复杂度,关键就是要分析循环结构的运行情况。

int i;      
for(i = 0; i < n; i++){
    /*时间复杂度为O(1)的程序步骤序列*/
}

上面这段代码,它的循环的时间复杂度为O(n), 因为循环体中的代码须要执行n次。

对数阶

int count = 1;      
while (count < n){
   count = count * 2;
  /*时间复杂度为O(1)的程序步骤序列*/
}

    由于每次count乘以2之后,就距离n更近了一分。 也就是说,有多少个2相乘后大于n,则会退出循环。 由2x=n 得到x=log2n。 所以这个循环的时间复杂度为O(log2n)。

nlong2n阶

int binarySearch(int *arr, int value, int low, int high)
{
    int mid = low + (high - low)/2;
    if(arr[mid] == value)
        return mid;
    if(arr[mid] > value)
        return binarySearch(arr, value, low, mid-1);
    if(arr[mid] < value)
        return binarySearch(arr, value, mid+1, high);
}

    我们可以把整个有序数组比作一个二叉树,根节点的左子树都小于根,右子树都大于根,二叉树有N个结点,则二叉树的高度就是:h≈log2N

    显然有N个结点的M叉树的高度就是logMN。

    此时最坏的情况就是把所有的结点都查找了一遍,即二分查找的时间复杂度就是:O(log2N);

平方阶

  for(int i=0;i<n;i++){   
      for(int j=0;j<n;i++){
         //复杂度为O(1)的算法
         ... 
      }
  }

    内层循环的时间复杂度在讲到线性阶时就已经得知是O(n),现在经过外层循环n次,那么这段算法的时间复杂度则为O(n²)。 如果外循环的循环次数改为了m,时间复杂度就变为O(m*n)。所以我们可以总结得出,循环的时间复杂度等于循环体的复杂度乘以该循环运行的次数。
    接下来我们看看下面算法的时间复杂度:

  for(int i=0;i<n;i++){   
      for(int j=i;j<n;i++){
         //复杂度为O(1)的算法
         ... 
      }
  }

    需要注意的是内循环中int j=i,而不是int j=0。当i=0时,内循环执行了n次;i=1时内循环执行了n-1次,当i=n-1时执行了1次,我们可以推算出总的执行次数为:

	n+(n-1)+(n-2)+(n-3)+……+1
	=(n+1)+[(n-1)+2]+[(n-2)+3]+[(n-3)+4]+……
	=(n+1)+(n+1)+(n+1)+(n+1)+……
	=(n+1)n/2
	=n(n+1)/2
	=n²/2+n/2

    根据此前讲过的推导大O阶的规则的第二条:只保留最高阶,因此保留n²/2。根据第三条去掉和这个项的常数,则去掉1/2,最终这段代码的时间复杂度为O(n²)。

立方阶

int i, j; 
for(i = 1; i < n; i++) 
	for(j = 1; j < n; j++) 
		for(j = 1; j < n; j++){ 
			/*时间复杂度为O(1)的程序步骤序列*/ 
		}

    这里循环了(12+22+32+……+n2) = n(n+1)(2n+1)/6次,按照上述大O阶推导方法,时间复杂度为O(n^3)。

5、常见时间复杂度

常见的时问复杂度如表所示。

常用的时间复杂度所耗费的时间从小到大依次是:

O(1)<O(logn)<O(n)<O(nlogn)<O()<O()<O(2)<O(n!)
6、常见算法的时间复杂度

二、算法的空间复杂度分析

    我们在写代码时,完全可以用空间来换取时间,比如说,要判断某某年是不是闰年,你可能会花一点心思写了一个算法,而且由于是一个算法,也就意味着,每次给一个年份,都是要通过计算得到是否是闰年的结果。 还有另一个办法就是,事先建立一个有2050个元素的数组(年数略比现实多一点),然后把所有的年份按下标的数字对应,如果是闰年,此数组项的值就是1,如果不是值为0。这样,所谓的判断某一年是否是闰年,就变成了查找这个数组的某一项的值是多少的问题。此时,我们的运算是最小化了,但是硬盘上或者内存中需要存储这2050个0和1。这是通过一笔空间上的开销来换取计算时间的小技巧。到底哪一个好,其实要看你用在什么地方。
    算法的空间复杂度通过计算算法所需的存储空间实现,算法空间复杂度的计算公式记作:S(n)= O(f(n)),其中,n为问题的规模,f(n)为语句关于n所占存储空间的函数。
    一般情况下,一个程序在机器上执行时,除了需要存储程序本身的指令、常数、变量和输入数据外,还需要存储对数据操作的存储单元,若输入数据所占空间只取决于问题本身,和算法无关,这样只需要分析该算法在实现时所需的辅助单元即可。若算法执行时所需的辅助空间相对于输入数据量而言是个常数,则称此算法为原地工作,空间复杂度为0(1)。
    通常, 我们都使用"时间复杂度"来指运行时间的需求,使用"空间复杂度"指空间需求。当不用限定词地使用"复杂度’时,通常都是指时间复杂度。

猜你喜欢

转载自blog.csdn.net/sinat_33924041/article/details/83587333