百萬資料排序:優化的選擇排序(堆排序)
本博文介紹首先介紹直接選擇排序,然後針對直接選擇排序的缺點改進的“堆排序”,堆排序非常適合:陣列規模非常大(數百萬或更多) + 嚴格要求輔助空間的場景。
直接選擇排序
(一)概念及實現
直接選擇排序的原理:將整個陣列視為虛擬的有序區和無序區,重複的遍歷陣列,每次遍歷從無序區中選出一個最小(或最大)的元素,放在有序區的最後,每一次遍歷排序過程都是有序區元素個數增加,無序區元素個數減少的過程,直到無序區元素個數位0。
具體如下(實現為升序):
設陣列為a[0…n-1]。
1.將原序列分成有序區和無序區。a[0…i-1]為有序區,a[i…n-1]為無序區。初始化有序區為0個元素。
2.
3.重複步驟2,直到無序區元素個數為0。
實現程式碼:
public static void Sort<T>(IList<T> arr) where T : IComparable<T> { if (arr == null) throw new ArgumentNullException("arr"); int length = arr.Count(); if (length > 1) { int minValueIndex = 0; T minValue = default(T); // 迴圈length - 2次,最後一個元素無需再比較 for (int i = 0; i < length - 1; i++) { minValueIndex = i; minValue = arr[i]; // 內部迴圈,查詢本次迴圈的最小值 for (int j = i + 1; j < length; j++) { if (minValue.CompareTo(arr[j]) > 0) { minValueIndex = j; minValue = arr[j]; } } if (minValueIndex == i) continue; // 交換:將本次迴圈選出的最小值,順序放在有序區序列的最後(即與無序區的第一個元素交換) arr[minValueIndex] = arr[i]; arr[i] = minValue; } } }
示例:
89,-7,999,-89,7,0,-888,7,-7
排序的過程:
[-888][-7999-8970897-7]
[-888-89][999-770897-7]
[-888-89-7][99970897-7]
[-888-89-7-7][70897999]
……
……
[-888 -89 -7 -7 0 7 7 89 999] []
(二)演算法複雜度
1.時間複雜度:O(n^2)
直接選擇排序耗時的操作有:比較 + 交換賦值。時間複雜度如下:
1)最好情況:序列是升序排列,在這種情況下,需要進行的比較操作需
2)最壞情況:序列是降序排列,那麼此時需要進行的比較共有次。交換賦值n-1 次(交換次數比氣泡排序少多了),直接選擇排序的效率比較穩定,最好情況和最壞情況差不多。即O(n^2)
3)漸進時間複雜度(平均時間複雜度):O(n^2)
2.空間複雜度:O(1)
從實現原理可知,直接選擇插入排序是在原輸入陣列上進行交換賦值操作的(稱“就地排序”),所需開闢的輔助空間跟輸入陣列規模無關,所以空間複雜度為:O(1)
(三)穩定性
直接選擇排序是不穩定的。
因為每次遍歷比較完後會使用本次遍歷選擇的最小元素和無序區的第一個元素交換位置,所以如果無序區第一個元素後面有相同元素的,則可能會改變相同元素的相對順序。
(四)優化改進
1.相同元素:如果陣列元素重複率高,可以考慮使用輔助空間在每一次迴圈的時候,將本次選擇的數及相同元素的索引記錄下來,一起處理。
2.堆排序:直接選擇排序中,為了從a[0..n-1]中選出關鍵字最小的記錄,必須進行n-1次比較,然後在a[1..n-1]中選出關鍵字最小的記錄,又需要做n-2次比較。事實上,後面的n-2次比較中,有許多比較可能在前面的n-1次比較中已經做過,但由於前一趟排序時未保留這些比較結果,所以後一趟排序時又重複執行了這些比較操作。堆排序可通過樹形結構儲存部分比較結果,可減少比較次數。(這種效果在陣列規模越大越能體現效果)
堆排序
(一)概念及實現
堆排序(Heapsort)的原理:是指利用“二叉堆”這種資料結構所設計的一種排序演算法,可以利用陣列的特點快速定位指定索引的元素。
1.二叉堆
是完全二叉樹或者是近似完全二叉樹,它有兩種形式:最大堆(大頂堆、大根堆)和最小堆(小頂堆、小根堆)。
2.二叉堆滿足二個特性
1)父結點的鍵值總是大於或等於(小於或等於)任何一個子節點的鍵值。
2)每個結點的左子樹和右子樹都是一個二叉堆(最大堆或最小堆)。
3.二叉堆一般用陣列來表示
如果根節點在陣列中的位置是0,第n個位置的子節點分別在2n+1和 2n+2,其父節點的下標是 (n-1)/2 。
4.示例
原陣列:
初始化為最大堆:
具體如下(實現為升序):
設陣列為a[0…n-1]。
1.將原序列分成有序區和無序區。a[0…i-1]為無序區,a[i…n-1]為有序區。初始化有序區為0個元素。
2.(從下往上)從陣列最後一個根節點開始 (maxIndex - 1)/2 ,將原陣列初始化為最大堆。(如上圖)
3.(從上往下)將堆頂元素與無序區的最後一個元素交換(即插入有序區的第一個位置),將剩餘的無序區元素重建最大堆。
4.重複步驟3,每一次重複都是有序區元素個數增加,無序區元素個數減少的過程,直到無序區元素個數位0
實現程式碼:
/// <summary> /// 堆排序 /// </summary> public class Heap { public static void Sort<T>(IList<T> arr) where T : IComparable<T> { if (arr == null) throw new ArgumentNullException("arr"); int length = arr.Count(); if (length > 1) { // 1、初始化最大堆 InitMaxHeap<T>(arr, length - 1); // 2、堆排序 // 將堆頂資料與末尾資料交換,再將i=N-1長的堆調整為最大堆;不斷縮小待排序範圍直到,無序區元素為0。 for (int i = length - 1; i > 0; i--) { // 2.1 將堆頂資料與末尾資料交換 Swap<T>(arr, 0, i); // 2.2 縮小陣列待排序範圍 i - 1 ,重新調整為最大堆 AdjustMaxHeap<T>(arr, 0, i - 1); } } } /// <summary> /// 構建最大堆 (還未進行排序) /// </summary> /// <param name="arr">待排序陣列</param> /// <param name="maxIndex">待排序陣列最大索引</param> private static void InitMaxHeap<T>(IList<T> arr, int maxIndex) where T : IComparable<T> { // 從完全二叉樹最後一個非葉節點 : // 如果根節點在陣列中的位置是0,第n個位置的子節點分別在2n+1和 2n+2,其父節點的下標是 (n-1)/2 。 for (int i = (maxIndex - 1) / 2; i >= 0; i--) { AdjustMaxHeap<T>(arr, i, maxIndex); } } /// <summary> /// 調整指定父節點的二叉樹為最大堆 /// </summary> /// <param name="arr">待排序陣列</param> /// <param name="parentNodeIndex">指定父節點</param> /// <param name="maxIndex">待排序陣列最大索引</param> private static void AdjustMaxHeap<T>(IList<T> arr, int parentNodeIndex, int maxIndex) where T : IComparable<T> { if (maxIndex > 0) // 只有堆頂一個元素,就不用調整了 { int resultIndex = -1; // 下標為i的節點的子節點是2i + 1與2i + 2 int leftIndex = 2 * parentNodeIndex + 1; int rightIndex = 2 * parentNodeIndex + 2; if (leftIndex > maxIndex) { // 該父節點沒有左右子節點 return; } else if (rightIndex > maxIndex) resultIndex = leftIndex; else // 比較左右節點。 resultIndex = Max<T>(arr, leftIndex, rightIndex); // 父節點與較大的子節點進行比較 resultIndex = Max<T>(arr, parentNodeIndex, resultIndex); if (resultIndex != parentNodeIndex) { // 如果最大的不是父節點,則交換。 Swap<T>(arr, parentNodeIndex, resultIndex); // 交換後子樹可能不是最大堆,所以需要重新調整交換元素的子樹 AdjustMaxHeap<T>(arr, resultIndex, maxIndex); } } } /// <summary> /// 獲取較大數的陣列索引 /// </summary> /// <param name="arr">待排序陣列</param> /// <param name="leftIndex">左節點索引</param> /// <param name="rightIndex">右節點索引</param> /// <returns>返回較大數的陣列索引</returns> private static int Max<T>(IList<T> arr, int leftIndex, int rightIndex) where T : IComparable<T> { // 相等,以左節點為大 return arr[leftIndex].CompareTo(arr[rightIndex]) >