算法基礎 第〇章 通論

參考書就是那本算法導論(別問我是哪本).算法

有人說,程序就是數據結構加上算法. 照這麼看, 算法應該是定義在數據結構上的操做. 算法接受一系列的數據做爲參數, 並輸出一些咱們想要的數據, 這即是算法的意義.服務器

算法的度量

標準計算模型

不少狀況下, 咱們研究算法不僅是解決從0到1的問題, 而是解決從 1 到 100 的問題. 好比, 不少狀況下咱們須要一些優秀的算法來減小某些資源的使用, 好比減小內存的使用, 更多狀況下咱們考慮的是算法的時間性能. 也就是說, 不少狀況下, 咱們但願拿處處理一樣的數據而使用時間短一些的算法. 固然, 你也能夠花更多錢購置更多性能更高的機器, 加速本身手頭的工做. 但更經濟有效的方法, 是研究出更優秀的算法. 數據結構

咱們假定咱們的優化目標是運行時間, 在保證結果正確的前提下, 咱們更青睞運行時間短的算法. 這就涉及到一套公正的測試標準來衡量算法的性能. 函數

在哪一臺機器上跑做爲標準呢? 難道咱們計算機科學也要想 SI 學習, 搞一套標準服務器放在博物館裏, 供你們測性能嗎? 這顯然是不現實的. 所以, 咱們虛擬一臺計算機, 以在它上面算法"運行時間"做爲標準. 這臺計算模型叫作隨機訪存機(RAM). 在他上面支持的運算有: +, -, *, /, 2的冪, 取餘, 取整, 位運算等基本算術運算; 條件判斷, 循環, 函數調用等結構; 以及隨機地址訪存, 數據移動語句. 每條語句用僞代碼表示, 每條語句執行時間爲某一常數. 你可能以爲乘法和除法怎麼能跟加減法相提並論? 或者有人故意在一條語句中寫了特別多的操做, 這條語句仍是常數時間嗎? 固然這些問題頗有道理, 而咱們不會再繼續嚴格約束僞代碼的寫法, 但寫僞代碼的時候本身應該注意. 什麼樣的語句能夠單獨拿出來做爲一條常數時間指令本身內心應該清楚.性能

具體度量方法

咱們以一段簡單的代碼爲例:學習

//A SIMPLE DEMO     // 執行時間
i = 1               // c1
while i <= n       // c2*(n+1)
    A[i] += B[i]    // c3*(n)
    B[i] /= C[i]    // c4*(n)
    if A[i] > C[i]  // c5*(n)
    then ++ B[i]    // ti*c6*(n)
    ++ i            // c7*(n)

每條語句後面是它的執行時間, 其中須要注意的幾個地方. while 這條語句要比循環內的其餘語句多執行一次, 由於最後有一次離開循環體的判斷(即 i == n+1). 判斷語句 if 的執行時間是不肯定的, 所以前面乘了一個係數 ti, ti 的取值與 i 有關(由於是 i 的循環內), 只能取 0 或 1. 這個 ti 和具體執行狀況有關, 咱們沒法預測. 測試

因而總的運行時間 $$ T = c_1 + c_2*(n+1) + (c_3+c_4+c_5+c_7)*n + ti*c_6*n $$優化

因爲 T 的取值是不肯定的, 因此若是咱們要分析這個算法還要作進一步的近似. code

最好的狀況即是判斷語句總不成立, 這樣就會避免執行 if 塊內的語句. 這個狀況咱們稱之爲最佳運行時間遞歸

$$ T_Best = c_1 + c_2*(n+1) + (c_3+c_4+c_5+c_7)*n $$

一樣還有最差運行時間

$$ T_Worst = c_1 + c_2*(n+1) + (c_3+c_4+c_5+c_6+c_7)*n $$

若是咱們對判斷狀況作粗略的平均, 即咱們假定判斷成功和失敗是等機率的, 那麼有平均運行時間

$$ T_Average = c_1 + c_2*(n+1) + (c_3+c_4+c_5+c_6/2+c_7)*n $$

在這個例子中, 這三個時間其實相差不大, 整理一下獲得:

$$ T = A*n + B$$

A, B 是常數. 將那麼多的 ci 加到一塊兒粗略的獲得某個常數, 是由於咱們並不關心是 2n 仍是 200n,
咱們只關心數量級 n, 將其記做:

$$ T = \Theta (n)$$

粗略的意思是: T 與 n 同量級.

漸進記號

咱們在前面知道了, 再研究算法的性能時咱們考慮的是運行時間 T 關於輸入規模 n 的量級, 表示爲:

$$ T = \Theta (f(n)) $$

各個算法進行性能比較的時候, 咱們就看 f(n) 以及 n 的取值範圍. 除了這個記號, 一般還有另外幾個記號.

$$ T = O(f(n)), T = \Omega(f(n)) $$

前者讀作做"大O", 意思是存在一個常數 C, 使得當 n 足夠大之後, C*f(n) 總比 T(n) 的值要大; 後者偏偏相反. 也就是說, O(f(n)) 描述了算法再差不會超過這個量級, 然後者描述了算法最好也就是這個量級. 通常的, O(f(n)) 是最經常使用的記號, 由於平均運行時間每每和最差運行時間同量級. 用 f(n) 更有意義.

到這裏你可能有些疑惑, 若是我把 f(n) 定義成這樣:

$$ T(n) = O(n^n) $$

咱們一般見到的算法, 在 n 很大之後, 基本上不會超過這個量級. 若是是考試, 我全寫這樣的 f(n), 豈不是躺着就能及格嗎? 所以咱們雖然在定義上不作限制, 但在具體分析的時候, 仍是要保證 f(n) 儘量準確地表述 T(n) 的量級.

遞歸式的分析

根據定義咱們能夠輕鬆地計算出串行程序, 或者中間夾雜着循環的程序的運行時間. 那若是程序中含有遞歸調用怎麼辦呢?

function foo (val, m, n)
    if m-n <= 1
    then return val
    
    val_next = foo(val, m, (m+n)/2) + foo(val, (m+n)/2+1, n)
    return val+val_next

這樣的遞歸代碼, 假設輸入規模是 n , 咱們能夠將其時間寫做

$$ T(n) = C_0, (n <= 1) $$
$$ T(n) = C_1*T(n/2)+C_2*T(n/2)+C_0, (n > 1)$$

這即是遞歸形式的運行時間, 咱們再用一些數學技巧將其非遞歸化. 觀察得知, 從 n 一直除以 2, 分解到最後的 1. 這個過程能夠近似的當作一棵 n 個葉的徹底二叉樹, 這個樹的高度就是運行時間(若是能徹底並行化).

$$ T(n) = O(log_2(n)) $$

記爲:

$$ T(n) = O(lg(n)) $$

2017.9.14
Osinovsky


*有一個好玩的地方在於, Omega 這個字母的名稱, 本意就是"大O".

相關文章
相關標籤/搜索