程序員那些必須掌握的排序算法二

接着上一篇的排序算法,我們廢話不多說,直接進入主題。

1.快速排序
快速排序(Quicksort)是對冒泡排序的一種改進。
快速排序由C. A. R. Hoare在1960年提出。它的基本思想是:通過一趟排序將要排序的數據分割成獨立的兩部分,其中一部分的所有數據都比另外一部分的所有數據都要小,然後再按此方法對這兩部分數據分別進行快速排序,整個排序過程可以遞歸進行,以此達到整個數據變成有序序列。
演示:

代碼如下:

    public static void quickSort(int[] arr, int left, int right) {
        int l = left;// 左下標
        int r = right;// 右下標
        int pivot = arr[(left + right) / 2];// 找到中間的值
        // 將比pivot小的值放在其左邊,比pivot大的值放在其右邊
        while (l < r) {
            // 在pivot左邊尋找,直至找到大於等於pivot的值才退出
            while (arr[l] < pivot) {
                l += 1;// 將l右移一位
            }
            // 在pivot右邊尋找,直至找到小於等於pivot的值才退出
            while (arr[r] > pivot) {
                r -= 1;// 將r左移一位
            }
            if (l >= r) {
                // 左右下標重合,尋找完畢,退出循環
                break;
            }
            // 交換元素
            int temp = arr[l];
            arr[l] = arr[r];
            arr[r] = temp;

            //倘若發現值相等的情況,則沒有比較的必要,直接移動下標即可
        
            // 如果交換完後,發現arr[l]==pivot,此時應將r左移一位
            if (arr[l] == pivot) {
                r -= 1;
            }
            // 如果交換完後,發現arr[r]==pivot,此時應將l右移一位
            if (arr[r] == pivot) {
                l += 1;
            }
        }
        // 如果l==r,要把這兩個下標錯開,否則會出現無限遞歸,導致棧溢出的情況
        if (l == r) {
            l += 1;
            r -= 1;
        }
        // 向左遞歸
        if (left < r) {
            quickSort(arr, left, r);
        }
        // 向右遞歸
        if (right > l) {
            quickSort(arr, l, right);
        }
    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
測試代碼:

    public static void main(String[] args) {
        int[] arr = { 3, 44, 38, 5, 47, 15, 36, 26, 27, 2, 46, 4, 19, 50, 48 };
        quickSort(arr, 0, arr.length - 1);
        System.out.println(Arrays.toString(arr));
    }
1
2
3
4
5
運行結果:

[2, 3, 4, 5, 15, 19, 26, 27, 36, 38, 44, 46, 47, 48, 50]
1
快速排序的實現原理很簡單,就是將原數組分成兩部分,然後以中間值爲標準,比它小的就放其左邊,比它大的就放其右邊,然後在左右兩邊又以相同的方式繼續排序。
所以在代碼實現過程中,首先要創建兩個移動的變量,一個從最左邊開始往右移動,一個從最右邊開始往左移動,通過這兩個變量來遍歷左右兩部分的元素。當發現左邊有大於中間數的元素,右邊有小於中間數的元素,此時就進行交換。當兩個變量重合也就是相等的時候遍歷結束,然後左右兩部分作遞歸處理。

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

歸併排序使用了一種分治思想,分治思想的意思就是’分而治之",就是把一個複雜的問題分成兩個或更多的相同或相似的子問題,再把子問題分成更小的子問題……直到最後子問題可以簡單地直接求解。
通過這個動圖來看的話,相信很多人都一臉懵,沒關係,我們通過靜態圖來分析一下:

假設現在有一個待排序的序列,[5,2,4,7,1,3,2,2],那麼我們就需要將該序列進行分治,先將其分成兩份:[5,2,4,7]和[1,3,2,2],再將這兩份分別分成兩份:[5,2]和[4,7];[1,3]和[2,2],最後將這四部分再次分別分爲兩份,最後就將整個序列分爲了八份。需要注意的是,在分的過程中,不需要遵循任何規則,關鍵在於歸併,歸併的過程中便實現了元素的排序。
代碼如下:

    public static void mergeSort(int[] arr, int left, int right, int[] temp) {
        // 分解
        if (left < right) {
            int mid = (left + right) / 2;// 中間索引
            // 向左遞歸進行分解
            mergeSort(arr, left, mid, temp);
            // 向右遞歸進行分解
            mergeSort(arr, mid + 1, right, temp);// mid + 1,中間位置的後一個位置纔是右邊序列的開始位置
            // 每分解一輪便合併一輪
            merge(arr, left, right, mid, temp);
        }
    }

    /**
     * 合併的方法
     * 
     * @param arr   待排序的數組
     * @param left  左邊有序序列的初始索引
     * @param right 中間索引
     * @param mid   右邊有序序列的初始索引
     * @param temp  做中轉的數組
     */
    public static void merge(int[] arr, int left, int right, int mid, int[] temp) {
        int i = left; // 初始化i,左邊有序序列的初始索引
        int j = mid + 1;// 初始化j,右邊有序序列的初始索引(右邊有序序列的初始位置即爲中間位置的後一個位置)
        int t = 0;// 指向temp數組的當前索引,初始爲0
        // 先把左右兩邊的數據(已經有序)按規則填充到temp數組
        // 直到左右兩邊的有序序列,有一邊處理完成爲止
        while (i <= mid && j <= right) {
            // 如果左邊有序序列的當前元素小於或等於右邊有序序列的當前元素,就將左邊的元素填充到temp數組中
            if (arr[i] <= arr[j]) {
                temp[t] = arr[i];
                t++;// 索引後移
                i++;// i後移
            } else {
                // 反之,將右邊有序序列的當前元素填充到temp數組中
                temp[t] = arr[j];
                t++;// 索引後移
                j++;// j後移
            }
        }
        // 把有剩餘數據的一邊的元素填充到temp中
        while (i <= mid) {
            // 此時說明左邊序列還有剩餘元素
            // 全部填充到temp數組
            temp[t] = arr[i];
            t++;
            i++;
        }
        while (j <= right) {
            // 此時說明左邊序列還有剩餘元素
            // 全部填充到temp數組
            temp[t] = arr[j];
            t++;
            j++;
        }
        // 將temp數組的元素複製到原數組
        t = 0;
        int tempLeft = left;
        while (tempLeft <= right) {
            arr[tempLeft] = temp[t];
            t++;
            tempLeft++;
        }
    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
關於歸併排序的算法思想確實比較繞,所以我也在代碼中寫了很多註釋。
我們先來測試一下:

    public static void main(String[] args) {
        int[] arr = { 3, 44, 38, 5, 47, 15, 36, 26, 27, 2, 46, 4, 19, 50, 48 };
        int[] temp = new int[arr.length];
        mergeSort(arr, 0, arr.length - 1, temp);
        System.out.println(Arrays.toString(arr));
    }
1
2
3
4
5
6
運行結果:

[2, 3, 4, 5, 15, 19, 26, 27, 36, 38, 44, 46, 47, 48, 50]
1
來分析一下吧,對於該排序算法,有兩個部分組成,分解和合並。首先講講分解,在前面也說到了,我們需要將待排序的序列不停地進行分解,通過兩個索引變量控制,一個初始索引,一個結尾索引。只有當兩索引重合才結束分解。此時序列被分解成了十五個小份,這樣分解工作就完成了。接下來是合併,合併操作也是最麻煩的,也是通過兩個索引變量i,j。開始i在左邊序列的第一個位置,j在右邊序列的第一個位置,然後就是尋找左右兩個序列中的最小值,放到新序列中,這時可能會出現一邊的元素都放置完畢了,而另外一邊還存在元素,此時只需將剩餘的元素按順序放進新序列即可,因爲這時左右兩邊的序列已經是有序的了,最後將新序列複製到舊序列。這裏也特別需要注意,因爲合併的過程是分步的,而並非一次合併完成,所以數組的索引是在不斷變化的。

自己手動畫了個圖,左右兩個箭頭就是索引變量i,j,當i所指的元素也就是1和j所指的元素也就是2進行比較,發現1小,就將1放到新數組的第一個位置,此時應該將i和新數組的索引都右移一位,然後繼續比較,以此類推,相信這樣大家應該能理解了吧。

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

基數排序的基本思想是:
將所有待比較的數值統一爲同樣的數位長度,數位較短的數前面補零。然後,從最低位開始,依次進行一次排序,這樣從最低位排序一直到最高位排序完成以後,數列就變爲了一個有序序列。
這樣說可能過於抽象,我們通過詳細步驟來分析一下:
我們假設有一個待排序數組[53,3,542,748,14,214],那麼如何使用基數排序對其進行排序呢?
首先我們有這樣的十個一維數組,在基數排序中也叫桶。

那麼第一輪排序開始,我們依次遍歷每個元素,並得到元素的個位數。拿到的第一個元素爲53,其個位數爲3,所以將53放入編號爲3的桶中,第二個元素3的個位數也是3,所以也放在編號爲3的桶中,而第三個元素542的個位數爲2,所以將542放入編號爲2的桶中,以此類推。
所以結果爲:

將元素全部放入桶中之後,我們需要按照桶的順序(也就是一維數組的下標)依次取出數據,並放回原來的數組。
那麼很簡單,按順序取出數據並放回原數組之後,原數組將變爲[542,53,3,14,214,748]。
這樣第一輪就完成了,接下來開始第二輪。
第二輪排序和第一輪類似,也要去遍歷數組元素,但不同的是第二輪的存放順序取決於十位數。
取出數據的第一個元素爲542,十位數爲4,所以放入編號爲4的桶;第二個元素53,十位數爲5,所以放入編號爲5的桶;第三個元素3,十位數爲0,所以放入編號爲0的桶,以此類推。
所以結果爲:

然後同樣按照桶的順序將數據從中取出並放入原數組,此時原數組變爲[3,14,214,542,748,53]。
接下來又進行第三輪排序,以元素的百位數進行區分,結果爲:

按順序取出數據後,原數組變爲[3,14,53,214,542,748]。這時的數組已經完成排序。
從中我們也可以知道,基數排序的排序輪數取決於數組元素中最大位數的元素。

代碼如下:

    public static void raixSort(int[] arr) {
        // 第一輪(針對每個元素的個位進行排序處理)

        // 定義一個二維數組,模擬桶,每個桶就是一個一維數組
        // 爲了防止放入數據的時候桶溢出,我們應該儘量將桶的容量設置得大一些
        int[][] bucket = new int[10][arr.length];
        // 記錄每個桶中實際存放的元素個數
        // 定義一個一維數組來記錄每個桶中每次放入的元素個數
        int[] bucketElementCounts = new int[10];

        for (int j = 0; j < arr.length; j++) {
            // 取出每個元素的個位
            int digitOfElement = arr[j] % 10;
            // 將元素放入對應的桶中
            // bucketElementCounts[digitOfElement]就是桶中的元素個數,初始爲0,放在第一位
            bucket[digitOfElement][bucketElementCounts[digitOfElement]] = arr[j];
            // 將桶中的元素個數++
            // 這樣接下來的元素就可以排在前面的元素後面
            bucketElementCounts[digitOfElement]++;
        }
        // 按照桶的順序取出數據並放回原數組
        int index = 0;
        for (int k = 0; k < bucket.length; k++) {
            // 如果桶中有數據,才取出放回原數組
            if (bucketElementCounts[k] != 0) {
                // 說明桶中有數據,對該桶進行遍歷
                for (int l = 0; l < bucketElementCounts[k]; l++) {
                    // 取出元素放回原數組
                    arr[index++] = bucket[k][l];
                }
            }
            // 第一輪處理後,需要將每個bucketElementCounts[k]置0
            bucketElementCounts[k] = 0;
        }
        System.out.println("第一輪:" + Arrays.toString(arr));

        // ----------------------------

        // 第二輪(針對每個元素的十位進行排序處理)
        for (int j = 0; j < arr.length; j++) {
            // 取出每個元素的十位
            int digitOfElement = arr[j] / 10 % 10;
            // 將元素放入對應的桶中
            // bucketElementCounts[digitOfElement]就是桶中的元素個數,初始爲0,放在第一位
            bucket[digitOfElement][bucketElementCounts[digitOfElement]] = arr[j];
            // 將桶中的元素個數++
            // 這樣接下來的元素就可以排在前面的元素後面
            bucketElementCounts[digitOfElement]++;
        }
        // 按照桶的順序取出數據並放回原數組
        index = 0;
        for (int k = 0; k < bucket.length; k++) {
            // 如果桶中有數據,才取出放回原數組
            if (bucketElementCounts[k] != 0) {
                // 說明桶中有數據,對該桶進行遍歷
                for (int l = 0; l < bucketElementCounts[k]; l++) {
                    // 取出元素放回原數組
                    arr[index++] = bucket[k][l];
                }
            }
            // 第二輪處理後,需要將每個bucketElementCounts[k]置0
            bucketElementCounts[k] = 0;
        }
        System.out.println("第二輪:" + Arrays.toString(arr));

        // ----------------------------

        // 第三輪(針對每個元素的百位進行排序處理)
        for (int j = 0; j < arr.length; j++) {
            // 取出每個元素的百位
            int digitOfElement = arr[j] / 100 % 10;
            // 將元素放入對應的桶中
            // bucketElementCounts[digitOfElement]就是桶中的元素個數,初始爲0,放在第一位
            bucket[digitOfElement][bucketElementCounts[digitOfElement]] = arr[j];
            // 將桶中的元素個數++
            // 這樣接下來的元素就可以排在前面的元素後面
            bucketElementCounts[digitOfElement]++;
        }
        // 按照桶的順序取出數據並放回原數組
        index = 0;
        for (int k = 0; k < bucket.length; k++) {
            // 如果桶中有數據,才取出放回原數組
            if (bucketElementCounts[k] != 0) {
                // 說明桶中有數據,對該桶進行遍歷
                for (int l = 0; l < bucketElementCounts[k]; l++) {
                    // 取出元素放回原數組
                    arr[index++] = bucket[k][l];
                }
            }
            // 第三輪處理後,需要將每個bucketElementCounts[k]置0
            bucketElementCounts[k] = 0;
        }
        System.out.println("第三輪:" + Arrays.toString(arr));
    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
爲了方便大家理解,這裏我並沒有使用循環處理,而是詳細寫出了每一輪的步驟,代碼註釋也很詳細。
接下來編寫測試代碼:

    public static void main(String[] args) {
        int[] arr = { 53, 3, 542, 748, 14, 214 };
        raixSort(arr);
    }
1
2
3
4
運行結果:

第一輪:[542, 53, 3, 14, 214, 748]
第二輪:[3, 14, 214, 542, 748, 53]
第三輪:[3, 14, 53, 214, 542, 748]
1
2
3
如果你能夠看懂上面的代碼,那麼接下來就是整合了,通過循環對上面的代碼進行優化。
代碼如下:

    public static void raixSort(int[] arr) {
        // 得到數組中最大的數
        int max = arr[0];// 假設第一個數就是數組中的最大數
        for (int i = 1; i < arr.length; i++) {
            if (arr[i] > max) {
                max = arr[i];
            }
        }
        // 得到最大數是幾位數
        // 通過拼接一個空串將其變爲字符串進而求得字符串的長度,即爲位數
        int maxLength = (max + "").length();

        // 定義一個二維數組,模擬桶,每個桶就是一個一維數組
        // 爲了防止放入數據的時候桶溢出,我們應該儘量將桶的容量設置得大一些
        int[][] bucket = new int[10][arr.length];
        // 記錄每個桶中實際存放的元素個數
        // 定義一個一維數組來記錄每個桶中每次放入的元素個數
        int[] bucketElementCounts = new int[10];

        // 通過變量n幫助取出元素位數上的數
        for (int i = 0, n = 1; i < maxLength; i++, n *= 10) {
            for (int j = 0; j < arr.length; j++) {
                // 針對每個元素的位數進行處理
                int digitOfElement = arr[j] / n % 10;
                // 將元素放入對應的桶中
                // bucketElementCounts[digitOfElement]就是桶中的元素個數,初始爲0,放在第一位
                bucket[digitOfElement][bucketElementCounts[digitOfElement]] = arr[j];
                // 將桶中的元素個數++
                // 這樣接下來的元素就可以排在前面的元素後面
                bucketElementCounts[digitOfElement]++;
            }
            // 按照桶的順序取出數據並放回原數組
            int index = 0;
            for (int k = 0; k < bucket.length; k++) {
                // 如果桶中有數據,才取出放回原數組
                if (bucketElementCounts[k] != 0) {
                    // 說明桶中有數據,對該桶進行遍歷
                    for (int l = 0; l < bucketElementCounts[k]; l++) {
                        // 取出元素放回原數組
                        arr[index++] = bucket[k][l];
                    }
                }
                // 每輪處理後,需要將每個bucketElementCounts[k]置0
                bucketElementCounts[k] = 0;
            }
        }
    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
測試代碼:

    public static void main(String[] args) {
        int[] arr = { 53, 3, 542, 748, 14, 214 };
        raixSort(arr);
        System.out.println(Arrays.toString(arr));
    }
1
2
3
4
5
運行結果:

[3, 14, 53, 214, 542, 748]
1
這樣,基數排序就完成了。大家不要看到代碼很多就怕了、煩了,覺得好難,其實也不能說一點難度都沒有吧,只是要去理解這個過程,所以對於排序過程的分析我寫了很多,也是爲了能讓你們更加理解,掌握了過程之後,相信理解這些代碼也不是難事了。
其它:
這裏說一說基數排序的一些其它內容,爲什麼單獨只說基數排序呢?我們在前面提到了,基數排序是用空間換時間的經典算法,所以基數排序對於元素排序是非常快的。不信我們可以測試一下(先測試八十萬個數據的排序時間):

    public static void main(String[] args) {
        int[] arr = new int[800000];
        for (int i = 0; i < arr.length; i++) {
            arr[i] = (int) (Math.random() * 8000000);
        }
        Date date = new Date();
        SimpleDateFormat format = new SimpleDateFormat("HH:mm:ss");
        String dateStr = format.format(date);
        System.out.println("排序前的時間是:" + dateStr);
        raixSort(arr);
        Date date2 = new Date();
        String dateStr2 = format.format(date2);
        System.out.println("排序前的時間是:" + dateStr2);
    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
運行結果:

排序前的時間是:17:37:21
排序前的時間是:17:37:21
1
2
一秒鐘時間不到就完成排序了。
我們再測試一下八百萬個數據的排序:

    public static void main(String[] args) {
        int[] arr = new int[8000000];
        for (int i = 0; i < arr.length; i++) {
            arr[i] = (int) (Math.random() * 8000000);
        }
        Date date = new Date();
        SimpleDateFormat format = new SimpleDateFormat("HH:mm:ss");
        String dateStr = format.format(date);
        System.out.println("排序前的時間是:" + dateStr);
        raixSort(arr);
        Date date2 = new Date();
        String dateStr2 = format.format(date2);
        System.out.println("排序前的時間是:" + dateStr2);
    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
運行結果:

排序前的時間是:17:38:04
排序前的時間是:17:38:05
1
2
只需要一秒鐘即完成了排序。
我們再測試一下八千萬個數據的排序:

    public static void main(String[] args) {
        int[] arr = new int[80000000];
        for (int i = 0; i < arr.length; i++) {
            arr[i] = (int) (Math.random() * 8000000);
        }
        Date date = new Date();
        SimpleDateFormat format = new SimpleDateFormat("HH:mm:ss");
        String dateStr = format.format(date);
        System.out.println("排序前的時間是:" + dateStr);
        raixSort(arr);
        Date date2 = new Date();
        String dateStr2 = format.format(date2);
        System.out.println("排序前的時間是:" + dateStr2);
    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
運行結果:

排序前的時間是:17:41:07
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
    at com.itcast.sort.RadixSortDemo.raixSort(RadixSortDemo.java:42)
    at com.itcast.sort.RadixSortDemo.main(RadixSortDemo.java:22)
1
2
3
4
結果爲堆內存溢出,所以在對大量數據進行排序的時候,基數排序顯然不是一個好的選擇,因爲提升排序效率的條件是犧牲大量的內存空間,當數據足夠多時,內存空間就不夠用了。

4.堆排序
本來這篇文章打算介紹四種排序算法,這最後一種便是堆排序,但是寫着寫着,回過頭髮現竟然也寫了這麼多了,考慮到不宜篇幅過長,我打算將堆排序放到後面說,如果你能掌握前面的這七種排序算法並能隨時手寫出來,也是很了不起了。

本篇文章雖然只介紹了三種排序算法,但這三種排序算法都有一定的難度,想要徹底掌握它們大家還是要多花點心思啊!

推薦閱讀
————————————————
版權聲明:本文爲CSDN博主「~wangweijun」的原創文章,遵循 CC 4.0 BY-SA 版權協議,轉載請附上原文出處鏈接及本聲明。
原文鏈接:https://blog.csdn.net/qq_42453117/article/details/100036347

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