選擇排序與歸併排序

選擇排序


直接選擇排序

【基本思想】

每一趟在待排序列中選出最小(或最大)的元素,依次放在已排好序的元素序列後面(或前面),直至全部的元素排完爲止。

直接選擇排序也稱爲簡單選擇排序。首先在待排序列中選出最小的元素,將它與第一個位置上的元素交換。然後選出次小的元素,將它與第二個位置上的元素交換。以此類推,直至所有元素排成遞增序列爲止。

選擇排序是對整體的選擇。只有在確定了最小數(或最大數)的前提下才進行交換, 大大減少了交換的次數。

【空間複雜度】O(1)

【時間複雜度】

平均情況:O(n^2)

最好情況:O(n^2),此時不發生交換,但仍需進行比較

最壞情況:O(n^2)

【穩定性】不穩定

【優點】

交換數據的次數已知(n-1)次

【缺點】

不穩定,比較的次數多

【算法實現】
  1.     /** 
  2.      * 直接選擇排序 
  3.      * @param arr 
  4.      */  
  5.     public static void selectSort(int[] arr) {  
  6.         int minIndex;  
  7.         for (int i = 0; i < arr.length - 1; i++) {  
  8.             //初始化默認i爲最小值索引  
  9.             minIndex = i;  
  10.             for (int j = i + 1; j < arr.length; j++) {  
  11.                 if (arr[j] < arr[minIndex]) {  
  12.                     //記錄最小值的索引  
  13.                     minIndex = j;  
  14.                 }  
  15.             }  
  16.             //若minIndex的值改變了,說明有其它最小值,則交換  
  17.             if (minIndex != i) {  
  18.                 swap(arr, minIndex, i);  
  19.             }  
  20.         }  
  21.     }  
  22.     public static void swap(int[] arr, int i, int j) {  
  23.         int temp = arr[i];  
  24.         arr[i] = arr[j];  
  25.         arr[j] = temp;  
  26.     }  

【本算法解讀】

初始時無序區爲整個待排序列。算法默認待排序列的第一個元素是最小值,然後從該元素的下一位開始(對應代碼:for(int j = i +1, j < arr.length; j ++)),直到最後的元素,都和索引minIndex位置上的元素進行比較。如果發現更小的值,就將該值的索引存放在minIndex中。當所有比較進行完畢後,索引minIndex位置上的元素就是最小值,將其和首元素進行交換。這樣就完成了一趟選擇排序,此時序列的首元素即處於有序區中,剩下的元素處於無序區中。繼續將無序區中的首元素作爲最小值,重複上面的操作,並將找到的最小值和無序區首元素交換即可。直至完成所有排序。

【舉個栗子】

對於待排序列3,1,4,2
首先認爲3是最小值,將其索引0保存在minIndex中,從元素1開始與minIndex位置上的元素(此時是3)進行比較,1<3,則minIndex保存元素1的索引。繼續將minIndex位置上的元素(此時是1)與元素4比較,4>1,繼續與2比較,1<2,不需要改變。沒有再比較的元素了,此時將minIndex位置上的元素和首元素進行交換。則完成一趟選擇排序,序列爲1,3,4,2。有序區爲1,無序區爲3,4,2。繼續下一趟排序,默認無序區首元素3爲最小值,重複上述操作,將找到的無序區最小值,和無序區首元素進行交換。一趟選擇排序結束後,序列爲1,2,4,3。有序區爲1,2,無序區爲4,3。重複上述操作直到完成排序。



堆排序

堆排序是藉助堆來實現的選擇排序。

什麼是堆呢?堆是滿足下列性質的數列{r1, r2, r3, r4, ..., rn}:

ri<=r2i且ri<=r(2i+1)或者是ri>=r2i且ri>=r(2i+1),前者稱爲小頂堆,後者稱爲大頂堆。

例如大頂堆:{10,34,24,85,47,33,53,90},位置i(i從1開始)上的元素小於2i位置上的元素,且小於2i+1位置上的元素

【基本思想】

對一組待排序列的元素,首先將它們按照堆的定義排成一個序列,常稱爲建堆,從而輸出堆頂的最大或最小關鍵字。然後對剩餘的元素再建堆,常稱爲重新調整成堆,即可得到次大(次小)元素,如此反覆進行,直到全部元素排成有序序列爲止。

實現堆排序有兩個關鍵步驟,建堆和調整堆
 如何建堆:首先將待排序列畫成一顆完全二叉樹,然後把得到的完全二叉樹再轉換成堆。從最後一個分支節點開始(n/2取下限的節點),依次將所有以分支節點爲根的二叉樹調整成堆,當這個過程持續到根節點時,整個二叉樹就調整成了堆,即建堆完成。

如何調整堆:假設被調整的分支節點爲A,它的左孩子爲B,右孩子爲C。那麼當A開始進行堆調整時,以B和以C爲根的二叉樹都已經爲堆。如果節點A的值大於B和C的值(以大頂堆爲例),那麼以A爲根的二叉樹已經是堆。如果A節點的值小於B節點或C節點的值,那麼節點A與值最大的那個孩子變換位置。此時需要將A繼續與和它交換的那個孩子的原來的兩個孩子進行比較,依次類推,直到節點A向下滲透到適當的位置爲止。

如果要從小到大排序,則使用大頂堆,如果要從大到小排序,則使用小頂堆。原因是堆頂元素需要交換到序列尾部

【空間複雜度】O(1)

【時間複雜度】

平均情況:O(nlog2n)

最好情況:O(nlog2n)

最壞情況:O(nlog2n)它的最壞性能接近於平均性能

【穩定性】不穩定

【優點】

在最壞情況下性能優於快速排序。由於在直接選擇排序的基礎上利用了比較結果形成。效率提高很大。

【缺點】

不穩定,初始建堆所需比較次數較多,因此記錄數較少時不宜採用

【算法實現】
  1. /** 
  2.  * 堆排序 
  3.  * @param arr 
  4.  */  
  5. public static void heapSort(int[] arr) {  
  6.     //建堆,通過調整堆達到建堆的目的  
  7.     //從二叉樹最後一個分支節點開始(n/2取下限的節點)  
  8.     for (int i = arr.length / 2; i >= 0; i--) {  
  9.         heapAdjust(arr, i, arr.length - 1);  
  10.     }  
  11.     // 由大頂堆得到有序序列  
  12.     for (int i = arr.length - 1; i >= 1; i--) {  
  13.         swap(arr, 0, i);  
  14.         heapAdjust(arr, 0, i - 1);  
  15.     }  
  16. }  
  17.    //調整堆  
  18. private static void heapAdjust(int[] arr, int start, int end) {  
  19.     int target = arr[start];  
  20.     for (int i = 2 * start + 1; i <= end; i = 2 * i + 1) {  
  21.         //將i指向左孩子和右孩子中的較大值  
  22.         if (i < end && arr[i] < arr[i + 1]) {  
  23.             i++;  
  24.         }  
  25.         //已經是大頂堆,不再需要調整  
  26.         if (target >= arr[i]) {  
  27.             break;  
  28.         }  
  29.         arr[start] = arr[i];  
  30.         start = i;  
  31.     }  
  32.     arr[start] = target;  
  33. }  
  34. public static void swap(int[] arr, int i, int j) {  
  35.     int temp = arr[i];  
  36.     arr[i] = arr[j];  
  37.     arr[j] = temp;  
  38. }  

【本算法解讀】

算法也是按照先建堆再調整堆的步驟執行的。第一個for循環,從n/2節點開始依次通過調用headAdjust()來調整堆,最終完成建堆。第二個for循環,利用之前已經建好的大頂堆(首元素爲最大值),將首元素交換到序列末尾。然後去掉該元素,再調整堆,再次獲得大頂堆(首元素爲次大值),將其首元素交換到倒數第二個位置,以此類推。

算法的關鍵點在於堆調整headAdjust()方法。該方法調整的分支節點爲start位置的元素(稱其爲目標元素)。該元素的左右孩子分別是2*start+1,2*start+2。若目標元素大於等於它的的兩個孩子,則已經是大頂堆,不需要調整了。否則,目標元素和兩個孩子中的較大值交換(對應代碼arr[start] = arr[i],即向下滲透),並將start設置爲目標元素交換後所在的位置,重複上述操作,直到目標元素滲透到適當的位置。

【舉個栗子】

對於待排序列1,4,3,2
首先爲了便於理解,我們可以將其畫成二叉樹:


轉換方法是將待排序列的元素,從上到下,從左到右,依次填入到二叉樹的節點中。

開始建堆。本例中實際上只需要調整節點1,所以以調整節點1爲例:過程如下圖


節點1作爲目標元素,先找到其左右孩子(4和3)的較大值4,即比較目標元素1和4,1<4,則交換位置。目標元素1滲透到元素2原來的位置。在此位置上繼續尋找,其左右孩子,此時只有一個左孩子,元素2,與目標元素做比較,1<2,則交換位置,此時目標元素1已經向下滲透到最終位置。建堆成功,序列爲4,2,3,1。然後通過大頂堆,得到首元素最大值4,並將其移動到序列尾部。取掉元素4後,再次建堆,重複上述操作,完成排序。


歸併排序

【基本思想】

所謂歸併是指,把兩個或兩個以上的待排序列合併起來,形成一個新的有序序列。2-路歸併是指,將兩個有序序列合併成爲一個有序序列。

2-路歸併排序的基本思想是,對於長度爲n的無序序列來說,歸併排序把它看成是由n個只包括一個元素的有序序列組成,然後進行兩兩歸併,最後形成包含n個元素的有序序列

【空間複雜度】O(n)

由於在實現過程中用到了一個臨時序列來暫存歸併過程中的中間結果,所以算法的空間複雜度爲O(n)

【時間複雜度】

平均情況:O(nlog2n)

最好情況:O(nlog2n),此時不發生交換,但仍需進行比較

最壞情況:O(nlog2n)

對於長度爲n的序列,需要進行logn趟2-路歸併,而每趟歸併的時間開銷是O(n),故在任何情況下,2-路歸併的時間複雜度都爲O(nlogn)

【穩定性】穩定

【優點】

穩定排序。若採用單鏈表作爲存儲結構,可實現就地排序

【缺點】

需要O(n)的額外空間

【算法實現】
  1. /** 
  2.  * 歸併排序 
  3.  * @param arr 
  4.  * @param left 
  5.  * @param right 
  6.  */  
  7. public static void mergeSort(int[] arr, int left, int right) {  
  8.     if (left >= right) {  
  9.         return;  
  10.     }  
  11.     int mid = (right + left) / 2;  
  12.     //遞歸劃分子序列  
  13.     mergeSort(arr, left, mid);  
  14.     mergeSort(arr, mid + 1, right);  
  15.     //合併子序列  
  16.     merge(arr, left, mid, right);  
  17. }  
  18.   
  19. private static void merge(int[] arr, int left, int mid, int right) {  
  20.     int[] temp = new int[right - left + 1];  
  21.     int i = left;  
  22.     int j = mid + 1;  
  23.     int k = 0;  
  24.     //比較兩個子序列元素的大小  
  25.     while (i <= mid && j <= right) {  
  26.         if (arr[i] <= arr[j]) {  
  27.             temp[k++] = arr[i++];  
  28.         } else {  
  29.             temp[k++] = arr[j++];  
  30.         }  
  31.     }  
  32.     //處理左邊子序列剩餘元素  
  33.     while (i <= mid) {  
  34.         temp[k++] = arr[i++];  
  35.     }  
  36.     //處理右邊子序列剩餘元素  
  37.     while (j <= right) {  
  38.         temp[k++] = arr[j++];  
  39.     }  
  40.     //將合併後的臨時序列替換到原始序列  
  41.     for (int p = 0; p < temp.length; p++) {  
  42.         arr[left + p] = temp[p];  
  43.     }  
  44. }  

【本算法解讀】

算法首先通過遞歸,不斷將待排序列劃分成兩個子序列,子序列再劃分成兩個子序列,直到每個子序列只含有一個元素(對應代碼:if(left >= rihgt){return;}),然後對每對子序列進行合併。合併子序列是通過merge()方法實現,首先定義了一個臨時的輔助空間,長度是兩個子序列之和。然後逐個比較兩個子序列中的元素,元素較小的先放入輔助空間中。若兩個子序列長度不同,則必定有一個子序列有元素未放入輔助空間。算法後面分別對左邊子序列和右邊子序列做了處理。最後,兩個子序列的合併結果都存在於輔助空間中,將輔助空間中的有序序列替換到原始序列的對應位置上。

【舉個栗子】

對於待排序列1,4,3,2
第一次遞歸,mid = 1,將待排序列分成(1,4),(3,2)。繼續對每個子序列劃分子序列。對於序列(1,4,),(3,2),mid 都是0,即分別被劃分成(1)(4),(3)(2)直到每個子部分只含有一個元素。然後開始合併,合併(1)(4)得到序列(1,4) ,合併(3)(2)得到序列(2,3)。再次合併(1,4)(2,3),得到最終有序序列(1,2,3,4)


可以發現我們本篇所彙總的算法,時間複雜度最低也就是O(nlog2n),包括上一篇彙總講到的交換排序和插入排序也是同樣的結果。其實,基於比較的排序算法,時間複雜度的下界就是O(nlog2n),而下一篇將要進行彙總的排序算法可以突破下界O(nlog2n),達到O(n)。

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