1. 程式人生 > >演算法之常見排序演算法-氣泡排序、歸併排序、快速排序

演算法之常見排序演算法-氣泡排序、歸併排序、快速排序

引言

    對於程式設計中琳琅滿目的演算法,本人向來是不善此道也不精於此的,而說起排序演算法,也只是會氣泡排序。還記得當初剛做開發工作面試第一家公司時,面試官便讓手寫氣泡排序(入職之後才知道,這面試官就是一個氣泡排序"病態"愛好者,逢面試必考氣泡排序-__-)。後來看吳軍的一些文章,提到提高效率的關鍵就是少做事情不做無用功,便對這不起眼的排序演算法有了興趣。剛好今天週末有閒,遂研究一二,與各位道友共享。

    氣泡排序時間之所以效率低,就是因為將所有數都一視同仁不做區分挨個比較,這是最普通的做事方法,所以效率也是最普通的,時間複雜度為N的平方;而歸併排序效率高,則是採用了分治的思想,將一個整體分成多個小份,每個小份排好序之後再互相比較,這樣就比冒泡快了不少,時間複雜度為NlogN;快速排序的平均時間複雜度也是NlogN,但是實際的耗費時間會比歸併排序快兩三倍(當然快排在最壞的情況下時間複雜度還是N的平方,比歸併排序大),它的平均執行時間能比歸併更快一些是因為它每次分組時不是隨機分組而是相對有序的分組,即先從陣列中隨機取一個數作為基數,然後將資料移動,使得基數一邊的數都比它小,另一邊的數都比它大,再在兩邊各取一個基數進行相同的移動、分組操作,遞迴下去,這樣每個細分的小組都在整體的大陣列中有個位置,合併時直接按從小到大將各個分組合並起來即可,所以一般情況下會比歸併快一些。

    瞭解了思想之後,再用程式碼實現相對就會容易很多。此處就再借用一個直觀一點的例子來說明歸併與快排二者的區別。假設有1000個學生,想對他們的成績進行排序。方法1借用歸併排序的思想,具體這樣做:將這1000個人分成10組,將每組的100人進行排序,排完之後再在各組之間從小到大依次進行比較,最後得到整個的成績排名。方法2借用快速排序的思想,具體需這樣做:將1000個人也是分成10組,但是是按分數段分,0-10分的放在一組,10-20分的放在一組,20-30分的放在一組,依次類推,分完組之後再在各個小組中進行排序,而當你合併各個小組時,只需將其按從小到大的順序直接合並就行,無需跟方法1一樣將各小組中的資料取出來跟其他小組中的資料挨個比較。看到這裡,想必各位道友對快排比歸併排序還要快一些的原因就有了解了。

    演算法可以理解成做事的技巧或者說套路,我們對其的理解可以不止於程式設計,完全可以推廣出去。比如歸併的分治法,將一個大事情拆解成多個小事件,解決起來就會方便很多。閒話扯了一大堆,下面就將我自己寫的排序給大家貼出來,附帶上註釋講解,如果有之前不瞭解的道友,相信看完之後便會念頭通透,原地飛昇 >_<。

正文

歸併排序

// 歸併排序
    public static void mergeSort (int[] arr) {
        // 建一個臨時資料來存放資料
        int[] temp = new int[arr.length];
        mergeSort(arr, 0, arr.length - 1, temp);
    }

    private static void mergeSort(int[] arr, int left, int right, int[] temp) {
        if (left < right) { // 如果起始下標跟結束下標差值小於1,則不進行操作
            int mid = (left + right) / 2;
            mergeSort(arr, left, mid, temp); // 分組,將左邊分為一組,遞迴呼叫進行排序
            mergeSort(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; // 給temp臨時陣列用的指標
        while (i <= mid && j <= right) { // 設定左右指標的移動邊界
            if (arr[i] <= arr[j]) { // 此處是升序,故誰小誰先賦給臨時陣列
                temp[t++] = arr[i++];
            } else {
                temp[t++] = arr[j++];
            }
        }
        while (i <= mid) { // 如果左邊有剩餘,則放在temp中
            temp[t++] = arr[i++];
        }
        while (j <= right) { // 如果右邊有剩餘,依次放入temp中
            temp[t++] = arr[j++];
        }
        t = 0;
        // 此時temp中已經是arr陣列中下標從left到right之間排好序的資料了,因為temp每次都是從0開始賦值,所以需將排好序的數放回arr的對應位置
        while (left <= right) {
            // 將left到right之間排好序的資料放回arr中,此時left到right之間的數就是最終排好序的數
            arr[left++] = temp[t++];
        }
    }

快速排序

 1 // 快速排序
 2     public static void quickSort (int[] arr, int left, int right) {
 3         // 先將異常情況處理掉
 4         if (arr == null || arr.length < 2) {
 5             return;
 6         }
 7         if (right <= left) {
 8             return;
 9         }
10         if (right - left == 1 && arr[left] <= arr[right]) {
11             return;
12         }
13         // 取第一個數為基準數(基數取哪個都行,此處是為了方便)
14         int index = arr[left];
15         int i = left + 1; // 左指標
16         int j = right; // 右指標
17         while (i < j && i < right && j > left) { // 設定指標的移動邊界
18             while (arr[j] > index && j > left) {j--;} // 找到從右邊數第一個比index小的數
19             while (arr[i] < index && i < right) {i++;} // 找到從左邊數第一個比index大的數
20             if (i < j) { // 交換這兩個數  如果i == j,說明二者定位到了同一個位置,則不用交換;如果i > j,說明二者已經相遇然後背向而行了,也不交換
21                 int temp = arr[i];
22                 arr[i] = arr[j];
23                 arr[j] = temp;
24             }
25         }
26         // 執行完上面迴圈後,arr已經是左邊比index小,右邊比index大的陣列了,只是基準數仍在基準位置left處,需放到它應該在的位置
27         if (j != left && arr[j] != arr[left]) {
28             // j最後停留位置的數,肯定是一個小於等於index的值,所以如果不是同一個位置的話,直接將二者調換一下位置即可
29             int temp = arr[j];
30             arr[j] = arr[left];
31             arr[left] = temp;
32         }
33         quickSort(arr, left, j-1); // 將基準數左邊排序
34         quickSort(arr, j+1, right); // 將基準數右邊排序
35     }

這次的排序演算法就到這裡,如果有不妥之處,還請道友指正。後面如果遇到有意思的演算法題,也會跟道友們分享,下期見!