算法的复杂度

150 阅读5分钟

时间复杂度和空间复杂度

算法

怎么理解算法呢?

算法是指解题方案的准确而完整的描述,是一系列解决问题的清晰指令。

简单来说,算法是解决问题的指令。

对于同一个问题,可以使用不同的算法解决,但如何衡量不同的算法的好坏呢?

衡量代码的好坏,包括两个非常重要的指标。1:运行时间、2:占用空间。

举个例子,同一个功能

  • 别人写的代码跑起来占内存 100M,耗时 100 毫秒
  • 你写的代码也许跑起来占内存 500M,耗时 1000 毫秒

那你的代码就不是最优的。

但是代码没运行起来之前 怎么知道占多少内存和运行时间呢?

由于运行环境和输入规模的影响,代码的占内用存或运行时间确实算不出来。但我们可以预估代码的基本操作执行次数

这就要说到时间复杂度了

时间复杂度

运行时间

假设计算机执行一行代码需要一次运算

function hello() {
    let name = 'qzy' // 执行一次
    console.log('hello', name)  // 执行一次
}

那么这个函数需要 2 次运算。

function hello(n) {
    for (let i = 0; i < n; i++) {  // 执行(n+1)次
        let name = 'qzy' // 执行n次
        console.log('hello', name)  // 执行n次
    }
}

那么这个函数需要执行 n+1+n+n = 3n+1 次运算。

我们把算法的运算次数T(n)函数表示

有了函数 T(n),是否就可以分析和比较一段代码的运行时间了呢?还是有一定的困难。

比如算法 A 的时间是 T(n) = 100n,算法 B 的时间是 T(n) = 5n^2,这两个到底谁的运行时间更长一些?这就要看n的取值了。这时候就有了渐进时间复杂度。

渐进时间复杂度

官方的定义如下:

若存在函数 f(n),使得当 n 趋近于无穷大时,T(n) / f(n) 的极限值为不等于零的常数,则称 f(n) 是 T(n) 的同数量级函数。记作 T(n) = O(f(n)),称 O(f(n)) 为算法的渐进时间复杂度简称时间复杂度。O是数量级的符号

渐进时间复杂度用大写 O 来表示,所以也被称为大O表示法

它表示随着输入大小 n 的增大,算法执行需要的时间的增长速度可以用 f(n) 来描述

显然如果 T(n) = n^2,那么 T(n) = O(n^2),T(n) = O(n^3),T(n) = O(n^4) 都是成立的,差别只是数量级不同而已。但是因为第一个 f(n) 的增长速度与 T(n) 是最接近的,所以第一个是最好的选择,所以我们说这个算法的复杂度是 O(n^2)

推导原则

  1. 如果运行时间是常数量级,用常数1表示
  2. 只保留时间函数中的最高阶项
  3. 如果最高阶项存在,则省去最高阶项前面的系数

比如 T(n) = 2,运行时间是常数量级,所以时间复杂度为 O(1)

比如 T(n) = 3n + 1,只保留时间函数中的最高阶项,所以时间复杂度为 O(3n),如果最高阶项存在,则省去最高阶项前面的系数,因此最终的时间复杂度为 O(n)

实例
第一例
function sum(n) {
    for (let i = 0; i < n; i++) {
        for (let j = i; j < n; j++) {
            console.log('Hello World\n')
        }
    }
}
  • i = 0 时,内循环执行 n 次
  • i = 1 时,内循环执行 n-1 次
  • i = 2 时,内循环执行 n-2 次
  • i = n-1 时,内循环执行 1 次
  • i = n 时,内循环执行 0 次
  • T(n) = n + (n - 1) + (n - 2)……+ 2 + 1 + 0 = n(n + 1) / 2 = n^2 / 2 + n / 2
  • 此时时间复杂度为 O(n^2)
第二例
function sum(n) {
    for (let i = 1; i < n;) {
        i*=2
        console.log('Hello World\n')
    }
}
  • 假设循环次数为 t,则循环条件满足 2^t < n
  • t = log n,即 T(n) = log n,可见时间复杂度为 O(log n)

还有其他的时间复杂度,根据高等数学知识,我们可以得到:

由快到慢

复杂度名称
O(1)常数复杂度
O(logn)对数复杂度
O(n)线性时间复杂度
O(nlogn)线性对数复杂度
O(n²)平方
O(n³)立方
O(2^n)指数
O(n!)阶乘

时间复杂度的巨大差异

计算机性能越来越强,为什么要重视时间复杂度呢

通过例子来说明

算法A的相对时间规模是T(n)= 100n,时间复杂度是O(n)

算法B的相对时间规模是T(n)= 5n^2, 时间复杂度是O(n^2)

算法A运行在老旧电脑上,算法B运行在某台超级计算机上,运行速度是老旧电脑的100倍

那么,随着输入规模 n 的增长,两种算法谁运行更快呢?

clipboard (1).png

从表格中可以看出,当n的值很小的时候,算法A的运行用时要远大于算法B;当n的值达到1000左右,算法A和算法B的运行时间已经接近;当n的值越来越大,达到十万、百万时,算法A的优势开始显现,算法B则越来越慢,差距越来越明显。

这就是不同时间复杂度带来的差距。

空间复杂度

空间复杂度是对一个算法在运行过程中临时占用存储空间大小的一个量度(即除开原始序列大小的内存,在算法过程中用到的额外的存储空间),反映的对内存占用的趋势,而不是具体内存

大O符号表示法 S(n)=O(f(n))

常用的空间复杂度有 O(1)O(n)O(n²)

只要没有额外的空间增长,无论代码有多少行,空间复杂度也是 O(1)

function hello() {
    let name = 'qzy' 
    // ...
    console.log('hello', name)
}

用数组来存储值,n 的数值越大,需要分配的空间就需要越多,所以它的空间复杂度就是 O(n)

function foo(n){
    let arr = []
    for( let i = 1; i < n; i++ ) {
        arr[i] = i
    }
}

O(n²) 这种空间复杂度一般出现在比如二维数组,或是矩阵的情况下

let arr = [
    [1,2,3,4,5],
    [1,2,3,4,5],
    [1,2,3,4,5]
]