cv.cm

編碼 · 演算法

基數排序法教學:原理、程式碼與時間複雜度

閱讀約 6 分鐘

簡答

基數排序法是一種非比較式排序演算法,一次處理一個位數(或字元)來排序整數或固定長度的字串。常見的由最低位數優先(LSD)版本,先依最後一位排序,再依下一位,一路到最高位,每一回合都使用計數排序之類的穩定排序。對基底為 b、共 d 位的鍵值,時間為 O(d · (n + b)),額外空間為 O(n + b)。由於每一回合都是穩定的,最終結果完全有序,而且是穩定排序。

開啟行排序

基數排序解決什麼問題

計數排序是線性的,但需要與整個鍵值範圍一樣大的計數陣列,這對 32 位元或 64 位元整數是不可能的。基數排序把每個鍵值拆成小的位數,每個位數執行一次計數排序,所以每一回合只需要 b 個項目的計數陣列(十進位位數為 10,位元組為 256)。

任何能拆成固定長度小符號序列的鍵值都適用:非負整數、寫成 YYYYMMDD 的日期、郵遞區號之類的固定長度代碼、IP 位址,以及固定寬度的字串。

範例:十進位的 LSD 基數排序

將 [170, 45, 75, 90, 802, 24, 2, 66] 排序。最大的數有 3 位,所以要進行 3 回合。每一回合都是依單一位數做穩定排序:位數相同的元素保持上一回合的順序。

回合排序依據結果
開始[170, 45, 75, 90, 802, 24, 2, 66]
1個位數[170, 90, 802, 2, 24, 45, 75, 66]
2十位數[802, 2, 24, 45, 66, 170, 75, 90]
3百位數[2, 24, 45, 66, 75, 90, 170, 802]

第 2 回合後,數字已依最後兩位排好序。在第 3 回合,2、24、45、66、75 和 90 的百位數都是 0,穩定性讓它們維持第 2 回合產生的順序。這就是每一回合都必須穩定的原因。

Python 實作

def counting_pass(nums, exp, base):
    """Stable counting sort of nums by the digit at exp."""
    count = [0] * base
    for x in nums:
        count[(x // exp) % base] += 1
    for d in range(1, base):
        count[d] += count[d - 1]
    out = [0] * len(nums)
    for x in reversed(nums):
        d = (x // exp) % base
        count[d] -= 1
        out[count[d]] = x
    return out

def radix_sort(nums, base=10):
    """LSD radix sort for non-negative integers."""
    if not nums:
        return []
    exp, biggest = 1, max(nums)
    while biggest // exp > 0:
        nums = counting_pass(nums, exp, base)
        exp *= base
    return nums

print(radix_sort([170, 45, 75, 90, 802, 24, 2, 66]))
# [2, 24, 45, 66, 75, 90, 170, 802]
print(radix_sort([3, 0, 1, 0], base=256))   # [0, 0, 1, 3]

使用基底 256,表示每一回合看一個位元組,所以 32 位元整數只需 4 回合。若要包含負數,可以把負數與非負數分開排序再接起來,或把所有鍵值減去最小值做偏移。對固定長度的字串,則以位置 i 的字元碼作為位數,並從最後一個位置往前處理。

時間與空間複雜度

指標數值原因
時間(任何情況)O(d · (n + b))d 回合,每回合對 n 個鍵值與 b 種位數值做計數排序
額外空間O(n + b)每回合一個輸出緩衝區與一個計數陣列
32 位元鍵值的回合數基底 256 時為 432 位元 ÷ 每位數 8 位元
比較次數0位數被當作陣列索引使用

當 d 是常數(固定寬度的鍵值),且 b 選在接近 n 或固定值(如 256)時,時間為 O(n)。對 n 個互不相同的鍵值,d 至少要有 log_b n,所以對任意大小的鍵值,基數排序並不會勝過 O(n log n);它之所以快,是因為每一步都是單純的陣列操作。

基數排序與其他排序法的比較

演算法最佳平均最差額外空間穩定
基數排序(LSD)O(d · (n + b))O(d · (n + b))O(d · (n + b))O(n + b)是
計數排序O(n + k)O(n + k)O(n + k)O(n + k)是
合併排序O(n log n)O(n log n)O(n log n)O(n)是
快速排序O(n log n)O(n log n)O(n²)O(log n)否
堆積排序O(n log n)O(n log n)O(n log n)O(1)否
Timsort(Python 的 sorted)O(n)O(n log n)O(n log n)O(n)是

表中 n 是鍵值個數,k 是計數排序的鍵值範圍,d 是位數,b 是基底。只要每一回合的位數排序都是穩定的,LSD 基數排序就是穩定的。它需要結構固定的鍵值,而合併排序、快速排序這類比較式排序可以處理任何排序依據,包括自訂的比較函式。

LSD 與 MSD,以及常見錯誤

LSD(最低位數優先)MSD(最高位數優先)
回合順序從最後一位到第一位從第一位到最後一位,對每個桶遞迴進行
最適合固定長度的整數與字串可變長度的字串,遇到唯一前綴可提早結束
實作簡單的迴圈遞迴;小的桶通常會改用插入排序
穩定是,搭配穩定的回合是,搭配穩定的分桶

何時使用基數排序

基數排序很適合處理大量固定寬度的整數或鍵值,例如 32 位元 ID、時間戳記、IPv4 位址或固定長度字串,尤其是在 GPU 與高效能函式庫中,它規律的記憶體存取很划算。它用於後綴陣列的建構,也用在某些資料庫與圖形運算工作中。對一般的 Python 串列,sorted() 在實務上幾乎總是更快,因為它跑的是最佳化過的 C 程式碼。面試中,被問到如何以線性時間排序時會提到基數排序,例如 Maximum Gap 這道題。

常見問題

基數排序是穩定的嗎?

當每一回合的位數排序都使用計數排序之類的穩定排序時,LSD 基數排序是穩定的。演算法實際上也依賴這份穩定性才能正確。

基數排序的時間複雜度是多少?

O(d · (n + b)),其中 d 是位數,n 是鍵值個數,b 是基底。對固定寬度的鍵值,d 與 b 都是常數,所以對 n 是線性的。

基數排序比快速排序快嗎?

對大量固定寬度的整數,可能會更快,因為它不做比較,而且以規律的模式存取記憶體。對一般資料、小型陣列或自訂排序依據,快速排序或內建的排序通常更好。

LSD 與 MSD 基數排序有什麼不同?

LSD 從最低位數排到最高位數,每個位數只需一次簡單的回合。MSD 從最高位數開始,遞迴地排序每個桶,適合可變長度的字串。

基數排序能排序字串嗎?

可以。把每個字元當作一個位數。LSD 可直接處理等長的字串;MSD 基數排序則可以處理不同長度的字串。

全部編碼教學