動機
「程式 A 較快」並不是完整結論:我們必須說明輸入規模、計費操作、合法輸入,以及所討論的情形。課件以選擇排序作為動機;在某一部機器的測量中,陣列規模加倍時,時間約增至四倍。這可以顯示增長趨勢,卻不是證明,因為硬件、編譯器、記憶體行為,以及有限規模下的常數都會影響計時結果。
複雜度分析把問題轉化為數學問題。我們先定義成本函數 ,再計算基本操作次數,最後研究 增大時成本如何變化。結論必須依賴明確模型:固定長度 key 的比較可以視為常數時間,但任意長字串的比較則未必如此。本節要求先寫出精確計數或可驗證的界,再給出緊確的漸近類別,而不是憑迴圈層數猜測答案。
定義
定義 3.1.1
(輸入規模與 RAM 成本模型)
對輸入 ,令 為明確指定的規模參數, 為被計費的基本操作數。除非另有說明,本節採用 unit-cost RAM model:固定字長算術、索引運算、陣列存取、賦值,以及固定長度 key 的比較,均具有常數成本。 也必須標明所討論的情形;例如 worst-case cost 是在所有規模為 的合法輸入上取最大值。
時間與輔助空間是兩種不同資源。對運行時間的結論不會自動說明記憶體用量; helper function 的完整成本也必須計入,不能因為呼叫在原始碼中只佔一行便視為常數時間。
定義 3.1.2
(漸近上界、下界與緊確界)
設 最終非負,而且 最終為正。
- 若存在 ,使所有 都有 ,則 ;
- 若存在 ,使所有 都有 ,則 ;
- 若同時存在上下界,即 ,則 。
Big-O 只是最終上界,並不是增長率的唯一名稱。線性函數也屬於 ;若要表達與 相同的緊確漸近增長率,應使用 。
定義 3.1.3
(情形、期望成本與攤還成本)
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,對照所選偽代碼。這些是增長模型,不是實測時間或精確操作次數。
類 merge 的分層掃描 · O(n log n)
程式範例
for (int width = 1; width < n; width *= 2) {
for (int i = 0; i < n; i += 2 * width) {
merge_block(i, width);
}
}- 估計基本步數
- 64.00
- 如何理解
- 大約有 log n 輪,而每一輪仍然會處理線性數量的資料。
| 增長類別 | 數值 |
|---|---|
| O(1) | 1.00 |
| O(log n) | 4.00 |
| O(n) | 16.00 |
| O(n log n) | 64.00 |
| O(n^2) | 256.00 |
未定:增長模型決定實際運行時間
模型描述工作量的增長;常數、實作與硬件仍會影響實測時間。
要留意n=1 時,表中 log₂n 與 n log₂n 都為 0;對數與合併估值另用 max(1,log₂n),故為 1。兩者公式不同。滑桿選二的冪,數值框可輸入其間的整數。
負責任地比較增長類別
增長層級描述的是最終趨勢,並不是每一個有限輸入上的實測時間。常數很小的二次 implementation 可能在有限範圍內快過線性 implementation,cache 與 compiler 也會改變兩者的交叉點。漸近記號刻意忽略這些固定常數,以便獨立於某一部機器比較長期擴展性;實際工程判斷仍應結合漸近結論與目標輸入範圍內的測量。
當兩個成本都有正的緊確界時,比值可以給出精確比較。若 ,則 ,即 的階嚴格小於 ;若比值趨向有限正數,兩者屬於同一 類,但精確運行時間仍可不同;若比值無界增長,則 的階較大。這個方法比直接排列兩個 Big-O 歸屬更可靠,因為其中任何一個上界都可能並不緊確。
定理 / 命題
定理 3.1.4
(正首項多項式由最高次項控制)
設 ,其中 為非負整數,所有系數都是固定實數,,而且 最終非負,則 。
定理 3.1.5
(選擇排序的比較次數是三角形求和)
對 的隨機存取陣列,在每次 key comparison 為 的模型中,下文的 selection sort 對每一個輸入都恰好執行 次比較,因此時間為 。條件交換最多執行 次,不會改變緊確界。
證明思路或證明
證明
(最高次項定理的證明)
若 ,則每個 都有 ,所以 。以下設 。當 時,每個 ()都不超過 ,所以 ,得到所需的上界。另一方面, ,所以存在 threshold ,使低次項絕對值之和不超過 。於是 。同一個正函數給出上下常數倍界,所以 。固定系數、正首項與最終非負條件說明「刪去低次項」並不是可以任意套用的代數規則。
證明
(選擇排序比較次數的證明)
外層第 輪比較 次,第 輪比較 次,最後一輪比較一次。迴圈邊界不依賴 key 的排列,所以已排序、逆序與任意輸入的比較次數相同:
對 ,這個值介乎 與 之間,因此比較次數為 。常數時間的迴圈控制及至多線性次交換只增加 工作,而比較次數本身已給出 下界。
一般 comparison sorting 的 下界屬於比較 decision-tree model,並假設任意可排序輸入。它的證明,以及 quickselect、counting sort 和 radix sort 的參數化分析,都留待後續排序筆記;本節只釐清模型邊界,不會在缺少這些前提時套用該結論。
例題詳解
例題 3.1.6
(為甚麼 n^2 支配 n)
比較低次項與所提出的主導項:
這並不是說線性項從精確公式中消失,而是說它相對 愈來愈小;配合正首項,便可支持緊確的 結論。
例題 3.1.7
(常數時間語句)
int x = a + b;
int y = x * 2;
return y;
在固定字長 RAM model 中,每句只執行一次,連續語句的成本相加後仍是
。若 a、b 是 bit 長度隨輸入增長的 arbitrary-precision
integer,就必須另外計算算術操作的成本。
例題 3.1.8
(線性掃描)
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 便當作常數成本。
例題 3.1.9
(巢狀迴圈產生二次成本)
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。
例題 3.1.10
(選擇排序的增長)
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;
}
}
}
比較次數恰好是 。把 換成 或 ,主導二次式的比值分別趨近 與 。這解釋了課件中的計時趨勢;真正證明漸近類別的是計數定理,而不是有限數據表。
例題 3.1.11
(一個完整的化簡證明)
取 ,則所有 都有
再由 得到下界,所以緊確結論是 。 雖然也正確,卻不夠精確。
例題 3.1.12
(二分搜尋不需要完整掃描)
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。
例題 3.1.13
(為甚麼合併式結構得到 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。