你想要的排序算法,都在這!

寫在前面

最近寫代碼的時候用到堆排序解決一些問題,然後寫的過程中發現有點手生,就想着記錄一些,然後又琢磨着乾脆直接整理一份排序的文章,附帶對排序的分析以及實現代碼,給自己練練手。
所有代碼都搞懂熟練,你還會怕排序麼?如果覺得有所幫助,記得點個關注和點個贊哦

冒泡排序

冒泡排序(Bubble Sort),是一種計算機科學領域的較簡單的排序算法。它重複地走訪過要排序的元素列,依次比較兩個相鄰的元素,如果他們的順序(如從大到小、首字母從A到Z)錯誤就把他們交換過來。走訪元素的工作是重複地進行直到沒有相鄰元素需要交換,也就是說該元素已經排序完成。

實現原理

  • 比較相鄰的元素。如果第一個比第二個大,就交換他們兩個。
  • 對每一對相鄰元素做同樣的工作,從開始第一對到結尾的最後一對。在這一點,最後的元素應該會是最大的數。
  • 針對所有的元素重複以上的步驟,除了最後一個。
  • 持續每次對越來越少的元素重複上面的步驟,直到沒有任何一對數字需要比較。

在這裏插入圖片描述

算法穩定性

冒泡排序總的平均時間複雜度爲 O(N2)O(N^2)。冒泡排序就是把小的元素往前調或者把大的元素往後調。比較是相鄰的兩個元素比較,交換也發生在這兩個元素之間。所以,如果兩個元素相等,是不會再交換的;如果兩個相等的元素沒有相鄰,那麼即使通過前面的兩兩交換把兩個相鄰起來,這時候也不會交換,所以相同元素的前後順序並沒有改變,所以冒泡排序是一種穩定排序算法

代碼實現

public void bubbleSort(int[] sourceArray){
    for (int i = 1; i < sourceArray.length; i++){
        // 設定一個標記,若爲true,則表示此次循環沒有進行交換,也就是待排序列已經有序,排序已經完成。
        boolean flag = true;
        for (int j = 0; j < sourceArray.length - i; j++){
            if (sourceArray[j] > sourceArray[j + 1]){
                sourceArray[j] = sourceArray[j] ^ sourceArray[j + 1];
                sourceArray[j + 1] = sourceArray[j] ^ sourceArray[j + 1];
                sourceArray[j] = sourceArray[j] ^ sourceArray[j + 1];
                flag = false;
            }
        }

        if (flag){
            break;
        }
    }
}

快速排序

快速排序(英語:Quicksort),又稱劃分交換排序(partition-exchange sort),簡稱快排,一種排序算法,最早由東尼·霍爾提出。快速排序通常明顯比其他算法更快,因爲它的內部循環(inner loop)可以在大部分的架構上很有效率地達成。

實現原理

  • 從數列中挑出一個元素,稱爲 “基準”(pivot),
  • 重新排序數列,所有元素比基準值小的擺放在基準前面,所有元素比基準值大的擺在基準的後面(相同的數可以到任一邊)。在這個分區退出之後,該基準就處於數列的中間位置。這個稱爲分區(partition)操作。
  • 遞歸地(recursive)把小於基準值元素的子數列和大於基準值元素的子數列排序。

在這裏插入圖片描述

算法穩定性

在平均狀況下,排序個項目要 O(nlogn)O(nlogn) ,事實上,快速排序 O(nlogn)O(nlogn) 通常明顯比其他算法更快。交換 a[r]a[mid],完成一趟快速排序。在中樞元素和 a[r] 交換的時候,很有可能把前面的元素的穩定性打亂,比如序列爲5 3 3 4 3 8 9 10 11,現在中樞元素 53(第5個元素,下標從1開始計)交換就會把序列中,三個元素 3 的穩定性打亂(就是最後一個三到了最前面),所以快速排序是一個不穩定的排序算法

代碼實現

public void quickSort(int[] sourceArray, int left, int right){
    if (left >= right) return;
    int l = left;
    int r = right;
    int mid = sourceArray[left];
    while (l < r) {
        while (l < r && sourceArray[r] >= mid)
            r--;
        if (l < r)
            sourceArray[l++] = sourceArray[r];
        while (l < r && sourceArray[l] < mid)
            l++;
        if (l < r)
            sourceArray[r--] = sourceArray[l];
    }
    sourceArray[l] = mid;
    quickSort(sourceArray, left, l - 1);
    quickSort(sourceArray, l + 1, right);
}

快速選擇排序

這裏單獨提一下一個混合的算法,是基於快速排序而來的,類似對快速排序進行剪枝,用於計算第KK大(小)的數。快速選擇算法的平均時間複雜度爲 O(N)O(N)。就像快速排序那樣,本算法也是 Tony Hoare 發明的,因此也被稱爲 Hoare選擇算法。本方法大致上與快速排序相同。簡便起見,注意到第 k 個最大元素也就是第 N - k 個最小元素,因此可以用第 k 小算法來解決本問題。

首先,我們選擇一個樞軸,並在線性時間內定義其在排序數組中的位置。這可以通過 劃分算法 的幫助來完成。

爲了實現劃分,沿着數組移動,將每個元素與樞軸進行比較,並將小於樞軸的所有元素移動到樞軸的左側。

這樣,在輸出的數組中,樞軸達到其合適位置。所有小於樞軸的元素都在其左側,所有大於或等於的元素都在其右側。這樣,數組就被分成了兩部分。如果是快速排序算法,會在這裏遞歸地對兩部分進行快速排序,時間複雜度爲 O(NlogN)O(Nlog N)。而在這裏,由於知道要找的第 N - k 小的元素在哪部分中,我們不需要對兩部分都做處理,這樣就將平均時間複雜度下降到 O(N)O(N),最終的算法十分直接了當 :

  • 隨機選擇一個樞軸。
  • 使用劃分算法將樞軸放在數組中的合適位置 pos。將小於樞軸的元素移到左邊,大於等於樞軸的元素移到右邊。
  • 比較 pos 和 N - k 以決定在哪邊繼續遞歸處理。

在這裏插入圖片描述

選擇排序

選擇排序(Selection sort)是一種簡單直觀的排序算法。它的工作原理是每一次從待排序的數據元素中選出最小(或最大)的一個元素,存放在序列的起始位置,直到全部待排序的數據元素排完。 選擇排序是不穩定的排序方法。

實現原理

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

在這裏插入圖片描述

算法穩定性

選擇排序是 O(n2)O(n^2) 的時間複雜度,在一趟選擇,如果一個元素比當前元素小,而該小的元素又出現在一個和當前元素相等的元素後面,那麼交換後穩定性就被破壞了。比較拗口,舉個例子,序列 5 8 5 2 9,我們知道第一遍選擇第 1 個元素 5 會和 2 交換,那麼原序列中兩個 5 的相對前後順序就被破壞了,所以選擇排序是一個不穩定的排序算法

代碼實現

public void selectSort(int[] sourceArray){
    // 總共要經過 N-1 輪比較
    for (int i = 0; i < sourceArray.length - 1; i++){
        int cur = i;
        // 每輪需要比較的次數 N-i
        for (int j = i + 1; j < sourceArray.length; j++){
            if (sourceArray[cur] > sourceArray[j]) {
                // 記錄目前能找到的最小值元素的下標
                cur = j;
            }
        }

        // 將找到的最小值和i位置所在的值進行交換
        if (i != cur){
            sourceArray[i] = sourceArray[i] ^ sourceArray[cur];
            sourceArray[cur] = sourceArray[i] ^ sourceArray[cur];
            sourceArray[i] = sourceArray[i] ^ sourceArray[cur];
        }
    }
}

插入排序

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

實現原理

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

在這裏插入圖片描述

算法穩定性

平均來說插入排序算法複雜度爲 O(n2)O(n^2) ,如果碰見一個和插入元素相等的,那麼插入元素把想插入的元素放在相等元素的後面。所以,相等元素的前後順序沒有改變,從原無序序列出去的順序就是排好序後的順序,所以插入排序是穩定的

代碼實現

public void insertSort(int[] sourceArray){
    // 從下標爲1的元素開始選擇合適的位置插入,因爲下標爲0的只有一個元素,默認是有序的
    for (int i = 1; i < sourceArray.length; i++){
        // 記錄要插入的數據
        int temp = sourceArray[i];
        // 從已經排序的序列最右邊的開始比較,找到比其小的數
        int j = i;
        while (j > 0 && temp < sourceArray[j - 1]){
            sourceArray[j] = sourceArray[j - 1];
            j--;
        }

        // 存在比其小的數,插入
        if (j != i) sourceArray[j] = temp;
    }
}

歸併排序

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

實現原理

  • 第一步:申請空間,使其大小爲兩個已經 排序序列之和,該空間用來存放合併後的序列
  • 第二步:設定兩個 指針,最初位置分別爲兩個已經排序序列的起始位置
  • 第三步:比較兩個指針所指向的元素,選擇相對小的元素放入到合併空間,並移動指針到下一位置
  • 重複步驟3直到某一指針超出序列尾
  • 將另一序列剩下的所有元素直接複製到合併序列尾

在這裏插入圖片描述

算法穩定性

始終都是 O(nlogn)O(nlogn) 的時間複雜度,代價是需要額外的內存空間。可以發現,在1個或2個元素時,1個元素不會交換,2個元素如果大小相等也沒有人故意交換,這不會破壞穩定性。那麼,在短的有序序列合併的過程中,穩定性是否受到破壞?沒有,合併過程中我們可以保證如果兩個當前元素相等時,我們把處在前面的序列的元素保存在結果序列的前面,這樣就保證了穩定性。所以,歸併排序也是穩定的排序算法

代碼實現

public int[] mergeSort(int[] sourceArray){
    if (sourceArray.length < 2) return sourceArray;
    int mid = (int) Math.floor(sourceArray.length / 2);
    int[] left = Arrays.copyOfRange(sourceArray, 0, mid);
    int[] right = Arrays.copyOfRange(sourceArray, mid, sourceArray.length);

    return merge(mergeSort(left), mergeSort(right));
}

private int[] merge(int[] left, int[] right){
    int[] nums = new int[left.length + right.length];
    int l = 0;
    int r = 0;
    int i = 0;
    while (l < left.length && r < right.length){
        if (left[l] < right[r]) {
            nums[i] = left[l];
            l++;
        }else {
            nums[i] = right[r];
            r++;
        }
        i++;
    }

    while (l < left.length){
        nums[i] = left[l];
        l++;i++;
    }

    while (r < right.length){
        nums[i] = right[r];
        r++;i++;
    }

    return nums;
}

基數排序

基數排序(radix sort)屬於“分配式排序”(distribution sort),又稱“桶子法”(bucket sort)或bin sort,顧名思義,它是透過鍵值的部份資訊,將要排序的元素分配至某些“桶”中,藉以達到排序的作用,基數排序法是屬於穩定性的排序,其時間複雜度爲O (nlog®m),其中r爲所採取的基數,而m爲堆數,在某些時候,基數排序法的效率高於其它的穩定性排序法。

實現原理

假設原來有一串數值如下所示:73, 22, 93, 43, 55, 14, 28, 65, 39, 81,首先根據個位數的數值,在走訪數值時將它們分配至編號 09 的桶子中:

0
1 81
2 22
3 73 93 43
4 14
5 55 65
6
7
8 28
9 39

接下來將這些桶子中的數值重新串接起來,成爲以下的數列:81, 22, 73, 93, 43, 14, 55, 65, 28, 39,接着再進行一次分配,這次是根據十位數來分配:

0
1 14
2 22 28
3 39
4 43
5 55
6 65
7 73
8 81
9 93

接下來將這些桶子中的數值重新串接起來,成爲以下的數列:14, 22, 28, 39, 43, 55, 65, 73, 81, 93,這時候整個數列已經排序完畢;如果排序的對象有三位數以上,則持續進行以上的動作直至最高位數爲止。LSD的基數排序適用於位數小的數列,如果位數多的話,使用MSD的效率會比較好。MSD的方式與LSD相反,是由高位數爲基底開始進行分配,但在分配之後並不馬上合併回一個數組中,而是在每個“桶子”中建立“子桶”,將每個桶子中的數值按照下一數位的值分配到“子桶”中。在進行完最低位數的分配後再合併回單一的數組中。
在這裏插入圖片描述

算法穩定性

基數排序的時間複雜度爲 O(nlog(r)m)O (nlog(r)m) ,有時候有些屬性是有優先級順序的,先按低優先級排序,再按高優先級排序,最後的次序就是高優先級高的在前,高優先級相同的低優先級高的在前。基數排序基於分別排序,分別收集,所以其是穩定的排序算法

代碼實現

public void radixSort(int[] sourceArray){
    int maxDigit = getMaxDigit(sourceArray);
    int mod = 10;
    int dev = 1;

    for (int i = 0; i < maxDigit; i++, dev *= 10, mod *= 10){
        int[][] counter = new int[mod * 2][0];
        for (int j = 0; j < sourceArray.length; j++){
            int bucket = ((sourceArray[j] % mod) / dev) + mod;
            counter[bucket] = arrayAppend(counter[bucket], sourceArray[j]);
        }

        int pos = 0;
        for (int[] bucket : counter){
            for (int value : bucket){
                sourceArray[pos++] = value;
            }
        }
    }
}

private int getMaxDigit(int[] arr){
    int maxValue = getMaxValue(arr);
    return getNumLength(maxValue);
}

private int getMaxValue(int[] arr){
    int maxValue = arr[0];
    for (int value : arr){
        if (maxValue < value){
            maxValue = value;
        }
    }
    return maxValue;
}

private int getNumLength(long num){
    if (num == 0){
        return 1;
    }
    int length = 0;
    for (long temp = num; temp != 0; temp /= 10){
        length++;
    }
    return length;
}

private int[] arrayAppend(int[] arr, int value){
    arr = Arrays.copyOf(arr, arr.length + 1);
    arr[arr.length - 1] = value;
    return arr;
}

希爾排序(shell)

希爾排序(Shell’s Sort)是插入排序的一種又稱“縮小增量排序”(Diminishing Increment Sort),是直接插入排序算法的一種更高效的改進版本。希爾排序是非穩定排序算法。該方法因D.L.Shell於1959年提出而得名。

實現原理

先取一個小於 n 的整數 d1 作爲第一個增量,把文件的全部記錄分組。所有距離爲 d1 的倍數的記錄放在同一個組中。先在各組內進行直接插入排序;然後,取第二個增量 d2=1(<…<d2<d1),即所有記錄放在同一組中進行直接插入排序爲止。
在這裏插入圖片描述

算法穩定性

希爾排序的時間複雜度會比 O(n2)O(n^2) 好一些,由於多次插入排序,我們知道一次插入排序是穩定的,不會改變相同元素的相對順序,但在不同的插入排序過程中,相同的元素可能在各自的插入排序中移動,最後其穩定性就會被打亂,所以shell排序是不穩定的

代碼實現

public void shellSort(int[] sourceArray){
    int gap = sourceArray.length;

    while (true){
        gap /= 2;
        for (int i = 0; i < gap; i++){
            for (int j = i + gap; j < sourceArray.length; j += gap){
                int temp = sourceArray[j];
                int k = j - gap;
                while (k >= 0 && sourceArray[k] > temp){
                    sourceArray[k + gap] = sourceArray[k];
                    k -= gap;
                }
                sourceArray[k + gap] = temp;
            }
        }
        if (gap == 1) break;
    }
}

堆排序

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

實現原理

  • 創建一個堆 H[0……n-1];
  • 把堆首(最大值)和堆尾互換;
  • 把堆的尺寸縮小 1,並調用 shift_down(0),目的是把新的數組頂端數據調整到相應位置;
  • 重複步驟 2,直到堆的尺寸爲 1。

在這裏插入圖片描述

算法穩定性

堆排序的平均時間複雜度爲 O(nlogn)Ο(nlogn)。我們知道堆的結構是節點 i 的孩子爲 2 * i2 * i + 1 節點,大頂堆要求父節點大於等於其 2 個子節點,小頂堆要求父節點小於等於其 2 個子節點。在一個長爲 n 的序列,堆排序的過程是從第 n / 2 開始和其子節點共 3 個值選擇最大(大頂堆)或者最小(小頂堆),這 3 個元素之間的選擇當然不會破壞穩定性。但當爲 n / 2 - 1, n / 2 - 2, ... 1這些個父節點選擇元素時,就會破壞穩定性。有可能第 n / 2 個父節點交換把後面一個元素交換過去了,而第 n / 2 - 1 個父節點把後面一個相同的元素沒 有交換,那麼這 2 個相同的元素之間的穩定性就被破壞了。所以,堆排序不是穩定的排序算法

代碼實現

public void heapSort(int[] sourceArray){
    int len = sourceArray.length;
    buildMaxHeap(sourceArray, len);
    for (int i = len - 1; i > 0; i--){
        swap(sourceArray, 0, i);
        len--;
        heapify(sourceArray, 0, len);
    }
}

private void buildMaxHeap(int[] arr, int len){
    for (int i = (int) Math.floor(len / 2); i >= 0; i--){
        heapify(arr, i, len);
    }
}

private void heapify(int[] arr, int i, int len){
    int left = 2 * i + 1;
    int right = 2 * i + 2;
    int largest = i;

    if (left < len && arr[left] > arr[largest]){
        largest = left;
    }

    if (right < len && arr[right] > arr[largest]){
        largest = right;
    }

    if (largest != i){
        swap(arr, i, largest);
        heapify(arr, largest, len);
    }
}

private void swap(int[] arr, int i, int j){
    arr[i] = arr[i] ^ arr[j];
    arr[j] = arr[i] ^ arr[j];
    arr[i] = arr[i] ^ arr[j];
}

計數排序

計數排序(Counting sort)是一種穩定的線性時間排序算法。計數排序使用一個額外的數組CC,其中第 ii 個元素是待排序數組AA中值等於ii的元素的個數。然後根據數組CC來將AA中的元素排到正確的位置。

實現原理

  • 找出待排序的數組中最大和最小的元素
  • 統計數組中每個值爲i的元素出現的次數,存入數組C的第i項
  • 對所有的計數累加(從C中的第一個元素開始,每一項和前一項相加)
  • 反向填充目標數組:將每個元素i放在新數組的第C(i)項,每放一個元素就將C(i)減去1

在這裏插入圖片描述

算法穩定性

計數排序的時間複雜度是 O(n+k)O(n + k),由於用來計數的數組C的長度取決於待排序數組中數據的範圍(等於待排序數組的最大值與最小值的差加上 1),這使得計數排序對於數據範圍很大的數組,需要大量時間和內存。例如:計數排序是用來排序 0100之間的數字的最好的算法,但是它不適合按字母順序排序人名。但是,計數排序可以用在基數排序中的算法來排序數據範圍很大的數組。年齡有重複時需要特殊處理(保證穩定性),這就是爲什麼最後要反向填充目標數組,以及將每個數字的統計減去 1 的原因。所以計數排序是穩定的

代碼實現

private static void countingSort(int[] arr, int maxValue) {
    int bucketLen = maxValue + 1;
    int[] bucket = new int[bucketLen];

    for (int value : arr) {
        bucket[value]++;
    }

    int sortedIndex = 0;
    for (int j = 0; j < bucketLen; j++) {
        while (bucket[j] > 0) {
            arr[sortedIndex++] = j;
            bucket[j]--;
        }
    }
}

private static int getMaxValue(int[] arr) {
    int maxValue = arr[0];
    for (int value : arr) {
        if (maxValue < value) {
            maxValue = value;
        }
    }
    return maxValue;
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章