[数据结构与算法之美学习] 03 复杂度分析(上) :如何分析、统计算法的执行效率和资源消耗?

为什么需要复杂度分析

事后统计法:代码跑一遍通过统计、监控,就能得到算法执行的时间和占用的内存大小。

缺点:

  • 测试结果依赖环境
  • 需要具体的测试数据。

我们需要一个不用具体的测试数据来测试,就可以粗略地估计算法的执行效率的方法。

大O复杂度表示法

所有代码的执行时间 T(n) 与每行代码的执行次数 n 成正比。 即:

T ( n ) = O ( f ( n ) ) T(n)=O(f(n))

其中 T ( n ) T(n) 代表代码执行时间; n n 表示数据规模的大小; f ( n ) f(n) 表示每行代码执行次数的总和。公式中的 O,表示代码的执行时间 T ( n ) T(n) f ( n ) f(n) 表达式成正比。

例子:

 int cal(int n) {
   int sum = 0;
   int i = 1;
   int j = 1;
   for (; i <= n; ++i) {
     j = 1;
     for (; j <= n; ++j) {
       sum = sum +  i * j;
     }
   }
 }

设每个语句的执行时间是unit_time,此时: T ( n ) = O ( 2 n 2 + 2 n + 3 ) T(n)=O(2n^2+2n+3)

这就是大 O 时间复杂度表示法。大 O 时间复杂度实际上并不具体表示代码真正的执行时间,而是表示代码执行时间随数据规模增长的变化趋势,所以,也叫作渐进时间复杂度(asymptotic time complexity),简称时间复杂度

时间复杂度分析

1. 只关注循环执行次数最多的一段代码

在分析一个算法、一段代码的时间复杂度的时候,只关注循环执行次数最多的那一段代码就可以了。这段核心代码执行次数的 n 的量级,就是整段要分析代码的时间复杂度。

时间复杂度就是 O ( n ) O(n)

2. 加法法则:总复杂度等于量级最大的那段代码的复杂度

总的时间复杂度就等于量级最大的那段代码的时间复杂度。
注意:只要是一个常量的执行时间,与n的规模无关,无论它多大,我们都当做 O ( 1 ) O(1) .

int cal(int n) {
   int sum_1 = 0;
   int p = 1;
   for (; p < 100; ++p) {
     sum_1 = sum_1 + p;
   }

   int sum_2 = 0;
   int q = 1;
   for (; q < n; ++q) {
     sum_2 = sum_2 + q;
   }
 
   int sum_3 = 0;
   int i = 1;
   int j = 1;
   for (; i <= n; ++i) {
     j = 1; 
     for (; j <= n; ++j) {
       sum_3 = sum_3 +  i * j;
     }
   }
 
   return sum_1 + sum_2 + sum_3;
 }

3. 乘法法则:嵌套代码的复杂度等于嵌套内外代码复杂度的乘积

嵌套用乘法法则

int cal(int n) {
   int ret = 0; 
   int i = 1;
   for (; i < n; ++i) {
     ret = ret + f(i);
   } 
 } 
 
 int f(int n) {
  int sum = 0;
  int i = 1;
  for (; i < n; ++i) {
    sum = sum + i;
  } 
  return sum;
 }

几种常见时间复杂度实例分析

复杂度量级
粗略分为:多项式量级和非多项式量级。其中,非多项式量级只有两个: O ( 2 n ) O(2^n) O ( n ! ) O(n!)

1、 O ( 1 ) O(1)

O(1) 只是常量级时间复杂度的一种表示方法,并不是指只执行了一行代码。只要代码的执行时间不随n的增大而增长,这样时间代码的复杂度我们都记作 O ( 1 ) O(1) 。或者说,**一般情况下,只要算法中不存在循环语句、递归语句,即使有成千上万行代码,其时间复杂度也是 O ( 1 ) O(1) **。

2、 O ( l o g n ) O(logn) O ( n l o g n ) O(nlogn)

对数阶时间复杂度非常常见,同时也是最难分析的一种时间复杂度。

计算一下两个代码的时间复杂度:
(1)

 i=1;
 while (i <= n)  {
   i = i * 2;
 }

(2)

 i=1;
 while (i <= n)  {
   i = i * 3;
 }

(1)为 O ( l o g 2 n ) O(log_{2}n) ,(2)为 O ( l o g 3 n ) O(log_{3}n) ,但实际上,我们把所有对数阶的时间复杂度都记为 O ( l o g n ) O(logn)
l o g 3 n log_3n 就等于 l o g 3 2 l o g 2 n log_3{2}*log_2{n} ,所以 O ( l o g 3 n ) = O ( C l o g 2 n ) O(log_3{n}) = O(C * log_2{n}) ,其中 C=log32 是一个常量。基于我们前面的一个理论:在采用大 O 标记复杂度的时候,可以忽略系数,即 O ( C f ( n ) ) = O ( f ( n ) ) O(Cf(n)) = O(f(n))

3、O(m+n)、O(m*n)

代码的复杂度由两个数据的规模来决定。

int cal(int m, int n) {
  int sum_1 = 0;
  int i = 1;
  for (; i < m; ++i) {
    sum_1 = sum_1 + i;
  }

  int sum_2 = 0;
  int j = 1;
  for (; j < n; ++j) {
    sum_2 = sum_2 + j;
  }

  return sum_1 + sum_2;
}

m 和 n 是表示两个数据规模,我们无法事先评估 m 和 n 谁的量级大,所以我们在表示复杂度的时候,就不能简单地利用加法法则,省略掉其中一个。上面代码的时间复杂度就是 O ( m + n ) O(m+n)

O ( m + n ) O(m+n) 并行结构 O ( m n ) O(m*n) 嵌套结构

空间复杂度分析

空间复杂度的全称是渐进空间复杂度(asymptotic space complexity),表示算法的存储空间与数据规模之间的增长关系

参考资料:

数据结构与算法之美(极客时间)链接:
http://gk.link/a/10435


GitHub链接:
https://github.com/lichangke/LeetCode
知乎个人首页:
https://www.zhihu.com/people/lichangke/
CSDN首页:
https://me.csdn.net/leacock1991
欢迎大家来一起交流学习

发布了170 篇原创文章 · 获赞 16 · 访问量 2827

猜你喜欢

转载自blog.csdn.net/leacock1991/article/details/101316103