圖解排序算法,這五種最熱門!


文章首發於公眾號「陳樹義」及個人博客 shuyi.tech,歡迎關注訪問。

說到排序算法,大家估計都比較熟悉,但要你一下子寫出來又蒙圈了。所以這篇文章不會講解所有的排序算法,而是挑選最熱門的五種:冒泡排序、選擇排序、插入排序、快速排序、歸並排序。

我們通過圖文 + 流程解釋 的方式,讓大家能快速領悟到各個排序算法的思想,從而達到快速掌握的目的。此外每個排序算法都有對應的 Github 代碼實現,可供大家調試理解算法。同時也附上了文章中所畫圖的 draw.io 數據文件,方便大家根據自己的習慣進行修改。

排序算法的倉庫地址:java-code-chip/src/main/java/tech/shuyi/javacodechip/sort at master · chenyurong/java-code-chip

如果你已經不是第一次學習排序算法,那么我建議你按照這樣的思路學習:

  1. 通過圖解或調試,弄清楚每個算法的思想。
  2. 下載 Github 例子,嘗試自己手寫實現。
  3. 定期復習手寫實現,不斷鞏固知識點。

好了,廢話不多說,讓我們開始今天的圖解排序算法吧!

選擇排序

選擇排序,意思是每次從待排序的元素選出極值作為首元素,直到所有元素排完為止。 其詳細的排序邏輯如下圖所示:

  1. 第 1 次,index 下標對應值為 9,找出所有最小值為 1,將 9 與 1 交換位置,得到 ②。同時,index 下標加一。
  2. 第 2 次,index 下標對應值為 3,找出所有最小值為 3,將 3 與 2 交換位置,得到 ③。同時,index 下標加一。
  3. 第 3 次,index 下標對應值為 9,找出所有最小值為 3,將 9 與 3 交換位置,得到 ④。同時,index 下標加一。
  4. 一直這樣循環下去,直到 index 下標到達數組邊界,如 ⑥ 所示。

注意:灰色部分表示已經完成排序的部分。

選擇排序的算法比較簡單,如下所示:

public static void selectSort(int[] arr) {
    for (int i = 0; i < arr.length - 1; i++) {
        int min = i;//每一趟循環比較時,min用於存放較小元素的數組下標,這樣當前批次比較完畢最終存放的就是此趟內最小的元素的下標,避免每次遇到較小元素都要進行交換。
        for (int j = i + 1; j < arr.length; j++) {
            if (arr[j] < arr[min]) {
                min = j;
            }
        }
        //進行交換,如果min發生變化,則進行交換
        if (min != i) {
            swap(arr,min,i);
        }
    }
}

可調式代碼地址:java-code-chip/SelectSort.java at master · chenyurong/java-code-chip

簡單選擇排序通過上面優化之后,無論數組原始排列如何,比較次數是不變的;對於交換操作,在最好情況下也就是數組完全有序的時候,無需任何交換移動,在最差情況下,也就是數組倒序的時候,交換次數為n-1次。綜合下來,時間復雜度為O(n2)。

冒泡排序

冒泡排序,就是像池塘里的水泡一樣往上冒泡。我們可以理解成一個數不斷地往上冒泡(比較交換),一直到最上面(末尾)。通過不斷往上冒泡,每次冒泡都會將最值浮到最上層,最終達到完全有序。 其詳細的排序算法邏輯如下:

  1. 第 1 輪,9 大於 3,那么將 9 與 3 交換,接着繼續往下比較。9 大於 1,那么將 9 與 1 交換,接着往下比較,最終我們將 9 浮到數組頂端。此時 index 指向數組頂端,該數是有序的了,因此 index 減一。
  2. 第 2 輪,3 大於 1,那么 3 與 1 交換,接着往下比較。最終,我們只需要比較到 index 位置即可,最終我們將 7 浮到數組頂端。同時 index 也減一,此時 7、9 是有序的。
  3. 如此這樣反復循環,直到 index 下標達到 0 即可。

在冒泡排序的過程中,如果某一趟執行完畢,沒有做任何一次交換操作,那么就說明剩下的序列已經是有序的了。例如數組[5,4,1,2,3],執行了兩次冒泡之后,其數組變為 [1,2,3,4,5]。此時,index 下標指向 3 這個值。再執行第三次冒泡時,我們會發現 1<2<3,我們一次交換都沒有做,這就說明剩下的序列已經是有序的,排序操作已經完成,不需要再進行排序了。

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

可調試代碼地址:java-code-chip/BubbleSort.java at master · chenyurong/java-code-chip

插入排序

插入排序,即將元素一個個插入新的數組系列中,直到所有元素插完為止。 例如下圖的例子,第 1 次將元素 9 插入新的數組中

/**
 * 插入排序
 *
 * @param arr
 */
public static void insertSort(int[] arr) {
    for (int i = 1; i < arr.length; i++) {
        int j = i;
        while (j > 0 && arr[j - 1] > arr[j]) {
            swap(arr,j,j-1);
            j--;
        }
    }
}

可調式代碼地址:java-code-chip/InsertSort.java at master · chenyurong/java-code-chip

簡單插入排序在最好情況下,需要比較n-1次,無需交換元素,時間復雜度為O(n);在最壞情況下,時間復雜度依然為O(n2)。但是在數組元素隨機排列的情況下,插入排序還是要優於上面兩種排序的。

快速排序

快速排序,顧名思義其排序效率非常高,所以才叫快速排序。快速排序的核心思想是選取一個基准數,通過一趟排序將小於此基准數的數字放到左邊,將大於此基准數的數字放到右邊。之后再用遍歷不斷地對左右子串進行同樣的操作,從而達到排序的目的。 快速排序的時間復雜度在最壞情況下是 O(N2),平均的時間復雜度是 O(N*lgN)。

例如下圖中的例子,在第一趟排序里,我們選中了基准數為 9,那么此次排序就把所有比 9 小的數放到了左邊,所有比 9 大的數放到了右邊。在第二趟排序里,我們選中了基准數為 7,那么此次排序就把所有比 7 小的數放到了左邊,所有比 7 大的數放到了右邊。

我們以對 9 3 1 4 2 7 整數串進行排序為例,詳細講解整個快速排序的流程:

  1. 選取 9 為基准數,從 right 開始,從右到左找出第一個小於 9 的數。
  2. 第一個數是 7,小於 9,符合條件。於是將找到的這個數值放到 left 位置上,同時 left 加一。
  3. 從 left 開始,從左到右選取第一個大於 9 的數。
  4. 可以看到子串中並沒有一個大於 9 的數,於是 left 會一直累加到 right 的位置。
  5. 當 left >= right 時,單趟排序結束,將基准數填入 left 所在位置。
  6. 最終整個字符串被以 9 為基准數,切割成兩部分,左邊部分比 9 小,右邊部分比 9 大。

接着進行第二次排序,第二次排序的整體流程如下:

  1. 選取 7 為基准數,從 right 開始,從右到左找出第一個小於 9 的數。
  2. 第一個數是 2,小於 9,符合條件。於是將找到的這個數值放到 left 位置上,同時 left 加一,此時數組變為:2 3 1 4 2 9。
  3. 從 left 開始,從左到右選取第一個大於 9 的數。
  4. 可以看到子串中並沒有一個大於 9 的數,於是 left 會一直累加到 right 的位置。
  5. 當 left >= right 時,單趟排序結束,將基准數填入 left 所在位置。
  6. 最終整個字符串被以 7 為基准數,切割成兩部分,左邊部分比 7 小,右邊部分比 7 大。

剩余的子串都進行同樣的處理邏輯,最終我們可以得到一個排序的整數串。

-w405

代碼實現:

/**
 * v
 * @param arr -- 待排序的數組
 * @param l -- 數組的左邊界(例如,從起始位置開始排序,則l=0)
 * @param r -- 數組的右邊界(例如,排序截至到數組末尾,則r=arr.length-1)
 */
public static void quickSort(int arr[], int l, int r) {
    if (l < r) {
        int i,j,x;

        i = l;
        j = r;
        x = arr[i];
        while (i < j)
        {
            // 從右向左找第一個小於x的數
            while(i < j && arr[j] > x) {
                j--;
            }
            if(i < j) {
                arr[i] = arr[j];
                i++;
            }
            // 從左向右找第一個大於x的數
            while(i < j && arr[i] < x) {
                i++;
            }
            if(i < j) {
                arr[j] = arr[i];
                j--;
            }
        }
        arr[i] = x;
        quickSort(arr, l, i-1);
        quickSort(arr, i+1, r);
    }
}

可調式代碼地址:java-code-chip/QuickSort.java at master · chenyurong/java-code-chip

參考:快速排序 - 如果天空不死 - 博客園

歸並排序

歸並排序,其英文名為 Merge Sort,其意思是將排序串拆分成最小的單位之后,再一個個合並成有序的子串。 例如下圖的整數串,將其拆分成最小的子串就是每個只有一個整數。之后再將每個單個的子串合並起來,例如:8 與 4 合並起來成為有序子串 4、8,5 與 7 合並起來成為有序子串 5、74、85、7 再合並成為有序子串 4、5、7、8

可以看到在這個過程中,最關鍵是合並兩個有序子串的算法。這里我們以 [4,5,7,8] 和 [1,2,3,6] 為例,講解有序子串合並的算法流程。

  1. 首先聲明一個與原有數組相同的長度的臨時數組 temp。
  2. 接着 i 指向子串 1 開始的位置,j 指向子串 2 開始的位置。接着比較 arr1[i] 與 arr2[j] 的值,找出較小值。因為兩個子串都是有序的,所以這兩個值中的最小值,就是整個串中的最小值。找出最小值后將其值放入 temp 的開始位置,最小值對應的子串下標加 1。這里可以看到是 4 < 1,即子串 arr2 的值較小,那么將 1 放入 temp[0] 位置,接着 j 加一,此時 j 指向 2。
  3. 接着繼續對比 i 和 j 兩個數的大小,繼續對比步驟 2 的邏輯。這里可以看到 arr[i]=4 < arr[j]=2,那么應該將較小值放入 temp 數組中,即將 2 放入數組中,並且將 j + 1,即 j = 2,此時 j 指向的值 為 3。
  4. 按着上述的步驟繼續不斷重復步驟 2 的內容,我們會看到子串 2 首先到末尾。此時子串 1 還剩下一些數值,這些數值肯定是更大的值,那么直接將這些數值復制到 temp 數組中即可。如果子串 1 先到末尾,那么就應該將子串 2 剩余的數值寫入 temp 數組。
  5. 最后,將 temp 的數值寫回原有數組中即可。

代碼實現:

public static void sort(int []arr){
    //在排序前,先建好一個長度等於原數組長度的臨時數組,避免遞歸中頻繁開辟空間
    int []temp = new int[arr.length];
    sort(arr,0,arr.length-1,temp);
}
private static void sort(int[] arr,int left,int right,int []temp){
    if(left<right){
        int mid = (left+right)/2;
        //左邊歸並排序,使得左子序列有序
        sort(arr,left,mid,temp);
        //右邊歸並排序,使得右子序列有序
        sort(arr,mid+1,right,temp);
        //將兩個有序子數組合並操作
        merge(arr,left,mid,right,temp);
    }
}
private static void merge(int[] arr, int left, int mid, int right, int[] temp){
    //左序列指針
    int i = left;
    //右序列指針
    int j = mid+1;
    //臨時數組指針
    int t = 0;
    while (i<=mid && j<=right){
        if(arr[i]<=arr[j]){
            temp[t++] = arr[i++];
        }else {
            temp[t++] = arr[j++];
        }
    }
    //將左邊剩余元素填充進temp中
    while(i<=mid){
        temp[t++] = arr[i++];
    }
    //將右序列剩余元素填充進temp中
    while(j<=right){
        temp[t++] = arr[j++];
    }
    t = 0;
    //將temp中的元素全部拷貝到原數組中
    while(left <= right){
        arr[left++] = temp[t++];
    }
}

可調試代碼地址:java-code-chip/MergeSort.java at master · chenyurong/java-code-chip

參考:圖解排序算法(四)之歸並排序 - dreamcatcher-cx - 博客園

算法對比

選擇排序與冒泡排序的區別?

選擇排序是每次選出最值,然后放到數組頭部。而冒泡排序則是不斷地兩兩對比,將最值放到數組尾部。本質上,他倆每次都是選出最值,然后放到一邊。

其最大的不同點是:選擇排序只需要做一次交換,而冒泡排序則需要兩兩對比交換,所以冒泡排序的效率相對來說會低一些,因為會做多一些無意義的交換操作。

快速排序與歸並排序的區別?

剛剛看了一下,快速排序和歸並排序,我覺得差別可以提現在拆分合並的過程中,比較的時機。

快排和歸並,都是不斷拆分到最細。但是歸並更純粹,拆分時不做比較,直接拆!而快排還是會比較一下的。所以在拆分階段,快排會比歸並耗時一些。

而因為快排在拆分階段會比較,所以其拆得沒有歸並多層級,因此其在合並階段就少做一些功夫,會快一些。

所以快排和歸並排序的區別,本質上就是拆分、合並的區別。

參考資料

文章首發於公眾號「陳樹義」及個人博客 shuyi.tech,歡迎關注訪問。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM