一文拿下面试必考题:O 时间空间复杂度

0 阅读4分钟

在面试的时候 算法时必考的,而O复杂度分析是所有技术问题的敲门砖。无论你写出的代码逻辑多么精妙,如果无法准确评估其运行效率和资源消耗,往往很难打动面试官。本文将结合我的实际代码案例,带你深度剖析 时间复杂度空间复杂度 的核心逻辑。 完整项目结构及源码链接:gitee.com/hong-strong…


一、 如何评价算法的好坏?

在计算机科学中,评价一个算法的优劣通常不看它的代码量多少,而是看它对资源的利用效率。主要从两个维度进行考量:

  1. 时间复杂度 (Time Complexity): 指执行算法所需要的时间。在代码层面,它反映为指令执行的次数。
  2. 空间复杂度 (Space Complexity): 指执行算法时占用的内存空间大小。

一个优秀的算法往往需要在“跑得快”和“省内存”之间寻找平衡。


二、 时间复杂度:抓主要矛盾

1. 什么是 T(n)T(n)O(n)O(n)

时间复杂度并不是直接统计代码运行了多少秒(因为不同硬件环境差异巨大),而是反映一种趋势

  • T(n)T(n) 代表代码执行的次数。
  • O(n)O(n) 大 O 表示法,它只关注随着输入规模 nn 的增大,执行时间的增长趋势。

在计算时,我们遵循“抓主要矛盾”的原则:忽略常数项、忽略低阶项、忽略系数。

2. 线性阶:O(n)O(n)

我们来看文件 1.js 中的代码:

// T(n)
function traverse(arr) {
  var len = arr.length; // 执行 1 次
  for (var i = 0; i < len; i++) { // 定义、判断、自增:1 + (n+1) + n
    console.log(arr[i]); // 执行 n 次
  }
}

深度解析:

通过累加可以得到 T(n)=1+1+(n+1)+n+n=3n+3T(n) = 1 + 1 + (n + 1) + n + n = 3n + 3

当我们用大 O 表示法时,常数 33 和系数 33 都可以忽略,因为当 nn 趋近于无穷大时,它们对趋势的影响微乎其微。最终该算法的时间复杂度为 O(n)O(n)

3. 平方阶:O(n2)O(n^2)

再看嵌套循环的情况,如文件 2.js

function traverse(arr) {
  var outlen = arr.length; 
  for (var i = 0; i < outlen; i++) { 
    var inlen = arr[i].length; 
    for (var j = 0; j < inlen; j++) {
      console.log(arr[i][j]); 
    }
  }
}

深度解析:

  • 外层循环执行 nn 次,内层循环针对每个外层元素又执行 nn 次。
  • 计算得出 T(n)=3n2+5n+1T(n) = 3n^2 + 5n + 1
  • 根据抓主要矛盾原则,保留最高阶项并去掉系数,结果为 O(n2)O(n^2) 。这类复杂度常见于双重循环遍历二维数组。

4. 对数阶:O(logN)O(\log N)

对数阶通常出现在数据规模以倍数缩减或增长的场景中,如文件 3.js

for (var i = 1; i < len; i = i * 2) {
  console.log(arr[i]);
}

深度解析:

这里的变量 ii 每次乘以 22,意味着 2x<n2^x < n。求得执行次数 x=log2nx = \log_2 n。在复杂度分析中,底数通常省略,统一记作 O(logN)O(\log N) 。这是非常高效的算法,常见于二分查找。


三、 空间复杂度:内存的度量

空间复杂度衡量的是算法在运行过程中临时占据存储空间的大小。

1. 常数阶:O(1)O(1)

如果一个算法只是定义了几个固定的变量,无论输入数据 nn 多大,它消耗的额外内存都不变,那么它的空间复杂度就是 O(1)O(1)

2. 线性阶:O(n)O(n)

观察文件 4.js 中的内存开辟:

JavaScript

function init(n) {
  var arr = []; // 新开辟空间
  for (var i = 0; i < n; i++) {
    arr[i] = i; // 空间随 n 的增大而增大
  }
  return arr;
}

深度解析:

在这段代码中,我们创建了一个数组 arr,其长度直接取决于输入规模 nn。这意味着程序运行需要额外申请 nn 个单位的存储空间,因此空间复杂度为 O(n)O(n)


四、 常见复杂度排行

在面试中,理解不同复杂度的性能差异至关重要。

从优到劣的排序如下:

  1. O(1)O(1) :常数阶(极好)
  2. O(logN)O(\log N) :对数阶(优秀)
  3. O(n)O(n) :线性阶(良好)
  4. O(nlogN)O(n \log N) :线性对数阶(常见于高效排序算法)
  5. O(n2)O(n^2) :平方阶(一般,需警惕)
  6. O(2n)O(2^n) :指数阶(差,容易造成崩溃)

五、 总结

  • 面试技巧: 当面试官问到复杂度时,先找循环。单层循环看 nn,双层循环看 n2n^2,折半查找看 logN\log N
  • 空间换时间: 在实际开发中,我们经常为了追求更快的执行速度(降低时间复杂度)而增加内存占用(提高空间复杂度),这是一种常见的架构权衡。

掌握了 OO 表示法,你不仅能写出跑得通的代码,更能写出跑得高效的代码。