6.1 Algorithm 的時間複雜度及遞迴程式
algorithm 演算法
- 為指令之集合, 用以達到某一特定之任務
- 需滿足:
- input ≥ 0 個
- output ≥ 1 個
- definiteness 明確性: 指令皆是清楚且不模糊的
- finiteness 有限性: 在有限的指令數之後會求得結果
- effectiveness 有效性: 只可利用紙,筆即可追蹤
- Note:
Program vs Algorothm => 差別在於4, Program 可以有無窮迴圈
Time Complexity 時間複雜度
- Def:
- 用來衡量 algorithm 執行時間
- 利用時間函數:
T(n)
表示 隨著資料量的成長, 所需的執行次數之變動
- Ex1: 時間函數計算
1 | i = 1; |
- Ex2:
1 | for(i = 1; i <= n; i++) |
i | 1 | 2… | n |
---|---|---|---|
j | 2~n | 3~n | n+1~n |
x=x+1 | n-1 | n-2 | 0 |
- 思考:
2n+1
2n-1
n+2
n+100
n大到一定程度, +1 -1....不重要
漸進式表示法 asymptotic notation
- 於 Time Complexity 中, 常用漸進式來表示一 algo 的時間的成長漸進曲線, 以便快速了解 algo 之複雜程度
- 衡量方式:
- Big Oh: upper bound of
ƒ(n)
- Omega: lower bound of
ƒ(n)
- Theta: more precision than Big Oh and Omega
- Big Oh: upper bound of
- 概念:
Big Oh
- Def: 若
ƒ(n)
為一時間函數, 則ƒ(n) = O(g(n))
, if and only if 存在兩正數 c 和 n0, 使得ƒ(n) ≤ c * g(n)
, for all n ≥ n0 - Ex1:
ƒ(n) = 3n + 2
=> O(n) , 因為存在 2 正數,c = 4, n0 = 2
, 此時,3n+2 ≤ 4*n
, for all n ≥ 2
定理:
ƒ(n) = am*n^m + am-1*n^m-1 +...+ a1*n^1 + a0*n^0
, 其中 m 為最高指數項, 則ƒ(n) = O(n^m)
- Ex2:
1 | ƒ(n) = 3n^5 + 2n^3 + 1000 => O(n^5) |
-
Ex3: O(logn^2) vs O(n) 誰大?
= 2log n < O(n)
2 常數不看 -
常見的 Big Oh: 由小 -> 大排序
O(1) < O(log n) < O(n) < O(nlog n) < O(n^2) < O(n^3) <…< O(n^c) < O(2^n) < O(n!) < O(n^n)
越大越複雜 -
Proof:
ƒ(n) = am*n^m + ...+ a1*n^1 + a0*n^0 = O(n^m)
1 | = ∑ai*n^i ≤ ∑|ai|n^i |
Omega
- Def: 若 ƒ(n) 為一時間函數, 則
ƒ(n) = O(g(n))
, if and only if 存在兩正數 C 和 n0, 使得ƒ(n) ≥ c * g(n)
, for all n ≥ n0
常見的 Time complexity 求算
- Ex1:
T(n) = T(n-1) + n
-> 快速排序的 worst case
1 | T(n) = T(n-1) + n |
- Ex2:
T(n) = 2T(n/2)+n
, 求 O(?) -> 快速排序的 Best case
1 | T(n) = 2T(n/2) + n |
- Ex3:
T(n) = T(n/2) + 1
, 求 O(?)
1 | T(n) = T(n/2) + 1 // Binary Search |
Recursive 遞迴
-
Def: 指函式執行過程反覆呼叫自身函式 => self-calling
-
種類:
- 直接 recursive: 在程式中直接呼叫自身
1
2
3
4
5
6fun()
{
...
fun() // self-calling
...
}- 間接 recursive: 在程式中先呼叫其他函式, 再由他呼叫到原本的程式
- Note: 不建議採用, 易發生後續維護上的不易性
1
2
3
4
5
6
7
8
9
10
11
12
13
14funA()
{
...
funB()
...
}
funB()
{
...
funA()
...
}
// calling cycle- 尾端 recursive: 函式會在最後再次呼叫自身
- 建議改採用 “iterative”(non-recursive) 方式撰寫, 以提高效能 -> 做 loop 快, 比做 context-switching 快, 節省時間和空間
- Note:
- recursive 會採用到系統的「stack」 後進先出
- recursive & non-recursive 適用性, 沒有誰好誰壞
1
2
3
4
5fun()
{
...
fun();
}
Recursive vs Non-Recursive (iterative/loop)
- Recursive
- 優點:
- 特性:
- 程式精簡 -> 程式較省 space
- 易理解
- 表達力更佳
- Space
- 暫存變數需求少 (int, temp…)
- 特性:
- 缺點:
- Space
- 執行時需耗用 “stack” 空間 -> 需系統 stack 支援(會一直追加 Mem.)
- Time
- 執行時間較久 -> 效率較差(因為需做 function calling)
- Space
- 優點:
- Non-recursive
- 優點:
- Space
- 執行時不需耗用 “stack” 空間
- Time
- 執行時間較快 -> 效率較好
- Space
- 缺點:
- 特性:
- 程式冗長
- 不易理解
- 表達力較差
- Space
- 暫存變數需求多
- 特性:
- 優點:
常見的 Recursive
Example1: N!
- 提示:
- N! = 1, if N = 0
- N! = N * (N-1)!. if N ≥ 1
1 | int F(int N) |
- 呈上例: 問
F(3)
= ? 又呼叫F()
幾次? - Sol:
F(3) -> 3*F(2) -> 2*F(1) -> 1 * F(0)
,F(3) = 6
, 呼叫F()
4次 - iterative:
1 | int sum(int n) |
Example2: Sum(n) = 1+2+…+n 寫出其 Recursive algo.
- 提示:
- sum(n) = 0, if n = 0
- sum(n-1) + n, if n = 1, otherwise
1 | int sum(n) |
Example3: 費氏數列 (Fibonacci Number)
- 提示:
- Fn = 0, if n = 0
- Fn = 1, if n = 1
- Fn = Fn-1 + Fn-2, otherwise
1 | int F(int n) |
n | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
---|---|---|---|---|---|---|---|---|---|---|---|
Fn | 0 | 1 | 1 | 2 | 3 | 5 | 8 | 13 | 21 | 34 | 55 |
- 呈上例, 問
F(5)
= ? 又呼叫F()
幾次? - Sol:
1 | F(5) = F(4) + F(3) |
- Note: 變形
1 | if (n == 0 || n == 1) |
n | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
---|---|---|---|---|---|---|---|---|---|---|---|
Fn | 1 | 1 | 2 | 3 | 5 | 8 | 13 | 21 | 34 | 55 | 89 |
- 若 n 輸入 -3, 問結果?
- 因為 recursive 會耗用 stack, 而 -3 無法收斂, 故 stack 會耗盡 -> stack out of memory
Example4: G.C.D (Greatest Common Divison) 最大公因數
- 提示:
- G.C.D(A, B) = B, if (A mod B) = 0
- G.C.D(A, B) = GCD(B, A mod B), if (A mod B) 不等於 0
1 | int GCD(int A, int B) |
- 呈上例, GCD(12, 9) = ?
- Sol: GCD(12, 9) -> GCD(9, 3) -> 3
- 又 GCD(9, 12) = ?
- Sol: GCD(9, 12) -> GCD(12, 9) -> GCD(9, 3) -> 3
- iterative
1 | int GCD (int A, int B) |
Example5: Binomial coefficient 二項式係數
- 說明:
- 提示:
1 | int B(int n, int m) |
- Ex1:
- Sol:
1 | B(5, 2) = B(4, 2) + B(4, 1) |
- 思考: 請用更有效率的方法 iterative
1 | int B(int n, int m) |
Example6: Ackerman’s
- 背:
A(1, 2) = 4, A(2, 1) = 5, A(2, 2) = 7
- 提示:
A(m, n) = n+1, if m = 0
A(m-1, 1), if n = 0
A(m-1, A(m, n-1)), otherwise
1 | int A(int m, int n) |
- 呈上例:
A(2, 2)
= ?, 又A()
呼叫幾次? - Sol: 7, 呼叫27次
Example7: Tower of Hanoi (河內塔)
- 說明: 3 pegs(柱子) problem
- Recursive algorithm
1 | void Hanoi (n:disc, A, B, C:pegs) |
-
Ex1: 試列出 n = 3 的移動順序
-
Sol:
- move 1 from A to C
- move 2 from A to B
- move 1 from C to B
- move 3 from A to C
- move 1 from B to A
- move 2 from B to C
- move 1 from A to C
-
Ex2: 令
T(n)
為搬動 n 個 disc, 所需的次數, 則T(n) = T(n-1) + 1 + T(n-1)
1 | T(n) = T(n-1) + 1 + T(n-1) |
Example8: Permutation (排列組合)
- 概念:
- n個 data 會有 n! 種組合
- Ex: data a, b, c 之 permutation, 若有 3 筆data => 3! = 3 * 2 * 1 = 6
- abc, acb, bac, bca, cab, cba
- 思考:
Perm(a, b, c)
1 | a + perm(b, c) |
- Recursive (in C)
1 | void perm (char list[], int i , int n) |