堆排序與快速排序
前言
前面差不多學習了插入排序、選擇排序、氣泡排序、歸併排序。這些排序除了歸併排序在時間上消耗為:θ(nlgn)外,其餘排序時間消耗都為:θ(n2).
接下來要講的就是兩種比較優雅的比較排序演算法:堆排序和快速排序。 堆排序最壞情況下可以達到上界:ο(nlgn).快速排序平均情況下可以達到:θ(nlgn)。
堆排序
堆排序的關鍵在於完全二叉樹。堆排序開始要構建一個完全二叉樹,且該完全二叉樹必須滿足某一個結點大於左子節點的值和右子結點的值。我們假設根結點中的值為陣列中下標為0的元素,根結點的左子結點下標為1,根結點的右子結點下標為2...依此類推。假設某一個結點的值對應陣列中下標為i的元素,那麼該結點的左子結點的值對應陣列的陣列元素下標為(2*i+1),右子結點的值對應陣列元素下標為(2*i+2)。堆排序的步驟是:
1. 首先要構造一個以陣列中下標為0的元素作為根結點的完全堆二叉樹。此二叉樹構造完成之後可以保證根結點的值為陣列中最大的元素且對於堆中的所有結點的值都大於它的左子結點和右子結點的值。
2. 將堆中的根結點和堆中最後一個結點替換,然後重新構造以陣列下標為0,長度為陣列長度減1的完全堆二叉樹。
3. 重複第二部直到需要構建的堆中元素只剩下唯一一個。
具體程式碼實現如下:
1 public static void main(String[] args) 2 { 3 int[] arr = {1, 12, 11, 4, 5, 7, 3, 1, 23, 56, 34, 76, 25}; 4 heapSort(arr); 5 } 6 7 /** 8 * 堆排序 9 * @param arr 待排序的陣列 10 */ 11 private static void heapSort(int[] arr) { 12 for (int i=arr.length/2; i>=0; i--) 13 { 14 buildHeapTree(arr, i, arr.length); 15 } 16 17 for (int i=arr.length-1; i>=0; i--) 18 { 19 swap(arr, 0, i); // 交換根結點和堆樹中最後一個結點 20 buildHeapTree(arr, 0, i); // 再次以下標為0的元素開始構建層次遞減的堆樹 21 } 22 23 System.out.println(Arrays.toString(arr)); 24 } 25 26 /** 27 * 構建堆樹(滿足時一顆完全二叉樹,且任何結點大於他的子結點) 28 * @param arr 傳入的陣列 29 * @param rootIndex 根結點對應的陣列下標(即以當前下標作為根結點來將陣列中改下標之後的元素作為其子節點的堆樹) 30 * @param deep 堆的深度(對於超過堆深度的陣列中的元素不進行堆樹的構建) 31 */ 32 private static void buildHeapTree(int[] arr, int rootIndex, int deep) { 33 int leftChildIndex = rootIndex * 2 + 1; 34 int rightChildIndex = rootIndex * 2 + 2; 35 int maxIndex = rootIndex; 36 if (leftChildIndex < deep && arr[leftChildIndex] > arr[rootIndex]) 37 { 38 // 如果左子結點大於右子結點,說明當前最大結點的下標應該為左子結點的下標 39 maxIndex = leftChildIndex; 40 } 41 42 if (rightChildIndex < deep && arr[rightChildIndex] > arr[maxIndex]) 43 { 44 // 如果右子結點的值大於與左子結點比較之後得到的最大的結點的值,則說明最大結點的下標應該為右子結點的下標 45 maxIndex = rightChildIndex; 46 } 47 48 if (maxIndex != rootIndex) 49 { 50 // 以當前下標從陣列中的元素開始沒法夠造成堆樹,此時需要在樹中將最大值所在的結點和根結點的位置互換,又因為堆樹的構建是自下往上的,互換了之後需要考慮對前面最大值所在堆樹的影響,所以需要再次遞迴呼叫以前面最大值元素所在位置為根結點構建堆樹、 51 swap(arr, maxIndex, rootIndex); 52 buildHeapTree(arr, maxIndex, deep); 53 } 54 } 55 56 private static void swap(int[] arr, int index1, int index2) { 57 int temp = arr[index1]; 58 arr[index1] = arr[index2]; 59 arr[index2] = temp; 60 }
快速排序:
快速排序的思想其實和歸併排序有些類似。都是通過分治的思想來實現的。只不過歸併排序先是將陣列均分排序,然後再進行歸併整理。快速排序是先得到分開點,然後再對分出來的兩個陣列進行分別快速排序。
廢話不多說直接上程式碼:
1 public static void main(String[] args) 2 { 3 int[] arr = {1, 12, 11, 4, 5, 7, 3, 1, 23, 56, 34, 76, 25}; 4 quickSort(arr); 5 } 6 7 private static void quickSort(int[] arr) { 8 divideQuickSort(arr, 0, arr.length - 1); 9 System.out.println(Arrays.toString(arr)); 10 } 11 12 private static void divideQuickSort(int[] arr, int start, int end) { 13 if (start >= end) 14 { 15 return; 16 } 17 18 int r = partition(arr, start, end); 19 divideQuickSort(arr, start, r - 1); 20 divideQuickSort(arr, r + 1, end); 21 } 22 23 /** 24 * 得到快速排序的分割點 25 * @param arr 傳入的待排序的陣列 26 * @param start 排序的起始元素下標 27 * @param end 排序的終止元素下標 28 * @return 分割點的下標 29 */ 30 private static int partition(int[] arr, int start, int end) { 31 // 先要找一個對比的元素,這裡取終止元素作為對比的元素 32 int compareValue = arr[end]; 33 34 // 從起始元素到比較元素前一個元素迴圈與終止元素進行比對 35 int placeStartIndex = start; 36 for (int i = start; i <= end-1; i++) 37 { 38 if (arr[i] < compareValue) 39 { 40 // 發現比比較元素小的元素,通過交換位置依次從起始元素位置開始擺放 41 swap(arr, i, placeStartIndex++); 42 } 43 } 44 45 // 將比較元素放到比它都小的元素的右側 46 swap(arr, placeStartIndex, end); 47 48 // 此時返回當前比較元素交換之後的位置 49 return placeStartIndex; 50 } 51 52 private static void swap(int[] arr, int index1, int index2) { 53 int temp = arr[index1]; 54 arr[index1] = arr[index2]; 55 arr[index2] = temp; 56 }
以上兩種演算法都是比較優雅的比較演算法。實際上他們在實現的時候不需要額外的分配記憶體,直接在陣列之內進行比較和位置調換。特別是堆排序,完美的實現了將陣列又作為線性的元素列表,又做為樹結點。可以看到比較算的時間極限應該就在Ω(nlgn),對於其他的一些關於陣列長度線性的排序演算法必須依賴於陣列的某些特性,而那些也不能稱作完全的比較演算法。