前言
啊~~~痛苦的暑期实习面试开始了。
笔者在面试写算法题时就经常被面试官问到:"这个方法的时间复杂度是多少?空间复杂度是多少?"
但笔者只对算法复杂度略知一二,回答的时候都犹犹豫豫不敢确定:"emm,是O(n)吗?"
所以为了避免以上情景再次发生,笔者决定要好好整理一下算法复杂度分析,并以此作为我暑期实习和秋招复习第一个专题,给自己打气也分享给一起找工作的朋友们!
算法复杂度
为什么需要算法复杂度
首先,有时候用算法解决问题时,我们不单单需要考虑问题能否解决,还需要考虑解决问题的性能开销,比如要花多长时间,要消耗多少空间。算法的性能开销需要一个衡量标准,所以我们很需要算法复杂度。
其次,算法复杂度会不会计算也是衡量程序员的一个重要标准,算法复杂度的计算可以帮助程序员更好地理解算法的运行,优化程序的性能,并因地制宜地选择算法。
算法复杂度分析的是什么
算法复杂度主要用来分析算法的时间开销和空间开销,我们需要了解时间复杂度 和空间复杂度。
- 时间复杂度:衡量运行算法完需要的时间。
- 空间复杂度:衡量运行算法需要的空间。
大O计法
大O计法常常用来描述算法复杂度,它既可以用来描述时间复杂度也可以描述空间复杂度。
以时间复杂度 T(n) = O(f(n)) 为例子,这里 T(n) 表示算法执行的总时间,n 表示数据的规模,f(n) 表示每行代码的执行总次数,O 表示执行时间与 f(n) 的表达式成正比。
O的具体讲解可以看这里啦:# 一文搞懂算法复杂度分析:大O符号你都搞不懂,所以只能搬砖到秃顶?
时间复杂度
算法时间复杂度是通过计算算法所需要运行的时间实现的,算法的时间复杂度计作:T(n) = O(f(n)) , n 为数据的规模,f(n) 表示每行代码的执行总次数,O 表示执行时间与 f(n) 的表达式成正比。
用大O计法来表示的时间复杂度并不代表算法真正运行的时间,而是表示代码运行时间会随数据规模的变化而变化,大O计法也称为渐进时间复杂度。
PS: 笔者感觉用大O计法的时候,只需要关注关键细节,比如循环啊,对于常数次的运算可以忽略哈。
常见的时间复杂度
O(1) 常数级
在下面的代码中,T(n) = O(1),a和b无论怎么变,代码的执行次数还是这么多,运算时间也很稳定。
对于常数级的时间复杂度来说,代码的执行时间不会随着数据规模的增加而增加。
js
const sum = (a, b) => {
return a + b; // t
}
O(n) 线性阶
在下面的这段代码中,T(n) = O(2n+1), 当数据规模n无限增大的时候,低阶、常量都可以忽略不计,T(n) 可以简化表示为 O(n),代码的执行时间会随着数据规模的增加而增加。
js
const sum = (n) => {
let count = 0 ; //t 一个时间单位
for(let i = 0; i < n; i++){ // t*n
count = count + i; // t*n
}
return count;
}
O(m+n) 线性阶
在下面的这段代码中,T(n) = O(m+n), 因为算法运行的时间与两个变量 m、n 相关。
js
const sum = (m, n) => {
let sum1 = 0 ; //t 一个时间单位
for(let i = 0; i < m; i++){ // t*m
sum1 = sum1 + i; // t*m
}
let sum2 = 0 ; //t 一个时间单位
for(let i = 0; i < n; i++){ // t*n
sum2 = sum2 + i; // t*n
}
return sum1 + sum2;
}
O(m*n)
在下面的这段代码中,我们可以看到循环中的关键代码要运行mn次,故 T(n) = O(mn)。
比较特殊的情况是,当 m = n 时, T(n) = O(n*n),代码执行时间随着数据规模的增加而平方增长。
js
const sum = (m, n) => {
let count = 0 ; //t 一个时间单位
for(let i = 0; i < m; i++){ // t*m
for(let j = 0; j < n; j++){ // t*m*n
count = count + i*j; // t*m*n
}
}
return count;
}
O(n*n)
在下方的这段代码中,代码执行时间随着数据规模n的增加而平方增长,时间复杂度 T(n) = O(n*n)。
js
const sum = (n) => {
let count = 0 ; //t 一个时间单位
for(let i = 0; i < n; i++){ // t*n
for(let j = 0; j < n; j++){ // t*n*n
count = count + i*j; // t*n*n
}
}
return count;
}
O(logn) 对数阶
在下方的这段代码中,代码执行时间随着数据规模n的增加而对数增长,所以时间复杂度 T(n) = O(logn)
js
const sum = (n) => {
let count = 0;
let i = 1;
while (i < n) {
i=i*2;
count = count + i;
}
return count;
}
参考文档中的 O(logn) 推导过程:
在代码中,while循环中每次将i乘以2,直到i大于n,结束此循环。
当循环x次后,i>n,循环结束,也就是2的x次方等于n,即x=log2n,也就是说循环log2n次以后,循环就结束了。代码的时间复杂度为O(logn)。
找出while循环中的结束条件,判断时间复杂度。
带有while循环的代码,大概率是对数时间复杂度为O(logn)
时间复杂度比较
各个算法时间复杂度的大小比较大约是酱紫的:
O(1) <<< O(logn) <<< O(n) <<< O(nlogn) <<< O( <math xmlns="http://www.w3.org/1998/Math/MathML"> n 2 n^2 </math>n2) <<< O( <math xmlns="http://www.w3.org/1998/Math/MathML"> n 3 n^3 </math>n3) <<< O( <math xmlns="http://www.w3.org/1998/Math/MathML"> 2 n 2^n </math>2n)<<< O(n!)
空间复杂度
算法空间复杂度是通过计算算法所需要存储空间实现的,算法的空间复杂度计作:S(n) = O(f(n)) , n 为数据的规模,f(n) 表示 n 所占的存储空间的函数,O 表示存储空间与 f(n) 的表达式成正比。
用大O计法来表示的空间复杂度并不代表算法真正占用的存储空间,而是表示算法占用的存储空间与数据规模的增长关系,大O计法也称为渐进空间复杂度。
常见的空间复杂度
O(1) 常数级
在下面的代码中,算法只占用了容量为1的数组,S(n) = O(1)。
js
const array = () => {
let arr = new Array(1).fill(0);
return arr;
}
O(n) 线性阶
在下面的这段代码中,算法开辟了容量为n的数组,算法占用的存储空间会随着数据规模n的增加而增加,所以S(n) = O(n)。
js
const array = () => {
let arr = new Array(n).fill(0);
return arr;
}
O(n*n)
在下面的这段代码中,算法创建了一个nn的二维数组,算法占用的存储空间会随着数据规模n的增加而平方增加S(n) = O(nn)。
js
const matrix = (n) => {
let m = new Array(n).fill(0).map(item => new Array(n).fill(0));
return m;
}
空间复杂度比较
一般我们会用到的空间复杂度有 O(1), O(n), O( <math xmlns="http://www.w3.org/1998/Math/MathML"> n 2 n^2 </math>n2), 它们占用的空间大小关系为:
O(1) <<< O(n)<<< O( <math xmlns="http://www.w3.org/1998/Math/MathML"> n 2 n^2 </math>n2)