常用排序算法 Java 實現

本文是對《算法 第四版》中排序章節的總結,包括 選擇排序,插入排序,希爾排序,歸併排序,快速排序,堆排序和冒泡排序

各種排序算法的性能特點

有多種排序算法存在,就是因爲各種算法擁有不同的性能特點,各有所長,適用於不同場合,下面是書中對各種排序算法的性能特點的總結:

算法 時間複雜度 空間複雜度 是否穩定
選擇排序 最差:N^2,平均:N^2,最優:N^2 1 不穩定
插入排序 最差:N^2,平均:N^2,最優:N 1 穩定
希爾排序 最差:N*logN,平均:N*logN,最優:與遞增序列有關 1 不穩定
快速排序 最差:N^2,平均:N*logN,最優:N*logN lgN 不穩定
歸併排序 最差:N*logN,平均:N*logN,最優:N*logN N 穩定
堆排序 最差:N*logN,平均:N*logN,最優:N*logN 1 不穩定
冒泡排序 最差:N^2,平均:N^2,最優:N 1 穩定

本文使用 Java 實現以上幾種排序算法,並對《算法 第四版》書中的代碼稍有修改,作爲演示,只針對 int[] 類型進行排序,因此文中排序算法的輸入源都是 int[] 類型,並且將一些公共方法抽離出來,比如比較兩個數大小的 less() 和交換兩個數的 exchange() 方法,公共方法放在抽象類 SortModel.java 中,其他具體的排序方法只需要繼承它,並實現自己特有的 sort() 方法即可

模版

將排序算法的公共方法放在一個抽象類中,具體的排序算法類只需要繼承自這個抽象類,並實現自己的 sort() 方法即可,具體代碼如下:

public abstract class SortModel {

    //記錄排序消耗的時間
    protected long usedTime = 0;

    //具體的排序方法,由子類實現
    protected abstract void sort(int[] a);

    //比較 a 和 b 的大小,如果 a 小於 b,則返回 true
    protected boolean less(int a, int b) {
        return a < b;
    }

    //交換數組中的兩個數
    protected void exchange(int[] arr, int a, int b) {
        int temp = arr[a];
        arr[a] = arr[b];
        arr[b] = temp;
    }

    //打印數組
    protected void show(int[] arr, int count) {
        System.out.println("\n\n使用 " + getSortMethod() + " 對 " + count + " 個數排序用時: " + usedTime + "ms");
        for (int i : arr) {
            System.out.print(" " + i);
        }
    }

    //獲取當前使用的排序方法名稱
    protected abstract String getSortMethod();
}

接下來開始總結這些排序算法的具體實現

選擇排序

選擇排序是比較基礎的排序算法,也是一種很容易想到的排序算法,具體描述是這樣的:首先找到數組中最小的元素(這是一個遍歷比較的過程),然後將它和數組中的第一個元素交換位置,接着在剩下的元素中找到最小的元素,將它與第二個元素交換位置,如此循環,直到將整個數組排序完成

在選擇排序中一個主要的操作就是在數組中找到最小的元素,如何在一個給定的數組中找到值最小的那個元素呢?這個過程分爲兩步:

  1. 遍歷:
    遍歷最簡單的形式就是使用一個 for 循環,從開始索引,到結束索引,依次訪問數組中的元素

  2. 比較:
    比較至少需要兩個元素,在遍歷的時候,每次只訪問數組中的一個元素,因此爲了能夠比較,需要有一個臨時索引指向的元素來和當前訪問的元素進行比較,如果當前元素小於臨時索引指向的元素,就把當前元素的索引賦值給臨時索引

通過以上這兩步,在遍歷中比較,在滿足當前元素小於臨時索引指向的元素的條件時,就將當前元素的索引賦值給臨時索引,如此循環,遍歷結束後,臨時索引指向的元素值便是最小元素值

假設有一組數:16,13,18,11,14,12

找最小元素的過程是:

默認臨時索引爲數組第一個元素,即索引爲 0,開始遍歷數組

  • 當前元素是索引爲 0 的元素 16,和臨時索引爲 0 的元素比較,相等,不賦值

  • 當前元素是索引爲 1 的元素 13,和臨時索引爲 0 的元素比較,小於,當前索引賦值給臨時索引

  • 當前元素是索引爲 2 的元素 18,和臨時索引爲 1 的元素比較,大於,不賦值

  • 當前元素是索引爲 3 的元素 11,和臨時索引爲 1 的元素比較,小於,當前索引賦值給臨時索引

  • 當前元素是索引爲 4 的元素 14,和臨時索引爲 3 的元素比較,大於,不賦值

  • 當前元素是索引爲 5 的元素 12,和臨時索引爲 3 的元素比較,大於,不賦值

遍歷結束,臨時索引爲 3,因此索引爲 3 的元素就是這個數組中的最小元素

以上是一次尋找最小元素的過程,需要執行的比較次數與遍歷的數組長度成正比,如果遍歷的數組長爲 N,則查找最小元素需要 N 次比較

選擇排序的排序過程是:

  • 遍歷索引[0-5],找到最小元素 11,對應的索引爲 3,將它與索引爲 0 的元素交換,交換後如下

  • 遍歷索引[1-5],找到最小元素 12,對應的索引爲 5,將它與索引爲 1 的元素交換,交換後如下

  • 遍歷索引[2-5],找到最小元素 13,對應的索引爲 5,將它與索引爲 2 的元素交換,交換後如下

  • 遍歷索引[3-5],找到最小元素 14,對應的索引爲 4,將它與索引爲 3 的元素交換,交換後如下

  • 遍歷索引[4-5],找到最小元素 16,對應的索引爲 4,將它與索引爲 4 的元素交換,交換後如下

  • 遍歷索引[5-5],找到最小元素 18,對應的索引爲 5,將它與索引爲 5 的元素交換,交換後如下

以上是選擇排序的過程,從這個過程中可以分析到,在一次選擇排序過程中需要* N+(N-1)+(N-2)+…+3+2+1 = N(N-1)/2 ~ N^2/2* 次比較和 N 次交換,需要的比較次數屬於 N^2 級別。並且,在選擇排序中不存在最優與最壞情況,無論輸入的數據情況怎樣,選擇排序都需要固定次數的比較和交換,

選擇排序有兩個特點:

  • 運行時間和輸入無關
    從上面的分析可以知道,選擇排序中不存在最優情況和最壞情況,即使輸入數據已經整體有序,選擇排序所需要的比較和交換次數依然是固定的,只和輸入數組的大小有關

  • 數據移動次數最少
    選擇排序所需的交換次數和輸入數組的大小有關,如果輸入數組大小爲 N,則選擇排序所需交換次數爲 N

代碼實現

public class SelectionSort extends SortModel {
    @Override
    protected String getSortMethod() {
        return "選擇排序";
    }

    @Override
    protected void sort(int[] a) {
        long startTime = System.currentTimeMillis();

        int N = a.length;
        for (int i = 0; i < N; i++) {
            int min = i;
            for (int j = i + 1; j < N; j++) {//循環找到剩餘元素中最小的,賦值給 min
                if (less(a[j], a[min])) {
                    min = j;
                }
            }
            exchange(a, i, min);//交換 N 次
        }
        long endTime = System.currentTimeMillis();

        usedTime = endTime - startTime;
    }
}

插入排序

插入排序的思想如同玩紙牌時抽牌,每次抽一張,將其插入手中已經有序的牌組中,這樣,每次插入前的牌組都是有序的,而要插入的這張牌就在已經有序的牌組中從高到低依次比較,在合適的位置插入。插入排序的具體實現是:在數組中遍歷,索引每次加一,將新增索引對應的值插入當前索引之前的數組中(當前索引之前的數組已經有序),在當前索引位置開始往前兩兩比較,如果後者小於前者就交換,如此循環,直到全部有序

插入排序的主要操作是插入,插入的過程是從當前索引往前兩兩比較,因此插入過程分爲兩步:

  1. 逆向遍歷:

    使用 for 循環,從當前索引,到起始索引,依次訪問數組中的元素

  2. * 比較:*

    將當前索引的元素和其前一位的元素進行比較,如果當前索引元素小於前一位元素值,則交換(如果要求升序排序,就是小於,如果是降序排序,則是大於)

假設有一組數:16,13,18,11,14,12

執行一次插入的操作是這樣:

假設當前索引爲 4,也就是說數組的[0-3]部分已經有序,接下來需要將索引爲 4 的元素插入到數組的[0-4]部分中

插入排序的排序過程是:(注意初始索引是 1 而不是 0)

  • 當前索引 1,逆向遍歷[1-0],兩兩比較:

    • 當前索引爲 1 的元素值 13,前一位元素爲 16,小於,交換

    比較 1 次,交換 1 次,交換後的數組:

  • 當前索引 2,逆向遍歷[2-0],兩兩比較

    • 當前索引爲 2 的元素值 18,前一位元素爲 16,大於,不交換

    • 當前索引爲 1 的元素值 16,前一位元素爲 13,大於,不交換

    比較 2 次,交換 0 次,交換後的數組:

  • 當前索引 3,逆向遍歷[3-0],兩兩比較:

    • 當前索引爲 3 的元素值 11,前一位元素爲 18,小於,交換

    • 當前索引爲 2 的元素值 11,前一位元素爲 16,小於,交換

    • 當前索引爲 1 的元素值 11,前一位元素爲 13,小於,交換

    比較 3 次,交換 3 次,交換後的數組:

  • 當前索引 4,逆向遍歷[4-0],兩兩比較:

    • 當前索引爲 4 的元素值 14,前一位元素爲 18,小於,交換

    • 當前索引爲 3 的元素值 14,前一位元素爲 16,小於,交換

    • 當前索引爲 2 的元素值 14,前一位元素爲 13,大於,不交換

    • 當前索引爲 1 的元素值 13,前一位元素爲 11,大於,不交換

    比較 4 次,交換 2 次,交換後的數組:

  • 當前索引 5,逆向遍歷[5-0],兩輛比較

    • 當前索引爲 5 的元素值 12,前一位元素爲 18,小於,交換

    • 當前索引爲 4 的元素值 12,前一位元素爲 16,小於,交換

    • 當前索引爲 3 的元素值 12,前一位元素爲 14,小於,交換

    • 當前索引爲 2 的元素值 12,前一位元素爲 13,小於,交換

    • 當前索引爲 1 的元素值 12,前一位元素爲 11,大於,不交換

    比較 5 次,交換 4 次,交換後的數組:

從上面的執行過程分析,在插入排序中,比較次數和交換次數都是和輸入有關的,因此會存在最優情況和最壞情況,很好理解,最優情況是在輸入數組已經基本有序的時候,最壞情況是在輸入數組爲逆序的時候,下面分別從最優情況,最壞情況和平均情況分析插入排序的性能

  • 最優情況:
    當輸入數據已經基本有序時,比如輸入數據爲 11,12,13,14,16,18 ,則需要的比較次數爲 1+1+1+1+1=5 次,交換次數爲 0 次,延伸到長度爲 N 的輸入數據中,比較次數爲 N,交換次數爲 0。

  • 最壞情況:
    當輸入數據爲逆序是,比如輸入數據爲 18,16,14,13,12,11 ,則需要的比較次數爲 1+2+3+4+5=15 次,交換次數爲 1+2+3+4+5=15 次,延伸到長度爲 N 的輸入數據中,比較次數爲 1+2+3+…+(N-2)+(N-1)+N = N(N-1)/2 ~ N^2/2,交換次數爲 1+2+3+…+(N-2)+(N-1)+N = N(N-1)/2 ~ N^2/2

  • 平均情況:
    已經知道了最優情況和最壞情況下的比較次數和交換次數,平均情況就是 (最優情況 + 最壞情況) / 2,因此,平均情況下,插入排序需要的比較次數爲 ~ N^2/4,交換次數爲 ~ N^2/4

通過以上對插入排序的分析,可以總結幾點:

  1. 插入排序對部分有序的數組十分高效,也很適合小規模數組

  2. 可以對插入排序進行優化,比如在內循環中將較大的元素向右移動而不總是交換兩個相鄰的元素

代碼實現:

public class InsertionSort extends SortModel {
    @Override
    protected String getSortMethod() {
        return "插入排序";
    }

    @Override
    protected void sort(int[] a) {
        long startTime = System.currentTimeMillis();

        int N = a.length;
        for (int i = 1; i < N; i++) {
            //從當前索引位置往前遍歷,如果找到滿足"後者小於前者"條件的,就交換兩者的位置
            //這裏有個改進的寫法,之前的寫法是這樣的:
            //  for (int j = i; j > 0; j--) {
            //      if(less(a[j], a[j - 1])){
            //          exchange(a, j, j - 1);
            //      }
            //  }
            //這種寫法會導致每次的 for 循環都會一直進行到底,就會導致插入排序的比較次數是固定的
            //而下面這種寫法,在不滿足條件的情況下,就會結束 for 循環,因此比較次數是跟輸入有關的
            //因而是可以存在最優情況的
            for (int j = i; j > 0 && less(a[j], a[j - 1]); j--) {
                    exchange(a, j, j - 1);
            }
        }

        long endTime = System.currentTimeMillis();
        usedTime = endTime - startTime;
    }
}

希爾排序

希爾排序是基於插入排序的,改進了插入排序對於大規模亂序數組排序很慢的缺點,比如,如果一個值最小的元素正好在數組的盡頭,要將它移到正確的位置(數組起始位置),如果使用插入排序,需要移動 N-1 次,希爾排序改進的方法就是交換不相鄰的元素以對數組的局部進行排序,並最終用插入排序將局部有序的數組排序。希爾排序使數組中任意間隔爲 h 的元素都是有序的,然後再逐漸縮小 h 的值,直到 h=1,整個數進行插入排序,最終使數組有序。書中給出的增量 h 的計算公式爲 h=1/2(3^k-1),其中 k = 1,2,3,4,5…,這樣得到的 h 值是 1,4,13,40,121,364…的序列,在 h 小於數組的三分之一時(即 h < N/3)h 值開始遞減至 1。

假設有一組數:16,13,18,11,14,12

16 13 18 11 14 12
索引 0 1 2 3 4 5

希爾排序的排序過程是:

  • 根據公式計算得到遞增序列的最大值,計算得到 h = 4

  • h 值爲 4,即遞減量爲 4,遍歷 [4,5]

    • 當前索引值爲 4,逆向遍歷[4,4],遞減序列爲 (4,0),比較 1 次,交換 1 次
14 13 18 11 16 12
索引 0 1 2 3 4 5

- 當前索引值爲 5,逆向遍歷[5,4],遞減序列爲 (5,1),比較 1 次,交換 1 次

14 12 18 11 16 13
索引 0 1 2 3 4 5

- h 值爲 1,即遞減量爲 1,遍歷 [1,5]

  • 當前索引值爲 1,逆向遍歷[1,1],遞減序列爲 (1,0),比較 1 次,交換 1 次
12 14 18 11 16 13
索引 0 1 2 3 4 5

- 當前索引值爲 2,逆向遍歷[2,1],遞減序列爲 (2,1,0),比較 2 次,交換 0 次

12 14 18 11 16 13
索引 0 1 2 3 4 5

- 當前索引值爲 3,逆向遍歷[3,1],遞減序列爲 (3,2,1,0),比較 3 次,交換 3 次

11 12 14 18 16 13
索引 0 1 2 3 4 5

- 當前索引值爲 4,逆向遍歷[4,1],遞減序列爲 (4,3,2,1,0),比較 2 次,交換 1 次

11 12 14 16 18 13
索引 0 1 2 3 4 5

- 當前索引值爲 5,逆向遍歷[5,1],遞減序列爲 (5,4,3,2,1,0),比較 4 次,交換 3 次

11 12 13 14 16 18
索引 0 1 2 3 4 5

代碼實現:

public class ShellSort extends SortModel {
    @Override
    protected String getSortMethod() {
        return "希爾排序";
    }

    @Override
    protected void sort(int[] a) {

        long startTime = System.currentTimeMillis();

        int N = a.length;
        int h = 1;

        while (h < N / 3) {//根據 N 計算遞增序列中的最大值,1,4,13,40,121...
            h = 3 * h + 1;
        }

        while (h >= 1) {//逐漸縮小遞增序列進行排序,13,4,1
            System.out.println("h: "+h);
            for (int i = h; i < N; i++) {//在最後一個增量段中遍歷 i
                System.out.print(" i: "+i);
                for (int j = i; j >= h; j -= h) {//從 i 還是逐增量,得到的一個相隔增量段的序列進行插入排序
                    System.out.print(" j: "+j);
                    if (less(a[j], a[j - h])) {
                        exchange(a, j, j - h);
                    }
                }
                System.out.println();
            }
            h = h / 3;//增量遞減
        }

        long endTime = System.currentTimeMillis();
        usedTime = endTime - startTime;
    }
}

歸併排序

歸併排序的思想是:要將一個數組排序,可以先將它分成兩個子數組分別排序,然後將結果歸併起來。在歸併排序中,排序的過程是,把一個數據分成兩個子數組,每個子數組進行排序(遞歸的盡頭,一個子數組中只有兩個元素,這時的排序只需要簡單的比較兩個元素的大小),然後再把有序的子數組歸併成一個整體有序的數組,總結起來就是,先分解成最小單元,再組合成一個整體。

假設有一組數:16,13,18,11,14,12
歸併排序的排序過程是:(其中 lo 爲數組最低位索引,mid 爲數組中間位置索引,hi 爲數組最高位索引)

  • 對數組排序:[0,5], lo=0, mid=2, hi=5
  • 對左半部分排序:[0,2], lo=0, mid=1, hi=2
    • 對左半部分�92序:[0,1], lo=0, mid=0, hi=1
    • 對左半部分排序:[0,0]
    • 對右半部分排序:[1,1]
    • 對數組歸併: lo=0, mid=0, hi=1
    • 對右半部分排序:[2,2]
    • 對數組歸併: lo=0, mid=1, hi=2
  • 對右半部分排序:[3,5], lo=3, mid=4, hi=5
    • 對左半部分排序:[3,4], lo=3, mid=3, hi=4
    • 對左半部分排序:[3,3]
    • 對右半部分排序:[4,4]
    • 對數組歸併:lo=3, mid=3, hi=4
    • 對右半部分排序:[5,5]
    • 對數組歸併:lo=3, mid=4, hi=5
  • 對數組歸併:lo=0, mid=2, hi=5

代碼實現:

public class MergeSort extends SortModel {
    int[] temp;


    @Override
    protected String getSortMethod() {
        return "歸併排序";
    }

    @Override
    protected void sort(int[] a) {
        long startTime = System.currentTimeMillis();
        temp = new int[a.length];
        sortT2B(a, 0, a.length - 1);
        long endTime = System.currentTimeMillis();

        usedTime = endTime - startTime;
    }

    private void sortT2B(int[] a, int lo, int hi) {
        if (hi > lo) {
            int mid = lo + (hi - lo) / 2;//取數組中間位置索引
            sortT2B(a, lo, mid);//遞歸排序左邊的元素
            sortT2B(a, mid + 1, hi);//遞歸排序右邊的元素
            merge(a, lo, mid, hi);//歸併
        }
    }


    /**
     * 歸併操作,將兩個有序的數組歸併成一個有序的數組
     * <p>
     * 首先將數組複製到臨時數組 temp 中
     * 將臨時數組分爲左右兩部分,左邊部分索引起始位置爲 i,右邊部分索引起始位置爲 j
     * 將左右兩部分歸併到原來的數組中
     *
     * @param a   數組
     * @param lo  數組第一個元素
     * @param mid 數組中間的元素
     * @param hi  數組最後一個元素
     */
    private void merge(int[] a, int lo, int mid, int hi) {
        int i = lo, j = mid + 1;
        for (int k = lo; k <= hi; k++) {//複製數組
            temp[k] = a[k];
        }

        for (int k = lo; k <= hi; k++) {

            if (i > mid) {//如果左邊用盡,則取右邊的元素
                a[k] = temp[j++];
            } else if (j > hi) {//如果右邊用盡,則取左邊的元素
                a[k] = temp[i++];
            } else if (less(temp[j], temp[i])) {//如果右邊當前元素小於左邊當前元素,則取右邊元素
                a[k] = temp[j++];
            } else {//右邊當前元素大於左邊當前元素,則取左邊元素
                a[k] = temp[i++];
            }
        }
    }
}

快速排序

快速排序是應用最廣泛的排序算法,它將一個長度爲 N 的數組排序所需要的時間和 N*lgN 成正比,快速排序的基本思路是:將一個數組分成兩個子數組,將兩部分獨立的排序,當兩個子數組都有序時,整個數組也就有序了,這與歸併排序有所不同,歸併排序中,將一個數組分成兩個數組後,需要對兩個子數組進行歸併,在歸併的過程中排序,而快速排序是在將一個數組分成兩個數組的過程中進行排序,當分到盡頭的時候,數組就已經有序了,不需要再進行其它任何操作

快速排序中重點是找到將一個數組分爲兩個數組的切分點,在歸併排序中,其實也是有這樣的切分點的,就是 mid,也就是說歸併排序默認將一個數組等分,而在快速排序中,對一個數組的切分,並不一定是等分,需要根據具體的切分點的位置來進行切分,所以,找到合適的切分點的位置是很重要的,直接影響到整個排序的性能。

尋找切分點

切分點需要滿足三個條件:(假設切分點索引爲 k)
- 對於某個索引 k,數組中對應索引的值 a[k] 是確定的
- 數組索引[lo,k-1] (即切分點左邊的所有元素) 中的所有元素的值都不大於切分點元素的值( <= )
- 數組索引[k+1,hi] (即切分點右邊的所有元素) 中的所有元素的值都不小於切分點元素的值( >= )

假設有一組數:16,13,18,11,14,12
尋找切分點的過程是:(其中 lo 爲數組最低位索引,hi 爲數組最高位索引,從左往右遍歷的指針爲 i,從右往左遍歷的指針爲 j)

  • 隨意取 a[lo] 的值作爲初始切分點元素的值,即切分點索引爲 0,值爲 16
    • 從數組左端向右遍歷[1,5],當遇到一個大於等於切分點的元素,即索引爲 2 的元素,停止遍歷,此時 i=2
    • 從數組右端向左遍歷[5,0],當遇到一個小於等於切分點的元素,即索引爲 5 的元素,停止遍歷,此時 j=5
  • 交換 i 和 j 對應的值,交換後數組爲:16,13,12,11,14,18
    • 從數組左端向右遍歷[3,5],當遇到一個大於等於切分點的元素,即索引爲 5 的元素,停止遍歷,此時 i=5
    • 從數組右端向左遍歷[4,0],當遇到一個小於等於切分點的元素,即索引爲 4 的元素,停止遍歷,此時 j=4
  • 當 i>=j 時停止循環,不會執行 i 和 j 的交換,而是將切分點元素和 j 元素交換,交換後數組爲:14,13,12,11,16,18
  • 到此,尋找第一個切分點完成,切分點索引爲 4,對應的值爲 16

找到切分點,接下來將數組按照切分點分成兩部分,從上面的執行結果可以知道,數組將被分爲 [0,3] 和 [5,5],接下來就是對 [0,3] 部分重複尋找切分點的過程:

  • 隨意取 a[lo] 的值作爲初始切分點元素的值,即切分點索引爲 0,值爲 14
    • 從數組左端向右遍歷[1,3],當遇到一個大於等於切分點的元素,沒有找到,遍歷到數組盡頭,此時 i=3
    • 從數組右端向左遍歷[3,0],當遇到一個小於等於切分點的元素,即索引爲 3 的元素,停止遍歷,此時 j=3
  • 當 i>=j 時停止循環,不會執行 i 和 j 的交換,而是將切分點元素和 j 元素交換,交換後數組爲:11,13,12,14,16,18
  • 到此,尋找第二個切分點完成,切分點索引爲 3,對應的值爲 14

同理,數組將按照切分點分爲 [0,2] 和[3,3],接下來對 [0,2] 部分重複尋找切分點:

  • 隨意取 a[lo] 的值作爲初始切分點元素的值,即切分點索引爲 0,值爲 11
    • 從數組左端向右遍歷[1,2],當遇到一個大於等於切分點的元素,即索引爲 1 的元素,停止遍歷,此時 i=1
    • 從數組右端向左遍歷[2,0],當遇到一個小於等於切分點的元素,沒有找到,遍歷到數組起始位置,此時 j=0
  • 當 i>=j 時停止循環,不會執行 i 和 j 的交換,而是將切分點元素和 j 元素交換,交換後數組爲:11,13,12,14,16,18
  • 到此,尋找第三個切分點完成,切分點索引爲 0,對應的值爲 11

此時,由於切分點位置爲 0,所以只能切分出一個子數組,即 [1,2],繼續尋找切分點

  • 隨意取 a[lo] 的值作爲初始切分點元素的值,即切分點索引爲 1,值爲 13
    • 從數組左端向右遍歷[2,2],當遇到一個大於等於切分點的元素,沒有找到,遍歷到數組盡頭,此時 i=2
    • 從數組右端向左遍歷[2,1],當遇到一個小於等於切分點的元素,即索引爲 2 的元素,此時 j=2
    • 當 i>=j 時停止循環,不會執行 i 和 j 的交換,而是將切分點元素和 j 元素交換,交換後數組爲:11,12,13,14,16,18
    • 到此,尋找第四個切分點完成,切分點索引爲 2,對應的值爲 12

到此,數據已經有序了,整個過程中尋找了四次切分點

排序過程

下面整理完整的排序過程,將會忽略尋找切分點的過程,直接給出找到的切分點

假設有一組數:16,13,18,11,14,12
快速排序的過程是:

  • 尋找切分點,索引爲 4,值爲16,進行了 2 次交換,第 1 次交換後的數組是:16,13,12,11,14,18,第二次交換後的數組是:14,13,12,11,16,18
  • 對左半部分排序:[0,3]
    • 尋找切分點,索引爲 3,值爲 14,進行了 1 次交換,交換後的數組是:11,13,12,14,16,18
    • 對左半部分排序:[0,2]
      • 尋找切分點,索引爲 0,值爲 11,進行了 1 次交換,交換後的數組是:11,13,12,14,16,18
      • 對左半部分排序:[0,-1]
      • 對右半部分排序:[1,2]
        • 尋找切分點,索引爲 2,值爲 12,進行了 1 次交換,交換後的數組是:11,12,13,14,16,18
        • 對左半部分排序:[1,1]
        • 對右半部分排序:[3,2]
    • 對右半部分排序:[4,3]
  • 對右半部分排序:[5,5]

代碼實現:

public class QuickSort extends SortModel {
    @Override
    protected String getSortMethod() {
        return "快速排序";
    }

    @Override
    protected void sort(int[] a) {

        long startTime = System.currentTimeMillis();

        sort(a, 0, a.length - 1);

        long endTime = System.currentTimeMillis();

        usedTime = endTime - startTime;

    }

    private void sort(int[] a, int lo, int hi) {
        if (hi > lo) {
            int j = partition(a, lo, hi);//找到切分點
            sort(a, lo, j - 1);//對左半部分排序
            sort(a, j + 1, hi);//對右半部分排序
        }
    }

    /**
     * 找到切分點,使得切分點左邊所有元素都不大於切分點,右邊有所有素都不小於切分點
     *
     * @param a  數組
     * @param lo 數組起始位置
     * @param hi 數組結束爲止
     * @return 切分點
     */
    private int partition(int[] a, int lo, int hi) {

        //這裏 j 取 hi+1,是因爲在循環中用的 --j,是先執行減操作再比較,因此在首次執行時如果直接取 j 爲 hi,會忽略hi 這個元素,直接比較 hi-1
        //而 i 取 lo,沒有取 lo-1 是因爲默認取第一個元素爲切分點,正好需要略過第一個元素,所以這樣第一個比較的元素其實是 lo+1
        int i = lo, j = hi + 1;
        int v = a[lo];

        while (true) {
            while (less(a[++i], v)) {//從起始位置向右掃描,直到找到一個大於等於切分點的元素
                if (i == hi) {
                    break;
                }
            }
            while (less(v, a[--j])) {//從右端向左掃描,直到找到一個小於等於切分點的元素
                if (j == lo) {
                    break;
                }
            }
            if (i >= j) {//當兩個指針相遇,則中斷循環
                break;
            }
            exchange(a, i, j);//交換兩個元素
        }
        exchange(a, lo, j);//交換切分點和左子數組最右端的元素交換,這樣返回的就是最新的切分點元素值了
        return j;
    }
}

堆排序

在瞭解堆排序之前,需要先了解二叉堆這種數據結構,它的定義是:二叉堆是一組能夠用堆有序(每個節點都大於等於它的兩個字節點時,堆有序)的完全二叉樹排序的元素,並在數組中按照層級儲存(不使用數組的第一個位置,就是不使用索引 0 ,索引從 1 開始)

在二叉堆中存在這樣一種關係:索引位置爲 k 的元素,他的父節點索引位置爲 k/2,而它的兩個字節點索引位置分別爲 2*k 和 2*k+1。這樣就能很容易在二叉堆中上下移動,比如,從 a[k] 位置向上層移動就令 k=k/2,向下層移動就令 k=2*k 或者 k=2*k+1

在堆中有兩個比較重要的操作,也是用來排序的操作,即由下至上的堆有序化(上浮)和由上至下的堆有序化(下沉)

  • 上浮:如果某個節點的值比它的父節點還大,就需要通過上浮操作,使它和它的父節點交換,以此來達到從上至下的有序狀態
  • 下沉:如果某個節點的值比它的兩個子節點中較大者還小,就需要通過下沉操作,使它和它的子節點中較大者交換,以此來達到從上至下的有序狀態

堆排序正是利用了二叉堆的這個性質來完成排序的。堆排序的過程大致分爲兩步:初始構造堆,然後是下沉排序。

初始構造堆就是把要排序的數組構造成一個二叉堆,初始構造的堆只需要滿足:當前節點大於它的兩個字節點並且小於它的父節點即可,並不需要在初始構造階段進行排序

下沉排序是從數組末尾開始逆序遍歷將所有的數都放在數組起始位置(索引爲 1),然後讓他根據下沉規則找到合適的位置,這樣,當遍歷到數組起始位置時,數組就有序了

假設有一組數:16,13,18,11,14,12

堆排序分爲兩個過程:構造堆和下沉排序,其中構造堆就是將一個數組構造成具有堆的性質(當前節點大於其左右子節點的值),構造完成的堆滿足從上至下的有序狀態,即當前節點大於其子節點,但是在其兩個子節點中並不存在有序狀態,於是需要使用下沉操作將堆從下至上進行排序,使堆整體有序。

構造堆:

未完待續。。。

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