Evanalysis
3.2預計閱讀時間: 18 分鐘

3.2 選擇、quickselect 與線性時間排序

區分排序與選擇,在嚴格限定模型的前提下證明比較排序下界,並分析 quickselect、counting sort 與 LSD radix sort 何時正確而高效。

課程目錄

選擇問題只要求一個指定排名的元素,排序卻要給出全部元素的完整次序。這個 區別使 quickselect 可以捨棄不會影響目標排名的工作。Counting sort 與 LSD radix sort 更進一步:它們利用 key 的額外結構跨過比較排序下界,但必須清楚 說明值域、位數與計算模型。

動機

假設一個 list 有數百萬個值,而我們只需要中位數。先完整排序當然能夠得到 答案,可是完整次序所含的資訊遠多於一個中位數。選擇演算法之前,先確認題目 實際要求甚麼 output,是重要的演算法設計習慣。

倉庫內的 tutorial 依次給出三層思路。完整排序是容易理解的基線;partial selection sort 只放好最前面的若干 order statistics;quickselect 重用 quicksort 的 partition,但只進入可能包含目標的一邊。其後,linear-time sorting 講義改變計算模型:有界整數可以直接計數,多位 key 則可以逐位穩定 排序。

本節處理的是靜態、單次查詢。若資料不更新而要查詢很多個排名,先排序一次 可能合理;若查詢之間還會頻繁插入和刪除,dynamic selection tutorial 指向 維護 subtree size 的高度平衡搜尋樹。它的對數保證依賴平衡性和 metadata 的 正確維護,所以完整實作應放在 tree notes;這裏只交代靜態選擇與 dynamic order statistics 的邊界。

定義

定義

選擇問題

給定含 nn 個 key 的 list AA,以及滿足 1≤k≤n1 \le k \le n 的整數 kk,返回 第 kk 小的 key。排名採用 1-based,偽代碼中的 array index 可以採用 0-based。重複值按出現次數分別計入:在 [2,2,5][2,2,5] 中,第 1 小和第 2 小都是 22。

k=1k=1 與 k=nk=n 分別是 minimum 與 maximum,只需一次掃描,所以 comparison model 下的 selection 並不存在一般性的 Ω(nlog⁡n)\Omega(n\log n) 下界。後文的下界 針對輸出完整次序的排序,而不是只找一個排名。

Partial selection sort 執行 selection sort 的前 kk 輪。第 ii 輪找出剩餘 suffix 的最小值,與位置 ii 交換,最後返回 A[k - 1];array 實作無需真的 刪除元素。比較次數恰好是

∑i=0k−1(n−i−1)=k(n−1)−k(k−1)2,\sum_{i=0}^{k-1}(n-i-1) =k(n-1)-\frac{k(k-1)}2,

所以是 O(kn)O(kn)。固定常數 kk 時為線性;當 k=Θ(n)k=\Theta(n) 時則為二次。

定義

Partition 契約

對長度為 mm 的目前 subarray,Partition 返回 index pp,把 pivot 放在 A[p],並保證 pp 左邊每個 key 都不大於 A[p],右邊每個 key 都不小於 A[p]。因此即使有重複值,pivot value 也是局部第 p+1p+1 小的一個合法值。 這個「pivot 最終 index」契約比只返回兩個區域邊界的 partition 更強。

若 kk 是相對於目前 subarray 的 1-based rank,正確分支結構是:

Quickselect(A, m, k):
    require 1 ≤ k ≤ m
    if m == 1: return A[0]
    p = Partition(A, m)          // p 是 pivot 最終的 0-based index
    if k - 1 == p: return A[p]
    if k - 1 is below p: return Quickselect(A[0:p], p, k)
    return Quickselect(A[p+1:m], m-p-1, k-p-1)

相等分支不可省略。進入右邊時,新 rank 要減去左邊的 pp 個位置和 pivot 本身。

每次 recursive call 開始時都保持這個 invariant:目前 subarray 含有原題所求的 order statistic,而參數 kk 是它在目前 subarray 內的 rank。左邊 call 沒有 丟棄任何更小位置,所以保留 kk;右邊 call 恰好丟棄 p+1p+1 個位置,所以使用 k−p−1k-p-1。Equality return 在 pivot 處停止,而且兩個 recursive branches 都 嚴格短於 parent,因此 base case 必會到達。若 partition 返回的是 Hoare-style boundary 而不是 pivot 最終 index,就需要另一套論證,不能原樣代入這段 pseudocode。

Randomized three-way quickselect

上面的 source teaching trace 使用 two-way final-index partition。若 input 可能有 duplicates,而我們要證明 expected-time guarantee,應另用 three-way variant:從目前 records 中 uniform random 選一個 pivot,再分成 strict-less block LL、equal block EE 與 strict-greater block GG。令 kk 是 1-based local rank,ℓ=∣L∣\ell=|L|、e=∣E∣e=|E|:

RandomizedQuickselect(A, m, k):
    pivot = key of a uniformly random record in A
    (L, E, G) = ThreeWayPartition(A, pivot)
    if k ≤ |L|:       return RandomizedQuickselect(L, |L|, k)
    if k ≤ |L| + |E|: return pivot
    return RandomizedQuickselect(G, |G|, k-|L|-|E|)

這裏保持同一個 recursive-call invariant,但 greater branch 丟棄的 prefix 大小 變成 ℓ+e\ell+e。每次 recursive call 都只進入 strict block,所以規模嚴格 縮小;若所有 keys 相等,EE 就是整個 input,algorithm 做一次 Θ(m)\Theta(m) partition 後直接 return。這個 three-way contract 也不同於 Hoare boundary,必須按自身語義實作和證明。

定義

穩定排序

若具有相同 key 的 records 在 output 中保持 input 的相對次序,sorting routine 就是 stable。當後續 pass 按 compound key 的另一部分排序,而又 必須保留較早 pass 建立的次序時,stability 是正確性條件。

定義

Counting sort

若整數 key 落在 inclusive range 0,1,…,K0,1,\ldots,K,標準 stable counting sort 需要 K+1K+1 個 counters。它先統計頻率,再求 prefix counts,最後從右到左掃描 input,把 records 放入獨立 output array。時間為 Θ(n+K)\Theta(n+K);若同時計算 output 與 count arrays,auxiliary space 為 Θ(n+K)\Theta(n+K)。這個標準 stable 版本不是 in-place。

定義

LSD radix sort

設每個非負 key 在 base bb 下有 dd 位。Least-significant-digit (LSD) radix sort 先對 digit 00 做 stable sort,再處理 digit 11,直至 digit d−1d-1。若每一輪用 counting sort 處理 bb 種 digit value,總時間為 Θ(d(n+b))\Theta(d(n+b)),auxiliary space 為 Θ(n+b)\Theta(n+b)。

對於 BB-bit machine words,若每個 digit 取 rr bits,則 d=⌈B/r⌉d=\lceil B/r\rceil、b=2rb=2^r,所以成本為 Θ(⌈B/r⌉(n+2r))\Theta(\lceil B/r\rceil(n+2^r))。較大的 digit 減少 passes,卻擴大 counter array;較小的 digit 使用更緊湊的 counters,卻要多次掃描 array。

所以「linear」是有條件的:K=O(n)K=O(n) 時 counting sort 才是 Θ(n)\Theta(n);若 representation 使 dd 為常數且 b=O(n)b=O(n),LSD radix sort 才是 Θ(n)\Theta(n)。負數、變長表示或巨大 key universe 都需要額外處理,不能直接 宣稱無條件線性。

定理 / 命題

定理

Quickselect 為何不需要完整排序

假設 Partition 滿足 pivot-final-index 契約。若返回 index pp,局部第 p+1p+1 小就是 pivot;更小的目標 rank 在 left subarray;更大的目標 rank 在 right subarray,並調整為 k−p−1k-p-1。因此每次 partition 後至多只需一次 recursive call。

定理

比較排序的 decision-tree 下界

對於由 nn 個互異 key 組成的任意 input,任何只透過 key comparison 得知 相對次序的 deterministic sorting algorithm,都存在需要 Ω(nlog⁡n)\Omega(n\log n) 次 comparisons 的 worst-case execution。若有界整數可以 用作 array index,這個模型假設便不再適用。

定理

LSD radix sort 的正確性

完成 digit 0,1,…,j0,1,\ldots,j 的 stable passes 後,array 按照由這 j+1j+1 個 低位組成的 base-bb suffix 排好。處理全部 dd 位後,這個 suffix 就是完整 key,所以 array 完全有序。

證明思路

Quickselect 的分支正確性與成本

Partition 後,pivot 位於它在完整 sorted current subarray 中可以佔據的最終 位置;左邊所有 key 不大於它,右邊所有 key 不小於它。因此 target index 小於 pp 時毋須右邊,大於 pp 時毋須左邊,相等時立即 return。兩邊內部毋須已經 有序。

每次 partition 在長度 mm 的 subproblem 上花 Θ(m)\Theta(m)。若 pivot 每次都 極端,size 依次為 n,n−1,…,1n,n-1,\ldots,1,所以 T(n)=T(n−1)+Θ(n)=Θ(n2)T(n)=T(n-1)+\Theta(n)=\Theta(n^2)。若每次恰好縮半, n+n/2+n/4+⋯n+n/2+n/4+\cdots 的 geometric sum 只是線性成本的直覺,不能單獨當作 average-case proof。

對 randomized three-way variant,明確假設每次從目前 subarray 中 uniform、independent 地選擇一個 record,並且 comparison 與 swap 是 constant time。把相等 occurrences 任意賦予 sorted order 中連續的 ranks。 所選 record 的 rank 落在中間一半的概率至少為 1/21/2;一旦發生,它的 strict-less 與 strict-greater blocks 都至多為 3m/43m/4。目標要麼在 equal block 直接 return,要麼只在其中一個 strict block 繼續。得到一次這種 constant-factor shrink 所需嘗試次數期望至多為 2,每個 size scale 的 expected work 是 O(m)O(m)。對逐層縮小的 scales 求和得到 expected O(n)O(n); 第一次 partition 又給出 Ω(n)\Omega(n),所以 expected time 為 Θ(n)\Theta(n)。 All-equal input 只需一次 linear pass;若 keys 互異而 pivots 每次都極端, worst case 仍為 Θ(n2)\Theta(n^2)。

Decision-tree 下界證明

nn 個互異 arbitrary keys 有 n!n! 種相對次序。Deterministic comparison sort 可以表示成 binary decision tree:internal node 是一次 comparison,leaf 必須識別一種 input permutation,所以正確演算法至少需要 n!n! 個 leaves。 高度 hh 的 binary tree 最多有 2h2^h 個 leaves,因此 h≥log⁡2(n!)h\ge\log_2(n!)。令 q=⌊n/2⌋q=\lfloor n/2\rfloor。n!n! 中最大的 qq 個 factors 都至少為 ⌈n/2⌉\lceil n/2\rceil,所以

log⁡2(n!)≥⌊n2⌋log⁡2 ⁣(⌈n2⌉)=Ω(nlog⁡n).\log_2(n!)\ge \left\lfloor\frac n2\right\rfloor \log_2\!\left(\left\lceil\frac n2\right\rceil\right) =\Omega(n\log n).

所以至少一個 input 要走過這麼長的 path。Counting sort 與 radix sort 使用 digit extraction、arithmetic 和 direct addressing,並限制 key representation,因此不違反這個只針對 comparison model 的結論。

LSD radix invariant

對 pass 數作 induction。Digit 00 排完後,array 按最低位有序。假設已經按 digits 00 至 j−1j-1 組成的 suffix 有序;下一次 stable pass 按 digit jj 排序。Current digit 不同的 records 由本輪直接排好;current digit 相同的 records 保持上一輪相對次序,因此仍按較低位 suffix 排好。於是處理 digit jj 後,array 按更長 suffix 有序。直至 digit d−1d-1,suffix 等於完整 key。

例題詳解

例題

Partial selection 的成本

對 tutorial array [11,6,43,7,14,28,9,2,4,37,18,34,8][11,6,43,7,14,28,9,2,4,37,18,34,8] 找第 4 小,partial selection 依次把 2,4,6,72,4,6,7 放到前四個位置。n=13,k=4n=13,k=4 時,四次 suffix scan 共作 12+11+10+9=4212+11+10+9=42 次 comparisons,返回 A[3]=7。

完整 selection sort 要作 7878 次 comparisons;這裏節省工作是因為 kk 小。 若找 median,k=Θ(n)k=\Theta(n),partial selection 仍為 quadratic。

例題

Quickselect partition trace

在 [56,25,37,58,95,19,73,30][56,25,37,58,95,19,73,30] 中找第 2 小,並採用 tutorial 的 first-element pivot。

  1. 以 5656 partition,得到 [19,25,37,30,56,95,73,58][19,25,37,30,56,95,73,58]。Pivot index 為 44、rank 為 55,所以只保留 left subarray [19,25,37,30][19,25,37,30]。
  2. 以 1919 partition,pivot 局部 rank 為 11。目標在右邊,新 rank 是 2−1=12-1=1。
  3. 在 [25,37,30][25,37,30] 中,pivot 2525 的局部 rank 為 11;相等分支 return 2525。

被捨棄部分毋須 sort。這個 trace 亦說明 equality return 與 right-rank adjustment 缺一不可。

例題

Counting sort:從計數到 stable placement

令 A=[0,5,3,2,3,0,5,1,5,2]A=[0,5,3,2,3,0,5,1,5,2] 且 K=5K=5。Keys 00 至 55 的六個 counters 為

C=[2,1,2,2,0,3].C=[2,1,2,2,0,3].

Prefix accumulation 後得到

C′=[2,3,5,7,7,10],C'=[2,3,5,7,7,10],

其中 C′[i]C'[i] 是不大於 ii 的 key 數。從右到左掃描 AA,對 key xx 執行 B[--C'[x]] = record。例如最右的 22 先把 counter 55 減為 44,再放到 zero-based index 44。最終

B=[0,0,1,2,2,3,3,5,5,5].B=[0,0,1,2,2,3,3,5,5,5].

若相同 key 帶有不同 record identity,從右到左處理會讓 input 中較後的 record 佔據較後的 output position,從而保持相對次序。三階段成本分別是 Θ(n)\Theta(n)、Θ(K+1)\Theta(K+1)、Θ(n)\Theta(n),合計 Θ(n+K)\Theta(n+K)。

例題

Radix sort 為何需要 stability

對 [329,457,657,839,436,720,355][329,457,657,839,436,720,355] 執行 base-1010 LSD radix sort:

  1. stable ones-digit pass:[720,355,436,457,657,329,839][720,355,436,457,657,329,839];
  2. stable tens-digit pass:[720,329,436,839,355,457,657][720,329,436,839,355,457,657];
  3. stable hundreds-digit pass:[329,355,436,457,657,720,839][329,355,436,457,657,720,839]。

以 tens pass 為例,tens digit 同為 22 的 720720 與 329329 保持上一輪根據 ones digit 建立的次序。Unstable pass 可能把它們逆轉,破壞 induction hypothesis。

常見錯誤

  • 接受 k=0k=0 或 k>nk>n,或混淆 1-based rank 與 0-based index。
  • 把普通 partition boundary 當成 pivot 最終 index。
  • 省略 quickselect 的 equality return,或進入右邊時沒有減去 p+1p+1。
  • 說 quickselect 永遠 linear,沒有 uniform randomized-pivot model 就把 balanced shrink 當成 average-case proof,或在允許 duplicates 時漏掉 three-way equal block。
  • 寫 sorting「至少是 O(nlog⁡n)O(n\log n)」。下界要寫 Ω\Omega,並要說明 arbitrary distinct inputs 與 comparison-only assumptions。
  • 對 inclusive range 0,…,K0,\ldots,K 只分配 KK 個 counters,或沒有把全部 K+1K+1 個 counters 初始化為零。
  • 把標準 stable counting sort 說成 in-place,或在遞減 prefix position 時 從左到右掃描 input。
  • 對所有 radix-sort organization 作同一個 stability 聲明;本節定理專指 whole-array LSD passes。

總結

  • Selection 只返回一個 order statistic;sorting 返回全部 rank information。
  • Partial selection 成本為 O(kn)O(kn),只在 kk 小時有吸引力。
  • Source two-way final-index trace 讓 quickselect 只進入一邊;duplicate-safe uniform randomized three-way variant 會在 equal block 直接 return,expected time 為 Θ(n)\Theta(n)。All-equal input 是 Θ(n)\Theta(n),worst case 是 Θ(n2)\Theta(n^2)。
  • Arbitrary distinct keys 的 comparison sorting 由 decision-tree proof 得到 worst-case Ω(nlog⁡n)\Omega(n\log n) comparisons。
  • 對 keys 0,…,K0,\ldots,K,stable counting sort 使用 K+1K+1 個 counters,時間 Θ(n+K)\Theta(n+K),從右到左掃描以保證 stability;標準 output-array 版本不是 in-place。
  • 含 dd 個 base-bb digits 的 LSD radix sort 使用 stable digit passes,成本 Θ(d(n+b))\Theta(d(n+b));是否線性取決於 representation parameters。

練習

思考檢查

為甚麼 full sorting 比 selection 做更多工作?

比較題目要求的 output。

解答 · 答案

Selection 只要一個 ranked element;sorting 要知道所有元素的完整次序。

思考檢查

Counting sort 線性時間需要甚麼額外假設?

想 count array 的大小。

解答 · 答案

Key range 必須受控;常見寫法是 K = O(n)。

  1. 解釋 quickselect 為甚麼只 recurse 一邊,並包括 equality case。
  2. 各舉一個 partial selection sort 合理與不合理的情況。
  3. 解釋 LSD radix sort 為甚麼需要 stable digit subroutine。
  4. 當 n=20,k=5n=20,k=5 時,求 suffix-scan partial selection 的準確 comparison count。
  5. 從 prefix counts [2,3,5,7,7,10][2,3,5,7,7,10] 出發,說明最右的 key 22 放到哪裏, 其 counter 變成多少。
  6. 嚴格解釋 counting sort 與 radix sort 為甚麼沒有違反 comparison-sorting lower bound。

答案與解答

解答 · 引導解答
  1. Pivot 最終 index 已知後,目標要麼等於 pivot,要麼嚴格在左邊或右邊。 相等便 return;其餘情況只有相關一邊可能包含目標。
  2. 當 kk 是小常數,只需少量 minimum-selection rounds 時合理;找 median 等 k=Θ(n)k=\Theta(n) 的情況會變成 quadratic。
  3. Stability 保存 lower-digit passes 在 current-digit ties 中建立的次序; 否則後續 pass 會破壞較早的 digit 資訊。
  4. 19+18+17+16+15=8519+18+17+16+15=85 次 comparisons。
  5. Counter 從 55 減為 44,record 放在 zero-based output index 44。
  6. 下界假設 arbitrary distinct keys 且只透過 comparisons 學習次序。 Counting 與 radix sort 限制 key representation,並使用 arithmetic、 digit extraction 和 direct addressing。

本單元重點詞彙