wangjing013 / blog

📝 记录

Home Page:https://wangjing013.github.io/blog/

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

如何分析和统计算法执行效率和消耗资源

wangjing013 opened this issue · comments

如何分析和统计算法执行效率和消耗资源

数据结构和算法本身解决的是“快”和“省”的问题, 即如何让代码运行得更快,如何让代码更省存储空间.

执行效率是算法一个非常重要的考量指标. 那如何来衡量你编写的算法代码的执行效率呢? 这里就要用到我们今天要讲的内容:时间、空间复杂度分析.

  • 为什么需要复杂度分析?

通常一般用的方式是事后统计法, 也就是我把代码跑一遍,通过统计、监控,就能得到算法执行的时间和占用的内存大小.

按照正常来说这种衡量是正确的,但是这种方式存在弊端的.

  • 依赖执行环境

同一套代码可能不同执行环境相应效率和占用资源不一样

  • 测试结果受数据规模的影响很大

    • 例如待排序数据的有序度不一样,排序的执行时间就会有很大的差别. 例如, 如果数据已经是有序的,那排序算法就不需要做任何操作.

    • 同样如过测试数据规模太小,测试结果可能无法真实地反应算法的性能.

在这样情况下, 我们需要一个不用具体的测试数据来测试,就可以粗略估计算法的执行效率方法. 时间、空间复杂度分析法

大 O 复杂度表示法

算法的执行效率, 粗略地讲, 就是算法代码执行的时间. 如在在不执行代码的情况下,用肉眼得到一段代码的执行时间呢?

从CPU角度来看,代码每一行都执行着类似的操作: 读数据-运算-写数据. 现在我们假设每一行执行的时间为 unit_time, 先来看看下面的案例:

  • 1.1 案例一
function cal(n) {
  let sum = 0; // unit_time
  let i = 1;  //  unit_time
  for (; i <= n; ++i) { // unit_time
    sum = sum + i;  //     unit_time
  }
  return sum;
}

第二行、第三行分别为: unit_time, 第四、五行执行n次,需要的时间为 2n * unit_time. 上面代码总体执行的时间为: (2n + 2) * unit_time. 可以看出来,所有代码的执行时间 T(n) 与每行代码的执行次数成正比.

  • 1.2 案例二
function cal(n) {
  let sum = 0; // unit_time
  let i = 1;   // unit_time
  let j = 1;   // unit_time
  for (; i <= n; ++i) { // n * unit_time
    j = 1;              // n * unit_time
    for (; j <= n; ++j) { // n * n * unit_time
      sum = sum + i * j;  // n * n * unit_time
    }
  }
}

按照上面每行代码执行时间为: unit_time, 下面分析代码总执行时间为多少:

  • 第二、第三、第四行代码分别为: unit_time 总的时间为 3 * unit_time
  • 第五、第六行代码分别为: n * unit_time 总的时间为 2n * unit_time
  • 第七、第八行代码分别为: n * unit_time 由于代码存在嵌套循环,所以代码执行时间需要乘以外层执行次数. 也就是 n * 2n * unit_time.

上述代码总的执行时间为: T(n) = (3+ 2n + 2n^2) * unit_time.

从上述两段代码, 我们可以得出规律: 所有代码的执行时间 T(n) 与每行代码的执行次数 f(n) 成正比.

我们可以把这种规律总结成一个公式:

O

这就是大O 表示法.

  • T(n) 总的执行时间
  • n 表示数据规模大小
  • f(n) 表示每行代码执行次数的总和

现在把上面两个案例通过大O表示法分别对应为:

  T(n) = O(2n + 2)
  T(n) = O(3+ 2n + 2n^2)

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

n 很大时,你可以把它想象成 10000、100000. 而公式中的低阶常量系数三部分并不左右增长趋势, 所以都可以忽略. 我们只需要记录一个最大量级就可以了,如果用大 O 表示法表示刚讲的那两段代码的时间复杂度,就可以记为:T(n) = O(n)T(n) = O(n2)

时间复杂度分析

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

我们在分析一个算法、一段代码的时间复杂度的时候,也只关注循环执行次数最多的那一段代码就可以了。

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

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

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

复杂度量级

  • 常量阶 O(1)

只要时间不随着 n 的增大而增长, 这样代码的时间复杂度我们都可以记做 O(1); 或者说, 一般情况下,只要算法中不存在循环语句、递归语句,即使有成千上万行的代码,其时间复杂度也是Ο(1)

  • 对数阶 O(log^n)
let i = 1
while (i < n) {
  i = i * 2
}
  • 线性阶 O(n)

  • 线性对数阶 O(nlog^n)

  • 平方阶 O(n^2)

  • 立方阶 O(n^3)

  • 指数阶 O(2^n)

  • 阶乘阶 O(n!)