1. 程式人生 > 實用技巧 >十大經典排序演算法最強總結(含Java、Python碼實現)

十大經典排序演算法最強總結(含Java、Python碼實現)

引言

所謂排序,就是使一串記錄,按照其中的某個或某些關鍵字的大小,遞增或遞減的排列起來的操作。排序演算法,就是如何使得記錄按照要求排列的方法。排序演算法在很多領域得到相當地重視,尤其是在大量資料的處理方面。一個優秀的演算法可以節省大量的資源。在各個領域中考慮到資料的各種限制和規範,要得到一個符合實際的優秀演算法,得經過大量的推理和分析。

兩年前,我曾在部落格園釋出過一篇《十大經典排序演算法最強總結(含JAVA程式碼實現)》博文,簡要介紹了比較經典的十大排序演算法,不過在之前的博文中,僅給出了Java版本的程式碼實現,並且有一些細節上的錯誤。所以,今天重新寫一篇文章,深入瞭解下十大經典排序演算法的原理及實現。

簡介

排序演算法可以分為內部排序外部排序,內部排序是資料記錄在記憶體中進行排序,而外部排序是因排序的資料很大,一次不能容納全部的排序記錄,在排序過程中需要訪問外存。常見的內部排序演算法有:插入排序希爾排序選擇排序氣泡排序歸併排序快速排序堆排序基數排序等,本文只講解內部排序演算法。用一張圖概括:

圖片名詞解釋:

  • n:資料規模
  • k:“桶”的個數
  • In-place:佔用常數記憶體,不佔用額外記憶體
  • Out-place:佔用額外記憶體

術語說明

  • 穩定:如果A原本在B前面,而A=B,排序之後A仍然在B的前面。
  • 不穩定:如果A原本在B的前面,而A=B,排序之後A可能會出現在B的後面。
  • 內排序:所有排序操作都在記憶體中完成。
  • 外排序:由於資料太大,因此把資料放在磁碟中,而排序通過磁碟和記憶體的資料傳輸才能進行。
  • 時間複雜度: 定性描述一個演算法執行所耗費的時間。
  • 空間複雜度:定性描述一個演算法執行所需記憶體的大小。

演算法分類

十種常見排序演算法可以分類兩大類別:比較類排序非比較類排序

常見的快速排序歸併排序堆排序以及氣泡排序等都屬於比較類排序演算法。比較類排序是通過比較來決定元素間的相對次序,由於其時間複雜度不能突破O(nlogn),因此也稱為非線性時間比較類排序。在氣泡排序之類的排序中,問題規模為n,又因為需要比較n次,所以平均時間複雜度為O(n²)。在歸併排序

快速排序之類的排序中,問題規模通過分治法消減為logn次,所以時間複雜度平均O(nlogn)

比較類排序的優勢是,適用於各種規模的資料,也不在乎資料的分佈,都能進行排序。可以說,比較排序適用於一切需要排序的情況。

計數排序基數排序桶排序則屬於非比較類排序演算法。非比較排序不通過比較來決定元素間的相對次序,而是通過確定每個元素之前,應該有多少個元素來排序。由於它可以突破基於比較排序的時間下界,以線性時間執行,因此稱為線性時間非比較類排序。 非比較排序只要確定每個元素之前的已有的元素個數即可,所有一次遍歷即可解決。演算法時間複雜度O(n)

非比較排序時間複雜度底,但由於非比較排序需要佔用空間來確定唯一位置。所以對資料規模和資料分佈有一定的要求。

氣泡排序(Bubble Sort)

氣泡排序是一種簡單的排序演算法。它重複地遍歷要排序的序列,依次比較兩個元素,如果它們的順序錯誤就把它們交換過來。遍歷序列的工作是重複地進行直到沒有再需要交換為止,此時說明該序列已經排序完成。這個演算法的名字由來是因為越小的元素會經由交換慢慢“浮”到數列的頂端。

演算法步驟

  1. 比較相鄰的元素。如果第一個比第二個大,就交換它們兩個;
  2. 對每一對相鄰元素作同樣的工作,從開始第一對到結尾的最後一對,這樣在最後的元素應該會是最大的數;
  3. 針對所有的元素重複以上的步驟,除了最後一個;
  4. 重複步驟1~3,直到排序完成。

圖解演算法

程式碼實現

JAVA

/**
 * 氣泡排序
 * @param arr
 * @return arr
 */
public static int[] BubbleSort(int[] arr) {
    for (int i = 1; i < arr.length; i++) {
        // Set a flag, if true, that means the loop has not been swapped, 
        // that is, the sequence has been ordered, the sorting has been completed.
        boolean flag = true;
        for (int j = 0; j < arr.length - i; j++) {
            if (arr[j] > arr[j + 1]) {
                int tmp = arr[j];
                arr[j] = arr[j + 1];
                arr[j + 1] = tmp;
			    // Change flag
                flag = false;
            }
        }
        if (flag) {
            break;
        }
    }
    return arr;
}

Python

def BubbleSort(arr):
    for i in range(len(arr)):
        is_sorted = True
        for j in range(len(arr)-i-1):
            if arr[j] > arr[j+1]:
                arr[j], arr[j+1] = arr[j+1], arr[j]
                is_sorted = False
        if is_sorted:
            break
    return arr

此處對程式碼做了一個小優化,加入了is_sorted Flag,目的是將演算法的最佳時間複雜度優化為O(n),即當原輸入序列就是排序好的情況下,該演算法的時間複雜度就是O(n)

選擇排序(Selection Sort)

選擇排序是一種簡單直觀的排序演算法,無論什麼資料進去都是O(n²)的時間複雜度。所以用到它的時候,資料規模越小越好。唯一的好處可能就是不佔用額外的記憶體空間了吧。它的工作原理:首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然後,再從剩餘未排序元素中繼續尋找最小(大)元素,然後放到已排序序列的末尾。以此類推,直到所有元素均排序完畢。

演算法步驟

  1. 首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置
  2. 再從剩餘未排序元素中繼續尋找最小(大)元素,然後放到已排序序列的末尾。
  3. 重複第2步,直到所有元素均排序完畢。

圖解演算法

程式碼實現

JAVA

/**
 * 選擇排序
 * @param arr
 * @return arr
 */
public static int[] SelectionSort(int[] arr) {
    for (int i = 0; i < arr.length - 1; i++) {
        int minIndex = i;
        for (int j = i + 1; j < arr.length; j++) {
            if (arr[j] < arr[minIndex]) {
                minIndex = j;
            }
        }
        if (minIndex != i) {
            int tmp = arr[i];
            arr[i] = arr[minIndex];
            arr[minIndex] = tmp;
        }
    }
    return arr;
}

Python

def SelectionSort(arr):
    for i in range(len(arr)-1):
        # 記錄最小值的索引
        minIndex = i
        for j in range(i+1, len(arr)):
            if arr[j] < arr[minIndex]:
                minIndex = j
        if minIndex != i:
            arr[i], arr[minIndex] = arr[minIndex], arr[i]
    return arr

插入排序(Insertion Sort)

插入排序是一種簡單直觀的排序演算法。它的工作原理是通過構建有序序列,對於未排序資料,在已排序序列中從後向前掃描,找到相應位置並插入。插入排序在實現上,通常採用in-place排序(即只需用到O(1)的額外空間的排序),因而在從後向前掃描過程中,需要反覆把已排序元素逐步向後挪位,為最新元素提供插入空間。

插入排序的程式碼實現雖然沒有氣泡排序和選擇排序那麼簡單粗暴,但它的原理應該是最容易理解的了,因為只要打過撲克牌的人都應該能夠秒懂。插入排序是一種最簡單直觀的排序演算法,它的工作原理是通過構建有序序列,對於未排序資料,在已排序序列中從後向前掃描,找到相應位置並插入。

插入排序和氣泡排序一樣,也有一種優化演算法,叫做拆半插入。

演算法步驟

  1. 從第一個元素開始,該元素可以認為已經被排序;
  2. 取出下一個元素,在已經排序的元素序列中從後向前掃描;
  3. 如果該元素(已排序)大於新元素,將該元素移到下一位置;
  4. 重複步驟3,直到找到已排序的元素小於或者等於新元素的位置;
  5. 將新元素插入到該位置後;
  6. 重複步驟2~5。

圖解演算法

程式碼實現

JAVA

/**
 * 插入排序
 * @param arr
 * @return arr
 */
public static int[] InsertionSort(int[] arr) {
    for (int i = 1; i < arr.length; i++) {
        int preIndex = i - 1;
        int current = arr[i];
        while (preIndex >= 0 && current < arr[preIndex]) {
            arr[preIndex + 1] = arr[preIndex];
            preIndex -= 1;
        }
        arr[preIndex + 1] = current;
    }
    return arr;
}

Python

def InsertionSort(arr):
    for i in range(1, len(arr)):
        preIndex = i-1
        current = arr[i]
        while preIndex >= 0 and current < arr[preIndex]:
            arr[preIndex+1] = arr[preIndex]
            preIndex -= 1
        arr[preIndex+1] = current
    return arr

希爾排序(Shell Sort)

希爾排序是希爾(Donald Shell)於1959年提出的一種排序演算法。希爾排序也是一種插入排序,它是簡單插入排序經過改進之後的一個更高效的版本,也稱為遞減增量排序演算法,同時該演算法是衝破O(n²)的第一批演算法之一。

希爾排序的基本思想是:先將整個待排序的記錄序列分割成為若干子序列分別進行直接插入排序,待整個序列中的記錄“基本有序”時,再對全體記錄進行依次直接插入排序。

演算法步驟

我們來看下希爾排序的基本步驟,在此我們選擇增量gap=length/2,縮小增量繼續以gap = gap/2的方式,這種增量選擇我們可以用一個序列來表示,{n/2, (n/2)/2, ..., 1},稱為增量序列。希爾排序的增量序列的選擇與證明是個數學難題,我們選擇的這個增量序列是比較常用的,也是希爾建議的增量,稱為希爾增量,但其實這個增量序列不是最優的。此處我們做示例使用希爾增量。

先將整個待排序的記錄序列分割成為若干子序列分別進行直接插入排序,具體演算法描述:

  • 選擇一個增量序列{t1, t2, …, tk},其中(ti>tj, i<j, tk=1)
  • 按增量序列個數k,對序列進行k趟排序;
  • 每趟排序,根據對應的增量t,將待排序列分割成若干長度為m的子序列,分別對各子表進行直接插入排序。僅增量因子為1時,整個序列作為一個表來處理,表長度即為整個序列的長度。

圖解演算法

程式碼實現

JAVA

/**
 * 希爾排序
 *
 * @param arr
 * @return arr
 */
public static int[] ShellSort(int[] arr) {
    int n = arr.length;
    int gap = n / 2;
    while (gap > 0) {
        for (int i = gap; i < n; i++) {
            int current = arr[i];
            int preIndex = i - gap;
            // Insertion sort
            while (preIndex >= 0 && arr[preIndex] > current) {
                arr[preIndex + gap] = arr[preIndex];
                preIndex -= gap;
            }
            arr[preIndex + gap] = current;

        }
        gap /= 2;
    }
    return arr;
}

Python

def ShellSort(arr):
    n = len(arr)
    # 初始步長
    gap = n // 2
    while gap > 0:
        for i in range(gap, n):
            # 根據步長進行插入排序
            current = arr[i]
            preIndex = i - gap
            # 插入排序
            while preIndex >= 0 and arr[preIndex] > current:
                arr[preIndex+gap] = arr[preIndex]
                preIndex -= gap
            arr[preIndex+gap] = current
        # 得到新的步長
        gap = gap // 2
    return arr

歸併排序(Merge Sort)

歸併排序是建立在歸併操作上的一種有效的排序演算法。該演算法是採用分治法(Divide and Conquer)的一個非常典型的應用。歸併排序是一種穩定的排序方法。將已有序的子序列合併,得到完全有序的序列;即先使每個子序列有序,再使子序列段間有序。若將兩個有序表合併成一個有序表,稱為2-路歸併。

和選擇排序一樣,歸併排序的效能不受輸入資料的影響,但表現比選擇排序好的多,因為始終都是O(nlogn)的時間複雜度。代價是需要額外的記憶體空間。

演算法步驟

歸併排序演算法是一個遞迴過程,邊界條件為當輸入序列僅有一個元素時,直接返回,具體過程如下:

  1. 如果輸入內只有一個元素,則直接返回,否則將長度為n的輸入序列分成兩個長度為n/2的子序列;
  2. 分別對這兩個子序列進行歸併排序,使子序列變為有序狀態;
  3. 設定兩個指標,分別指向兩個已經排序子序列的起始位置;
  4. 比較兩個指標所指向的元素,選擇相對小的元素放入到合併空間(用於存放排序結果),並移動指標到下一位置;
  5. 重複步驟3 ~4直到某一指標達到序列尾;
  6. 將另一序列剩下的所有元素直接複製到合併序列尾。

圖解演算法

程式碼實現

JAVA

/**
 * 歸併排序
 *
 * @param arr
 * @return arr
 */
public static int[] MergeSort(int[] arr) {
    if (arr.length <= 1) {
        return arr;
    }
    int middle = arr.length / 2;
    int[] arr_1 = Arrays.copyOfRange(arr, 0, middle);
    int[] arr_2 = Arrays.copyOfRange(arr, middle, arr.length);
    return Merge(MergeSort(arr_1), MergeSort(arr_2));
}

/**
 * Merge two sorted arrays
 * 
 * @param arr_1
 * @param arr_2
 * @return sorted_arr
 */
public static int[] Merge(int[] arr_1, int[] arr_2) {
    int[] sorted_arr = new int[arr_1.length + arr_2.length];
    int idx = 0, idx_1 = 0, idx_2 = 0;
    while (idx_1 < arr_1.length && idx_2 < arr_2.length) {
        if (arr_1[idx_1] < arr_2[idx_2]) {
            sorted_arr[idx] = arr_1[idx_1];
            idx_1 += 1;
        } else {
            sorted_arr[idx] = arr_2[idx_2];
            idx_2 += 1;
        }
        idx += 1;
    }
    if (idx_1 < arr_1.length) {
        while (idx_1 < arr_1.length) {
            sorted_arr[idx] = arr_1[idx_1];
            idx_1 += 1;
            idx += 1;
        }
    } else {
        while (idx_2 < arr_2.length) {
            sorted_arr[idx] = arr_2[idx_2];
            idx_2 += 1;
            idx += 1;
        }
    }
    return sorted_arr;
}

Python

def Merge(arr_1, arr_2):
    sorted_arr = []
    idx_1 = 0
    idx_2 = 0
    while idx_1 < len(arr_1) and idx_2 < len(arr_2):
        if arr_1[idx_1] < arr_2[idx_2]:
            sorted_arr.append(arr_1[idx_1])
            idx_1 += 1
        else:
            sorted_arr.append(arr_2[idx_2])
            idx_2 += 1
    if idx_1 < len(arr_1):
        while idx_1 < len(arr_1):
            sorted_arr.append(arr_1[idx_1])
            idx_1 += 1
    else:
        while idx_2 < len(arr_2):
            sorted_arr.append(arr_2[idx_2])
            idx_2 += 1
    return sorted_arr

def MergeSort(arr):
    if len(arr) <= 1:
        return arr
    mid = int(len(arr)/2)
    arr_1, arr_2 = arr[:mid], arr[mid:]
    return Merge(MergeSort(arr_1), MergeSort(arr_2))

演算法分析

穩定性:穩定

時間複雜度:最佳:O(nlogn) 最差:O(nlogn) 平均:O(nlogn)

空間複雜度:O(n)

快速排序(Quick Sort)

快速排序用到了分治思想,同樣的還有歸併排序。乍看起來快速排序和歸併排序非常相似,都是將問題變小,先排序子串,最後合併。不同的是快速排序在劃分子問題的時候經過多一步處理,將劃分的兩組資料劃分為一大一小,這樣在最後合併的時候就不必像歸併排序那樣再進行比較。但也正因為如此,劃分的不定性使得快速排序的時間複雜度並不穩定。

快速排序的基本思想:通過一趟排序將待排序列分隔成獨立的兩部分,其中一部分記錄的元素均比另一部分的元素小,則可分別對這兩部分子序列繼續進行排序,以達到整個序列有序。

演算法步驟

快速排序使用分治法(Divide and conquer)策略來把一個序列分為較小和較大的2個子序列,然後遞迴地排序兩個子序列。具體演算法描述如下:

  1. 從序列中隨機挑出一個元素,做為 “基準”(pivot);
  2. 重新排列序列,將所有比基準值小的元素擺放在基準前面,所有比基準值大的擺在基準的後面(相同的數可以到任一邊)。在這個操作結束之後,該基準就處於數列的中間位置。這個稱為分割槽(partition)操作;
  3. 遞迴地把小於基準值元素的子序列和大於基準值元素的子序列進行快速排序。

圖解演算法

程式碼實現

JAVA

/**
 * Swap the two elements of an array
 * @param array
 * @param i
 * @param j
 */
private static void swap(int[] arr, int i, int j) {
    int tmp = arr[i];
    arr[i] = arr[j];
    arr[j] = tmp;
}

/**
 * Partition function
 * @param arr
 * @param left
 * @param right
 * @return small_idx
 */
private static int Partition(int[] arr, int left, int right) {
    if (left == right) {
        return left;
    }
    // random pivot
    int pivot = (int) (left + Math.random() * (right - left + 1));
    swap(arr, pivot, right);
    int small_idx = left;
    for (int i = small_idx; i < right; i++) {
        if (arr[i] < arr[right]) {
            swap(arr, i, small_idx);
            small_idx++;
        }
    }
    swap(arr, small_idx, right);
    return small_idx;
}

/**
 * Quick sort function
 * @param arr
 * @param left
 * @param right
 * @return arr
 */
public static int[] QuickSort(int[] arr, int left, int right) {
    if (left < right) {
        int pivotIndex = Partition(arr, left, right);
        sort(arr, left, pivotIndex - 1);
        sort(arr, pivotIndex + 1, right);
    }
    return arr;
}

Python

def QuickSort(arr, left=None, right=None):
    left = 0 if left == None else left
    right = len(arr)-1 if right == None else right
    if left < right:
        pivotIndex = Partition(arr, left, right)
        QuickSort(arr, left, pivotIndex-1)
        QuickSort(arr, pivotIndex+1, right)
    return arr

def Partition(arr, left, right):
    if left == right:
        return left
    # 隨機pivot
    swap(arr, random.randint(left, right), right)
    pivot = right
    small_idx = left
    for i in range(small_idx, pivot):
        if arr[i] < arr[pivot]:
            swap(arr, i, small_idx)
            small_idx += 1
    swap(arr, small_idx, pivot)
    return small_idx

def swap(arr, i, j):
    arr[i], arr[j] = arr[j], arr[i]

演算法分析

穩定性:不穩定

時間複雜度:最佳:O(nlogn) 最差:O(nlogn) 平均:O(nlogn)

空間複雜度:O(nlogn)

堆排序(Heap Sort)

堆排序是指利用堆這種資料結構所設計的一種排序演算法。堆是一個近似完全二叉樹的結構,並同時滿足堆的性質:即子結點的值總是小於(或者大於)它的父節點

演算法步驟

  1. 將初始待排序列(R1, R2, ……, Rn)構建成大頂堆,此堆為初始的無序區;
  2. 將堆頂元素R[1]與最後一個元素R[n]交換,此時得到新的無序區(R1, R2, ……, Rn-1)和新的有序區(Rn),且滿足R[1, 2, ……, n-1]<=R[n]
  3. 由於交換後新的堆頂R[1]可能違反堆的性質,因此需要對當前無序區(R1, R2, ……, Rn-1)調整為新堆,然後再次將R[1]與無序區最後一個元素交換,得到新的無序區(R1, R2, ……, Rn-2)和新的有序區(Rn-1, Rn)。不斷重複此過程直到有序區的元素個數為n-1,則整個排序過程完成。

圖解演算法

程式碼實現

JAVA

// Global variable that records the length of an array;
static int heapLen;

/**
 * Swap the two elements of an array
 * @param arr
 * @param i
 * @param j
 */
private static void swap(int[] arr, int i, int j) {
    int tmp = arr[i];
    arr[i] = arr[j];
    arr[j] = tmp;
}

/**
 * Build Max Heap
 * @param arr
 */
private static void buildMaxHeap(int[] arr) {
    for (int i = arr.length / 2 - 1; i >= 0; i--) {
        heapify(arr, i);
    }
}

/**
 * Adjust it to the maximum heap
 * @param arr
 * @param i
 */
private static void heapify(int[] arr, int i) {
    int left = 2 * i + 1;
    int right = 2 * i + 2;
    int largest = i;
    if (right < heapLen && arr[right] > arr[largest]) {
        largest = right;
    }
    if (left < heapLen && arr[left] > arr[largest]) {
        largest = left;
    }
    if (largest != i) {
        swap(arr, largest, i);
        heapify(arr, largest);
    }
}

/**
 * Heap Sort
 * @param arr
 * @return
 */
public static int[] HeapSort(int[] arr) {
    // index at the end of the heap
    heapLen = arr.length;
    // build MaxHeap
    buildMaxHeap(arr);
    for (int i = arr.length - 1; i > 0; i--) {
        // Move the top of the heap to the tail of the heap in turn
        swap(arr, 0, i);
        heapLen -= 1;
        heapify(arr, 0);
    }
    return arr;
}

Python

def HeapSort(arr):
    global heapLen
    # 用於標記堆尾部的索引
    heapLen = len(arr)
    # 構建最大堆
    buildMaxHeap(arr)
    for i in range(len(arr)-1, 0, -1):
        # 依次將堆頂移至堆尾
        swap(arr, 0, i)
        heapLen -= 1
        heapify(arr, 0)
    return arr

def buildMaxHeap(arr):
    for i in range(len(arr)//2-1, -1, -1):
        heapify(arr, i)

def heapify(arr, i):
    left = 2*i + 1
    right = 2*i + 2
    largest = i
    if right < heapLen and arr[right] > arr[largest]:
        largest = right
    if left < heapLen and arr[left] > arr[largest]:
        largest = left
    if largest != i:
        swap(arr, largest, i)
        heapify(arr, largest)

def swap(arr, i, j):
    arr[i], arr[j] = arr[j], arr[i]

演算法分析

穩定性:不穩定

時間複雜度:最佳:O(nlogn) 最差:O(nlogn) 平均:O(nlogn)

空間複雜度:O(1)

計數排序(Counting Sort)

計數排序的核心在於將輸入的資料值轉化為鍵儲存在額外開闢的陣列空間中。 作為一種線性時間複雜度的排序,計數排序要求輸入的資料必須是有確定範圍的整數

計數排序(Counting sort)是一種穩定的排序演算法。計數排序使用一個額外的陣列C,其中第i個元素是待排序陣列A中值等於i的元素的個數。然後根據陣列C來將A中的元素排到正確的位置。它只能對整數進行排序

演算法步驟

  1. 找出陣列中的最大值max、最小值min
  2. 建立一個新陣列C,其長度是max-min+1,其元素預設值都為0;
  3. 遍歷原陣列A中的元素A[i],以A[i]-min作為C陣列的索引,以A[i]的值在A中元素出現次數作為C[A[i]-min]的值;
  4. C陣列變形,新元素的值是該元素與前一個元素值的和,即當i>1C[i] = C[i] + C[i-1]
  5. 建立結果陣列R,長度和原始陣列一樣。
  6. 從後向前遍歷原始陣列A中的元素A[i],使用A[i]減去最小值min作為索引,在計數陣列C中找到對應的值C[A[i]-min]C[A[i]-min]-1就是A[i]在結果陣列R中的位置,做完上述這些操作,將count[A[i]-min]減小1。

圖解演算法

程式碼實現

JAVA

/**
 * Gets the maximum and minimum values in the array
 * 
 * @param arr
 * @return
 */
private static int[] getMinAndMax(int[] arr) {
    int maxValue = arr[0];
    int minValue = arr[0];
    for (int i = 0; i < arr.length; i++) {
        if (arr[i] > maxValue) {
            maxValue = arr[i];
        } else if (arr[i] < minValue) {
            minValue = arr[i];
        }
    }
    return new int[] { minValue, maxValue };
}

/**
 * Counting Sort
 * 
 * @param arr
 * @return
 */
public static int[] CountingSort(int[] arr) {
    if (arr.length < 2) {
        return arr;
    }
    int[] extremum = getMinAndMax(arr);
    int minValue = extremum[0];
    int maxValue = extremum[1];
    int[] countArr = new int[maxValue - minValue + 1];
    int[] result = new int[arr.length];

    for (int i = 0; i < arr.length; i++) {
        countArr[arr[i] - minValue] += 1;
    }
    for (int i = 1; i < countArr.length; i++) {
        countArr[i] += countArr[i - 1];
    }
    for (int i = arr.length - 1; i >= 0; i--) {
        int idx = countArr[arr[i] - minValue] - 1;
        result[idx] = arr[i];
        countArr[arr[i] - minValue] -= 1;
    }
    return result;
}

Python

def CountingSort(arr):
    if arr.size < 2:
        return arr
    minValue, maxValue = getMinAndMax(arr)
    countArr = [0]*(maxValue-minValue+1)
    resultArr = [0]*len(arr)
    for i in range(len(arr)):
        countArr[arr[i]-minValue] += 1
    for i in range(1, len(countArr)):
        countArr[i] += countArr[i-1]
    for i in range(len(arr)-1, -1, -1):
        idx = countArr[arr[i]-minValue]-1
        resultArr[idx] = arr[i]
        countArr[arr[i]-minValue] -= 1
    return resultArr


def getMinAndMax(arr):
    maxValue = arr[0]
    minValue = arr[0]
    for i in range(len(arr)):
        if arr[i] > maxValue:
            maxValue = arr[i]
        elif arr[i] < minValue:
            minValue = arr[i]
    return minValue, maxValue

演算法分析

當輸入的元素是n0k之間的整數時,它的執行時間是 O(n+k)。計數排序不是比較排序,排序的速度快於任何比較排序演算法。由於用來計數的陣列C的長度取決於待排序陣列中資料的範圍(等於待排序陣列的最大值與最小值的差加上1),這使得計數排序對於資料範圍很大的陣列,需要大量額外記憶體空間。

穩定性:穩定

時間複雜度:最佳:O(n+k) 最差:O(n+k) 平均:O(n+k)

空間複雜度:O(k)

桶排序(Bucket Sort)

桶排序是計數排序的升級版。它利用了函式的對映關係,高效與否的關鍵就在於這個對映函式的確定。為了使桶排序更加高效,我們需要做到這兩點:

  1. 在額外空間充足的情況下,儘量增大桶的數量
  2. 使用的對映函式能夠將輸入的 N 個數據均勻的分配到 K 個桶中

桶排序的工作的原理:假設輸入資料服從均勻分佈,將資料分到有限數量的桶裡,每個桶再分別排序(有可能再使用別的排序演算法或是以遞迴方式繼續使用桶排序進行。

演算法步驟

  1. 設定一個BucketSize,作為每個桶所能放置多少個不同數值;
  2. 遍歷輸入資料,並且把資料依次對映到對應的桶裡去;
  3. 對每個非空的桶進行排序,可以使用其它排序方法,也可以遞迴使用桶排序;
  4. 從非空桶裡把排好序的資料拼接起來。

圖解演算法

程式碼實現

JAVA

/**
 * Gets the maximum and minimum values in the array
 * @param arr
 * @return
 */
private static int[] getMinAndMax(List<Integer> arr) {
    int maxValue = arr.get(0);
    int minValue = arr.get(0);
    for (int i : arr) {
        if (i > maxValue) {
            maxValue = i;
        } else if (i < minValue) {
            minValue = i;
        }
    }
    return new int[] { minValue, maxValue };
}

/**
 * Bucket Sort
 * @param arr
 * @return
 */
public static List<Integer> BucketSort(List<Integer> arr, int bucket_size) {
    if (arr.size() < 2 || bucket_size == 0) {
        return arr;
    }
    int[] extremum = getMinAndMax(arr);
    int minValue = extremum[0];
    int maxValue = extremum[1];
    int bucket_cnt = (maxValue - minValue) / bucket_size + 1;
    List<List<Integer>> buckets = new ArrayList<>();
    for (int i = 0; i < bucket_cnt; i++) {
        buckets.add(new ArrayList<Integer>());
    }
    for (int element : arr) {
        int idx = (element - minValue) / bucket_size;
        buckets.get(idx).add(element);
    }
    for (int i = 0; i < buckets.size(); i++) {
        if (buckets.get(i).size() > 1) {
            buckets.set(i, sort(buckets.get(i), bucket_size / 2));
        }
    }
    ArrayList<Integer> result = new ArrayList<>();
    for (List<Integer> bucket : buckets) {
        for (int element : bucket) {
            result.add(element);
        }
    }
    return result;
}

Python

def BucketSort(arr, bucket_size):
    if len(arr) < 2 or bucket_size == 0:
        return arr
    minValue, maxValue = getMinAndMax(arr)
    bucket_cnt = (maxValue-minValue)//bucket_size+1
    bucket = [[] for i in range(bucket_cnt)]
    for i in range(len(arr)):
        idx = (arr[i]-minValue) // bucket_size
        bucket[idx].append(arr[i])
    for i in range(len(bucket)):
        if len(bucket[i]) > 1:
            # 遞迴使用桶排序,也可使用其它排序演算法
            bucket[i] = BucketSort(bucket[i], bucket_size//2)
    result = []
    for i in range(len(bucket)):
        for j in range(len(bucket[i])):
            result.append(bucket[i][j])
    return result


def getMinAndMax(arr):
    maxValue = arr[0]
    minValue = arr[0]
    for i in range(len(arr)):
        if arr[i] > maxValue:
            maxValue = arr[i]
        elif arr[i] < minValue:
            minValue = arr[i]
    return minValue, maxValue

演算法分析

穩定性:穩定

時間複雜度:最佳:O(n+k) 最差:O(n²) 平均:O(n+k)

空間複雜度:O(k)

基數排序(Radix Sort)

基數排序也是非比較的排序演算法,對元素中的每一位數字進行排序,從最低位開始排序,複雜度為O(n×k)n為陣列長度,k為陣列中元素的最大的位數;

基數排序是按照低位先排序,然後收集;再按照高位排序,然後再收集;依次類推,直到最高位。有時候有些屬性是有優先順序順序的,先按低優先順序排序,再按高優先順序排序。最後的次序就是高優先順序高的在前,高優先順序相同的低優先順序高的在前。基數排序基於分別排序,分別收集,所以是穩定的。

演算法步驟

  1. 取得陣列中的最大數,並取得位數,即為迭代次數N(例如:陣列中最大數值為1000,則N=4);
  2. A為原始陣列,從最低位開始取每個位組成radix陣列;
  3. radix進行計數排序(利用計數排序適用於小範圍數的特點);
  4. radix依次賦值給原陣列;
  5. 重複2~4步驟N

圖解演算法

程式碼實現

JAVA

/**
 * Radix Sort
 * 
 * @param arr
 * @return
 */
public static int[] RadixSort(int[] arr) {
    if (arr.length < 2) {
        return arr;
    }
    int N = 1;
    int maxValue = arr[0];
    for (int element : arr) {
        if (element > maxValue) {
            maxValue = element;
        }
    }
    while (maxValue / 10 != 0) {
        maxValue = maxValue / 10;
        N += 1;
    }
    for (int i = 0; i < N; i++) {
        List<List<Integer>> radix = new ArrayList<>();
        for (int k = 0; k < 10; k++) {
            radix.add(new ArrayList<Integer>());
        }
        for (int element : arr) {
            int idx = (element / (int) Math.pow(10, i)) % 10;
            radix.get(idx).add(element);
        }
        int idx = 0;
        for (List<Integer> l : radix) {
            for (int n : l) {
                arr[idx++] = n;
            }
        }
    }
    return arr;
}

Python

def RadixSort(arr):
    if len(arr) < 2:
        return arr
    maxValue = arr[0]
    N = 1
    for i in range(len(arr)):
        if arr[i] > maxValue:
            maxValue = arr[i]
    while maxValue // 10 != 0:
        maxValue = maxValue//10
        N += 1
    for n in range(N):
        radix = [[] for i in range(10)]
        for i in range(len(arr)):
            idx = arr[i]//(10**n) % 10
            radix[idx].append(arr[i])
        idx = 0
        for j in range(len(radix)):
            for k in range(len(radix[j])):
                arr[idx] = radix[j][k]
                idx += 1
    return arr

演算法分析

穩定性:穩定

時間複雜度:最佳:O(n×k) 最差:O(n×k) 平均:O(n×k)

空間複雜度:O(n+k)

基數排序 vs 計數排序 vs 桶排序

這三種排序演算法都利用了桶的概念,但對桶的使用方法上有明顯差異:

  • 基數排序:根據鍵值的每位數字來分配桶
  • 計數排序:每個桶只儲存單一鍵值
  • 桶排序:每個桶儲存一定範圍的數值

參考文章

https://www.cnblogs.com/guoyaohua/p/8600214.html

https://en.wikipedia.org/wiki/Sorting_algorithm

https://sort.hust.cc/