时间、空间复杂度

217 阅读6分钟

1. 前言

一方面,算法的好坏主要是从执行时所占的 时间空间 两个方面去考量。但它真正耗费的时间和空间只有上机测试才能获得,所以将具体的时间空间抽成两个概念:时间/空间复杂度,用它们进行理论上的衡量;
另一方面,面试时经常会遇到算法题,从而也会经常被问到 时间/空间复杂度,而leetcode的初级算法第一题就涉及了这个概念。。
所以在看算法之前,时间/空间复杂度 是我们必须要知道的。

2. 时间复杂度T(n)

2.1. 概念

维基百科的解释是这样的:

在计算机科学中,算法的时间复杂度(Time complexity)是一个函数,它定性描述该算法的运行时间。这是一个代表算法输入值的字符串的长度的函数。时间复杂度常用大O符号表述,不包括这个函数的低阶项和首项系数。使用这种方式时,时间复杂度可被称为是渐近的,亦即考察输入值大小趋近无穷时的情况。

所以我们总结一下:

  • 大O符号表示法:T(n) = O(f(n)),这个公式的全称是算法的渐进时间复杂度;

    • 时间复杂度记作T(n)f(n)表示每行代码执行次数之和,O表示正比例关系;
    • 大O符号表示法并不是用于来真实代表算法的执行时间的,它是用来表示代码执行时间的增长变化趋势的;
  • 计算时间复杂度T(n)注意点:

    • 我们通常认为变量n是无穷大的;
    • 计算时通常以某一行代码运行的时间为一个单位时间;
    • 忽略f(n)低阶项首项系数、对数中的底数

2.2. 常见时间复杂度量级

时间复杂度的大小比较:
常数阶O(1) < 对数阶O(logN) < 线性阶O(n) < 线性对数阶O(nlogN) < 平方阶O(n²) < 立方阶O(n³) < K次方阶O(n^k) < 指数阶(2^n)

时间复杂度越大,执行效率越低。

2.2.1. 常数阶 - O(1)

一个算法的执行时间总量是一个常量,或者说它的执行时间不随着n的变化而变化,那它的时间复杂度就是O(1);

案例1

let i = 1;
let j = 2;
++i;
j++;
let m = i + j;

解析:上述代码有5行,一共消耗5个单位时间,用大O可以表示为Tn = O(5)。忽略常数项,所以它最终的时间复杂度为T(n) = O(1).

案例2

const arr = [123];
for(let i = 0; i < arr.length; i++) {
  console.log(arr[i]);
}

解析:上述代码虽然有一个for循环,但arr.length是一个常量,第一行消耗一个单位时间,for循环消耗1 * 3个单位时间,所以用大O可以表示为Tn = O(1 + 1 * 3)。忽略常数项,所以它最终的时间复杂度为T(n) = O(1).

2.2.2. 线性阶 - O(n)

如果一个算法的时间复杂度为O(n),则称这个算法具有线性时间,或O(n)时间。非正式地说,这意味着对于足够大的输入,运行时间增加的大小与输入成线性关系。 比如:O(5n + 3n + 1)O(5n + 3n)O(100n)等的时间复杂度都是T(n) = O(n)

for(let i = 0; i <= n; i++) {
  console.log(i);
  console.log(n);
}

解析:上述代码虽然与常数阶的案例2类似,但它的for循环会消耗2 * n个单位时间,用大O可以表示为Tn = O(2n)。忽略系数,所以它最终的时间复杂度为T(n) = O(n).

2.2.3. 平方阶 - O(n²)

如果把O(n)的代码再嵌套循环一遍,它的时间复杂度就是O(n²)了。

比如:

for(let i = 0; i < n; i++) {
  for(let j = 0; j < n; j++) {
    console.log(i);
    console.log(j);
  }
}

解析:上述代码用大O可以表示为Tn = O(n * (n * 2))。忽略首相系数,所以它最终的时间复杂度为T(n) = O(n²).

2.2.4. 立方阶 - O(n³)、K次方阶 - O(n^k)

参考平方阶去理解就好,这里就不赘述了。

2.2.5. 对数阶 - O(logN)

由于计算机使用二进制的记数系统,对数常常以2为底(即,有时写作)。然而,由对数的换底公式,和只有底数不同,在大O记法忽略底数,记作O(logn)

let i = 1;
while(i < n) {
  i = i * 2;
}

从上面代码可以看到,在while循环里面,每次都将i乘以2,乘完之后,i距离n就越来越近了。我们试着求解一下,假设循环x次之后,i就大于2了,此时这个循环就退出了,也就是说2的x次方等于n,那么。用大O可以表示为 Tn = O()。忽略系数,所以它最终的时间复杂度为T(n) = O(logn).

2.2.6. 线性对数阶 - O(nlogN)

线性对数阶O(nlogN)其实非常容易理解,将时间复杂度为O(logn)的代码循环N遍的话,那么它的时间复杂度就是n * O(logN),也就是O(nlogN)

比如:

for(let j = 1; j < n; m++){
    let i = 1;
    while(i < n) {
        i = i * 2;
    }
}

解析:上述代码用大O可以表示为。忽略底数,所以它最终的时间复杂度为T(n) = O(nlogn).

2.2.7. 其他

除此之外,其实还有 平均时间复杂度、均摊时间复杂度、最坏时间复杂度、最好时间复杂度 的分析方法,有点复杂,这里就不展开了。

3. 空间复杂度S(n)

3.1. 概念

空间复杂度(Space Complexity)是对一个算法在运行过程中临时占用存储空间大小的量度,记做S(n)=O(f(n))

空间复杂度的计算与时间复杂度一致。

3.2. 常见的空间复杂度

3.2.1. 空间复杂度O(1)

const arr = [123];
for(let i = 0; i < arr.length; i++) {
  console.log(arr[i]);
}

解析:上述代码虽然有一个for循环,但arr.length是一个常量,第一行消耗一个单位空间,for循环消耗1 * 3个单位空间,所以用大O可以表示为Sn = O(1 + 1 * 3)。忽略常数项,所以它最终的时间复杂度为S(n) = O(1).

3.2.2. 空间复杂度O(n)

for(let i = 0i < n; i++) {
  console.log(i);
}

解析:上述代码用大O可以表示为Sn = O(n * 1)。所以它最终的时间复杂度为S(n) = O(n).

3.2.3. 空间复杂度O(n²)

for(let i = 0; i < n; i++) {
  for(let j = 0; j < n; j++) {
    console.log(i);
    console.log(j);
  }
}

解析:上述代码用大O可以表示为Sn = O(n * (n * 2))。忽略首相系数,所以它最终的时间复杂度为S(n) = O(n²).

4. 参考