本文是站在前端的角度来看时间复杂度和空间复杂度,所以阐述的都很基础。
时间复杂度
时间复杂度:定性描述算法的运行时间。只是定性,而不是定量的描述算法的运算时间。
它用一个函数O表示,比如O(1), O(n), O(logn)...
记住下面这个图:
O(1)
表示不管你输入的是多少,里面的代码只会执行一次。一般是没有循环的代码,复杂度就是O(1)。
let i = 0;
i++
O(n)
与传入的数据量成正比,你输入多少,我就执行多少次
for(let i = 0; i < n; i++) {
console.log(i)
}
把两个时间复杂度放在一起执行,总的时间复杂度就是增长趋势更快的那个时间复杂度。下面的代码中当n足够大的时候,O(1)就可以忽略: O(1)+ O(n) = O(n)
let i = 0;
i++
for(let i = 0; i < n; i++) {
console.log(i)
}
O(n^2)
O(n) * O(n) = O(n^2),这是一种非常糟糕的算法,随着数据量的增大,运算时间会增加的非常快,可以看前面的那个图。在工作中尽量少使用,除非你能确定你最大的数据量是多少。
for(let i = 0; i < n; i++) {
for (let j = 0; j < n; j++) {}
}
log(n)
最典型的是二分法,每一次查找就砍一半。这种时间复杂度是非常好的,随着数据量的增加,运算时间增长的并不是很大,这种是工作中追求的一种时间复杂度。
let i = 0;
while(i < n) {
console.log(i)
i *= 2
}
nlog(n)
在一个循环里面嵌套一个二分法
function(n) {
for(let i = 0; i < n; i ++) {
// 或者来一个二分查找
while(i < n) {
i *= 2
}
}
}
空间复杂度
空间复杂度:算法在运行过程中临时占用存储空间大小的度量。
它用一个函数O表示,比如O(1), O(n), O(logn)...
O(1)
let i = 0;
i++
这里只声明了单个变量,单个变量的空间复杂度是O(1)
O(n)
const list = []
for(let i = 0; i < n; i++) {
list.push(i)
}
空间复杂度是o(n),因为声明了一个list数据,数组里面有n个值,需要占据内存空间n个内存单元。
O(n^2)
O(n^2)的典型代表就是矩阵,而矩阵在数组中就是一个二维数组。
const matrix = []
for (let i = 0; i < n; i++) {
matrix.push([])
for (let j = 0; j < n; j++) {
matrix[i].push(j)
}
}
前端开发更注重时间复杂度
如果你没有复杂度的概念和敏感度,写程序是非常危险的,比如代码功能都是正常的,但是一旦数据量大了之后,程序就崩溃了。
对于前端开发来说尤其是时间复杂度,毕竟页面是离用户最近的,如果页面经常卡死,那么没人会使用。所以,我们常说的前端性能优化就是说的是时间复杂度。
对于一个算法来说,O(n^2)基本上是不可用的,一般是要控制在O(n),最好是O(logn)。
从一个例子来实践时间复杂度和空间复杂度
算法题:将一个数组旋转k步
- 输入一个数组[1,2,3,4,5,6,7]
- k = 3,即旋转3步
- 输出[5,6,7,1,2,3,4]
两种思路
思路一:把末尾的元素挨个pop,然后unshift到数组前面
export function rotate1(arr: number[], k: number): number[] {
const length = arr.length
if (!k || length === 0) return arr
// 如果k是负值,或者k大于length,这里做了处理
const step = Math.abs(k % length) // abs 取绝对值
// 时间复杂度是O(n^2) 空间复杂度O(1)
for (let i = 0; i < step; i++) {
const n = arr.pop()
if (n != null) {
arr.unshift(n) // 数组是一个有序结构,unshift 操作非常慢!!! O(n)
}
}
return arr
}
思路二:把数组拆分,最后concat到一起
export function rotate2(arr: number[], k: number): number[] {
const length = arr.length
if (!k || length === 0) return arr
const step = Math.abs(k % length) // abs 取绝对值
// 时间复杂度 O(1) 空间复杂度是O(n)
const part1 = arr.slice(-step) // O(1)
const part2 = arr.slice(0, length - step)
const part3 = part1.concat(part2)
return part3
}
上面两种方法的时间和空间复杂度如何呢?
- 思路一:时间复杂度为O(n^2),空间复杂度为O(1)
- 思路二:时间复杂度为O(1), 空间复杂度为O(n)
性能测试
const arr1 = []
for (let i = 0; i < 10 * 10000; i++) {
arr1.push(i)
}
console.time('rotate1')
rotate1(arr1, 9 * 10000)
console.timeEnd('rotate1') // 968ms O(n^2)
const arr2 = []
for (let i = 0; i < 10 * 10000; i++) {
arr2.push(i)
}
console.time('rotate2')
rotate2(arr2, 9 * 10000)
console.timeEnd('rotate2') // 1ms O(1)
可以看到思路一的运行时间是思路二的几百倍。这是因为有这么一段代码:arr.unshift(n)。
数组是一个有序的集合,当你往数组前面插入一个元素的时候,其余的元素都要往后移动一位,也就是O(n),所以思路一的时间复杂度就是O(n^2)。
数组中的unshift, shift, splice的时间复杂度都是O(n),因为当它们操作时,都要改变数组中所有元素的位置。但是,pop和push就不会,所以这两个的时间复杂度是O(1)。
可见,如果你能掌握了算法,那么你就能写出好的代码。