总结:相对于实际运行的测试结果,复杂度分析更具备理论依据,能够在写代码的时候提供性能优劣的理论支撑;弥补实际运行测试的“过后诸葛亮”的缺点;因此复杂度分析并不是彻底决定了代码的运行,而只是做为一个代码性能优化的方向。算法
尝试分析一下下段代码的“运行时间”性能优化
function sum(n) { let sum = 0 for (let i = 1; i <= n; i++) { sum += i } return sum }
该段代码总共有7行,其中一、二、五、6被执行一遍,五、7行为{,三、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 }
一、二、9行代码被执行一遍,把五、6行代码看做代码块x,则三、四、x被执行n遍,在每一遍执行代码块x时五、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(fn(n))
,
第二段代码时间公式能够简化为T(n) = O(fn(n^2))
,优化
基于以上简化,能够得出时间复杂度分析的几个简单参考法则code
经过前面两段代码分析,咱们简化掉了低阶、常量、系数,由于这些对于总体趋势没有影响,因此在进行复杂度分析的时候只须要关注被循环执行次数最多的代码。排序
只关注量级最大的那段代码
以一下一段代码为例递归
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 }
分析其复杂度:一、二、三、七、11行代码执行1遍,四、5行代码执行10000遍,八、9行代码执行n遍,
故总时间:T(n) = O(f(2n + 10000 * 2 + 5))
仍然能够简化为T(n) = O(f(n))
即不管常数、系数有多大,当数据规模n趋紧很大时,描述时间变化趋势的T(n)依然能够省略掉这些常数、系数内存
嵌套部分复杂度等于内外复杂度之乘积
参考前面代码段2关于嵌套代码的复杂度分析
常见的时间复杂度主要有:
常数阶 O(1)
对数阶 O(logn)
线性阶 O(n)
线性对数阶 O(nlogn)
平方阶 O(n^2)
指数阶 O(2^n)
阶乘阶 O(n!)
后两种指数阶和阶乘阶成为非多项式量级,其余都称为多项式量级,这里可能都听过国际象棋盘放米粒的故事,这个故事用到的就是指数的威力,因此通常代码中极少须要指数阶和阶乘阶复杂度的代码,由于这种代码的时间趋势会随着数据规模的增加极速暴增。
基本上非循环和递归的代码,复杂度都为O(1)
let i = 0 let j = 1 let sum = i + j
这种代码的复杂度和数据规模无关
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)
let i = 0 let sum = 0 for (; i< n; i++) { sum += i }
若是有多个循环与多个数据规模变量有关,则不能肯定哪一个影响较大,测试复杂度须要具体分析
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(n)和O(lgn)的代码进行一层嵌套
很常见的一个例子就是双重循环
最多见的是斐波那契数列的算法
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的参数调用该函数
不经常使用,不作分析
既然时间复杂度表示的是代码执行时间趋势和数据规模之间的增加关系,很容易得出,空间复杂度则为算法的存储空间与数据规模之间的增加关系
一个程序的空间复杂度是指运行完一个程序所需内存的大小,利用程序的空间复杂度,能够对程序的运行所须要的内存多少有个预先估计。一个程序执行时除了须要存储空间和存储自己所使用的指令、常数、变量和输入数据外,还须要一些对数据进行操做的工做单元和存储一些为现实计算所需信息的辅助空间。程序执行时所需存储空间包括如下两部分。
(1)固定部分:这部分空间的大小与输入/输出的数据的个数多少、数值无关,主要包括指令空间(即代码空间)、数据空间(常量、简单变量)等所占的空间,这部分属于静态空间。
(2)可变空间:这部分空间的主要包括动态分配的空间,以及递归栈所需的空间等,这部分的空间大小与算法有关。一个算法所需的存储空间用f(n)表示。S(n)=O(f(n)),其中n为问题的规模,S(n)表示空间复杂度。
仍是用斐波那契数列的例子来分析一下该算法的空间复杂度
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)
TODO: 对常见的排序算法进行时间/空间复杂度分析