2

为什么需要复杂度分析

  1. 从理论角度出发分析代码性能的优劣
  2. 测试结果依赖运行的测试环境
  3. 测试结果受数据规模的影响

总结:相对于实际运行的测试结果,复杂度分析更具有理论依据,可以在写代码的时候提供性能优劣的理论支撑;弥补实际运行测试的“事后诸葛亮”的缺点;所以复杂度分析并非完全决定了代码的运行,而只是作为一个代码性能优化的方向。

大 O 复杂度表示法

实例

尝试分析一下下段代码的“运行时间”

function sum(n) {
    let sum = 0
    for (let i = 1; i <= n; i++) {
        sum += i
    }
    return sum
}

该段代码总共有7行,其中1、2、5、6被执行一遍,5、7行为{,3、4行的执行次数和n有关,现假定一行代码的执行时间为定值_time,故总执行时间为(2n + 5) * _time
上段代码的运行时间和n的大小成正比
按照这个思路再分析一下下段代码

function sum(n) {
    let sum = 0
    for (let i = 1; i <=n; i++) {
        let j = 1
        for (; j <= n; j++) {
            sum += (i + j)
        }
    }
    return sum
}

1、2、9行代码被执行一遍,把5、6行代码看作代码块x,则3、4、x被执行n遍,在每一遍执行代码块x时5、6又分别被执行n遍,故总耗时:(2 n ^ 2 + n + 2) _time
上段代码的运行时间和n^2成正比

实例小结

我们有一个前提假设:每行代码执行一遍的时间是相同的,所以很容易得出一个结论:代码的执行时间和代码的执行次数n成正比
抽象成一个公式:T(n) = O(f(n))
第一段代码:T(n) = O(f(2n + 5))
第二段代码:T(n) = O(f(2*n^2 + n + 2))
通过公式计算很容易得出结论:第二段代码理论上更耗时

大O时间复杂度表示法并不是代码运行的准确耗时,只是表示代码耗时随着数据规模变化而变化的一个趋势,如果数据规模n趋近于非常大,则
第一段代码时间公式可以简化为T(n) = O(f(n)),
第二段代码时间公式可以简化为T(n) = O(f(n^2)),

时间复杂度分析

基于以上简化,可以得出时间复杂度分析的几个简单参考法则

只关注循环最多的一段代码

通过前面两段代码分析,我们简化掉了低阶、常量、系数,因为这些对于整体趋势没有影响,所以在进行复杂度分析的时候只需要关注被循环执行次数最多的代码。

加法法则

只关注量级最大的那段代码
以以下这段代码为例

function foo(n) {
    let i = 0
    let sum = 0;
    for (; i < 10000; i++) {
        sum += i
    }
    let j = 0
    for (; j < n; j++) {
        sum += j
    }
    return sum
}

分析其复杂度:1、2、3、7、11行代码执行1遍,4、5行代码执行10000遍,8、9行代码执行n遍,
故总时间:T(n) = O(f(2n + 10000 * 2 + 5))
仍然可以简化为T(n) = O(f(n))
有高中数学基础的同学都知道,无论系数或者常数有多大,都左右不了表达式的变化趋势(是正比、平方、或者指数趋势),其实这里也说明了复杂度分析只是提供了代码耗时随数据规模变化的一个趋势,因为从上面表达式很容易看出当n比较小或者不足够大时,数据规模n并不是决定代码耗时的关键因素;
即无论常数、系数有多大,当数据规模n趋紧很大时,描述时间变化趋势的T(n)依然可以省略掉这些常数、系数

乘法法则

嵌套部分复杂度等于内外复杂度之乘积
参考前面代码段2关于嵌套代码的复杂度分析

常见的时间复杂度

常见的时间复杂度主要有:
常数阶 O(1)
对数阶 O(lgn)
线性阶 O(n)
线性对数阶 O(nlgn)
平方阶 O(n^2)
指数阶 O(2^n)
阶乘阶 O(n!)
后两种指数阶和阶乘阶称为非多项式量级,其他都称为多项式量级,这里可能都听过国际象棋盘放米粒的故事,这个故事用到的就是指数的威力,所以一般代码中极少需要指数阶和阶乘阶复杂度的代码,因为这种代码的时间趋势会随着数据规模的增长极速暴增。

O(1)

基本上非循环和递归的代码,复杂度都为O(1)

let i = 0
let j = 1
let sum = i + j

这种代码的复杂度和数据规模无关

O(lgn)

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

分析:该段代码包含一个循环,根据法则1,则影响时间复杂度的代码实际上只有第3行,设执行次数为x,则有
2 * x = n -> x = log2n -> T(n) = O(log2n) -> T(n) = O(log2e * lgn) -> T(n) = O(lgn)
注:(log2n表示以2为底n的对数)

O(n)

单次循环的复杂度一般为O(n)

let i = 0
let sum = 0
for (; i< n; i++) {
    sum += i
}

O(m + n) & O(m * n)

如果有多个循环与多个数据规模变量有关,则不能确定哪个影响较大,测试复杂度需要具体分析

let i = 0
let j = 0
let sum = 0
for (; i < m; i++) {
    sum += i
}
for (; j < n; j++) {
    sum += j
}

复杂度为O(m + n)

let i = 0
let sum = 0
for (; i < m; i++) {
    let j = 0
    for(; j < n; j++) {
        sum += j
    }
}

复杂度为O(m * n)

O(nlgn)

就是O(n)和O(lgn)的代码进行一层嵌套

O(n^2)

很常见的一个例子就是双重循环

O(2^n)

最常见的是斐波那契数列的算法

function fb(n){
    if(n <= 2){
        return 1;    
    }else{
        return fb(n-1) + fb(n-2);
    }
}

分析:上段代码执行最多的是第2行,因为每次递归调用都会执行这行代码,同时在前n - 2次的执行中都会执行第5行,每次第5行的执行必定会执行2次第2行,所以第2行的执行次数为n - 2 个 2的乘积即2^(n - 2),即
时间复杂度为T(n) = O(2^n)
温馨提示:不要以大于40的参数调用该函数

O(n!)

不常用,不做分析

空间复杂度分析

既然时间复杂度表示的是代码执行时间趋势和数据规模之间的增长关系,很容易得出,空间复杂度则为算法的存储空间与数据规模之间的增长关系

概念

一个程序的空间复杂度是指运行完一个程序所需内存的大小,利用程序的空间复杂度,可以对程序的运行所需要的内存多少有个预先估计。一个程序执行时除了需要存储空间和存储本身所使用的指令、常数、变量和输入数据外,还需要一些对数据进行操作的工作单元和存储一些为现实计算所需信息的辅助空间。程序执行时所需存储空间包括以下两部分。 

(1)固定部分:这部分空间的大小与输入/输出的数据的个数多少、数值无关,主要包括指令空间(即代码空间)、数据空间(常量、简单变量)等所占的空间,这部分属于静态空间。 

(2)可变空间:这部分空间的主要包括动态分配的空间,以及递归栈所需的空间等,这部分的空间大小与算法有关。一个算法所需的存储空间用f(n)表示。S(n)=O(f(n)),其中n为问题的规模,S(n)表示空间复杂度。

  1. 空间复杂度是对一个算法在运行过程中临时占用存储空间大小的量度。
  2. 一个算法在计算机上占用的内存包括:程序代码所占用的空间、输入输出数据所占用的空间、辅助变量所占用的空间这三个方面。程序代码所占用的空间取决于算法本身的长短,输入输出数据所占用的空间取决于要解决的问题,是通过参数表调用函数传递而来,只有辅助变量是算法运行过程中临时占用的存储空间,与空间复杂度相关。
  3. 通常来说,只要算法不涉及到动态分配的空间以及递归、栈所需的空间,空间复杂度通常为O(1)。
  4. 算法的空间复杂度并不是计算实际占用的空间,而是计算整个算法的辅助空间单元的个数,与问题的规模没有关系。

小栗子

还是用斐波那契数列的例子来分析一下该算法的空间复杂度

function fb(n){
    if(n <= 2){
        return 1;    
    }else{
        return fb(n-1) + fb(n-2);
    }
}

我们知道函数的执行是一个入栈/出栈的过程,当某一个函数被调用的时候,运行时会为其分配内存空间,并将其压入执行栈,直到该函数执行完毕便将其弹出执行栈,并释放其占用的内存空间,对于该递归函数,调用栈中最多时的数量为n - 2个函数调用,故空间复杂度为O(n)

相较于时间复杂度,空间复杂度比较简单,而且随着硬件的提升(内存大小),一般不太过分关注空间的占用。

总结

复杂度也叫渐进复杂度,包括时间复杂度和空间复杂度,用来分析算法执行效率与数据规模之间的增长关系,可以粗略地表示,越高阶复杂度的算法,执行效率越低。常见的复杂度并不多,从低阶到高阶有:O(1)、O(lgn)、O(n)、O(nlgn)、O(n2),非多项式类型O(2^n)、O(n!)

TODO: 对常见的排序算法进行时间/空间复杂度分析


innocence
11 声望1 粉丝

undefined