0 p* ?- S- F+ M" n5 z7 h. M非多项式阶:随着数据规模的增长,算法的执行时间和空间占用暴增,这类算法性能极差。包括, O( 2^n )(指数阶)、O(n!)(阶乘阶) 7 p7 Q0 @ s" S) c+ j- Z/ z( F- r$ G0 M3 o# R% Y6 v
复杂度量级排序:1 ]' ]- ^! o: |# {9 i$ ?
8 c3 ^6 J6 E; K/ f9 O& l# p
1 R8 E* @7 `5 m
1. 常数阶O(1). k$ ]& Z& p' e/ F) q
无论代码执行了多少行,其他区域不会影响到操作,这个代码的时间复杂度都是O(1) ' y7 N4 b! O1 B2 q 8 N8 {1 ?) t7 S; R4 T$ }# v8 t* U L9 `- O
void swapTwoInts(int &a, int &b)/ ~: _- s/ W' C. e5 ?
{ - @, _ U5 Z+ N& b int temp = a;9 f8 B' Y# b6 p( P, u# u
a = b; c, {% D- p3 E+ L2 W+ d+ ?
b = temp; 0 q7 j; w" |; \9 q p} $ D5 \" `5 W Q; u8 v- q1 0 k$ }, @7 Z) x# L5 u3 w$ K! T2) m( T- H e& o2 E/ V: f
3 ( \+ Q9 z3 h. w& s I0 k4 M8 E4 H4 e1 ]8 k" z2 J1 g5- m6 e8 h% i8 B
60 u$ n m7 L' t2 R/ s, R/ Y
2. 线性阶O(n) 5 b; y) O& ]7 B! m8 O 8 A2 x+ f- w" D$ i# [, C在下面这段代码,for循环里面的代码会执行 n 遍,因此它消耗的时间是随着 n 的变化而变化的,因此可以用O(n)来表示它的时间复杂度。/ X: u1 K+ Y: e8 L) I* m
0 V, o: J4 _$ N9 Q6 A
int sum ( int n ) 6 D( F% {. U8 G) P! r$ l{ ( _! y ?$ N1 M' | int ret = 0; 5 Q1 \( i- p7 G; P0 ~$ i% @ for ( int i = 0 ; i <= n ; i ++) 3 ~0 x1 v# ?8 H {" ?2 ], q9 M8 e7 _$ B& K1 K1 D
ret += i; ! j. Y6 l# |0 _6 w }3 `% ~1 a5 T* |. V2 G9 A
return ret; , v8 v" Z0 s- X& \! J! _}/ T; t4 \) t. D8 V8 a0 I7 H2 C
1# |0 t, ]- D2 d) z, V
2& N) }1 M; A/ v
3 0 [/ S+ e$ B! Q8 }8 j4 9 Z3 D1 h" J/ Z( J, o* I" Y$ }5 8 ~7 ^; E) B9 [6 e( _- }! p: P6# i" w; i; y- }2 D
77 W# E/ A: `$ O/ C {
8 8 t# I* [6 `" S+ o3 Y98 o; k4 d2 l. b0 _
3. 平方阶O(n2) # ~& g# c- _6 L( H当存在双重循环的时候,即把 O(n) 的代码再嵌套循环一遍,它的时间复杂度就是 O(n2) 了。% Y) `7 O# F6 a" l! I7 e# I
+ v5 W+ v. _4 \' q- b& F0 B - r/ z8 K+ _- U- U# i2 d& cvoid selectionSort(int arr[],int n){ ( E+ j4 z1 W- ^1 O6 d; _ for(int i = 0; i < n ; i++){ p# b; C9 [+ v" ]0 x3 `4 Z& L int minIndex = i; 5 _) {8 a# v" N for (int j = i + 1; j < n ; j++ )3 X) M' l9 c% o2 Q8 c( V2 F' d
if (arr[j] < arr[minIndex]) 4 ~5 G9 ~7 T& P- v minIndex = j;$ Z& L% w9 c5 x3 M. {. Q
swap ( arr, arr[minIndex]); ( B; J! u R" ~. D% `: Y0 M! c } 9 U4 K# }, _6 A, R5 n } 8 g) T. j& G u$ c5 w1/ R; R' M5 {6 R0 c/ E. e
24 c& @" y4 d6 ^* n* Z3 Y8 y2 i
3( F, J. D! m$ G
4/ H; l6 f/ H- U8 \7 }5 q0 }
5 % f7 W8 [0 G8 w6 : \( u8 s7 x; R9 }5 i7 3 B& m' B# D# e' f9 A* S5 b8$ Y5 B( _' C/ \' q' o$ J
9 * R! T4 P6 ], x3 d' G0 ?' h6 _, A这里简单的推导一下 7 v, J3 l' L. E& l & g9 g2 G- ^. n8 Y+ H& ?当 i = 0 时,第二重循环需要运行 (n - 1) 次+ ?2 `0 b9 H4 q: ]" D" C8 e% R
. R6 W4 h U# M- t当 i = 1 时,第二重循环需要运行 (n - 2) 次 3 k( ^% }$ I7 l7 f2 y4 h1 f( g2 U9 u6 { S- k) e; P E
。。。。。。3 H0 A8 B$ l/ J' }6 o" y
6 c9 {8 C! `3 _" N, w) w
不难得到公式: % ` A$ |# |5 e+ ^5 D3 ^: ` 0 G5 s5 T; O' w% E& p(n - 1) + (n - 2) + (n - 3) + ... + 0 - s4 w l% w& Y9 _5 t# u= (0 + n - 1) * n / 2: c5 G/ T3 o5 @; S! Q' S- H
= O (n ^2) " p% W% ^5 _; G( l12 o+ s5 [/ H7 I6 I
2 9 R8 C) s" E1 f1 o$ C7 [3 r; U7 a8 }: E/ p, b. ?; @
4. 对数阶O(logn)! G; z1 s8 Q4 R- v+ [
( R' \% I, a1 L
6 H, b* P- G3 u# l: I W
int binarySearch( int arr[], int n , int target){ ; Q+ I1 `; c+ F; p int l = 0, r = n - 1;' w8 l3 |# E7 @; f
while ( l <= r) { # j ?/ d) H9 E% a8 u2 P int mid = l + (r - l) / 2;4 D4 i" P/ L9 L
if (arr[mid] == target) return mid; $ y V* g$ T k% E# P! d if (arr[mid] > target ) r = mid - 1;7 @0 G' s% w. H% p8 Q& E2 S3 |- D
else l = mid + 1; - U+ L1 b g$ Y: \ }% B4 o. A& X. i4 F3 @2 E
return -1;3 [0 ]4 e0 ]& v$ H
} - b2 c# D7 a }5 K1 . A2 H {* g& A+ m8 r: s2/ G3 G2 h' Z" D0 p& Z
3 # g2 ~% _0 s8 Q' D4 / ]/ q: A# d! m9 ^) `5 }4 T* n7 C5 R# i
6 % j& Q5 ^- R* m& [) S# _9 k$ Q. Z. r- I7 o/ o6 F7 m8 d
8 ) P) T6 B+ j# n9 r& V/ d9; g) o& W# Q* O& K) N9 w4 `% u( l: D
10 - U* m0 Z( z! U1 d; w9 p& T5 G在二分查找法的代码中,通过while循环,成 2 倍数的缩减搜索范围,也就是说需要经过 log2^n 次即可跳出循环。 ) d# {- m, ^( ~* }0 L, `2 T/ E' H [) h/ J- a$ i
五、不常见的时间复杂度' L, B' O4 H. p
1. 最好情况时间复杂度(best case time complexity) + W) j( M0 J* k s& Y Y9 @最好情况时间复杂度就是,在最理想的情况下,执行这段代码的时间复杂度。在最理想的情况下,要查找的变量 x 正好是数组的第一个元素,这个时候对应的时间复杂度就是最好情况时间复杂度。 ( E h" e0 a1 Y& j% E( F- {$ \ 5 ^ e# ?* P$ ^& a, w. _2. 最坏情况时间复杂度(worst case time complexity) " G9 u7 z" }2 W# d6 v9 `1 k最坏情况时间复杂度就是,在最糟糕的情况下,执行这段代码的时间复杂度。如果数组中没有要查找的变量 x,我们需要把整个数组都遍历一遍才行,所以这种最糟糕情况下对应的时间复杂度就是最坏情况时间复杂度。 G: D; @1 u" b x# |
1 ]' ], l& s; u: i
最好、最坏情况时间复杂度指的是特殊情况下的时间复杂度。 6 E, d1 c4 K# V( }3 A P% s; O动图表明的是在数组 array 中寻找变量 x 第一次出现的位置,若没有找到,则返回 -1;否则返回位置下标。 ' z% f: B( O6 i; g6 E% N 4 J+ t; `9 a7 w4 t/ x$ _int find(int[] array, int n, int x) { 8 K1 g' J: x1 q: b H* q, _ for ( int i = 0 ; i < n; i++) {( P0 J+ k& `" ]6 ^9 Q
if (array == x) { / W; X9 C" E! b, s4 B return i;" j3 a% x+ x0 S& u) r3 Q
break; " e6 T$ L8 _4 S$ A0 p } - ~, h4 b& B+ K q2 s$ _' k }# L+ T4 t# R" b
return -1; 0 A( G' d L; Q* z( M6 ?1 k}. C4 ^2 [) m y' k# e# }
1" ^2 z, v" N6 n4 M2 _: p
28 Y- K& h9 w, ^8 b0 n
3 4 w$ l; G7 ?: f; o, q4 3 s8 Z- L9 }4 q: d4 a; T5 # q5 N0 M3 z1 }* R6# F! q- t( g) [( z
7& m5 f- V! F- `2 t9 M
8* A2 {/ u$ D; @
93 K/ i0 E" d- L) m+ x
在这里当数组中第一个元素就是要找的 x 时,时间复杂度是 O(1);而当最后一个元素才是 x 时,时间复杂度则是 O(n)。9 @) p3 T. c k5 k/ F# n1 c7 P
7 w) _% w. m n1 W( \1 x最好情况时间复杂度就是在最理想情况下执行代码的时间复杂度,它的时间是最短的;最坏情况时间复杂度就是在最糟糕情况下执行代码的时间复杂度,它的时间是最长的。 9 ^6 j O3 w) \- ^% G! \6 T( ^ Z1 O: H. ^7 E
3. 平均情况时间复杂度(average case time complexity) & E5 L: m( W! {( G; s最好、最坏时间复杂度反应的是极端条件下的复杂度,发生的概率不大,不能代表平均水平。那么为了更好的表示平均情况下的算法复杂度,就需要引入平均时间复杂度。 3 D& N0 B1 s' k- C" b% S- E" y4 D e/ ^' v
平均情况时间复杂度可用代码在所有可能情况下执行次数的加权平均值表示。1 {5 \9 u u- c( d! [
. O6 K, U' L0 J$ V5 Q: q
还是以 find 函数为例,从概率的角度看, x 在数组中每一个位置的可能性是相同的,为 1 / n。那么,那么平均情况时间复杂度就可以用下面的方式计算: # `) `9 i) @, x6 i I5 } 3 N/ K" J( t: b5 ]8 y" I# T((1 + 2 + … + n) / n + n) / 2 = (3n + 1) / 4 8 s$ K9 }' c& [' q1 h" s1: v* g& C3 {. ]* J; @, O7 G
/ ~6 d U" n1 v
find 函数的平均时间复杂度为 O(n)。 5 p. F4 |7 w, G$ A 7 s, C) L# C+ O7 |# _8 w4. 均摊时间复杂度(amortized time complexity) 2 q' }- l2 C- M: R" R/ h) U+ Y我们通过一个动态数组的 push_back 操作来理解 均摊复杂度。 4 v% c; ], t n h0 F8 ~% J ! ?7 Z* ?% S$ l+ V. |" z4 j2 e2 _; g: K
template <typename T> : @: K5 b; f5 T$ |3 f$ L5 i class MyVector{6 d& g0 T! u1 I; A+ A
private: - \; s1 n! u+ ^) g6 J$ q6 o9 x) J* o. L T* data; % L! k7 \( a, {) Z' {+ l4 ` int size; // 存储数组中的元素个数 ! T; j& C3 z" I1 v% r( W int capacity; // 存储数组中可以容纳的最大的元素个数 I" i7 }( A1 ~! T' ]6 w
// 复杂度为 O(n)7 x( V5 ]6 }+ n: H6 [ h
void resize(int newCapacity){4 p7 m5 I' R0 F9 f5 v+ |/ K
T *newData = new T[newCapacity];0 h6 D- @* P$ ] i
for( int i = 0 ; i < size ; i ++ ){9 ^( \4 S8 A" x, g
newData = data;, [; C( `% F; S' }9 {5 Y; M
} - D* p& x6 V% a! B& V5 k" b data = newData;4 J" P/ V* u9 Q2 C3 x
capacity = newCapacity;$ A9 {' x7 U& S8 [7 D s6 c
} W8 v& B7 A2 T5 y3 K% M
public:, T! N5 Q. R: {7 S6 J, m+ K
MyVector(){7 r+ D7 L: M0 |& W
data = new T[100]; 8 @) y% B+ G; P* z4 _ size = 0; 1 k( U: \) b+ P" T. `& h; i capacity = 100;7 Q( O$ m0 J2 N8 M+ C
} & _) O) |' c/ f! X& X' W // 平均复杂度为 O(1)& {: l5 I1 s$ I4 U2 R9 ~ V
void push_back(T e){$ a) ~+ J' R' `; t: ?
if(size == capacity)& Z4 ]# i0 r" d6 h
resize(2 * capacity); 5 H" C2 y# Y1 l& i$ q; U data[size++] = e;1 E6 z( L! q8 Y# J8 U
} r2 G* D1 c; S$ N! V7 v
// 平均复杂度为 O(1)3 E; b9 K- h! F" `
T pop_back(){ 4 ?1 s! ?% l' I6 u* s% G size --; ) C2 g R$ ?& O( s4 l1 ^! l return data[size]; 0 c7 G& P! L9 D3 X6 s1 K: A } ( f1 i- w1 D* w4 z * v; b0 U1 ?6 T# b1 k+ M}; & t& _4 b2 [( A7 h1 V ' Y& w. W" ^- q6 A0 s. P* o& |9 ]7 p$ z
1 i- |+ ]0 j+ D8 S5 V- @2 & R$ ]( D2 a" y I% V( R) X34 _5 d9 V) W4 ^8 u$ c5 v- n
40 R/ J) w, C) u* n) `+ N* h( @
5 @8 L9 D" `$ n! H5 g% S
6 % A0 Y d: g6 x3 ?' ^) o# v( G) C7 4 Y/ L' |9 { v, }* Z8 ) Y, s7 ^! z% n4 C6 y& r9 # J1 K Q* r6 W" g1 c3 O: g- U% N5 j10 % ?9 U3 w1 _! ^$ M. j# t11 ( s6 _4 W, i8 c, [9 [/ C% j/ ?12& e8 g! I: ?+ x7 s- x! d1 { Q
13( M0 E6 S- V' e
146 E0 Y9 L# h' e$ O/ y
15 0 A! _2 d; O3 H2 Z16 Q- M* {( s0 M17 " K+ R) T6 z7 N( L" s18 , i v6 G/ C7 ?! q3 d6 L19) S. Y* o8 T. s+ q( `
200 n& z" O& o8 y/ k/ u9 i
217 U+ X1 c- [8 d+ k1 g) K, L4 q
22, ]. I1 W5 V) L2 g
23 , D2 U* \8 q& G% H) X4 k1 L24" m7 U3 g) J/ ^6 P5 E- u/ g
25 ; v- ~, l8 i V7 q26* l1 e3 Z- C% H$ k3 y: m2 Y
27% P# ?, k3 f V( E7 T
28+ V& ]. L( j0 q, h! I8 h6 S
298 U0 l# Z* p. o# D% Y
30% u: f7 x0 r% O5 ?0 D: U) l1 r
31 , z5 A/ r, F; W, r/ K! ~327 o3 P1 [: J% Y' i% x: x1 Z! |
338 k3 l- K6 e2 H, @9 K# u
34 ( |* x1 c' Y! F# A& u35 q# L6 Y% g& d. W5 o" o/ }push_back实现的功能是往数组的末尾增加一个元素,如果数组没有满,直接往后面插入元素;如果数组满了,即 size == capacity ,则将数组扩容一倍,然后再插入元素。0 ]7 G4 S7 V& Y* X- U
0 g8 d7 A q4 x( O
例如,数组长度为 n,则前 n 次调用 push_back 复杂度都为 O(1) 级别;在第 n + 1 次则需要先进行 n 次元素转移操作,然后再进行 1 次插入操作,复杂度为 O(n)。 5 L9 x$ | I8 i7 P- n+ A * @+ R0 k+ S8 g% E. s7 r因此,平均来看:对于容量为 n 的动态数组,前面添加元素需要消耗了 1 * n 的时间,扩容操作消耗 n 时间 ,6 B9 [( G) H- P; M5 i( b- C& t& X
总共就是 2 * n 的时间,因此均摊时间复杂度为 O(2n / n) = O(2),也就是 O(1) 级别了。* h2 \. A% K. q# @9 O& v! [
2 V+ T* n$ J# y( R( q! x
可以得出一个比较有意思的结论:一个相对比较耗时的操作,如果能保证它不会每次都被触发,那么这个相对比较耗时的操作,它所相应的时间是可以分摊到其它的操作中来的。 7 v, e$ }3 B3 m4 |" Z9 N' C9 A. v4 c* F2 p
六、如何掌握好复杂度分析方法?! x7 B4 a3 x5 O) u: B3 W, p6 ~
复杂度分析关键在于多练,所谓孰能生巧。 5 m- F, s, @" N. \* H7 B1 z7 \+ I1 u) f+ o+ I7 `
1. 大O标记! m; b8 S( {2 z# U5 y$ _9 O6 @
从 CPU 的角度来看,这段代码的每一行都执行着类似的操作:读数据-运算-写数据。尽管每行代码对应的 CPU 执行的个数、执行的时间都不一样,但是,我们这里只是粗略估计,所以可以假设每行代码执行的时间都一样,为 单位时间(unit_time)。 , i4 a) F8 Y( X6 }* h) E. Z1 W& P/ B, x
算法运行工作量(基本操作重复执行的次数总和)的大小是数据规模 n 的函数,记作:f ( n ) 。" f0 b) ?- S3 A& g2 o. d. L
6 D- X0 G3 b; n& \# j
则代码执行时间可表示为:4 N: g4 c& R* P: R* |% n
2 _+ w+ c# G VT(n)=O(f(n))# h' J5 s s# L
1 ! g9 Y3 L1 Y& r: D8 h' n( I大 O 时间复杂度实际上并不具体表示代码真正的执行时间,而是表示代码执行时间随数据规模增长的变化趋势,O表示算法执行时间的增长率和 f ( n ) 的增长率相同,称为渐进时间复杂度,简称时间复杂度。 - ]4 @, |" c* v a1 B Z% H 2 W0 C0 Q% F' u( y/ i时间复杂度的基本原则:3 X# G L* T0 |8 [9 y/ W
% q# U) V5 D8 t% C' l/ t4 _8 b, [只有常数项,认为其时间复杂度为O(1) 3 d6 g- V0 ~. y4 F" Z' h顺序结构,时间复杂度按加法进行计算1 A. @' r* G% N% A1 @" i
循环结构,时间复杂度按乘法进行计算/ `/ A# K; |6 G: ~! d" A; l
分支结构,时间复杂度取最大值/ I0 E; O3 {- }) H' B# v9 \/ e, u
判断一个算法的时间复杂度时,只需要关注最高次项,忽略最高次项的系数,且其它次要项和常数项也可以忽略5 E: l+ \* P T" A- M8 b
一般所分析的算法的时间复杂度都是指最坏时间复杂度 / T; o; z$ {6 _- v" {. z+ c4 p渐进式时间,空间复杂度分析只是一个理论模型,只能提供粗略的估计分析,我们不能直接就觉得O(logN)的算法一定优于O(n), 针对不同的宿主环境,不同的数据集,不同的数据量大小,在实际应用上面可能真正的性能会不同。 c4 a+ J/ m9 Y0 c% y