算法描述了解决问题的具体步骤和过程,专业的程序员必须学会对算法的执行时间和占用空间进行分析,找出瓶颈进行针对性的优化

算法时间复杂度分析

  • 事后统计分析方法:编写算法对应程序,统计其执行时间。但实际统计出来的时间会受到诸多因素的影响,例如程序设计语言(论执行效率汇编语言有话说),执行程序的环境(机器处理性能)等等
  • 事前估算分析方法:认为算法的执行时间是问题规模 n 的函数

求解步骤

  1. 求出算法所有原操作的执行次数(也称为频度),它是问题规模 n( 一般是正整数 )的函数,用 T(n)表示
  2. 算法执行时间大致 = 原操作所需的时间 × T(n)。即 T(n)与算法的执行时间成正比,可以用 T(n)表示算法的执行时间
  3. 比较不同算法的 T(n)大小得出算法执行时间的好坏

复杂度的渐进表示法

T(n)是问题规模 n 的某个函数 f(n),记作:

O 表示随问题规模 n 的增大,算法执行时间的增长率和 f(n)的增长率相同

即 f(n)是 T(n)的上界,通常取最接近的上界

当问题规模 n 最够大时,可以忽略低阶项和常系数,我们只需求出 T(n)的最高阶,比较 T(n)本质上在比较最高数量级

时间复杂度的比较关系

实际上 log 的底数并不一定是 2,底数会根据具体的情况而变化,在后面的例题中我们会详细讲解

O(1)

没有循环的算法。其执行时间与问题规模 n 无关

O(n)

只有一重循环的算法。其执行时间与问题规模 n 的增长呈线性关系

就拿我们最熟悉的素数判断算法来说,常见的两种判断算法

  • 算法一
for( i = 2; i     if( i % n == 0 ){
      printf("No");
      break;
    }
  }
  printf("Yes");
  • 算法二
for( i = 2; i <= sqrt(n); i++ ){
    if( i % n == 0 ){
      printf("No");
      break;
    }
  }
  printf("Yes");

显然,算法一的时间复杂度是O(n),算法二的时间复杂度是O(sqrt(n)),相比之下算法二的效率更高。

在处理 5,13 这样小规模的数据时两者几乎没有差别(计算机的执行时间通常是以纳秒为单位,差了几纳秒时通常可以忽略)。

一旦给出 100000 这样大规模的数据时,我们就要花很长的时间去等算法一的程序出结果,如果再大,大的无法想象,可能到世界末日都等不出结果。

上下文和嵌套的关系

设算法 A 的复杂度

算法 B 的复杂度

AB互为上下文关系时:

AB互为嵌套关系时:

举个栗子

算法A

for( i = 0; i  printf("%d", i);
}

算法B

for( j = 0; j  printf("%d", j);
}

不难看出

当 AB 互为嵌套关系时

for( i = 0; i  printf("%d", i);
 for( j = 0; j   printf("%d", j);
 }
}

简化的算法时间复杂度分析

算法中的基本操作一般是最深层循环内的原操作。计算 T(n)时仅仅考虑基本操作的运算次数。

例题 1

求这段代码的时间复杂度

int cnt = 0;
 for( k = 1; k <= n; k *= 2 ){
  for( j = 1; j <= n; j++ ){
   cnt++;
 }

解:由题得,内层循环 n 次,设外层循环 x 次,由题设:

解得

同时两者互为嵌套关系,所以总时间复杂度为两者相乘

如果把 k *= 2 改为 k *= 3,又该如何计算呢?

显然我们只需要将底数换为 3 即可。所以 log 的底数要根据具体问题具体分析

例题 2

求这段代码的时间复杂度

int i = 0, s = 0;
 while( s   i++;
  s += i;
 }

解:变量 i 从 0 开始递增 1,s 可视为首项为 0,公差为 1 的等差数列的和。设循环次数为 x

循环结束时:

加上一个修正常量 k,把不等式变成等式

解得

只考虑最高数量级,即

算法的空间复杂度分析

空间复杂度用于量度一个算法在运行过程中「临时占用」的存储空间的大小。一般也作为问题规模 n 的函数,采用数量级形式描述,记作

如果一个算法的空间复杂度表示为 O(1),即空间复杂度与问题规模 n 无关,我们称这个算法为原地工作或就地工作算法。

平均时间复杂度

设一个算法的输入规模为 n,Dn是所有输入的集合,任一输入 I∈Dn,P(I) 是 I 出现的概率

T(I) 是算法在输入 I 下的执行时间,则算法平均时间复杂度为

算法的最坏时间复杂度 W(n)

算法的最好时间复杂度 B(n)

最好和最坏通常是指一种或几种特殊情况

例题

分析算法的平均时间复杂度。其中 1<=i<=n

intfun( int a[], int n, int i ){
 int j, max = a[0];
 for( j = 1; j <= i - 1; j++ ){
  if( a[j] > max ){
   max = a[j];
  }
 }

 return max;
}

对于求前 i 个元素的最大值时,需要元素比较 i-1 次(最大值初始化为 a[0],从 a[1]开始比较) 在每种情况出现的概率相等,概率均为 1/n 的情况下 平均时间复杂度:

平均时间算法复杂度为 O(n) 

当 i = n 时 ,有最坏时间复杂度 O(n) 

当 i = 1 时,有最好时间复杂度 O(1)

平均时间复杂度的求解难度比较大,在实际问题中我们一般只关心最好时间复杂度和最坏时间复杂度。

递归算法的时空复杂度分析

递归的算法是指算法中出现调用自己的成分。在进入下一层递归之前,机器会把当前的所有状态压入堆栈存储起来,等到最后递归到了最后一层,再逐个弹出。所以递归算法在时间上和空间上和非递归算法有很大的区别。递归算法也称为变长时空分析,非递归算法分析也称为定长时空分析。两者的分析方法大有不同。

求解递归算法时空复杂度的一般步骤

  1. 确定问题规模 n
  2. 确定终止情况
  3. 确定递推情况
  4. 由递推关系求出 T(n)
  5. 用复杂度表示 T(n)

例题

分析时间复杂度和空间复杂度

voidfun( int a[], int n, int k ){
 int i;
 if( k == n - 1 ){
  for( i = 0; i //执行n次
   printf("%d\n", a[i]);
  }
 }else{
  for( i = k; i //执行n-k次
   a[i] += i * i;
  }
  fun(a,n,k+1);
 }
}
时间复杂度

设 fun( a, n, k )的执行时间为 T(n),fun( a, n, k )的执行时间为 T( n, k ),有

递推关系

空间复杂度

设 fun( a, n, k )的空间为 S(n),fun( a, n, k )的空间为 S( n, k ),有

递推关系