動機
「程式 A 較快」並不是完整結論:我們必須說明輸入規模、計費操作、合法輸入, 以及所討論的情形。課件以選擇排序作為動機;在某一部機器的測量中,陣列規模 加倍時,時間約增至四倍。這可以顯示增長趨勢,卻不是證明,因為硬件、編譯器、 記憶體行為,以及有限規模下的常數都會影響計時結果。
複雜度分析把問題轉化為數學問題。我們先定義成本函數 ,再計算基本操作 次數,最後研究 增大時成本如何變化。結論必須依賴明確模型:固定長度 key 的比較可以視為常數時間,但任意長字串的比較則未必如此。本節要求先寫出精確 計數或可驗證的界,再給出緊確的漸近類別,而不是憑迴圈層數猜測答案。
定義
定義
輸入規模與 RAM 成本模型
對輸入 ,令 為明確指定的規模參數, 為被計費的基本操作數。 除非另有說明,本節採用 unit-cost RAM model:固定字長算術、索引運算、陣列 存取、賦值,以及固定長度 key 的比較,均具有常數成本。 也必須標明 所討論的情形;例如 worst-case cost 是在所有規模為 的合法輸入上取最大值。
時間與輔助空間是兩種不同資源。對運行時間的結論不會自動說明記憶體用量; helper function 的完整成本也必須計入,不能因為呼叫在原始碼中只佔一行便視為 常數時間。
定義
漸近上界、下界與緊確界
設 最終非負,而且 最終為正。
- 若存在 ,使所有 都有 ,則 ;
- 若存在 ,使所有 都有 ,則 ;
- 若同時存在上下界,即 ,則 。
Big-O 只是最終上界,並不是增長率的唯一名稱。線性函數也屬於 ; 若要表達與 相同的緊確漸近增長率,應使用 。
定義
情形、期望成本與攤還成本
Best-case 與 worst-case cost 分別在相同規模的合法輸入上取最小值與最大值。 Average-case cost 是期望值,因此必須明確指定輸入的機率分佈。Amortized cost 則不假設隨機輸入,而是分析指定操作序列。例如,令一個初始為 empty 的 dynamic array 具有固定正 initial capacity;每當容量用盡,就按固定因子 擴容, 並採用一致的整數 rounding rule。對只包含 次 push 的 sequence,普通 push 收取 constant cost,resize 時每複製一個 element 收取一個 unit。一次 resize push 可以是 ,但整個 sequence 的總成本為 ,所以每次 push 的攤還成本為 。
從程式碼推導成本函數
可靠的分析可以依照固定次序進行。首先聲明合法輸入與規模參數。陣列程式通常 以長度作為 ;圖演算法可能同時依賴 與 ;數值演算法則可能依賴 輸入的 bit 數,而不是數值本身。若把真正的雙參數問題強行壓縮成一個符號, 便可能隱藏決定成本的情形。
其次要說明被計費的操作。選擇排序特別適合計算 key comparison 次數,因為 迴圈邊界令該次數不依賴輸入排列。若賦值、記憶體配置或複製 key 的成本也很 重要,就應分別計算,待成本模型清楚後才合併。「迴圈成本是 」仍不完整; 還必須說明這 次執行各自進行了甚麼工作。
然後先把控制流程翻譯成算術,再作漸近化簡。連續程式區塊的成本相加;固定 成本的迴圈主體在矩形 iteration space 中重複時形成乘積;內層邊界隨外層 index 變化時形成求和,例如 。遞歸程式只有在遞歸呼叫的 數量、子問題規模及非遞歸工作都已說明後,才可以寫出 recurrence。
最後,若要聲稱緊確類別,就必須同時證明上界與下界。只有上界時,結論可能 刻意很寬鬆;相符的下界才說明該增長對指定 implementation 與指定情形無法 避免。這種先推導再分類的做法也會揭示隱藏前提,例如隨機存取是否為常數時間、 key 長度是否有界,以及 helper function 是否暗中完成了一次完整掃描。
固定底數 時,,換底只改變常數。對固定 參數 、、,增長層級可以寫成
因為 且 。這裏比較的是函數比值 或緊確類別,不能只憑兩個 Big-O 集合的歸屬便斷言哪一個「較快」。
邊讀邊試
在同一 n 比較漸進增長
這個工具現在把增長級別綁到具體程式形狀上,讓讀者可以改變 n,並把當前範例與比較表對照。
選擇演算法形狀
程式範例
for (int width = 1; width < n; width *= 2) {
for (int i = 0; i < n; i += 2 * width) {
merge_block(i, width);
}
}增長級別: O(n log n)
估計基本步數: 64.00
如何理解: 大約有 log n 輪,而每一輪仍然會處理線性數量的資料。
| Class | Value at n=16 |
|---|---|
| O(1) | 1.00 |
| O(log n) | 4.00 |
| O(n) | 16.00 |
| O(n log n) | 64.00 |
| O(n^2) | 256.00 |
負責任地比較增長類別
增長層級描述的是最終趨勢,並不是每一個有限輸入上的實測時間。常數很小的 二次 implementation 可能在有限範圍內快過線性 implementation,cache 與 compiler 也會改變兩者的交叉點。漸近記號刻意忽略這些固定常數,以便獨立於 某一部機器比較長期擴展性;實際工程判斷仍應結合漸近結論與目標輸入範圍內的 測量。
當兩個成本都有正的緊確界時,比值可以給出精確比較。若 ,則 ,即 的階嚴格小於 ;若比值趨向有限正數,兩者屬於同一 類,但精確運行時間仍可不同;若比值無界增長,則 的階較大。 這個方法比直接排列兩個 Big-O 歸屬更可靠,因為其中任何一個上界都可能並不 緊確。
定理 / 命題
定理
正首項多項式由最高次項控制
設 ,其中 為非負整數,所有系數 都是固定實數,,而且 最終非負,則 。
定理
選擇排序的比較次數是三角形求和
對 的隨機存取陣列,在每次 key comparison 為 的模型中, 下文的 selection sort 對每一個輸入都恰好執行 次比較,因此時間為 。條件交換 最多執行 次,不會改變緊確界。
證明思路或證明
證明
最高次項定理的證明
若 ,則每個 都有 ,所以 。以下設 。當 時,每個 ()都不超過 ,所以 ,得到所需的上界。另一方面, ,所以存在 threshold ,使低次項絕對值之和不超過 。於是 。同一個正函數給出上下常數倍界, 所以 。固定系數、正首項與最終非負條件說明「刪去低次項」 並不是可以任意套用的代數規則。
證明
選擇排序比較次數的證明
外層第 輪比較 次,第 輪比較 次,最後一輪比較一次。 迴圈邊界不依賴 key 的排列,所以已排序、逆序與任意輸入的比較次數相同:
對 ,這個值介乎 與 之間,因此比較次數為 。常數時間的迴圈控制及至多線性次交換只增加 工作, 而比較次數本身已給出 下界。
一般 comparison sorting 的 下界屬於比較 decision-tree model,並假設任意可排序輸入。它的證明,以及 quickselect、counting sort 和 radix sort 的參數化分析,都留待後續排序筆記;本節只釐清模型邊界,不會在 缺少這些前提時套用該結論。
例題詳解
例題
為甚麼 n^2 支配 n
比較低次項與所提出的主導項:
這並不是說線性項從精確公式中消失,而是說它相對 愈來愈小;配合正 首項,便可支持緊確的 結論。
例題
常數時間語句
int x = a + b;
int y = x * 2;
return y;
在固定字長 RAM model 中,每句只執行一次,連續語句的成本相加後仍是
。若 a、b 是 bit 長度隨輸入增長的 arbitrary-precision
integer,就必須另外計算算術操作的成本。
例題
線性掃描
int sum(const int a[], int n) {
int total = 0;
for (int i = 0; i < n; i++) {
total += a[i];
}
return total;
}
迴圈主體恰好執行 次,而且每次都有常數成本,所以總成本是
,其中 及 為固定常數。課件中的 Average
function 也是一次完整掃描加上最後一次除法;呼叫它是 操作,
不能因為原始碼只寫一行 function call 便當作常數成本。
例題
巢狀迴圈產生二次成本
int countPairs(int n) {
int c = 0;
for (int i = 0; i < n; i++) {
for (int j = 0; j < n; j++) {
c++;
}
}
return c;
}
這段程式的內層語句恰好執行 次,所以成本為 ; 結論來自迴圈邊界,而不是只因為看見兩個 loop 關鍵字。
Function call 也可能把乘法隱藏起來。課件中的 naive variance routine 在
次迴圈中,每次都呼叫成本為 的 Average(array,n),所以
主體工作為 ;最後再計算一次 average 只增加 。
若先在迴圈外計算 mean,多個連續線性階段的成本相加便成為 。
如果使用臨時儲存,還須另行報告 auxiliary-space cost。
同一計數方法也適用於非矩形迴圈。如果內層由 i + 1 運行至 n - 1,
執行次數是 ,即三角形求和;如果 index 每一輪
加倍,則由 得到 ;如果內層上界為 i,就必須
計算 ,不能寫成 乘一個固定數。因此真正需要分析的是迴圈
邊界描述的 iteration space,而不是原始碼中出現多少個 for。
例題
選擇排序的增長
void selectionSort(int a[], int n) {
for (int i = 0; i < n - 1; i++) {
int min = i;
for (int j = i + 1; j < n; j++) {
if (a[j] < a[min]) min = j;
}
if (min != i) {
int tmp = a[i];
a[i] = a[min];
a[min] = tmp;
}
}
}
比較次數恰好是 。把 換成 或 ,主導二次式的比值 分別趨近 與 。這解釋了課件中的計時趨勢;真正證明漸近類別的是 計數定理,而不是有限數據表。
例題
一個完整的化簡證明
取 ,則所有 都有
再由 得到下界,所以緊確結論是 。 雖然也正確,卻不夠精確。
例題
二分搜尋不需要完整掃描
int binarySearch(const int a[], int n, int target) {
int left = 0, right = n - 1;
while (left <= right) {
int mid = left + (right - left) / 2;
if (a[mid] == target) return mid;
if (a[mid] < target) left = mid + 1;
else right = mid - 1;
}
return -1;
}
前提是已排序、可隨機存取的陣列,而且 key comparison 為常數時間。第一次
probe 便命中時,best-case cost 為 ;worst case 中,每一輪至多
保留一半候選項,因此為 。這個 midpoint 寫法避免
left + right overflow。
搜尋失敗時仍有相同的對數 worst-case bound:候選區間不斷縮小,直至變成空。 若允許重複 key,這個版本可以回傳任何一個 matching position;尋找第一個或 最後一個 matching position 則需要修改 invariant。Binary search 也不會免費 把未排序搜尋變成對數成本:預先排序的成本只有在足夠多次後續查詢共同分擔時 才值得支付。Linked list 不能以常數時間定位 midpoint,因此 random-access assumption 是複雜度結論的一部分,而不是無關的 implementation detail。
例題
為甚麼合併式結構得到 n log n
若演算法不斷把問題對半分解,直至 subproblem 的規模為一,而且每一遞歸層的 總合併工作為 ,則共有 層,總成本為 。等價地,對二次冪規模, 有這個解;一般規模的 ceilings 與 floors 只會改變 常數。必須證明「每層線性工作」這個前提;divide-and-conquer 形式本身並不 保證此界。
逐層求和描述的是時間,不會自動給出空間界。若兩個 recursive call 依次完成, active call depth 為 ,而 merge 使用的臨時儲存仍可能達到 。改變 implementation 或改為 parallel execution,可能改變空間 輪廓,卻不一定改變計算 total work 的 recurrence,所以必須始終清楚指出正在 界定哪一種資源。
常見錯誤
常見錯誤
把 Big-O 當作緊確類別
同時屬於 與 。只憑兩個上界不能比較最終增長;應比較實際 函數的比值,或先證明 界。
常見錯誤
數迴圈關鍵字,而不數執行次數
兩個獨立、長度為 的巢狀迴圈產生 次主體執行;連續迴圈的成本相加, 三角形邊界需要求和,而每輪減半的迴圈只有對數輪。應先寫計數式,再作化簡。
常見錯誤
忽略前提、helper cost 或情形標籤
Binary search 要求已排序的 random-access input;average case 需要機率分佈; function call 貢獻完整成本。每個結論都應註明合法輸入、成本模型,以及 best、worst、expected 或 amortized case。
常見錯誤
把捨去低次項當作任意代數刪除
低次項仍然影響精確成本與有限規模。只有在固定系數、最終非負等條件下,並經 不等式或比值論證後,才可以使用主導項作漸近化簡。
常見錯誤
聲稱每次 dynamic-array push 都是常數時間
一次觸發 resize 的 push 可以是 。在幾何擴容下,指定操作序列中 每次 push 的攤還成本是 ;這不等於每一次 push 的 worst-case cost 都是常數。
總結
- 先聲明 、被計費的操作、資源及輸入情形;
- 、、 分別表示上界、下界與緊確界;
- 固定底數的對數只相差常數,固定正冪支配對數,固定底數的指數函數支配 多項式;
- 連續成本相加,獨立重複工作相乘,依賴外層 index 的邊界求和,遞歸工作則 寫 recurrence 或逐層計數;
- 選擇排序恰好比較 次,在上述模型中為 ;實測時間 只可說明而不能證明這個結論。
練習
思考檢查
若兩個互相獨立的巢狀迴圈都運行 n 次,總成本的緊確類別是甚麼?
假設迴圈主體一定執行,而且成本為 。
思考檢查
為甚麼 Theta(n log n) 成本在 n 足夠大時增長慢過 Theta(n^2)?
比較代表函數的比值。
思考檢查
0.0001n^3 + n 的緊確漸近類別是甚麼?
使用正首項多項式定理,而不只是說「刪去低次項」。
思考檢查
為甚麼選擇排序每輪只放置一個元素,成本仍然是二次?
計算逐輪縮短的內層比較次數。
答案與解答
解答 · 答案 1
迴圈主體執行 次,所以在給定獨立性與常數成本前提下,總成本 為 。
解答 · 答案 2
比值為 。配合正的緊確界,二次成本最終增長 得更快。
解答 · 答案 3
首項系數為正,其餘項次數較低,所以是 。它也屬於 ,但後者不是緊確類別。
解答 · 引導解答 4
各輪分別比較 次,總和為 ,而且不依賴輸入排列。每輪放置一個元素之前,仍須 掃描餘下 suffix。