1. 程式人生 > >十種排序方法

十種排序方法

什麼是演算法的穩定性?

簡單的說就是一組數經過某個排序演算法後仍然能保持他們在排序之前的相對次序就說這個排序方法是穩定的, 比如說,a1,a2,a3,a4四個數, 其中a2=a3,如果經過排序演算法後的結果是 a1,a3,a2,a4我們就說這個演算法是非穩定的,如果還是原來的順序a1,a2,a3,a4,我們就會這個演算法是穩定的

1.選擇排序

選擇排序,顧名思義,在迴圈比較的過程中肯定存在著選擇的操作, 演算法的思路就是從第一個數開始選擇,然後跟他後面的數挨個比較,只要後面的數比它還小,就交換兩者的位置,然後再從第二個數開始重複這個過程,最終得到從小到大的有序陣列

演算法實現:

public static void select(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[minIndex]>arr[j]){
                 // 記錄的是下標,而不是著急直接交換值,因為後面可能還有更小的
                 minIndex=j;
             }
         }
        // 當前最小的數字下標不是一開始我們標記出來的那個,交換位置
        if (minIndex!=i){
            int temp=arr[minIndex];
            arr[minIndex]=arr[i];
            arr[i]=temp;
        }
    }
}
}

時間複雜度: n + n-1 + n-2 + .. + 2 + 1 = n*(n+1)/2 = O(n^2)

穩定性: 比如: 5 8 5 2 7 經過一輪排序後的順序是2 8 5 5 7, 原來兩個5的前後順序亂了,因此它不穩定

推薦的使用場景: n較小時

輔助儲存: 就一個數組,結果是O(1)

2.插入排序

見名知意,在排序的過程中會存在插入的情況,依然是從小到大排序 演算法的思路: 選取第二個位置為預設的標準位置,檢視當前這個標準位置之前有沒有比它還大的元素,如果有的話就通過插入的方式,交換兩者的位置,怎麼插入呢? 就是找一箇中間變數記錄當前這個標準位置的值,然後讓這個標準位置前面的元素往前移動一個位置,這樣現在的標準位置被新移動過來的元素給佔了,但是前面空出來一個位置, 於是將這個存放標準值的中間元素賦值給這個空出來的位置,完成排序

程式碼實現:

/**
 * 思路: 從陣列的第二個位置開始,選定為標準位置,這樣開始的話,可以保證,從標準位置開始往前全部是有序的
 * @param arr
 */
private static void insetSort(int[] arr) {
    int temp;
    // 從第二個開始遍歷index=1 , 一共length 個數,一直迴圈到,最後一個數參加比較, 就是 1  <->  length 次
    for (int i=1;i<arr.length;i++){
        // 判斷大小,要是現在的比上一個小的話,準備遍歷當前位置以前的有序陣列
        if (arr[i] < arr[i-1]){
            // 存放當前位置的值
             temp=arr[i];
            int j;
            // 迴圈遍歷當前位置及以前的位置的有序陣列,只要是從當前位置開始,前面的數比當前位置的數大,就把這個大的數替插入到當前的位置
            // 隨著j的減少,實際上每次迴圈都是前一個插到後一個的位置上
            for (j=i-1;j>=0&&temp<arr[j];j--){
                arr[j+1]=arr[j];
            }
            // 直到找出一個數, 不比原來儲存的那個當前的位置的大,就把存起來的數,插到這個數的前面
            arr[j+1]=temp;
        }
    }
}

時間複雜度:

  • 最好的情況就是: 陣列本來就是有序的, 這樣演算法從第2個位置開始迴圈n-1次, 時間複雜度就是 n
  • 最壞的情況: 外層 n-1次, 記憶體分別是 1,2,3,4...n-2 ==> (n-2)(n-1)/2 = O(n^2)
  • 平均時間複雜度: O(n^2)

穩定性: 從第2個位置開始遍歷,標準位之前陣列一直是有序的,所以它肯定穩定

輔助儲存: O(1)

推薦的使用場景: n大部分有序時

3.氣泡排序

最熟悉的排序方式

  • 從零位的數開始,比較總長度-1大輪
  • 每一個大輪比較 總長度-1-當前元素的index 小輪

程式碼實現:

private static void sort(int[] ints) {
    System.out.println("length == "+ints.length);
    // 控制比較多少輪 0- length-1  一共length個數,最壞比較lenth-1次
    for (int i=0;i<ints.length-1;i++){
        // 每次都從第一個開始比較,每次比較的時候,最多比較到上次比較的移動的最新的下標的位置,就是  length-0-i
        for (int j=0;j<ints.length-1-i;j++){
            int temp;
            if (ints[j] > ints[j+1]) {
                temp=ints[j];
                ints[j]=ints[j+1];
                 ints[j+1]=temp;
            }
        }
    }
}

時間複雜度:

  • 做好的情況下: 陣列本身就有序,執行的就是最外圈的for迴圈中的n次
  • 最壞的情況: (n-1) + (n-2) + (n-3) +...+ 1 = n(n-1)/2 當n趨近於無限大時, 時間發雜度 趨近於n^2

穩定性: 穩定

輔助儲存空間: O(1)

推薦的使用場景: n越小越好

4.歸併排序

演算法的思路: 先通過遞迴將一個完整的大陣列[5,8,3,6]拆分成小陣列, 經過遞迴作用,最終最小的陣列就是[5],[8],[3],[6]

遞迴到最底層後就會有彈棧的操作,通過這時建立一個臨時陣列,將這些小陣列中的數排好序放到這個臨時陣列中,再用臨時陣列中的數替換待排序陣列中的內容,實現部分排序, 重複這個過程

/**
 *  為什麼需要low height 這種引數,而不是通過 arr陣列計算出來呢?
 *          --> 長個心,每當使用遞迴的時候,關於陣列的這種資訊寫在引數位置上
 * @param arr  需要排序的陣列
 * @param low  從哪裡開始
 * @param high 排到哪裡結束
 */
public static void mergeSort(int[] arr, int low, int high) {
    int middle = (high + low) / 2;
    if (low < high) {

        // 處理左邊;
        mergeSort(arr, low, middle);

        // 處理右邊
        mergeSort(arr, middle + 1, high);

        // 歸併
        merge(arr, low, middle, high);
    }
}

/**
 * @param arr
 * @param low
 * @param middle
 * @param high
 */
public static void merge(int[] arr, int low, int middle, int high) {
    // 臨時陣列,儲存歸併後的陣列
    int[] temp = new int[high - low + 1];

    // 第一個陣列開始的下標
    int i = low;

    // 第二個陣列開始遍歷的下標
    int j = middle + 1;

    // 記錄臨時陣列的下標
    int index = 0;

    // 遍歷兩個陣列歸併
    while (i <= middle && j <= high) {
        if (arr[i] < arr[j]) {
            temp[index] = arr[i];
            i++;

        } else {
            // todo 在這裡放個計數器++ , 可以計算得出 反序對的個數 (這樣的好處就是時間的複雜度是  nlogn)
            temp[index] = arr[j];
            j++;
        }
        index++;
    }

    while (j <= high) {
        temp[index] = arr[j];
        j++;
        index++;
    }
    while (i <= middle) {
        temp[index] = arr[i];
        i++;
        index++;
    }
    // 把臨時入陣列中的資料重新存原陣列
    for (int x = 0; x < temp.length; x++) {
        System.out.println("temp[x]== " + temp[x]);
        arr[x + low] = temp[x];
    }
}

按上面說的[5,8,3,6]來說,經過遞迴他們會被分成這樣

---------------壓棧--------------------
 左             右
[5,8]         [3,6]
[5] [8]      [3] [6]
------------下面的彈棧-----------------

[5]和[8]歸併,5<8  得到結果:   [5,8] 

[3]和[6]歸併, 3<6 得到結果    [3,6]

於是經過兩輪歸併就得到了結果
[5,8]         [3,6]


繼續歸併: 建立一個臨時陣列 tmp[]
5>3  tmp[0]=3
5<6  tmp[1]=5
8>6  tmp[2]=6

tmp[3]=8

然後讓tmp覆蓋原陣列得到最終結果

推薦使用的場景: n越大越好

時間複雜度: 最好,平均,最壞都是 O(nlogn) (這是基於比較的排序演算法所能達到的最高境界)

穩定效能: 穩定

空間複雜度: 每兩個有序序列的歸併都需要一個臨時陣列來輔助,因此是 O(N)

5.快速排序

是分支思想的體現, 演算法的思路就是每次都選出一個標準值,目標是經過處理,讓當前陣列中,標準值左邊的數都小於標準值, 標準值右邊的數都大於標準值, 重複遞迴下去,最終就能得到有序陣列

實現:

// 快速排序
public static void quickSort(int[] arr, int start, int end) {
    // 保證遞迴安全的進行
    if (start < end) {
        // 1. 找一箇中間變數,記錄標準值,一般是陣列的第一個,以這個標準的值為中心,把陣列分成兩部分
        int strand = arr[start];

        // 2. 記錄最小的值和最大的值的下標
        int low = start;
        int high = end;

        // 3. 迴圈比較,只要我的最開始的low下標,小於最大值的下標 就不停的比較
        while (low < high) {
            System.out.println("high== " + high);
            // 從右邊開始,如果右邊的數字比標準值大,把下標往前動
            while (low < high && strand <= arr[high]) {
                high--;
            }
            // 右邊的最high的數字比 標準值小, 把當前的high位置的數字賦值給最low位置的數字
            arr[low] = arr[high];

            // 接著從做low 的位置的開始和標準值比較, 如果現在的low位置的陣列比標準值小,下標往後動
            while (low < high && arr[low] <= strand) {
                low++;
            }
            // 如果low位置的數字的比 標準值大,把當前的low位置的數字,賦值給high位置的數字
            arr[high] = arr[low];

        }
        // 把標準位置的數,給low位置的數
        arr[low] = strand;

        // 開始遞迴,分開兩個部分遞迴
        // 右部分
        quickSort(arr, start, low);
        // 左部分
        quickSort(arr, low + 1, end);
    }
}

推薦使用場景: n越大越好

時間複雜度:

  • 最壞: 其實大家可以看到,上面的有三個while,但是每次工作的最多就兩個,如果真的就那麼不巧,所有的數兩兩換值,那麼最壞的結果和冒泡一樣 O(n^2)
  • 最好和平均都是: O(nlogn)

穩定性: 快排是不穩定的

6.希爾排序

希爾排序其實可以理解成一種帶步長的排序方式, 上面剛說了插入排序的實現方式,上面說我們預設從陣列的第二個位置開始算,實際上就是說步長是1,下標的移動每次都是1

對於希爾排序來說,它預設的步長是 arr.length/2 , 每次步長都減少一半, 最終的步長也會是1

程式碼實現:

/**
 *  希爾排序,在插入排序的基礎上,添加了步長 ,
 *  // todo 只要在本步長範圍內,這些數字為組成一個組進行 插入排序
 *  初始  步長=length/2
 *  後來: 步長= 步長/2
 *  直到最後: 步長=1; 正宗的插入排序
 * @param ints
 */
private static void shellSort(int[] ints) {
    // 記錄當前的步長
    int step=ints.length/2;

    // 步長==》控制遍歷幾輪, 並且每次步長都是上一次的一半大小
    for (int i=step;i>0;i/=2){

        // 遍歷當前步長下面的全部組,這裡的j1, 就相當於插入排序中第一次開始的位置1前面的0下標
        for (int j1=i;j1<ints.length;j1++){
            
            // 遍歷本組中全部元素 == > 從第二個位置開始遍歷
            // x 就相當於插入排序中第一次開始的位置1
            for(int x=j1+i;x<=ints.length-1;x+=i){

                // 從當前組的第二個元素開始,一旦發現它前面的元素比他小,
                // 插入排序, 1. 把當前的元素存起來  2. 迴圈它前面的元素往前移 3. 把當前元素插入到合適的位置
                if(ints[x]<ints[x-i]){
                    int temp=ints[x];
                    int j ;
                    for(j=x-i;j>=0&&ints[j]>temp;j=j-i)
                    {
                        ints[j+i]=ints[j];
                    }
                    ints[j+i]=temp;
                }
            }
        }
}

}

空間複雜度: 和插入排序一樣都是O(1)

穩定性: 希爾排序由於步長的原因,而不向插入排序,一經開始標準位置前的陣列即刻有序, 所以希爾排序是不穩定的

希爾排序的效能無法準確量化,跟輸入的資料有很大關係在實際應用中也不會用它,因為十分不穩定,雖然比傳統的插入排序快,但比快速排序等慢

其時間複雜度介於O(nlogn) 到 O(n^2) 之間

7.堆排序

堆排序是藉助了堆這種資料結構完成的排序方式,堆有大頂堆和小頂堆, 將陣列轉換成大頂堆然後進行排序的會結果是陣列將從小到大排序,小叮噹則相反

什麼是堆呢? 堆其實是可以看成一顆完全二叉樹的陣列物件, 那什麼是完全二叉樹呢? 比如說, 這顆數的深度是h,那麼除了h層外, 其他的1~h1層的節點都達到了最大的數量

演算法的實現思路: 通過遞迴,將陣列看成一個堆,從最後一個非葉子節點開始,將這個節點轉換成大頂堆, 什麼是大頂堆呢? 就是根節點總是大於它的兩個子節點, 重複這個過程一直遞迴到堆的根節點(此時根節點是最大值),此時整個堆為大頂堆, 然後交換根節點和最後一個葉子節點的位置,將最大值儲存起來

例: 假設待排序序列是a[] = {7, 1, 6, 5, 3, 2, 4},並且按大根堆方式完成排序

  • 第一步(構造初始堆):

  • 第二步(首尾交換,斷尾重構):

點選檢視例子參考連結:

程式碼實現:


    public static void sort(int[] ints) {
        // 開始位置是最後一個非葉子節點
        int start = ints.length / 2-1;

        // 調整成大頂堆
        for (int i = start; i >= 0; i--) {
            maxHeap(ints, ints.length, i);
        }

        // 調整第一個和最後一個數字, 在把剩下的轉換為大定堆,  j--實現了,不再調整本輪選出的最大的數
        for (int j = ints.length - 1; j > 0; j--) {
            int temp = ints[0];
            ints[0] = ints[j];
            ints[j] = temp;

            maxHeap(ints, j, 0);
        }


    }

    /**
     * 轉換為大頂堆, 其實就是比較根節點和兩個子節點的大小,調換他們的順序使得根節點的值大於它的兩個子節點
     *
     * @param arr
     * @param size
     * @param index  從哪個節點開始調整  (一開始轉換為大頂堆時,使用的是最後一個非夜之節點, 但是轉換完成之後,使用的就是0,從根節點開始調整)
     */
    public static void maxHeap(int[] arr, int size, int index) {
        // 當前節點的左子節點
        int leftNode = 2 * index + 1;
        // 當前節點的右子節點
        int rightNode = 2 * index + 2;
        // 找出 當前節點和左右兩個節點誰最大
        int max = index;
        if (leftNode < size && arr[leftNode] > arr[max]) {
            max = leftNode;
        }
        if (rightNode < size && arr[rightNode] > arr[max]) {
            max = rightNode;
        }

        // 交換位置
        if (max != index) {
            int temp = arr[index];
            arr[index] = arr[max];
            arr[max] = temp;
            // 交換位置後,可能破壞之前的平衡(跟節點比左右的節點小),遞迴
            // 有可能會破壞以max為定點的子樹的平衡
            maxHeap(arr, size, max);
        }
    }

推薦的使用場景: n越大越好

時間複雜度: 堆排序的效率與快排、歸併相同,都達到了基於比較的排序演算法效率的峰值(時間複雜度為O(nlogn))

空間複雜度: O(1)

穩定性: 不穩定

基數排序

演算法思路:

看上圖中的綠色部分, 假設我們有下標從0-9,一共10個桶

第一排是給我們排序的一組數

我們分別對取出第一排數的個位數,放入到對應下標中的桶中,再依次取出,就得到了第三行的結果, 再取出三行的十位數,放入到桶中,再取出,就得到最後一行的結果

// 基數排序
// 建立10個數組,索引從0-9
// 第一次按照個位排序
// 第二次按照十位排序
// 第三次按照百位排序
// 排序的次數,就是陣列中最大的數的位數
public static void radixSort(int[] arr){

    int max = Integer.MIN_VALUE;

    // 迴圈找到最大的數,控制比較的次數
    for (int i : arr) {
        if (i>max){
            max=i;
        }
    }

    System.out.println(" 最大值: "+max);
    // 求最大數字的位數,獲取需要比較的輪數
    int maxLength = (max+"").length();
    System.out.println(" 最大串的長度: "+maxLength);

    // 建立應用建立臨時資料的陣列, 整個大陣列中存放著10個小陣列, 這10個小陣列中真正存放的著元素
    int [][] temp = new int[10][arr.length]; // 10個 長度為arr.length長度的陣列
    // todo 用於記錄在temp中相應的陣列中存放的數字的數量
    int [] counts = new int[10];

    // 還需要新增另一個變數n 因為我們每輪的排序是從的1 - 10 - 100 - ... 開始求莫排序
    for(int i=0,n=1;i<maxLength;i++,n*=10){
        // 計算每一個數字的餘數,遍歷陣列,將符合要求的放到指定的陣列中
        for (int j=0;j<arr.length;j++){
            int x = arr[j]/n%10;
            // 把當前遍歷到的資料放入到指定位置的二維陣列中
            temp[x][counts[x]] = arr[j];
            // 更新二維陣列中新更改的陣列後的 新長度
            counts[x]++;
        }

        int index =0;
        // 把存放進去的資料重新取出來
        for (int y=0;y<counts.length;y++){
            // 記錄數量的那個陣列的長度不為零,我們才區取資料
            if (counts[y]!=0){
                // 迴圈取出元素
                for (int z =0;z<counts[y];z++){
                    // 取出
                    arr[index++] = temp[y][z];
                }
            // 把數量置為0
                counts[y]=0;
            }
        }
    }

}

穩定性: 穩定

時間複雜度: 平均、最好、最壞都為O(k*n),其中k為常數,n為元素個數

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

桶排序

演算法思路: 相對比較好想, 給我們一組數,我們在選出這組數中最大值和陣列的length的長度,選最大的值當成新陣列的長度,然後遍歷舊的陣列,將舊陣列中的值放入到新陣列中index=舊陣列中的值的位置

然後一次遍歷舊陣列中的值,就能得到最終的結果

程式碼實現:


private static void sort(int[] ints) {
    int max = Integer.MIN_VALUE;

    for (int anInt : ints) {
        if (anInt>max)
            max=anInt;
    }
    int maxLength = ints.length-max >0 ? ints.length:max;

    int[] result = new int[maxLength];

    for (int i=0;i<ints.length;i++){
        result[ints[i]] +=1 ;
    }

    for(int i=0 ,index = 0;i<result.length;i++){
        if (result[i]!=0){
            for (int j=result[i];j>0;j--){
              ints[index++] = i;
            }
        }
    }
}

時間複雜度:

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

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

穩定性:穩定

典型的用空間換時間的演算法

計數排序

演算法思路: 根據待排序集合中最大元素和最小元素的差值範圍,申請額外空間;
遍歷待排序集合,將每一個元素出現的次數記錄到元素值對應的額外空間內;
對額外空間內資料進行計算,得出每一個元素的正確位置;
將待排序集合每一個元素移動到計算得出的正確位置上。

程式碼實現:

public static int[] sort(int[] A) {
    //一:求取最大值和最小值,計算中間陣列的長度:中間陣列是用來記錄原始資料中每個值出現的頻率
    int max = A[0], min = A[0];
    for (int i : A) {
        if (i > max) {
            max = i;
        }
        if (i < min) {
            min = i;
        }
    }

    //二:有了最大值和最小值能夠確定中間陣列的長度
    //儲存5-0+1 = 6
    int[] pxA = new int[max - min + 1];

    //三.迴圈遍歷舊陣列計數排序: 就是統計原始陣列值出現的頻率到中間陣列B中
    for (int i : A) {
        pxA[i - min] += 1;//數的位置 上+1
    }

    //四.遍歷輸出
    //建立最終陣列,就是返回的陣列,和原始陣列長度相等,但是排序完成的
    int[] result = new int[A.length];
    int index = 0;//記錄最終陣列的下標

    //先迴圈每一個元素  在計數排序器的下標中
    for (int i = 0; i < pxA.length; i++) {
        //迴圈出現的次數
        for (int j = 0; j < pxA[i]; j++) {//pxA[i]:這個數出現的頻率
            result[index++] = i + min;//原來原來減少了min現在加上min,值就變成了原來的值
        }
    }
    return result;
}

也是典型的用空間換時間的演算法

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

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

穩定性: 穩定