常見面試必考排序算法解析


博客書寫不易,您的點贊收藏是我前進的動力,千萬別忘記點贊、 收**藏 ^ _ ^ !

十種常見排序算法可以分爲兩大類:

  1. 比較類排序:通過比較來決定元素間的相對次序,由於其時間複雜度不能突破O(nlogn),因此也稱爲非線性時間比較類排序。
  2. 非比較類排序:不通過比較來決定元素間的相對次序,它可以突破基於比較排序的時間下界,以線性時間運行,因此也稱爲線性時間非比較類排序。

在這裏插入圖片描述

排序的時間和空間複雜度如下:
在這裏插入圖片描述
關於對時間複雜度和空間複雜度的理解,後面會詳細講解

1. 冒泡排序(Bubble Sort)

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

1. 算法描述

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

2. 動畫演示
在這裏插入圖片描述
3. 代碼

  /**
     * 冒泡排序
     * 它的思想是每次內存循環確定其指定index上的數字
     * 先沉最大的數字
     */
    private int[] bubbleSort(int[] arr) {
        int len = arr.length;
        for (int i = 0; i < len - 1; i++) {
            for (int j = 0; j < len - 1 - i; j++) {
                // 相鄰元素兩兩對比
                if (arr[j] > arr[j + 1]) {
                    // 元素交換
                    int temp = arr[j + 1];
                    arr[j + 1] = arr[j];
                    arr[j] = temp;
                }
            }
        }
        return arr;
    }

4. 算法分析
冒泡排序的時間複雜度爲O(n2),空間複雜度爲O(1)。

2. 選擇排序 (Selection Sort)

選擇排序(Selection-sort)是一種簡單直觀的排序算法。它的工作原理:
1)首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置
2)然後,再從剩餘未排序元素中繼續尋找最小(大)元素(所有數據中第二小/大),然後放到已排序序列的末尾。
3)以此類推,直到所有元素均排序完畢。

1. 算法描述
n個記錄的直接選擇排序可經過n-1趟直接選擇排序得到有序結果。假設R[]代表有個數據,具體算法描述如下:

  1. 初始狀態:無序區爲R[1…n],有序區爲空;
  2. 第i趟排序(i=1,2,3…n-1)開始後,當前有序區和無序區分別爲R[1…i]和R(i+1…n)。
    該趟排序從當前無序區中-選出關鍵字最小的記錄 R[k],放在有序區末尾
  3. n-1趟結束,數組有序化了。

2. 動畫演示
在這裏插入圖片描述
3. 代碼

    fun selectionSort(arr: IntArray): IntArray {
        val len = arr.size
        var minIndex: Int
        var temp: Int
        for (i in arr.indices) {
            minIndex = i
            for (j in i + 1 until len) {
                // 尋找最小的數
                if (arr[j] < arr[minIndex]) {
                    // 將最小數的索引保存
                    minIndex = j
                }
            }
            //將第i輪的最小值求出來
            temp = arr[i]
            arr[i] = arr[minIndex];
            arr[minIndex] = temp;
        }
        return arr
    }

4. 算法分析
表現最穩定的排序算法之一,因爲無論什麼數據進去都是O(n2)的時間複雜度,所以用到它的時候,數據規模越小越好。

唯一的好處可能就是不佔用額外的內存空間了吧。理論上講,選擇排序可能也是平時排序一般人想到的最多的排序方法了吧。

3. 插入排序(Insertion Sort)

插入排序的算法描述是一種簡單直觀的排序算法。
它的工作原理是通過構建有序序列,對於未排序數據,在已排序序列中從後向前掃描,找到相應位置並插入。

1.算法描述
一般來說,插入排序都採用in-place在數組上實現。具體算法描述如下:

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

2. 動畫演示
在這裏插入圖片描述
3. 代碼

  fun insertionSort(arr: IntArray): IntArray {
        var preIndex: Int
        var current: Int
        for (i in arr.indices) {
            preIndex = i - 1
            current = arr[i]
            while (preIndex >= 0 && arr[preIndex] > current) {
                arr[preIndex + 1] = arr[preIndex];
                preIndex--
            }
            arr[preIndex + 1] = current
        }
        return arr
    }

4. 算法分析
插入排序在實現上,通常採用in-place排序(即只需用到O(1)的額外空間的排序)。
前面的數據都是已經排好了的,因而在從後向前掃描過程中,需要反覆把已排序元素逐步向後挪位,爲最新元素提供插入空間。

對於小規模數據和基本有序數據效率較高。

4. 希爾排序(Shell Sort)

希爾排序按其設計者希爾(Donald Shell)的名字命名,它是一種基於插入排序的快速排序算法,要了解希爾排序,必須先掌握插入排序的原理與實現。

它是第一個突破O(n2)的排序算法,它與插入排序的不同之處在於,它會優先比較距離較遠的元素。希爾排序又叫縮小增量排序。它的效率提升實質是希爾排序通過允許非相鄰的等間距元素進行交換來減少插入排序頻繁挪動元素的弊端

希爾排序的算法思想是:
1)通過將比較的全部元素分爲幾個區域來提升插入排序的性能。這樣可以讓一個元素可以一次性地朝最終位置前進一大步。
2)然後算法再取越來越小的步長進行排序,算法的最後一步就是普通的插入排序,此時相隔的步長爲1。
3)最後一步需排序的數據幾乎是已排好的了,此時插入排序較快,因爲此時是幾個有序組組成的。

步長
將記錄分割成N份,則N就是步長(且步長要小於數組長度),步長的最終數值是1,步長的選擇是希爾排序的重要部分。

步長會根據一定設計不斷變小,直至1。當步長爲1時,此時的算法就變爲了插入排序,保證了數據一定是有序的。

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

  1. 選擇一個增量序列{t1,t2,…,tk},其中t(i)>t(i+1),tk=1,如8個長度的序列,其增量序列可以是4、2、1,可以理解爲間距序列;
  2. 按增量序列個數k,對序列進行k 趟排序,如增量序列是4、2、1則需要3趟插入排序;
  3. 每趟排序,根據對應的增量ti,將待排序列分割成若干長度爲m 的組合,組合中數下標相隔ti增量,如index索引分別相隔4、2、1。分別對各子表進行直接插入排序
  4. 僅增量因子爲1 時,整個序列作爲一個表來處理,表長度即爲整個序列的長度。

2. 動畫演示
在這裏插入圖片描述
3. 例子分析
假設待排序數組{6, 5, 3, 1, 8, 7, 2, 4, 9, 0}

  1. 第一次步長 h=4,即可將數組分成四部分
[0]6 [4]8 [8]9
[1]5 [5]7 [9]0
[2]3 [6]2
[3]1 [7]4
  1. 對4組進行插入排序後獲得
[0]6 [4]8 [8]9
[1]0 [5]5 [9]7
[2]2 [6]3
[3]1 [7]4

此時數組的順序爲 {6, 0, 2, 1, 8, 5, 3, 4, 9, 7}

  1. 當步數縮小爲h=2時,數組分成了兩部分
[0]6 [2]2 [4]8 [6]3 [8]9
[1]0 [3]1 [5]5 [7]4 [9]7

進行插入排序後,獲得數組的排列順序{6, 0, 2, 1, 8, 5, 3, 4, 9, 7}

4.將步數縮短爲h=1,進行最後一步的插入排序,獲得數組排列順序{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}

4. 代碼
注意步長要設置好,有些博客在步長爲1時沒有考慮,寫的算法代碼是不可靠的。
從代碼上來看,希爾函數只是在普通插入函數外多了一成控制增量的循環而已

       /**
     * 希爾排序
     *
     * @param arr 需要排序的數組
     */
    void shellSort(int[] arr) {
        // 關於步長,取值沒有統一標準,必須小於size,最後一次步長要爲1
        int gap = 1;

        /* 計算首次步長,一般步數3比2好 */
        while (gap < arr.length / 3) {
            gap = 3 * gap;
        }
        while (gap >= 1) {
            // 插入排序
            insertionSort(arr, gap);
            gap = gap / 3;
        }
    }


   /**
     * @param arr 數組
     * @param gap 步長   gap=1 時就是一個普通的插入排序
     * @return 排序後的數組
     */
    int[] insertionSort(int[] arr, int gap) {
        int preIndex;
        int current;

        for (int i = gap; i < arr.length; i++) {
            preIndex = i - gap;
            current = arr[i];
            while (preIndex >= 0 && arr[preIndex] > current) {
                arr[preIndex + gap] = arr[preIndex];
                preIndex -= gap;
            }
            arr[preIndex + gap] = current;
        }
        return arr;
    }
void shell_sort() {
    int[] arr = new int[]{6, 5, 3, 1, 8, 7, 2, 4, 9, 0};
    int size = 10;

    int h = 1;
    /* 計算首次步長 */
    while (h < size / 3) {
        h = 3 * h + 1;
    }

    int i, j, temp;
    while (h >= 1) {
        for (i = h; i < size; ++i) {
            /* 將a[i]插入到a[i-h]、a[i-2h]、a[i-3h]...中 */
            for (j = i; j >= h && (arr[j] < arr[j - h]); j -= h) {
                temp = arr[j];
                arr[j] = arr[j - h];
                arr[j - h] = temp;
            }
        }

        /* 每輪內循環後輸出數組的現狀 */

// int k;
// printf(“the step=%d : “, h);
// for (k = 0; k < size; ++k) {
// printf(”%d “, arr[k]);
// }
// printf(”\n”);

        /* 計算下一輪步長 */
        h = h / 3;
    }
}

5. 算法分析
關於時間複雜度,步長的選取能決定希爾排序的效率,根據科學家的分析當相鄰增量之間的比例爲1:3時效果還行。

  • 也就是增量序列爲{1,4,13,40,121,364,1093,…}時效率較高。
  • {1,2,4,8,…} 2^i |^ 這種序列並不是很好的增量序列,它的時間複雜度(最壞情形)是O(n2)
  • {1,3,7,15…,2^k *^-1}, 這種序列的時間複雜是O(n1.5)
    在這裏插入圖片描述

關於穩定性,在多個分組多次插入排序的情況下可能會改變相同元素的相對位置,因爲它是跳躍排序,可能改變相同元素的前後位置,所以希爾排序是不穩定的排序,但插入排序是穩定

5. 歸併排序(Merge Sort)

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

1. 算法描述

  1. 把長度爲n的輸入序列分成兩個長度爲n/2的子序列;
  2. 對這兩個子序列分別採用歸併排序;
  3. 將兩個排序好的子序列合併成一個最終的排序序列。

歸併的核心:

  1. 爲每個需要排序的數組分配其對應長度的緩存數組
  2. 設定兩個指針,最初位置分別爲兩個已經排序序列的起始位置
  3. 比較兩個指針所指向的元素,選擇相對小的元素放入到合併空間,並移動指針到下一位置
  4. 重複步驟3直到某一指針超出序列尾
  5. 將另一序列剩下的所有元素直接複製到合併序列尾

2. 動畫演示
在這裏插入圖片描述

3. 代碼

    /**
     * 兩路歸併算法,兩個排好序的子序列合併爲一個子序列
     *
     * @param arr   需要合併的數組
     * @param left  左邊起始索引
     * @param mid   中間索引
     * @param right 右邊起始索引
     */
    public void merge(int[] arr, int left, int mid, int right) {
        //輔助數組
        int[] tmp = new int[right + 1 - left];
        //p1、p2是檢測指針,k是存放指針
        int p1 = left, p2 = mid + 1, k = 0;

        while (p1 <= mid && p2 <= right) {
            if (arr[p1] <= arr[p2]) {
                tmp[k++] = arr[p1++];
            } else {
                tmp[k++] = arr[p2++];
            }
        }

        //如果第一個序列未檢測完,直接將後面所有元素加到合併的序列中
        while (p1 <= mid) {
            tmp[k++] = arr[p1++];
        }

        //如果第二個序列未檢測完,直接將後面所有元素加到合併的序列中
        while (p2 <= right) {
            tmp[k++] = arr[p2++];
        }

        //複製回原素組
        if (right + 1 - left >= 0) {
            System.arraycopy(tmp, 0, arr, left, right + 1 - left);
        }
    }

    /**
     * 進行歸併排序
     *
     * @param arr        需要排序的數組
     * @param startIndex 數組的開始索引
     * @param endIndex   數組最後一個元素的索引
     */
    public void mergeSort(int[] arr, int startIndex, int endIndex) {
        //當子序列中只有一個元素時結束遞歸
        if (startIndex < endIndex) {
            //劃分子序列
            int mid = (startIndex + endIndex) / 2;
            //對左側子序列進行遞歸排序
            mergeSort(arr, startIndex, mid);
            //對右側子序列進行遞歸排序
            mergeSort(arr, mid + 1, endIndex);
            //合併
            merge(arr, startIndex, mid, endIndex);
        }
    }

4. 算法分析
歸併排序是一種穩定的排序方法。和選擇排序一樣,歸併排序的性能不受輸入數據的影響,但表現比選擇排序好的多,因爲始終都是O(nlogn)的時間複雜度。代價是需要額外的內存空間。

6. 快速排序 Quick Sort

快速排序是對冒泡排序的一種改進, 它是不穩定的。由C. A. R. Hoare在1962年提出的一種劃分交換排序,採用的是分治策略(一般與遞歸結合使用),以減少排序過程中的比較次數,它的最好情況爲O(nlogn),最壞情況爲O(n^2),平均時間複雜度爲O(nlogn)。

選擇一個基準數,通過一趟排序將要排序的數據分割成獨立的兩部分,其中一部分的所有數據都比另外一部分的所有數據都要小。
然後再按此方法對這兩部分數據分別進行快速排序,整個排序過程可以遞歸進行,以達到全部數據變成有序。

在計算機科學中,分治法就是運用分治思想的一種很重要的算法。分治法是很多高效算法的基礎,如快速排序,歸併排序,傅立葉變換(快速傅立葉變換)等等。

1.算法描述
快速排序使用分治法來把一個串(list)分爲兩個子串(sub-lists)。具體算法描述如下:

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

注意:基準元素/左遊標/右遊標都是針對單趟排序而言的, 也就是說在整個排序過程的多趟排序中,各趟排序取得的基準元素/左遊標/右遊標一般都是不同的。對於基準元素的選取,原則上是任意的,但是一般我們選取數組中第一個元素爲基準元素(假設數組隨機分佈)。

2. 動畫演示
在這裏插入圖片描述
3. 代碼

    /**
     * 1. 當low=height時,此時排序結束,數組已經有序了
     * 2. 獲取基準數pivot索引,一般第一個基準索引爲0
     * 3. 計算出基準值得所以後,此時左邊的數組都小於pivot,右邊的數據都大於pivot
     * 4. 採用歸併遞歸的思想,循環執行第3步,直到low-=height,此時排序完成
     *
     * @param arr  源數組
     * @param low  最低排序索引
     * @param high 最高排序索引
     */
    private void quickSort(int[] arr, int low, int high) {
        if (low < high) {
            // 找尋基準數據的正確索引
            int index = getPivotIndex(arr, low, high);
            // 進行迭代對index之前和之後的數組進行相同的操作使整個數組變成有序
            quickSort(arr, low, index - 1);
            quickSort(arr, index + 1, high);
        }
    }

    /**
     * 獲取 排序索引
     * 1.獲取數組arr的第一個數爲基準數pivot
     * 2.當low<height時,先從右到左掃描,如果arr[height]>=pivot,height--;
     * 如果arr[height]<pivot,它應該放在pivot左邊,此時執行arr[low] = arr[high],結束從右到左的掃描
     * 3.當low < high時,從右到左結束掃描後,開始從左到右的掃描。如果arr[low] <= pivot,low++繼續掃描;
     * 如果arr[low] > pivot,說明arr[low]應該放在pivot的右邊,此時執行arr[high] = arr[low],從右到左掃描結束
     * 4.while循環第一輪結束,如果low < high則繼續執行2、3操作查找pivot的索引,此時查找的範圍low~height已經縮小了
     * 5.直到low =height,此時low或者height就是pivot的索引
     *
     * @param arr    源數組
     * @param low    最低排序索引
     * @param height 最高排序索引
     * @return 基準值的索引位置
     */
    private int getPivotIndex(int[] arr, int low, int height) {
        // 基準數據
        int pivot = arr[low];
        while (low < height) {
            // 當隊尾的元素大於等於基準數據時,向前挪動high指針
            while (low < height && arr[height] >= pivot) {
                height--;
            }
            // 如果隊尾元素小於tmp了,需要將其賦值給low
            arr[low] = arr[height];
            // 當隊首元素小於等於tmp時,向前挪動low指針
            while (low < height && arr[low] <= pivot) {
                low++;
            }
            // 當隊首元素大於tmp時,需要將其賦值給high
            arr[height] = arr[low];
        }
        // 跳出循環時low和high相等,此時的low或high就是tmp的正確索引位置
        // 由原理部分可以很清楚的知道low位置的值並不是tmp,所以需要將tmp賦值給arr[low]
        arr[low] = pivot;
        return low;
    }

4. 算法分析
快速排序的核心是能準確有效的找到數組的基準數索引,我們以數組27,44,38,5,47,15,36,26,3,2,46,4,19,50,48爲例來演尋找pivot的過程
在這裏插入圖片描述

  • 第一次while循環
  1. 當從右到左掃描到19時,此時需要執行arr[low]=19。
    在這裏插入圖片描述
    在這裏插入圖片描述
  2. 切換爲從由左到右掃描,當比較44時,執行arr[height]=44。
    在這裏插入圖片描述
  3. 當1、2步執行完畢後如果low<height,繼續循環執行1、2步
  4. 當我們執行到如下情況時:
    在這裏插入圖片描述
    此時執行到從右向左掃描
    在這裏插入圖片描述
    此時low=height,分區操作完成。pivot的索引index=8。此時arr[low]=arr[height]=arr[8]。
  5. 循環執行1、2、3、4步驟即可完成快速排序。

基本上在任何需要排序的場景都可以使用快速排序,最好情況的時間複雜度爲O(nlogn),最壞情況時間複雜度爲O(n^2),但是由於基本不會出現,因此可以放心的使用快速排序。

最差情況下每一次取到的數(基準數)都是當前要比較的數中的最大/最小值,在這種情況下,每次都只能得到比上一次少1個數的子序列(即要麼全比基準數大,要麼全比基準小)。
此時相當於一個冒泡排序,比較的次數 = (n - 1) + (n - 2) + … + 2 + 1 = (n - 1) * n / 2,此時的時間複雜度爲:O(n^2)。最差情況一般出現在:待排序的數據本身已經是正序或反序排好了。

7. 堆排序 Heap Sort

堆排序(Heapsort)是指利用堆這種數據結構所設計的一種排序算法。關於堆的詳細介紹和堆如何構建請查看我的博文《》,詳細從源碼解析堆構建和排序的步驟和邏輯。

1.算法描述
1、將待排序的序列構造成一個大頂堆,根據大根堆的性質,當前堆的根節點就是序列中最大的元素;
2、將堆頂元素和最後一個元素交換,然後將剩下的節點重新構造成一個大頂堆;
3、重複步驟2,如此反覆,從第一次構建大頂堆開始,每一次構建,我們都能獲得一個序列的最大值,
然後把它放到大頂堆的尾部。最後就得到一個有序的序列。

2. 動畫演示
在這裏插入圖片描述
3. 代碼

    /**
     * 將指定堆構建成大堆根函數
     * 邏輯
     * 1. 如果起始索引無子節點,則跳出該方法
     * 2. 如果只有一個左子節點,進行大小比較並置換值
     * 3. 如果有兩個子節點,選擇最大值與父節點比較,然後置換其位置。
     * 如果子節點大於父節點,置換完成後,遞歸進行同樣操作,其子節點索引即是函數的start值
     *
     * @param array 源數組
     * @param start 起始索引
     * @param end   結尾索引
     */
    public void adjust(int[] array, int start, int end) {
        // 左子節點的位置
        int leftIndex = 2 * start + 1;
        if (leftIndex == end) {
            //只有一個左節點,進行比較並置換值
            if (array[leftIndex] > array[start]) {
                int temp = array[leftIndex];
                array[leftIndex] = array[start];
                array[start] = temp;
            }
        } else if (leftIndex < end) {
            //有兩個子節點
            int temp = array[leftIndex];
            int tempIndex = leftIndex;
            if (array[leftIndex + 1] > array[leftIndex]) {
                temp = array[leftIndex + 1];
                tempIndex = leftIndex + 1;
            }
            if (temp > array[start]) {
                array[tempIndex] = array[start];
                array[start] = temp;
            }
            adjust(array, tempIndex, end);
        }
    }

    /**
     * 堆排序
     *
     * @param array 源數組
     */
    public void heapSort(int[] array) {
        //從右向左,從下到上依次遍歷父節點,建立大根堆,時間複雜度:O(n*log2n)
        for (int i = (array.length - 1 - 1) / 2; i >= 0; i--) {
            adjust(array, i, array.length - 1);
        }

        int tmp;
        //要與root節點置換位置元素的索引
        int end = array.length - 1;
        //n個節點只用構建排序n-1次,最後只有1個元素不用在排序
        for (int i = array.length - 1; i > 0; i--) {
            tmp = array[0];
            array[0] = array[end];
            array[end] = tmp;

            end--;
            //頭尾置換後,將堆重新構建爲大堆根,置換尾部大元素不參加構建
            //因爲除了root節點,其他都是由大到小有序的,所以再次構建大根堆時,不用在進行adjust()前的那個循環
            adjust(array, 0, end);

        }
    }

4. 算法分析
堆排序是一種選擇排序,整體主要由構建初始堆+交換堆頂元素和末尾元素並重建堆兩部分組成。其中構建初始堆經推導複雜度爲O(n),在交換並重建堆的過程中,需交換n-1次,而重建堆的過程中,根據完全二叉樹的性質,[log2(n-1),log2(n-2)…1]逐步遞減,近似爲nlogn。所以堆排序時間複雜度一般認爲就是O(nlogn)級,空間複雜度O(1)。

8. 計數排序 Counting Sort

計數排序不是基於比較的排序算法,其核心在於將輸入的數據值轉化爲鍵存儲在額外開闢的數組空間中。 作爲一種線性時間複雜度的排序,計數排序要求輸入的數據必須是有確定範圍的整數。

1.算法描述

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

2. 動畫演示
在這裏插入圖片描述
3. 代碼

   /**
     * 計數算法,適合int類型數據排序,該方法有優化,支持有負數
     * 一種線性排序算法,不需要進行比較,時間複雜度爲O(n)。
     *
     * @param array 待排序數組
     * @param max   最大值
     * @param min   最小值
     * @return 返回排序後的數組
     */
    public int[] countSort(int[] array, int max, int min) {
        //1.爲了資源,數據不再從0開始,直接從最小到最大,創建臨時空間數組
        int[] coutArray = new int[max - min + 1];
        //2.對原始數組元素進行統計,對空間數組index=value - min進行自增,統計了同樣的數據
        for (int value : array) {
            coutArray[value - min]++;
        }

        //3.按順序,空間數組每個元素的值是多少則打印多少次
        int index = 0;
        for (int i = 0; i < coutArray.length; i++) {
            for (int j = 0; j < coutArray[i]; j++) {
                array[index++] = i + min;
            }
        }
        return array;
    }

4. 算法分析
計數排序是一個穩定的排序算法。當輸入的元素是 n 個 0到 k 之間的整數時,時間複雜度是O(n+k),空間複雜度也是O(n+k),其排序速度快於任何比較排序算法。當k不是很大並且序列比較集中時,計數排序是一個很有效的排序算法。

9. 桶排序 Bucket sort

桶排序又稱箱排序,是計數排序的升級版,同樣不屬於比較排序。其主要思想近乎徹底的分治思想,是鴿巢排序的一種歸納結果。其原理是:講待排序列(集合)中的元素分到數量有限的桶中,每個桶在進行排序。

對於桶的使用注意
1)在額外空間充足的情況下,儘量增大桶的數量
2)使用的映射函數能夠將輸入的 N 個數據均勻的分配到 K 個桶中 。該函數至關重要,高效與否的關鍵就在於這個映射函數的確定
3)待排列的數據必須是整數

1.算法描述
桶排序的思想原理:劃分多個範圍相同的區間,每個子區間自排序,最後合併。

  • 設置一定量的數組當空桶
  • 使用一定函數將數組儘可能均分到有限數量的桶裏。
  • 每個桶獲得數據後再分別排序(有可能再使用別的排序算法或是以遞歸方式繼續使用桶排序進行排序)。
  • 最後依次把各個桶中的數據取出得到有序序列。

2. 動畫演示
在這裏插入圖片描述

3. 代碼

    /**
     * 這是一個桶排序,建桶規則是將數據分到arr.length+1 個桶中,然後進行排序整理
     * 該代碼中如有9個數據,將數據期數據分到10個桶中。此分配規則不一樣,會影響排序效果
     *
     * @param arr 源數組
     */
    public static void bucketSort(int[] arr) {
        // 計算最大值與最小值
        int max = 0;
        int min = 0;
        for (int value : arr) {
            if (max < value) {
                max = value;
            }
            if (min > value) {
                min = value;
            }
        }

        // 計算桶的數量,並創建一個桶集合
        int bucketNum = (max - min) / arr.length + 1;
        List<List<Integer>> bucketArr = new ArrayList<>();
        for (int i = 0; i < bucketNum; i++) {
            bucketArr.add(new ArrayList<>());
        }

        // 將每個元素放入桶
        for (int i = 0; i < arr.length; i++) {
            int num = (arr[i] - min) / (arr.length);
            bucketArr.get(num).add(arr[i]);
        }

        // 對每個桶進行排序
        for (int i = 0; i < bucketArr.size(); i++) {
            Collections.sort(bucketArr.get(i));
        }

        // 將桶中的元素賦值到原序列
        int index = 0;
        for (int i = 0; i < bucketArr.size(); i++) {
            for (int j = 0; j < bucketArr.get(i).size(); j++) {
                arr[index++] = bucketArr.get(i).get(j);
            }
        }
    }

4. 實例演示
1)以63,157,189,51,101,147,141,121,157,156,194,117,98,139,67,133,181,113,158,109來爲例進行桶排序
2)獲取到最小值爲51,最大值194,構建桶將其分爲15組,以[10,20),[20,30)…[190,200)爲分組進行分配
3)對其進行分組後如圖
在這裏插入圖片描述
4)然後對非空桶進行排序,排序方法自己選定
在這裏插入圖片描述
5)最終獲得排序數組爲:51,63,67,98,101,109,113,117,121,133,139,141,147,156,157,157,158,181,189,194

5. 算法分析
桶排序是一種穩定排序,它的的最好時間複雜度爲O(n + k),最差時間複雜度是O(n ^ 2),其空間複雜度是O(n*k)。 影響桶排序的有兩大因素:

  • 對於桶中元素的排序,選擇何種比較排序算法對於性能的影響至關重要。

  • 桶劃分的大小問題,桶數越小,空間消耗越小,桶內元素越多,排序時間越大,反之亦反。在時間和空間消耗的平衡上,建桶建的好也至關重要。一般以10n和2n個數來建桶,或者取數列最大最小值均分建桶 。

建桶規則,也就是元素劃分到不同桶的映射規則需要有一定思考的設計。映射規則需要根據待排序集合的元素分佈特性進行選擇,若規則設計的過於模糊、寬泛,則可能導致待排序集合中所有元素全部映射到一個桶上,則桶排序向比較性質排序算法演變。若映射規則設計的過於具體、嚴苛,則可能導致待排序集合中每一個元素值映射到一個桶上,則桶排序向計數排序方式演化。

10. 基數排序(Radix Sort)

基數排序是桶排序的擴展,依然是一種非比較型數組,所以它可以突破比較排序時間NlogN。由於整數也可以表達字符串(比如名字或日期)和特定格式的浮點數,所以基數排序也不是隻能使用於整數。
1.算法描述

  1. 獲取數組中的最大值max,根據max的位長length,確定後面要切割排序length次
  2. 將數組arr中的每個元素按位數切割成不同的數字,由低位到高位切割排序。如先排序個位、十位、百位
  3. 切割後按獲得一位長度0-9的切割值index,按切割值將源數值加入對應桶0~9的第index個桶中
  4. 將每個非空桶中的數組按順序再次收集,改變源數組中元素的位置。從每個桶中收集數據時,從索引爲0開始收集,這點很重要。
  5. 重複2、3、4中的操作,直到將最高位也切割排序完畢,最終得到的數組arr就是經過排序的數組

2. 動畫演示
在這裏插入圖片描述
3. 案例演示
以待排數據50,21,34,104,235,5,15,36,126,46,7,527,35,48,19爲例,其最大數527的,位長3,需要桶排序3次,比較個位、十位、百位上的數據
1)第一次排序,比較個位數據。
在這裏插入圖片描述
2)第一次排序後收集數組:50,21,34,104,235,5,15,35,36,126,46,7,527,48,19
3)進行十位數上的排序
在這裏插入圖片描述
4)第二次排序完成後收集的數組:104,5,7,15,19,21,126,527,34,35,235,36,46,48,50
5)進行第三次,百位上的排序
在這裏插入圖片描述
6)第二次排序完成後收集的數組:5,7,15,19,21,34,35,36,46,48,50,104,126,235,527

3. 代碼

/**
     * 基數排序,該排序被優化,可以支持負數排序
     *
     * @param array 源數組
     */
    public void radixSort(int[] array) {
        //1. 獲取最大、最小值
        int max = 0;
        int min = 0;

        for (int value : array) {
            if (max < value) {
                max = value;
            }
            if (min > value) {
                min = value;
            }
        }
        //兼容負數排序
        int absMax = max - min;

        //2. 獲取最大值的位數長;
        int times = 0;
        while (absMax > 0) {
            absMax /= 10;
            times++;
        }

        //3. 由於數字的特殊性,數字尾數必定是0~9,可創建10個桶,用於分裝數據
        List<List<Integer>> queue = new ArrayList<>();
        for (int i = 0; i < 10; i++) {
            List<Integer> q = new ArrayList<>();
            queue.add(q);
        }

        //開始比較,重點
        for (int i = 0; i < times; i++) {
            reconstruction(queue, array, i, min);
        }
    }

    /**
     * 數組位置重構
     * 根據i=0,1,2,將個位,十位,百位的數據重新分組爲
     * 【1,11  2,22,42,132】、【1,2  11  22 132 42】、【1,2,11,22,42,132】
     *
     * @param queue 桶集合
     * @param array 源數組
     * @param i     數字位數,如 12345,i=1,2,3,4,5
     */
    private void reconstruction(List<List<Integer>> queue, int[] array, int i, int min) {
        for (int j = 0; j < array.length; j++) {
            // 當j=0,1,2 時分別取的是數字個位,十位,百位上的數據
            // array[j] - min是爲了兼容負數
            int x = (array[j] - min) % (int) Math.pow(10, i + 1) / (int) Math.pow(10, i);
            // 將指定位上爲X(x=0~9),放入第x個桶中
            List<Integer> q = queue.get(x);
            q.add(array[j] - min);
        }
        // 當位數i時,從0桶到9桶,一個個取出數據放入源數組中,此時數組順序第一次改變了
        int count = 0;
        for (int j = 0; j < 10; j++) {
            while (queue.get(j).size() > 0) {
                List<Integer> q = queue.get(j);
                array[count++] = q.get(0) + min;
                q.remove(0);
            }
        }
    }

4. 算法分析
基數排序是一種穩定排序,它的時間複雜度是平均、最好、最壞都爲O(k*n),其中k爲常數(最大值位長),n爲元素個數。空間複雜度是O(n+x),其中x爲桶的數量,一般來說n>>x,因此額外空間需要大概n個左右。

基數排序有兩種排序方式:LSD和MSD,最小位優先(從右邊開始)和最大位優先(從左邊開始)

總結

基數排序 VS 計數排序 VS 桶排序

  1. 此三種排序都是非比較排序,時間複雜度能打破NlogN的限制

  2. 這三種排序算法都利用了桶的概念,但對桶的使用方法上有明顯差異:
    1)基數排序:根據鍵值的每位數字來分配桶
    2)計數排序:每個桶只存儲單一鍵值
    3)桶排序:每個桶存儲一定範圍的數值

  3. 基數排序的性能比桶排序要略差,每一次關鍵字的桶分配都需要O(n)的時間複雜度,而且分配之後得到新的關鍵字序列又需要O(n)的時間複雜度。

  4. 桶排序是對計數排序的改進,計數排序申請的額外空間跨度從最小元素值到最大元素值,若待排序集合中元素不是依次遞增的,則必然有空間浪費情況。桶排序則是弱化了這種浪費情況,桶排序爲最小值到最大值之間每一個固定區域申請空間,儘量減少了元素值大小不連續情況下的空間浪費情況。

快速排序 VS 桶排序

  • 快速排序是將集合拆分爲兩個值域,這裏稱爲兩個桶,再分別對兩個桶進行排序,最終完成排序。桶排序則是將集合拆分爲多個桶,對每個桶進行排序,則完成排序過程。

  • 兩者不同之處在於,快排是在集合本身上進行排序,屬於原地排序方式,且對每個桶的排序方式也是快排。 桶排序則是提供了額外的操作空間,在額外空間上對桶進行排序,避免了構成桶過程的元素比較和交換操作,同時可以自主選擇恰當的排序算法對桶進行排序。

博客書寫不易,您的點贊收藏是我前進的動力,千萬別忘記點贊、 收**藏 ^ _ ^ !

相關推薦
1. 心中有堆:https://blog.csdn.net/luo_boke/article/details/106928990
2. 心中有棧:https://blog.csdn.net/luo_boke/article/details/106982563
3. 心中有樹——基礎:https://blog.csdn.net/luo_boke/article/details/106980011

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章