复杂度分析:如何分析、统计算法的执行效率和资源消耗?

咱们都知道,数据结构和算法自己解决的是“快”和“省”的问题,即如何让代码运行得更快,如何让代码更省存储空间。因此,执行效率是算法一个很是重要的考量指标。那如何来衡量你编写的算法代码的执行效率呢?这里就要用到咱们今天要讲的内容:时间、空间复杂度分析。算法

其实,只要讲到数据结构与算法,就必定离不开时间、空间复杂度分析。复杂度分析是整个算法学习的精髓,只要掌握了它,数据结构和算法的内容基本上就掌握了一半。数组

大 O 复杂度表示法

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;
     }
   }
 }

我来具体解释一下这个公式。其中,数据结构

  • T(n)表示代码执行的时间;
  • n 表示数据规模的大小;
  • f(n) 表示每行代码执行的次数总和。由于这是一个公式,因此用 f(n) 来表示。
  • O 表示代码的执行时间 T(n) 与 f(n) 表达式成正比。

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

时间复杂度分析

只关注循环次数最多的一段代码

大 O 这种复杂度表示方法只是表示一种变化趋势。咱们一般会忽略掉公式中的常量、低阶、系数,只须要记录一个最大阶的量级就能够了。因此,咱们在分析一个算法、一段代码的时间复杂度的时候,也只关注循环执行次数最多的那一段代码就能够了。这段核心代码执行次数的 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;
     }
   }
 }

其中第 二、3 行代码都是常量级的执行时间,与 n 的大小无关,因此对于复杂度并无影响。循环执行次数最多的是第 四、5 行代码,因此这块代码要重点分析。前面咱们也讲过,这两行代码被执行了 n 次,因此总的时间复杂度就是 O(n)。学习

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

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;
 }

综合这三段代码的时间复杂度,咱们取其中最大的量级。因此,整段代码的时间复杂度就为 O(n^2)。也就是说:等于量级最大的那段代码的时间复杂度spa

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

也就是说,假设 T1(n) = O(n),T2(n) = O(n^2),则 T1(n) * T2(n) = O(n^2)。落实到具体的代码上,咱们能够把乘法法则当作是嵌套循环,我举个例子给你解释一下。code

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;
 }

咱们单独看 cal() 函数。假设 f() 只是一个普通的操做,那第 4~6 行的时间复杂度就是,T1(n) = O(n)。但 f() 函数自己不是一个简单的操做,它的时间复杂度是 T2(n) = O(n),因此,整个 cal() 函数的时间复杂度就是,T(n) = T1(n) T2(n) = O(nn) = O(n^2)。排序

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

O(1)

首先你必须明确一个概念,O(1) 只是常量级时间复杂度的一种表示方法,并非指只执行了一行代码。好比这段代码,即使有 3 行,它的时间复杂度也是 O(1),而不是 O(3)。递归

int i = 8;
 int j = 6;
 int sum = i + j;

我稍微总结一下,只要代码的执行时间不随 n 的增大而增加,这样代码的时间复杂度咱们都记做 O(1)。或者说,通常状况下,只要算法中不存在循环语句、递归语句,即便有成千上万行的代码,其时间复杂度也是Ο(1)

O(logn)、O(nlogn)

对数阶时间复杂度很是常见,同时也是最难分析的一种时间复杂度。我经过一个例子来讲明一下。

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

根据咱们前面讲的复杂度分析方法,第三行代码是循环执行次数最多的。因此,咱们只要能计算出这行代码被执行了多少次,就能知道整段代码的时间复杂度。

从代码中能够看出,变量 i 的值从 1 开始取,每循环一次就乘以 2。当大于 n 时,循环结束。还记得咱们高中学过的等比数列吗?实际上,变量 i 的取值就是一个等比数列。若是我把它一个一个列出来,就应该是这个样子的:

因此,咱们只要知道 x 值是多少,就知道这行代码执行的次数了。经过 2^x=n 求解 x 这个问题咱们想高中应该就学过了,我就很少说了。x=log2n,因此,这段代码的时间复杂度就是 O(log2n)。

如今,我把代码稍微改下,你再看看,这段代码的时间复杂度是多少?

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

根据我刚刚讲的思路,很简单就能看出来,这段代码的时间复杂度为 O(log3n)。

实际上,不论是以 2 为底、以 3 为底,仍是以 10 为底,咱们能够把全部对数阶的时间复杂度都记为 O(logn)。为何呢?

咱们知道,对数之间是能够互相转换的,log(3)(n) 就等于 log(3)(2) log(2)(n),因此 O(log(3)(n)) = = O(C log(2)(n)),其中 C=log(3)(2) 是一个常量。基于咱们前面的一个理论:在采用大 O 标记复杂度的时候,能够忽略系数,即 O(Cf(n)) = O(f(n))。因此,O(log2n) 就等于 O(log3n)。所以,在对数阶时间复杂度的表示方法里,咱们忽略对数的“底”,统一表示为 O(logn)。

若是你理解了我前面讲的 O(logn),那 O(nlogn) 就很容易理解了。还记得咱们刚讲的乘法法则吗?若是一段代码的时间复杂度是 O(logn),咱们循环执行 n 遍,时间复杂度就是 O(nlogn) 了。并且,O(nlogn) 也是一种很是常见的算法时间复杂度。好比,归并排序、快速排序的时间复杂度都是 O(nlogn)。

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)。

针对这种状况,原来的加法法则就不正确了,咱们须要将加法规则改成:T1(m) + T2(n) = O(f(m) + g(n))。可是乘法法则继续有效:T1(m)T2(n) = O(f(m) f(n))。

空间复杂度分析

void print(int n) {
  int i = 0;
  int[] a = new int[n];
  for (i; i <n; ++i) {
    a[i] = i * i;
  }

  for (i = n-1; i >= 0; --i) {
    print out a[i]
  }
}

跟时间复杂度分析同样,咱们能够看到,第 2 行代码中,咱们申请了一个空间存储变量 i,可是它是常量阶的,跟数据规模 n 没有关系,因此咱们能够忽略。第 3 行申请了一个大小为 n 的 int 类型数组,除此以外,剩下的代码都没有占用更多的空间,因此整段代码的空间复杂度就是 O(n)。

咱们常见的空间复杂度就是 O(1)、O(n)、O(n^2 ),像 O(logn)、O(nlogn) 这样的对数阶复杂度平时都用不到。并且,空间复杂度分析比时间复杂度分析要简单不少。因此,对于空间复杂度,掌握刚我说的这些内容已经足够了。

小结

参考:https://time.geekbang.org/col...

本文做者: 荒古
本文连接: https://haxianhe.com/2019/07/...:如何分析、统计算法的执行效率和资源消耗?/ 版权声明: 本博客全部文章除特别声明外,均采用 CC BY-NC-SA 3.0 许可协议。转载请注明出处!
相关文章
相关标签/搜索