演算法視覺化:從入門到精通 10 大排序演算法

破曉L發表於2023-05-18

在本文中,我們將透過動圖視覺化加文字的形式,循序漸進全面介紹不同型別的演算法及其用途(包括原理、優缺點及使用場景)並提供 Python 和 JavaScript 兩種語言的示例程式碼。除此之外,每個演算法都會附有一些技術說明,比如使用大 \(O\) 符號來分析不同演算法的時間複雜度和空間複雜度等,也提到了一些多數人都很容易理解的一些高階概述。

篇幅很長,耐心讀完它,您會受益匪淺。

提要

  1. 什麼是排序演算法?

    • 排序演算法有什麼用?
    • 為什麼排序演算法如此重要?
    • 演算法的評價標準
    • 資料結構中不同型別的排序演算法

      • 基於比較的排序演算法
      • 基於非比較的排序演算法
      • 就地排序演算法
      • 穩定的排序演算法
      • 自適應排序演算法
  2. 您需要了解的 10 大排序演算法

    • 氣泡排序
    • 插入排序
    • 快速排序
    • 桶排序
    • 殼排序
    • 歸併排序
    • 選擇排序
    • 基數排序
    • 梳排序
    • 排序
  3. 比較所有排序演算法
  4. 什麼是最常見的排序演算法?

什麼是排序演算法?

從本質上講,排序演算法是一種計算機程式,它將資料組織成特定的順序,例如字母順序或數字順序,通常是升序或降序。

排序演算法有什麼用?

排序演算法主要用於以高效的方式重新排列大量資料,以便更容易地對其進行搜尋和操作。它們還用於提高搜尋和合並等其他演算法的效率,這些演算法的操作依賴於排序資料。

為什麼排序演算法如此重要?

排序演算法用於按特定順序組織資料,這使得搜尋、訪問和分析更加容易。在許多應用中,排序是資料處理流程的關鍵部分,排序演算法的效率對系統的整體效能產生重大影響。

  • 在資料庫中 排序用於按特定順序檢索記錄,例如按日期、字母順序或數字順序。這使使用者可以快速找到他們需要的資料,而無需手動搜尋大量未分類的資料。
  • 在搜尋引擎中 按相關性順序排列搜尋結果。透過以這種方式對結果進行排序,使用者可以快速找到他們想要的資訊,而不必篩選不相關的結果。
  • 在許多科學和工程應用中 研究人員可以進行資料分析和模擬,以深入瞭解複雜系統,並對未來行為做出更準確的預測。

演算法的評價標準

評估一個排序演算法的好壞,通常依照以下標準進行評價:

  • 時間複雜度 是指在電腦科學與工程領域完成一個演算法所需要的時間,是衡量一個演算法優劣的重要引數,使用大 \(O\) 符號表示。一般而言,好的效能是 \(O(n \log n)\) ,壞的效能是 \(O(n^2)\),對於一個排序理想的效能是 \(O(n)\),但平均而言不可能達到。
  • 空間複雜度 描述該演算法或程式執行所需要的儲存空間大小,和時間複雜度類似,空間複雜度通常也使用大 \(O\) 符號來漸進地表示,例如 \(O(n)\)、 \(O(2^n)\)、\(O(n \log n)\) 等,其中 \(n\) 用來表示輸入的長度,該值可以影響演算法的空間複雜度。
  • 遞迴 一些演算法要麼是遞迴的,要麼是非遞迴的,而其他演算法可能兩者都是(如歸併排序)。
  • 穩定性 穩定排序演算法會讓原本有相等鍵值的紀錄維持相對次序。也就是說,如果一個排序演算法是穩定的,當有兩個相等鍵值的紀錄 \(R\) 和 \( S \),且在原本的列表中 \( R \) 出現在 \( S \) 之前,在排序過的列表中 \( R \) 也會出現在 \( S \) 之前。
  • 它們是否是比較排序 比較排序僅透過使用比較運算子比較兩個元素來檢查資料。
  • 演算法是序列還是並行的
  • 自適應性 利用其輸入中的現有順序,則它屬於自適應排序系列

資料結構中不同型別的排序

有多種型別的排序可用,排序演算法的選擇取決於各種因素,例如資料集的大小、要排序的資料型別以及所需的時間和空間複雜度。

基於比較的排序演算法

比較資料集中的元素,並根據比較結果來確定兩個元素中哪個應該放在序列前面。基於比較的排序演算法包括:

基於非比較的排序演算法

這些不直接比較元素,而是使用資料集的其他屬性來確定它們的順序。基於非比較的排序演算法包括:

就地排序演算法

這些演算法對資料進行就地排序(In-place) ,這意味著他們不需要額外記憶體來儲存中間結果,這些演算法只需要少數幾個指標,所以它們的空間複雜度都是 \( O(log \ n) \)。就地排序演算法的示例包括:

穩定的排序演算法

穩定排序演算法(Stable sorting)會讓原本有相等鍵值的紀錄維持相對次序。也就是如果一個排序演算法是穩定的,當有兩個相等鍵值的紀錄 \( R \) 和 \( S \),且在原本的列表中 \( R \) 出現在 \( S \) 之前,在排序過的列表中 \( R \) 也將會是 \( S \) 之前。

這些保留了資料集中的相等元素的相對順序。穩定排序演算法的示例包括插入排序歸併排序Timsort 等。

自適應排序演算法

如果排序演算法利用其輸入中的現有順序,則它屬於自適應排序(Adaptive sorting)。 它受益於輸入序列中的預排序,通常透過修改現有的排序演算法來執行。自適應排序演算法的示例包括插入排序氣泡排序Timsort 等。

您需要知道的 10 大排序演算法

現在讓我們來看看在排序演算法中需要了解的十種常用的排序演算法。

氣泡排序

氣泡排序(Bubble sort)有時也稱為“下沉排序”,是一種簡單的排序演算法,該排序演算法是基於比較的演算法。它重複遍歷給定的資料列表,一次比較兩個元素,如果順序錯誤則交換它們,該演算法一直持續到它不交換任何項為止。

冒泡.gif

氣泡排序的歷史

氣泡排序的起源可以追溯到 20 世紀 50 年代後期,Donald Knuth 在其 1968 年的經典著作《計算機程式設計藝術》中對其進行了普及。

從那時起,它被廣泛用於各種應用,包括編譯器的排序演算法、資料庫中的元素排序,甚至撲克牌的排序。

冒泡的排序的優缺點

氣泡排序被認為是一種相對低效的排序演算法,因為它的平均複雜度和最壞情況複雜度都是 \( O(n^2) \), 這使得它的效率遠低於大多數其他排序演算法,例如快速排序或歸併排序等。

技術說明:複雜度 \( O(n^2) \) 意味著演算法完成所需的時間與輸入大小的平方成正比。這意味著更大的輸入會導致演算法花費更長的時間才能完成。

例如,考慮一種對數字陣列進行排序的演算法,對一個包含 10 個數字的陣列進行排序,可能需要 1 秒,但對包含 20 個數字的陣列進行排序,則可能就需要 4 秒。這是因為該演算法必須將陣列中的每個元素與其他所有元素進行比較,因此它必須對較大的陣列進行 20 次比較,而對較小的陣列只需 比較 10 次。

然而,它很容易理解和實現,並且經常被用作排序的入門以及更復雜演算法的構建塊,但如今它在實踐中很少被使用。

氣泡排序的用例

氣泡排序是一種簡單的的演算法,可用於對小型列表或元素陣列進行排序。它易於實現和理解,因此可以在簡單和清晰比效能更重要的情況下使用。

  • 教育目的 它經常被用在電腦科學課程中,作為簡單排序演算法的一個例子。學生們可以透過學習氣泡排序來了解基本的排序技術,並透過學習氣泡排序來了解演算法的工作原理。
  • 對小資料集進行排序 它可用來對最多幾百個元素的小型資料集進行排序。在效能不是關鍵問題的情況下,氣泡排序可以成為對小列表進行排序的一種快速而簡單的方法。
  • 預排序資料 它可以用作更復雜的排序演算法的一個初步步驟。例如,如果資料已經被部分排序,在執行更復雜的演算法之前,可以用氣泡排序來進一步排序資料。
  • 構建更復雜演算法的模組 它通常與歸併排序或快速排序結合使用,並使用插入排序對小型子陣列進行排序,因為這些其他演算法可以在更大的資料集上表現更好的效能。

氣泡排序的實現

  1. 使用巢狀迴圈來遍歷各個項。
  2. 比較列表中的相鄰的兩項。
  3. 如果順序不對,就進行交換。
  4. 直到列表被排序完成。
Python 中的氣泡排序
def bubble_sort(items):
    for i in range(len(items)):
        for j in range(len(items) - 1 - i):
            if items[j] > items[j+i]:
                items[j], items[j+1] = item[j+1], items[j]
    return items
items = [10, 14, 2, 9, 14, 37, 29]
print(bubble_sort(items))
JavaScript中的氣泡排序
function bubbleSort(items) {
    let swapped;
    do {
        swapped = false;
        for (let i = 0; i < items.length - 1; i++) {
            if(items[i] > items[i+1]) {
                [items[i], items[i+1]] = [items[i+1], items[i]]
                swapped = true;
            }
        }
    } while(swapped)
    return items;
}

items = [10, 14, 2, 9, 14, 37, 29]
console.log(bubbleSort(items))

插入排序

插入排序(Insertion Sort)是一種簡單直觀的排序演算法。它的工作原理是透過構建有序序列,對於未排序資料,在已排序序列中從後向前掃描,找到相應位置並插入。

插入排序在實現上,通常採用就地排序(即只需用到 \( O(1) \) 的額外空間的排序),因而在從後向前掃描過程中,需要反覆把已排序元素逐步向後挪位,為最新元素提供插入空間。

insertion-sort.gif

插入排序的歷史

在《計算機程式設計的藝術》中,Knuth 評論說插入排序“早在 1946 年約翰·莫奇利 (John Mauchly) 在首次發表的計算機排序討論中就提到了”,並將其描述為一種“自然”演算法,可以很容易地理解和實現。

到 1950 年代後期,Donald L. Shell 對他的 Shell 排序演算法(見下文)進行了一系列改進,該方法將元素之間的距離進行比較,每次透過時距離都會減小,從而將演算法的複雜度降低到 \( O(n^{3/2}) \) 和 \( O(n^{4/3}) \) 兩個不同的變體中。這聽起來可能不多,但對於實際應用來說這是一個相當顯著的改進!

技術說明:\( O(n^{3/2}) \) 和 \( O(n^{4/3}) \) 複雜度比 \( O(n^2) \) 複雜性更有效率,這意味著它們需要更少的時間來完成。這是因為他們不需要執行與 \( O(n^2) \) 複雜度一樣多的比較。

例如,使用 \( O(n^2) \) 演算法對包含 10 個數字的陣列進行排序可能需要 1 秒,但使用 \( O(n^{3/2}) \) 演算法對同一個陣列進行排序可能只需要 0.5 秒。這是因為在使用該演算法時可以執行更少的比較,從而導致更快的執行時間。

2006 年,Bender、Martin Farach-Colton和 Mosteiro 釋出了插入排序的一種新變體,稱為庫排序或“間隙插入排序(gapped insertion sort)”,它在整個陣列中留下少量未使用的空間(或“間隙”),進一步改進了執行時間到 \( O(n \log n) \)。

技術說明:\( O(n \log n) \) 複雜度比 \( O(n^2) \) 以及 \( O(n^{3/2}) \) 和 \( O(n^{4/3}) \) 複雜度更有效率。這是因為它採用了分而治之的方法,這意味著它可以將問題分解成更小的部分並更快地解決它們。

例如,使用一種 \( O(n^2) \) 演算法對包含 10 個數字的陣列進行排序可能需要 1 秒,使用一種 \( O(n^{3/2}) \) 演算法對同一個陣列進行排序需要 0.5 秒,但使用一種 \( O(n \log n) \) 演算法對同一個陣列進行排序可能僅需要 0.1 秒。這是因為該演算法可以將陣列分解成更小的部分並並行求解它們,從而加快執行時間。

插入排序的優缺點

插入排序通常在實踐中用於小資料集或作為更復雜演算法的構建塊。

就像氣泡排序一樣,它的最壞情況和平均情況時間複雜度是 \( O(n^2) \) 。但與氣泡排序不同的是,
插入排序可用於對資料集進行就地排序,這意味著它不需要額外的記憶體來儲存中間結果。

插入排序的用例

插入排序簡單高效,常用於輸入資料已經排序或輸入資料規模較小的情況。它還用於對小型資料集進行排序和構建更復雜演算法的塊,就像氣泡排序一樣。

  • 部分排序的資料 它非常適合資料已經部分排序的情況。在這種情況下,演算法可以快速地將新元素插入到正確的位置,而不需要複雜的排序操作。
  • 線上分揀 它通常用於輸入資料事先未知的線上排序應用程式中。在這些情況下,演算法可以在收到輸入資料時對其進行增量排序。
  • 自適應排序 插入排序是自適應排序的候選者,因為它可以利用輸入資料中的現有順序。隨著輸入資料變得更加有序,演算法的效能也會提高。

插入排序的實現

  1. 取一個未排序的列表,選擇第一個項作為 "樞軸(pivot)"。
  2. 遍歷列表,將樞軸插入到排序後列表中的正確位置。
  3. 對列表中的下一個項重複這一過程。
  4. 繼續下去,直到列表排序完畢。
Python 中的插入排序
def insertion_sort(items):
    for i in range(1, len(items)):
        j = i
        while j > 0 and items[j-1] > items[j]:
            items[j-1], items[j] = items[j], items[j-1]
            j -= 1
    return items

items = [6,20,8,19,56,23,87,41,49,53]
print(insertion_sort(items))
JavaScript 中的插入排序
function insertionSort(items) {
    for(let i = 1; i < items.length; i++) {
        let j = i;
        // 從後向前掃描
        while(j > 0 && items[j-1] > items[j]) {
            [items[j-1], items[j]] = [items[j], items[j-1]];
            j--;
        }
    }
    return items;
}

let items = [6, 20, 8, 19, 56, 23, 87, 41, 49, 53];
console.log(insertionSort(items));

快速排序

快速排序(Quicksort)是一種流行的分而治之排序演算法,其基於將陣列劃分為兩個子陣列的原則——一個包含小於“樞軸”元素的元素,另一個包含大於“樞軸”元素的元素,然後遞迴地對這兩個子陣列進行排序。

快速排序的基本步驟包括:

  1. 從陣列中選擇一個“樞軸”元素。
  2. 將陣列分成兩個子陣列,一個包含小於樞軸的元素,另一個包含大於樞軸的元素。
  3. 使用快速排序遞迴地對兩個子陣列進行排序。
  4. 合併兩個排序的子陣列。

quicksort.gif

快排的歷史

快速排序由 Tony Hoare 於 1959 年發明,Hoare 在英國的 Elliott Brothers 計算機公司)工作時開發了該演算法,作為對 Ferranti Mark I 計算機記憶體中的單詞進行排序的一種方法。

快速排序最初於 1961 年作為研究論文發表,由於其簡單、高效和易於實現,它很快成為使用最廣泛的排序演算法之一。

快排的優點

  • 它的平均用例時間複雜度為 \( O(n \log n) \)。
  • 它需要很少的額外記憶體,因為它對陣列進行就地排序。
  • 它易於實施並且被廣泛理解。
  • 它很容易地並行化。

快排的缺點

它的最壞情況時間複雜度是 \( O(n^2) \) ,當樞軸選擇不當時,使其在某些情況下比其他演算法(如歸併排序或堆排序)效率低。

技術說明:我們不想選擇太小或太大的樞軸,否則演算法將以二次方時間執行。理想的情況是選擇中位數作為基準,但這並不總是可行,除非我們事先了解資料分佈。

快速排序的用例

快速排序作為一種高效的排序演算法,有著廣泛的應用。

  • 大資料集 它的平均情況時間複雜度為 \( O(n \log n) \),這意味著它可以快速對大量資料進行排序。
  • 隨機資料 它在隨機排序的資料上表現良好,因為它依賴於樞軸元素將資料分成兩個子陣列,然後遞迴排序。當資料是隨機的時,樞軸元素很可能接近中位數,這會導致良好的效能。
  • 並行處理 它可以很容易地並行化,這使得它非常適合在多核處理器上對大型資料集進行排序。透過將資料分成更小的子陣列,該演算法可以同時在多個核心上執行,從而提高效能。
  • 外排序 它通常用作外排序演算法的一部分,用於對太大而無法放入記憶體的資料進行排序。在這種情況下,資料被分類為塊,然後使用歸併排序演算法將其合併。
  • 資料壓縮 它被用在一些資料壓縮演算法中,例如 bzip2壓縮軟體 中使用的 Burrows-Wheeler變換。該演算法用於對 Burrows-Wheeler 矩陣中的資料進行排序,然後對其進行轉換以生成壓縮資料。

快速排序的實現

  1. 選取“樞軸”點,最好是中位數,將列表分為兩部分。
  2. 快速對左側部分和右側部分進行排序。
  3. 繼續直到列表排序完畢。
Python 中的快速排序
def quick_sort(items):
    if len(items) > 1:
        privot = items[0]
        left = [i for i in items[1:] if i < privot]
        right = [i for i in items[1:] if i >= pivot]
        return quick_sort(left) + [pivot] + quick_sort(right)
    else:
        return items
        
items = [6,20,8,19,56,23,87,41,49,53]
print(quick_sort(items))
JavaScript 中的快速排序
function quickSort(items) {
    if(items.length > 1) {
        // 以第一項作為樞軸
        const pivot = items[0];
        let left = [], right = [];
        for(let i = 1; i < items.length; i++) {
            if(items[i] < pivot) {
                left.push(items[i]);
            } else {
                right.push(items[i]);
            }
        }
        return [...quickSort(left), pivot, ...quickSort(right)];
    } else {
        return items;
    }
}

const items = [6, 20, 8, 19, 56, 23, 87, 41, 49, 53];
console.log(quickSort(items));

桶排序

桶排序(Bucket sort)或稱為箱排序,是一種用於對均勻分佈的資料進行排序的有用演算法,它可以很容易地並行化以提高效能。

其工作的原理是將陣列分到有限數量的桶裡。每個桶再個別排序(有可能再使用別的排序演算法或是以遞迴方式繼續使用桶排序進行排序)。桶排序是鴿巢排序的一種歸納結果。當要被排序的陣列內的數值是均勻分配的時候,桶排序使用線性時間 \( O(n) \)。但桶排序並不是比較排序,他不受 \( O(n \log n) \) 下限的影響。

桶排序的基本步驟包括:

  1. 設定一個定量的陣列當作空桶子。
  2. 遍歷列表,並且把項一個一個放到對應的桶子去。
  3. 對每個不是空的桶子進行排序。
  4. 從不是空的桶子裡把項再放回原來的列表中。

bucket.gif

桶排序的歷史

桶排序的實現早在 1950 年代就已經存在,有訊息稱該方法自 1940 年代就已存在,它現在仍在廣泛使用。

桶排序的優點

  • 它對於均勻分佈的資料是非常有效率的,平均時間複雜度為 \( O(n+k) \),其中 \( n \) 是元素的數量,\( k \) 是桶的數量。
  • 它很容易的並行化,從而可以利用現代處理器中的多個核心。
  • 它是穩定的,這意味著它保留了原始陣列中的相等元素的相對順序。
  • 透過調整程式,它可以用於具有非均勻分佈的資料。
*技術說明:\( O(n+k) \) 的複雜性比 \( O(n^2) \) 、\( O(n^{3/2}) \) 和 \( O(n^{4/3}) \) 、\( O(n \log n) \)  複雜度更有效率。這是因為它只需要執行線性數量的操作,而不用考慮輸入的大小。
例如,考慮一種對數字陣列進行排序的演算法。使用 \( O(n^2) \) 演算法對一個由 10 個數字組成的陣列進行排序可能需要 1 秒 ,使用 \( O(n^{3/2}) \) 演算法對同一陣列進行排序可能需要 0.5 秒,使用 \( O(n \log n) \) 演算法對同一陣列進行排序可能需要 0.1 秒,但使用 \( O(n+k) \) 演算法對同一陣列進行排序僅需要 0.05 秒,這是因為該演算法不需要執行那麼多比較。

桶排序的缺點

對於非均勻分佈的資料,桶排序的效率低於其他排序演算法,最壞情況下的效能為 \( O(n^2) \) 。此外,它需要額外的記憶體來儲存桶,這對於非常大的資料集來說可能是個問題。

桶排序的用例

就像快速排序一樣,桶排序可以很容易地並行化並用於外排序,但是桶排序在處理均勻分佈的資料時特別有用。

  • 對浮點數進行排序 在這種情況下,劃分為固定數量的桶區間,每個桶代表輸入資料的一個子區間。然後將這些數字放入相應的桶中,並使用另一種演算法(例如插入排序)進行排序。最後,排序後的資料被連線成一個陣列。
  • 對字串進行排序 根據字串的第一個字母分組到桶中。然後使用另一種演算法對每個桶中的字元進行排序,或遞迴的使用桶排序。對字串中的每個後續字母重複此過程,直到整個集合排序完畢。
  • 直方圖生成 這可用於生成資料的直方圖,用於表示一組值的頻率分佈。在這種情況下,將資料範圍劃分為固定數量的桶,並統計每個桶中值的數量。生成的直方圖可用於視覺化資料的分佈。

桶排序的實現

  1. 將項的列表拆分為一定數量的“桶”。
  2. 每個桶使用不同排序演算法進行排序。
  3. 然後將這些桶合併回一個排序列表中。
Python 中的桶排序
def bucket_sort(items):
    buckets = [[] for _ in range(let(items))]
    for item in items:
        bucket = int(item/len(items))
        buckets[bucket].append(item)
    for bucket in buckets:
        bucket.sort()
    return [item for bucket in buckets for item in bucket]

items = [6,20,8,19,56,23,87,41,49,53]
print(bucket_sort(items))
JavaScript 中的桶排序
function bucketSort(items) {
    // 分成 items.length 個桶
    const buckets = new Array(items.length);
    for(let i = 0; i < buckets.length; i++) {
        buckets[i] = [] 
    }
    // 遍歷列表,根據條件往每個桶裡放對應的資料
    for(let j = 0; j < items.length; j++) {
        let bucket = Math.floor(items[j] / items.length)
        buckets[bucket].push(items[j]) 
    }

    // 對每個桶裡的資料進行排序
    for(let k = 0; k < buckets.length; k++) {
        buckets[k].sort(); 
    }
    // 將每個桶組合到一個列表中
    return [].concat(...buckets) 
 }

let items = [6, 20, 8, 19, 56, 23, 87, 41, 49, 53];
console.log(bucketSort(items));

Shell 排序

Shell 排序也稱遞減增量排序演算法,它不是一次對整個列表進行排序,而是將列表分成更小的子列表。然後使用插入排序演算法對這些子列表進行排序,從而減少排序列表所需的交換次數。Shell 排序是插入排序的一種更高效的改進版本,是非穩定排序演算法。

它也被稱為 "Shell 方法",其工作原理是,首先定義一個稱為增量序列的整數序列,增量序列用於確定將獨立排序的子列表大小,最常用的增量序列是 “Knuth 序列”,其定義如下(其中 \( h \) 是具有初始值的區間,\( n \) 是列表的長度)。

h = 1
while h < n:
  h = 3*h + 1

一旦定義了增量序列,Shell 排序演算法就會使用插入排序演算法對子列表進行排序,以增量序列作為步長,從最大增量開始,然後向下迭代到最小增量。

當增量大小為 1 時演算法停止,此時它等同於常規插入排序演算法。

shell1.gif

Shell 排序的歷史

Shell 排序是 Donald Shell 於 1959 年發明的,作為插入排序的變體,旨在透過將原始列表分解為更小的子列表並對這些子列表進行獨立排序來提高效能。

Shell 排序的優點

  • 它是插入排序的改進版本,因此易於理解和實現。
  • 對於許多輸入資料序列,它的時間複雜性優於 \( O(n^2) \)。
  • 這是一種就地排序演算法,這意味著它不需要額外的記憶體。

Shell 排序的缺點

很難預測 Shell 排序的時間複雜度,因為它取決於增量序列的選擇。

Shell 排序的用例

Shell 排序是一種通用演算法,用於在各種應用程式中對資料進行排序,尤其是在對大型資料集進行排序時,例如快速排序和桶排序等。

  • 對大部分已排序的資料進行排序 Shell 排序減少了資料排序所需的比較和交換次數,在這種特定情況下,這使得它比其他排序演算法(例如快速排序或並歸排序)更快。
  • 使用少量反轉對陣列進行排序 反轉是衡量一個陣列未被排序的程度,它被定義為順序錯誤的元素對的數量。在對具有少量反轉的陣列進行排序時,Shell 排序比其他一些演算法(如氣泡排序或插入排序)更有效。
  • 就地排序 Shell 排序不需要額外的記憶體來對輸入進行排序,這使得它在記憶體有限或不需要額外記憶體使用的情況下非常有用。
  • 在分散式環境中排序 透過將輸入資料分成更小的子列表並獨立排序,每個子列表可以在單獨的處理器或節點上排序,從而減少排序資料所需的時間。

Shell 排序的實現

  1. 將 \( n \) 個元素的陣列劃分為 \( n/2 \) 個子序列。
  2.  第一個資料和第 \( n/2 \) + \( 1 \) 個資料進行比較,第 \( 2 \) 個資料和第 \( n/2 \) + \( 2 \) 個資料進行比較,依次類推,完成第一輪排序。
  3. 然後,它變成 \( n/4 \) 個序列,並再次進行排序。
  4. 不斷重複上述過程,隨著順序的減少,最終變為一個,整個排序完成。
Python 中的 Shell 排序實現
def shell_sort(items):
    sublistcount = len(items) // 2
    while sublistcount > 0:
        for start in range(sublistcount):
            gap_insertion_sort(items, start, sublistcount)
        sublistcount = sublistcount // 2
    return items

def gap_insertion_sort(items, start, gap):
    for i in range(start+gap, len(items), gap):
        currentvalue = items[i]
        position = i
        while position >= gap and items[position-gap] > currentvalue:
            items[position] = items[position-gap]
            position = position-gap
        items[position] = currentvalue

items = [6,20,8,19,56,23,87,41,49,53]
print(shell_sort(items))
JavaScript 中的 Shell 排序
function shellSort(items) {
    let sublistcount = Math.floor(items.length / 2);
    while(sublistcount > 0) {
        for(let start = 0; start < sublistcount; start++) {
            gapInsertionSort(items, start, sublistcount);
        }
        sublistcount = Math.floor(sublistcount / 2);
    }
    return items;
}

function gapInsertionSort(items, start, gap) {
    for(let i = start + gap; i < items.length; i += gap) {
        let currentValue = items[i];
        let position = i;
        while(position >= gap && items[position - gap] > currentValue) {
            items[position] = items[position - gap];
            position = position - gap;
        }
        items[position] = currentValue;
    }
}


let items = [6, 20, 8, 19, 56, 23, 87, 41, 49, 53];
console.log(shellSort(items));

歸併排序

歸併排序(Merge sort)的基本思想是將輸入列表一分為二,使用歸併排序遞迴地對每一半進行排序,然後將排序後的兩半合併在一起。合併步驟是透過重複比較每一半的第一個元素並將兩者中較小的一個新增到排序列表中來執行的,重複此過程,直到所有元素都被重新合併在一起。

merge2.gif

歸併排序的歷史

歸併排序由 John von Neumann 於 1945 年發明,作為一種基於比較的排序演算法,其工作原理是將輸入列表劃分為更小的子列表,遞迴地對這些子列表進行排序,然後將它們合併回一起以生成最終的排序列表.

歸併排序的優點

歸併排序在最壞情況下的時間複雜度為 \( O(n \log n) \) ,這使得它比氣泡排序、插入排序或選擇排序等其他流行的排序演算法更高效。

歸併排序也是一種穩定排序演算法,這意味著它保留了相等元素的相對順序。

歸併排序的缺點

歸併排序在記憶體使用方面有一些缺點,該演算法在劃分步驟中需要額外的記憶體來儲存列表的兩半,以及在合併過程中需要額外的記憶體來儲存最終排序的列表。在對非常大的列表進行排序時,這可能是一個問題。

歸併排序的用例

歸併排序是一種通用排序演算法,可以並行化以對大型資料集進行排序和外排序(類似快速排序和桶排序),它也常用作更復雜演算法(如氣泡排序和插入排序)的構建塊。

  • 穩定排序 歸併排序的穩定排序意味著它保留了相等元素的相對順序。這使得它在維護相等元素的順序很重要的情況下非常有用,例如在金融應用程式中或出現於視覺化目的對資料進行排序時。
  • 實現二進位制搜尋 它用於有效地搜尋排序列表中的特定元素,因為它依賴於排序的輸入。歸併排序可用於有效地對二分搜尋和其他類似演算法的輸入進行排序。

歸併排序的實現

  1. 使用遞迴將列表拆分為較小的排序子列表。
  2. 將子列表重新合併在一起,在合併時對專案進行比較和排序。
Python 中的歸併排序實現
def merge_sort(items):
    if len(items) <= 1:
        return items

    mid = len(items) // 2
    left = items[:mid]
    right = items[mid:]

    left = merge_sort(left)
    right = merge_sort(right)

    return merge(left, right)

def merge(left, right):
    merged = []
    left_index = 0
    right_index = 0

    while left_index < len(left) and right_index < len(right):
        if left[left_index] > right[right_index]:
            merged.append(right[right_index])
            right_index += 1
        else:
            merged.append(left[left_index])
            left_index += 1

    merged += left[left_index:]
    merged += right[right_index:]

    return merged

items = [6,20,8,19,56,23,87,41,49,53]
print(merge_sort(items))
JavaScript 中的歸併排序
function mergeSort(items) {
    if(items.length <= 1) return items
    const mid = Math.floor(items.length / 2)
    const left = items.slice(0, mid)
    const right = items.slice(mid)
    return merge(mergeSort(left), mergeSort(right))
}

function merge(left, right) {
    const merged = []
    let leftIndex = 0
    let rightIndex = 0
    while(leftIndex < left.length && rightIndex < right.length) {
        if(left[leftIndex] > right[rightIndex]) {
            merged.push(right[rightIndex])
            rightIndex++
        } else {
            merged.push(left[leftIndex])
            leftIndex++
        }
    } 
    return merged.concat(left.slice(leftIndex)).concat(right.slice(rightIndex))
}

let items = [6, 20, 8, 19, 56, 23, 87, 41, 49, 53]
console.log(mergeSort(items))

選擇排序

選擇排序(Selection sort)從列表的未排序部分中,重複選擇最小的元素,並將其與未排序部分的第一個元素交換,這個過程一直持續到整個列表排序完成。

選擇排序.gif

紅色是當前最小值,黃色是排序列表,藍色是當前專案。

選擇排序的歷史

選擇排序是一種簡單直觀的排序演算法,自電腦科學早期就已存在。類似的演算法很可能是由研究人員在 1950 年代獨立開發的。

它是最早開發的排序演算法之一,並且仍然是用於教育目的和簡單排序任務的流行演算法。

選擇排序的優點

選擇排序用於某些應用程式中,在這些應用程式中,簡單性和易用性比效率更重要。它也可用作向學生介紹排序演算法及其屬性的教學工具,因為它易於理解和實現。

選擇排序的缺點

儘管選擇排序很簡單,但與歸併排序或快速排序等其他排序演算法相比,它的效率不是很高。它在最壞情況下的時間複雜度為 \( O(n^2) \),並且對大型列表進行排序可能需要很長時間。

選擇排序也不是穩定的排序演算法,這意味著它可能無法保留相等元素的順序。

選擇排序的用例

選擇排序與氣泡排序和插入排序類似,可用於小型資料集排序,其簡單性也使其成為排序演算法教學和學習的有用工具。其他用途包括:

  • 對記憶體有限的資料進行排序 它只需要恆定數量的額外記憶體來執行排序,這使得它在記憶體使用受限的情況下很有用。
  • 對具有唯一值的資料進行排序 它不依賴於大部分排序的輸入,這使其成為具有唯一值的資料集的不錯選擇,而其他排序演算法可能必須執行額外的檢查或最佳化。

選擇排序的實現

  1. 遍歷列表,選擇最小的項。
  2. 將最小的項與當前位置的項進行交換。
  3. 對列表的其餘部分重複上述過程。
在 Python 中的選擇排序
def selection_sort(items):
    for i in range(len(items)):
        min_idx = i
        for j in range(i+1, len(items)):
            if items[min_idx] > items[j]:
                min_idx = j
        items[i], items[min_idx] = items[min_idx], items[i]
    return items

items = [6,20,8,19,56,23,87,41,49,53]
print(selection_sort(items))
在 JavaScript 中的選擇排序
function selectionSort(items) {
    let minIndex;
    for(let i = 0; i < items.length; i++) {
         minIndex = i;
         for(let j = i + 1; j < items.length; j++) {
             if(items[j] < items[minIndex]) {
                 minIndex = j;
             }
         }
         let temp = items[i];
         items[i] = items[minIndex];
         items[minIndex] = temp;
    }
    return items
}

let items = [6, 20, 8, 19, 56, 23, 87, 41, 49, 53];
console.log(selectionSort(items));

基數排序

基數排序(Radix sort)是一種非比較型整數排序演算法,其原理是將整數按位數切割成不同的數字,然後按每個位數分別比較。由於整數也可以表達字串(比如名字或日期)和特定格式的浮點數,所以基數排序也不是隻能應用於整數。

其最壞情況下的效能為 \( {O(w\cdot n)} \),其中 \( n \) 是金鑰數量,\( w \) 是金鑰長度。

基數排序.gif

基數排序的歷史

基數排序由 Herman Hollerith 在 19 世紀末首次引入,作為一種對穿孔卡片上的資料進行高效排序的方法,其中每一列代表資料中的一個數字。

後來,它被 20 世紀中期的幾位研究人員改編並推廣,用於對二進位制資料進行排序,按二進位制表示中的每一個位元對資料進行分組。但它也被用來對字串資料進行排序,在排序中每個字元都被視為一個數字。

近年來,基數排序作為並行和分散式計算環境中的一種排序演算法再次受到關注,因為它很容易並行化,並且可以用來以分散式方式對大資料集進行排序。

radix-sort.png

IBM 的一種卡片分類器,對一大組穿孔卡片進行基數排序。圖片來源:Wikimedia Commons, Public Domain.

基數排序的優點

基數排序是一種線性時間排序演算法,這意味著它的時間複雜度與輸入資料的大小成正比。這使它成為對大型資料集進行排序的有效演算法,儘管它可能不如其他對較小資料集的排序演算法有效。

它的線性時間複雜性和穩定性使其成為對大型資料集進行排序的有用工具,它的可並行性使其對分散式計算環境中的資料排序非常有用。

基數排序也是一種穩定的排序演算法,這意味著它保留了相等元素的相對順序。

基數排序的用例

基數排序可用於需要對大型資料集進行高效排序的各種應用程式。它對字串資料和定長鍵的排序特別有用,也可用於並行和分散式計算環境中。

  • 並行處理 基數排序通常是對大資料集進行排序的首選(優於歸併排序、快速排序和桶排序)。而且和桶排序一樣,基數排序可以有效地對字串資料進行排序,這使得它適合於自然語言處理應用。
  • 對有固定長度鍵的資料進行排序 當對有固定長度鍵的資料進行排序時,基數排序特別有效,因為它可以透過一次檢查每個鍵的數字來執行排序。

基數排序的實現

  1. 比較列表中的每一項的數字。
  2. 根據數字對項進行分組。
  3. 按大小對各組進行排序。
  4. 對每個組進行遞迴排序,直到每個項都處於正確的位置。
Python 中的基數排序
def radix_sort(items):
    max_length = False
    tmp, placement = -1, 1

    while not max_length:
        max_length = True
        buckets = [list() for _ in range(10)]

        for i in items:
            tmp = i // placement
            buckets[tmp % 10].append(i)
            if max_length and tmp > 0:
                max_length = False

        a = 0
        for b in range(10):
            buck = buckets[b]
            for i in buck:
                items[a] = i
                a += 1

        placement *= 10
    return items

items = [6,20,8,19,56,23,87,41,49,53]
print(radix_sort(items))
JavaScript 中的基數排序
function radixSort(items) {
    let maxLength = false;
    let tmp = -1;
    let placement = 1;
    while(!maxLength) {
        maxLength = true;
        let buckets = Array.from({length: 10}, () => []);
        for(let i = 0; i < items.length; i++) {
            tmp = Math.floor(items[i]/placement);
            buckets[tmp % 10].push(items[i]);
            if(maxLength && tmp > 0) {
                maxLength = false;
            }
        }
        let a = 0;
        for(let b = 0; b < 10; b++) {
            let buck = buckets[b]
            for(let j = 0; j < buck.length; j++) {
                items[a] = buck[j];
                a++;
            }
        }
        placement *= 10;
    }
    return items;
}


let items = [6, 20, 8, 19, 56, 23, 87, 41, 49, 53];
console.log(radixSort(items));

梳排序

梳排序(Comb sort)比較相距一定距離的元素對,如果它們亂序則交換它們。對之間的距離最初設定為正在排序的列表的大小,然後在每次透過時減少一個因子(稱為“收縮因子”),直到達到最小值 \( 1 \),重複此過程,直到列表完全排序。

梳排序演算法類似於氣泡排序演算法,但比較元素之間的差距更大,這個更大的差距允許更大的值更快地移動到列表中的正確位置。

梳排序.gif

梳排序的歷史

梳狀排序演算法是一種相對較新的排序演算法,由 Włodzimierz Dobosiewicz 和 Artur Borowy 於 1980 年首次提出。該演算法的靈感來自使用梳子理順纏結的頭髮的想法,它使用類似的過程理順未排序值的列表。

梳排序的優點

梳排序在最壞情況下的時間複雜度為 \( O(n^2) \),但在實踐中,由於使用了收縮因子,它通常比其他 \( O(n ^2) \) 排序演算法(如氣泡排序)更快。收縮因子使演算法能夠快速將大值移向正確的位置,從而減少對列表進行完全排序所需的次數。

梳排序的用例

梳排序是一種相對簡單且高效的排序演算法,在各種應用中有很多用例。

  • 對具有大範圍值的資料進行排序 在比較元素之間使用更大的間隙允許更大的值更快的移動到它們在列表中的正確位置。
  • 在實時應用程式中對資料進行排序 作為一種穩定的排序演算法,梳排序保留了相等元素的相對順序。這對於在需要保留相等元素順序的實時應用程式中的資料排序非常有用。
  • 在記憶體受限的環境中對資料進行排序 梳排序不需要額外的記憶體來對資料進行排序,這對於在沒有額外記憶體或記憶體受限的環境中對資料進行排序非常有用。

梳排序的實現

  1. 從項之間較大的間距(\( gap \))開始。
  2. 比較間距末端的項,如果順序錯則交換它們。
  3. 縮小間距並重復該過程,直到 \( gap \) 為 \( 1 \)。
  4. 最後對剩餘的項進行氣泡排序。
Python 中的梳排序
def comb_sort(items):
    gap = len(items)
    shrink = 1.3
    sorted = False
    while not sorted:
        gap //= shrink
        if gap <= 1:
            sorted = True
        else:
            for i in range(len(items)-gap):
                if items[i] > items[i+gap]:
                    items[i],items[i+gap] = items[i+gap],items[i]
    return bubble_sort(items)

def bubble_sort(items):
    for i in range(len(items)):
        for j in range(len(items)-1-i):
            if items[j] > items[j+1]:
                items[j], items[j+1] = items[j+1], items[j]
    return items

items = [6,20,8,19,56,23,87,41,49,53]
print(comb_sort(items))
JavaScript 中的梳排序
function combSort(items) {
    let gap = items.length;
    let shrink = 1.3;
    let sorted = false;
    while(!sorted) {
        gap = Math.floor(gap / shrink);
        if(gap <= 1) {
            sorted = true;
        } else {
            for(let i = 0; i < items.length - gap; i++) {
                if(items[i] > items[i + gap]) {
                    [items[i], items[i+gap]] = [items[i+gap], items[i]]
                }
            }
        }
    }
    return bubbleSort(items);
}

function bubbleSort(items) {
    let swapped;
    do {
        swapped = false;
        for(let i = 0; i < items.length - 1; i++) {
            if(items[i] > items[i+1]) {
                [items[i], items[i+1]] = [items[i+1], items[i]]
                swapped = true;
            }
        }
    } while(swapped)
    return items;
}


let items = [6, 20, 8, 19, 56, 23, 87, 41, 49, 53];
console.log(combSort(items));

Timsort

Timsort 是一種混合穩定的排序演算法,源自歸併排序和插入排序,旨在較好地處理真實世界中各種各樣的資料。

它的工作原理是將輸入資料分成更小的子陣列,然後使用插入排序對這些子陣列進行排序,然後使用歸併排序將這些已排序的子陣列組合起來,生成一個完全排序的陣列。

Timsort 的最壞情況時間複雜度為 \( O(n \log n) \),這使得它可以高效的對大型資料集進行排序。它也是一種穩定的排序演算法,這意味著它保留了相等元素的相對順序。

timsort2.gif

Timsort 的歷史

Timsort 由 Tim Peters 於 2002 年開發,用於 Python 程式語言。它是一種混合排序演算法,結合了插入排序和歸併排序技術,旨在有效地對各種不同型別的資料進行排序。

由於它在處理不同型別資料方面的效率和多功能性,它後來被其他幾種程式語言採用,包括 Java 和 C#。

Timsort 排序的優點

Timsort 的一個關鍵特性是它能夠有效地處理不同型別的資料,它透過檢測”執行(runs)"來做到這一點,runs 是已經排序的元素序列。然後,Timsort 以一種方式組合這些 runs ,以最大限度地減少生成完全排序陣列所需的比較和交換次數。

Timsort 的另一個重要特性是它能夠處理部分排序的資料。在這種情況下,Timsort 可以檢測到部分排序的 runs,並使用插入排序對其進行快速排序,從而減少對資料進行完全排序所需的時間。

Timsort 排序的用例

作為一種高階演算法,Timsort 可用於在記憶體受限的系統上對資料進行排序。

在程式語言排序 Timsort 由於其處理不同型別資料的效率和能力,經常被用作這些語言中的預設排序演算法。
對真實世界的資料進行排序 Timsort 在對可能部分排序或包含已排序子陣列的真實資料進行排序時特別有效,因為它能夠檢測這些 runs 並使用插入排序來快速排序,從而減少了對資料進行完全排序所需的時間。
對不同型別的資料進行排序 它旨在有效地處理不同型別的資料,包括數字、字串和自定義物件。它可以檢測相同型別的資料 runs ,並使用歸併排序有效地組合它們,從而減少所需的比較和交換次數。

Timsort 排序的實現

  1. 將一個未排序的類別分成更小的、已排序的子列表。
  2. 合併子列表以形成更大的排序列表。
  3. 重複這個過程,直到整個列表排序完畢。
Python 中的 Timsort 實現
def insertion_sort(arr, left=0, right=None):
    if right is None:
        right = len(arr) - 1

    for i in range(left + 1, right + 1):
        key_item = arr[i]
        j = i - 1
        while j >= left and arr[j] > key_item:
            arr[j + 1] = arr[j]
            j -= 1
        arr[j + 1] = key_item

    return arr

def merge(left, right):
    if not left:
        return right

    if not right:
        return left

    if left[0] < right[0]:
        return [left[0]] + merge(left[1:], right)

    return [right[0]] + merge(left, right[1:])

def timsort(arr):
    min_run = 32
    n = len(arr)

    for i in range(0, n, min_run):
        insertion_sort(arr, i, min((i + min_run - 1), n - 1))

    size = min_run
    while size < n:
        for start in range(0, n, size * 2):
            midpoint = start + size - 1
            end = min((start + size * 2 - 1), (n-1))
            merged_array = merge(
                left=arr[start:midpoint + 1],
                right=arr[midpoint + 1:end + 1]
            )
            arr[start:start + len(merged_array)] = merged_array

        size *= 2

    return arr

items = [6,20,8,19,56,23,87,41,49,53]
print(timsort(items))
JavaScript 中的 Timsort 實現
function timsort(arr) {
  const minRun = 32;
  const n = arr.length;

  for (let i = 0; i < n; i += minRun) {
    insertionSort(arr, i, Math.min(i + minRun - 1, n - 1));
  }

  let size = minRun;
  while (size < n) {
    for (let start = 0; start < n; start += size * 2) {
      const midpoint = start + size - 1;
      const end = Math.min(start + size * 2 - 1, n - 1);
      const merged = merge(
        arr.slice(start, midpoint + 1),
        arr.slice(midpoint + 1, end + 1)
      );
      arr.splice(start, merged.length, ...merged);
    }
    size *= 2;
  }

  return arr;
}

function insertionSort(arr, left = 0, right = arr.length - 1) {
  for (let i = left + 1; i <= right; i++) {
    const keyItem = arr[i];
    let j = i - 1;
    while (j >= left && arr[j] > keyItem) {
      arr[j + 1] = arr[j];
      j--;
    }
    arr[j + 1] = keyItem;
  }
  return arr;
}

function merge(left, right) {
  let i = 0;
  let j = 0;
  const merged = [];

  while (i < left.length && j < right.length) {
    if (left[i] < right[j]) {
      merged.push(left[i]);
      i++;
    } else {
      merged.push(right[j]);
      j++;
    }
  }

  return merged.concat(left.slice(i)).concat(right.slice(j));
}

let items = [6, 20, 8, 19, 56, 23, 87, 41, 49, 53];
console.log(timsort(items));

比較所有排序演算法

請注意,表中列出的時間複雜度和空間複雜度是最壞情況下的複雜度,實際效能可能因具體實現和輸入資料而異。

演算法時間複雜度空間複雜度就地排序穩定排序自適應排序
氣泡排序\( O(n^2) \)\( O(1) \)
快速排序\( O(n \log n) \)\( O(\log n) \)
桶排序\( O(n+k) \)\( O(n+k) \)
Shell 排序\( O(n \log n) \)\( O(1) \)
歸併排序\( O(n \log n) \)\( O(n) \)
選擇排序\( O(n^2) \)\( O(1) \)
基數排序\( O(w \cdot n \))\( O(w+n) \)
梳排序\( O(n^2) \)\( O(1) \)
Timesort\( O(n \log n) \)\( O(n) \)

哪些是最常用的排序演算法?

最常用的排序演算法可能是快速排序,它廣泛用於許多程式語言,包括 C、C++、Java 和 Python,以及許多軟體應用程式和庫。快速排序因其在處理不同型別資料時的效率和通用性而受到青睞,並且經常被用作程式語言和軟體框架中的預設排序演算法。

然而,歸併排序和 Timsort 等其他排序演算法由於其效率和獨特的特性也常用於各種應用程式。

最後

學習演算法可以提升自己的邏輯能力,真正重要的是享受「學」的過程。雖然不一定就能直接用上,但是能在程式設計過程中思路更加清晰,工程能力也會更好。

參考:

相關文章