在面试的时候 算法时必考的,而O复杂度分析是所有技术问题的敲门砖。无论你写出的代码逻辑多么精妙,如果无法准确评估其运行效率和资源消耗,往往很难打动面试官。本文将结合我的实际代码案例,带你深度剖析 时间复杂度 与 空间复杂度 的核心逻辑。 完整项目结构及源码链接:gitee.com/hong-strong…
一、 如何评价算法的好坏?
在计算机科学中,评价一个算法的优劣通常不看它的代码量多少,而是看它对资源的利用效率。主要从两个维度进行考量:
- 时间复杂度 (Time Complexity): 指执行算法所需要的时间。在代码层面,它反映为指令执行的次数。
- 空间复杂度 (Space Complexity): 指执行算法时占用的内存空间大小。
一个优秀的算法往往需要在“跑得快”和“省内存”之间寻找平衡。
二、 时间复杂度:抓主要矛盾
1. 什么是 与 ?
时间复杂度并不是直接统计代码运行了多少秒(因为不同硬件环境差异巨大),而是反映一种趋势。
- : 代表代码执行的次数。
- : 大 O 表示法,它只关注随着输入规模 的增大,执行时间的增长趋势。
在计算时,我们遵循“抓主要矛盾”的原则:忽略常数项、忽略低阶项、忽略系数。
2. 线性阶:
我们来看文件 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 次
}
}
深度解析:
通过累加可以得到 。
当我们用大 O 表示法时,常数 和系数 都可以忽略,因为当 趋近于无穷大时,它们对趋势的影响微乎其微。最终该算法的时间复杂度为 。
3. 平方阶:
再看嵌套循环的情况,如文件 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]);
}
}
}
深度解析:
- 外层循环执行 次,内层循环针对每个外层元素又执行 次。
- 计算得出 。
- 根据抓主要矛盾原则,保留最高阶项并去掉系数,结果为 。这类复杂度常见于双重循环遍历二维数组。
4. 对数阶:
对数阶通常出现在数据规模以倍数缩减或增长的场景中,如文件 3.js:
for (var i = 1; i < len; i = i * 2) {
console.log(arr[i]);
}
深度解析:
这里的变量 每次乘以 ,意味着 。求得执行次数 。在复杂度分析中,底数通常省略,统一记作 。这是非常高效的算法,常见于二分查找。
三、 空间复杂度:内存的度量
空间复杂度衡量的是算法在运行过程中临时占据存储空间的大小。
1. 常数阶:
如果一个算法只是定义了几个固定的变量,无论输入数据 多大,它消耗的额外内存都不变,那么它的空间复杂度就是 。
2. 线性阶:
观察文件 4.js 中的内存开辟:
JavaScript
function init(n) {
var arr = []; // 新开辟空间
for (var i = 0; i < n; i++) {
arr[i] = i; // 空间随 n 的增大而增大
}
return arr;
}
深度解析:
在这段代码中,我们创建了一个数组 arr,其长度直接取决于输入规模 。这意味着程序运行需要额外申请 个单位的存储空间,因此空间复杂度为 。
四、 常见复杂度排行
在面试中,理解不同复杂度的性能差异至关重要。
从优到劣的排序如下:
- :常数阶(极好)
- :对数阶(优秀)
- :线性阶(良好)
- :线性对数阶(常见于高效排序算法)
- :平方阶(一般,需警惕)
- :指数阶(差,容易造成崩溃)
五、 总结
- 面试技巧: 当面试官问到复杂度时,先找循环。单层循环看 ,双层循环看 ,折半查找看 。
- 空间换时间: 在实际开发中,我们经常为了追求更快的执行速度(降低时间复杂度)而增加内存占用(提高空间复杂度),这是一种常见的架构权衡。
掌握了 表示法,你不仅能写出跑得通的代码,更能写出跑得高效的代码。