概要
本章介紹排序算法中的堆排序。
目錄
1. 堆排序介紹
2. 堆排序圖文說明
3. 堆排序的時間復雜度和穩定性
4. 堆排序實現
4.1 堆排序C實現
4.2 堆排序C++實現
4.3 堆排序Java實現
轉載請注明出處:http://www.cnblogs.com/skywang12345/p/3602162.html
更多排序和算法請參考:數據結構與算法系列 目錄
堆排序介紹
堆排序(Heap Sort)是指利用堆這種數據結構所設計的一種排序算法。
因此,學習堆排序之前,有必要了解堆!若讀者不熟悉堆,建議先了解堆(建議可以通過二叉堆,左傾堆,斜堆,二項堆或斐波那契堆等文章進行了解),然后再來學習本章。
我們知道,堆分為"最大堆"和"最小堆"。最大堆通常被用來進行"升序"排序,而最小堆通常被用來進行"降序"排序。
鑒於最大堆和最小堆是對稱關系,理解其中一種即可。本文將對最大堆實現的升序排序進行詳細說明。
最大堆進行升序排序的基本思想:
① 初始化堆:將數列a[1...n]構造成最大堆。
② 交換數據:將a[1]和a[n]交換,使a[n]是a[1...n]中的最大值;然后將a[1...n-1]重新調整為最大堆。 接着,將a[1]和a[n-1]交換,使a[n-1]是a[1...n-1]中的最大值;然后將a[1...n-2]重新調整為最大值。 依次類推,直到整個數列都是有序的。
下面,通過圖文來解析堆排序的實現過程。注意實現中用到了"數組實現的二叉堆的性質"。
在第一個元素的索引為 0 的情形中:
性質一:索引為i的左孩子的索引是 (2*i+1);
性質二:索引為i的左孩子的索引是 (2*i+2);
性質三:索引為i的父結點的索引是 floor((i-1)/2);
例如,對於最大堆{110,100,90,40,80,20,60,10,30,50,70}而言:索引為0的左孩子的所有是1;索引為0的右孩子是2;索引為8的父節點是3。
堆排序圖文說明
堆排序(升序)代碼
/* * (最大)堆的向下調整算法 * * 注:數組實現的堆中,第N個節點的左孩子的索引值是(2N+1),右孩子的索引是(2N+2)。 * 其中,N為數組下標索引值,如數組中第1個數對應的N為0。 * * 參數說明: * a -- 待排序的數組 * start -- 被下調節點的起始位置(一般為0,表示從第1個開始) * end -- 截至范圍(一般為數組中最后一個元素的索引) */ void maxheap_down(int a[], int start, int end) { int c = start; // 當前(current)節點的位置 int l = 2*c + 1; // 左(left)孩子的位置 int tmp = a[c]; // 當前(current)節點的大小 for (; l <= end; c=l,l=2*l+1) { // "l"是左孩子,"l+1"是右孩子 if ( l < end && a[l] < a[l+1]) l++; // 左右兩孩子中選擇較大者,即m_heap[l+1] if (tmp >= a[l]) break; // 調整結束 else // 交換值 { a[c] = a[l]; a[l]= tmp; } } } /* * 堆排序(從小到大) * * 參數說明: * a -- 待排序的數組 * n -- 數組的長度 */ void heap_sort_asc(int a[], int n) { int i; // 從(n/2-1) --> 0逐次遍歷。遍歷之后,得到的數組實際上是一個(最大)二叉堆。 for (i = n / 2 - 1; i >= 0; i--) maxheap_down(a, i, n-1); // 從最后一個元素開始對序列進行調整,不斷的縮小調整的范圍直到第一個元素 for (i = n - 1; i > 0; i--) { // 交換a[0]和a[i]。交換后,a[i]是a[0...i]中最大的。 swap(a[0], a[i]); // 調整a[0...i-1],使得a[0...i-1]仍然是一個最大堆。 // 即,保證a[i-1]是a[0...i-1]中的最大值。 maxheap_down(a, 0, i-1); } }
heap_sort_asc(a, n)的作用是:對數組a進行升序排序;其中,a是數組,n是數組長度。
heap_sort_asc(a, n)的操作分為兩部分:初始化堆 和 交換數據。
maxheap_down(a, start, end)是最大堆的向下調整算法。
下面演示heap_sort_asc(a, n)對a={20,30,90,40,70,110,60,10,100,50,80}, n=11進行堆排序過程。下面是數組a對應的初始化結構:
1 初始化堆
在堆排序算法中,首先要將待排序的數組轉化成二叉堆。
下面演示將數組{20,30,90,40,70,110,60,10,100,50,80}轉換為最大堆{110,100,90,40,80,20,60,10,30,50,70}的步驟。
1.1 i=11/2-1,即i=4
上面是maxheap_down(a, 4, 9)調整過程。maxheap_down(a, 4, 9)的作用是將a[4...9]進行下調;a[4]的左孩子是a[9],右孩子是a[10]。調整時,選擇左右孩子中較大的一個(即a[10])和a[4]交換。
1.2 i=3
上面是maxheap_down(a, 3, 9)調整過程。maxheap_down(a, 3, 9)的作用是將a[3...9]進行下調;a[3]的左孩子是a[7],右孩子是a[8]。調整時,選擇左右孩子中較大的一個(即a[8])和a[4]交換。
1.3 i=2
上面是maxheap_down(a, 2, 9)調整過程。maxheap_down(a, 2, 9)的作用是將a[2...9]進行下調;a[2]的左孩子是a[5],右孩子是a[6]。調整時,選擇左右孩子中較大的一個(即a[5])和a[2]交換。
1.4 i=1
上面是maxheap_down(a, 1, 9)調整過程。maxheap_down(a, 1, 9)的作用是將a[1...9]進行下調;a[1]的左孩子是a[3],右孩子是a[4]。調整時,選擇左右孩子中較大的一個(即a[3])和a[1]交換。交換之后,a[3]為30,它比它的右孩子a[8]要大,接着,再將它們交換。
1.5 i=0
上面是maxheap_down(a, 0, 9)調整過程。maxheap_down(a, 0, 9)的作用是將a[0...9]進行下調;a[0]的左孩子是a[1],右孩子是a[2]。調整時,選擇左右孩子中較大的一個(即a[2])和a[0]交換。交換之后,a[2]為20,它比它的左右孩子要大,選擇較大的孩子(即左孩子)和a[2]交換。
調整完畢,就得到了最大堆。此時,數組{20,30,90,40,70,110,60,10,100,50,80}也就變成了{110,100,90,40,80,20,60,10,30,50,70}。
第2部分 交換數據
在將數組轉換成最大堆之后,接着要進行交換數據,從而使數組成為一個真正的有序數組。
交換數據部分相對比較簡單,下面僅僅給出將最大值放在數組末尾的示意圖。
上面是當n=10時,交換數據的示意圖。
當n=10時,首先交換a[0]和a[10],使得a[10]是a[0...10]之間的最大值;然后,調整a[0...9]使它稱為最大堆。交換之后:a[10]是有序的!
當n=9時, 首先交換a[0]和a[9],使得a[9]是a[0...9]之間的最大值;然后,調整a[0...8]使它稱為最大堆。交換之后:a[9...10]是有序的!
...
依此類推,直到a[0...10]是有序的。
堆排序的時間復雜度和穩定性
堆排序時間復雜度
堆排序的時間復雜度是O(N*lgN)。
假設被排序的數列中有N個數。遍歷一趟的時間復雜度是O(N),需要遍歷多少次呢?
堆排序是采用的二叉堆進行排序的,二叉堆就是一棵二叉樹,它需要遍歷的次數就是二叉樹的深度,而根據完全二叉樹的定義,它的深度至少是lg(N+1)。最多是多少呢?由於二叉堆是完全二叉樹,因此,它的深度最多也不會超過lg(2N)。因此,遍歷一趟的時間復雜度是O(N),而遍歷次數介於lg(N+1)和lg(2N)之間;因此得出它的時間復雜度是O(N*lgN)。
堆排序穩定性
堆排序是不穩定的算法,它不滿足穩定算法的定義。它在交換數據的時候,是比較父結點和子節點之間的數據,所以,即便是存在兩個數值相等的兄弟節點,它們的相對順序在排序也可能發生變化。
算法穩定性 -- 假設在數列中存在a[i]=a[j],若在排序之前,a[i]在a[j]前面;並且排序之后,a[i]仍然在a[j]前面。則這個排序算法是穩定的!
堆排序實現
下面給出堆排序的三種實現:C、C++和Java。這三種實現的原理和輸出結果都是一樣的,每一種實現中都包括了"最大堆對應的升序排列"和"最小堆對應的降序排序"。
堆排序C實現
實現代碼(heap_sort.c)

1 /** 2 * 堆排序:C 語言 3 * 4 * @author skywang 5 * @date 2014/03/12 6 */ 7 8 #include <stdio.h> 9 10 // 數組長度 11 #define LENGTH(array) ( (sizeof(array)) / (sizeof(array[0])) ) 12 #define swap(a,b) (a^=b,b^=a,a^=b) 13 14 /* 15 * (最大)堆的向下調整算法 16 * 17 * 注:數組實現的堆中,第N個節點的左孩子的索引值是(2N+1),右孩子的索引是(2N+2)。 18 * 其中,N為數組下標索引值,如數組中第1個數對應的N為0。 19 * 20 * 參數說明: 21 * a -- 待排序的數組 22 * start -- 被下調節點的起始位置(一般為0,表示從第1個開始) 23 * end -- 截至范圍(一般為數組中最后一個元素的索引) 24 */ 25 void maxheap_down(int a[], int start, int end) 26 { 27 int c = start; // 當前(current)節點的位置 28 int l = 2*c + 1; // 左(left)孩子的位置 29 int tmp = a[c]; // 當前(current)節點的大小 30 for (; l <= end; c=l,l=2*l+1) 31 { 32 // "l"是左孩子,"l+1"是右孩子 33 if ( l < end && a[l] < a[l+1]) 34 l++; // 左右兩孩子中選擇較大者,即m_heap[l+1] 35 if (tmp >= a[l]) 36 break; // 調整結束 37 else // 交換值 38 { 39 a[c] = a[l]; 40 a[l]= tmp; 41 } 42 } 43 } 44 45 /* 46 * 堆排序(從小到大) 47 * 48 * 參數說明: 49 * a -- 待排序的數組 50 * n -- 數組的長度 51 */ 52 void heap_sort_asc(int a[], int n) 53 { 54 int i; 55 56 // 從(n/2-1) --> 0逐次遍歷。遍歷之后,得到的數組實際上是一個(最大)二叉堆。 57 for (i = n / 2 - 1; i >= 0; i--) 58 maxheap_down(a, i, n-1); 59 60 // 從最后一個元素開始對序列進行調整,不斷的縮小調整的范圍直到第一個元素 61 for (i = n - 1; i > 0; i--) 62 { 63 // 交換a[0]和a[i]。交換后,a[i]是a[0...i]中最大的。 64 swap(a[0], a[i]); 65 // 調整a[0...i-1],使得a[0...i-1]仍然是一個最大堆。 66 // 即,保證a[i-1]是a[0...i-1]中的最大值。 67 maxheap_down(a, 0, i-1); 68 } 69 } 70 71 /* 72 * (最小)堆的向下調整算法 73 * 74 * 注:數組實現的堆中,第N個節點的左孩子的索引值是(2N+1),右孩子的索引是(2N+2)。 75 * 其中,N為數組下標索引值,如數組中第1個數對應的N為0。 76 * 77 * 參數說明: 78 * a -- 待排序的數組 79 * start -- 被下調節點的起始位置(一般為0,表示從第1個開始) 80 * end -- 截至范圍(一般為數組中最后一個元素的索引) 81 */ 82 void minheap_down(int a[], int start, int end) 83 { 84 int c = start; // 當前(current)節點的位置 85 int l = 2*c + 1; // 左(left)孩子的位置 86 int tmp = a[c]; // 當前(current)節點的大小 87 for (; l <= end; c=l,l=2*l+1) 88 { 89 // "l"是左孩子,"l+1"是右孩子 90 if ( l < end && a[l] > a[l+1]) 91 l++; // 左右兩孩子中選擇較小者 92 if (tmp <= a[l]) 93 break; // 調整結束 94 else // 交換值 95 { 96 a[c] = a[l]; 97 a[l]= tmp; 98 } 99 } 100 } 101 102 /* 103 * 堆排序(從大到小) 104 * 105 * 參數說明: 106 * a -- 待排序的數組 107 * n -- 數組的長度 108 */ 109 void heap_sort_desc(int a[], int n) 110 { 111 int i; 112 113 // 從(n/2-1) --> 0逐次遍歷每。遍歷之后,得到的數組實際上是一個最小堆。 114 for (i = n / 2 - 1; i >= 0; i--) 115 minheap_down(a, i, n-1); 116 117 // 從最后一個元素開始對序列進行調整,不斷的縮小調整的范圍直到第一個元素 118 for (i = n - 1; i > 0; i--) 119 { 120 // 交換a[0]和a[i]。交換后,a[i]是a[0...i]中最小的。 121 swap(a[0], a[i]); 122 // 調整a[0...i-1],使得a[0...i-1]仍然是一個最小堆。 123 // 即,保證a[i-1]是a[0...i-1]中的最小值。 124 minheap_down(a, 0, i-1); 125 } 126 } 127 128 void main() 129 { 130 int i; 131 int a[] = {20,30,90,40,70,110,60,10,100,50,80}; 132 int ilen = LENGTH(a); 133 134 printf("before sort:"); 135 for (i=0; i<ilen; i++) 136 printf("%d ", a[i]); 137 printf("\n"); 138 139 heap_sort_asc(a, ilen); // 升序排列 140 //heap_sort_desc(a, ilen); // 降序排列 141 142 printf("after sort:"); 143 for (i=0; i<ilen; i++) 144 printf("%d ", a[i]); 145 printf("\n"); 146 }

1 /** 2 * 堆排序:C++ 3 * 4 * @author skywang 5 * @date 2014/03/11 6 */ 7 8 #include <iostream> 9 using namespace std; 10 11 /* 12 * (最大)堆的向下調整算法 13 * 14 * 注:數組實現的堆中,第N個節點的左孩子的索引值是(2N+1),右孩子的索引是(2N+2)。 15 * 其中,N為數組下標索引值,如數組中第1個數對應的N為0。 16 * 17 * 參數說明: 18 * a -- 待排序的數組 19 * start -- 被下調節點的起始位置(一般為0,表示從第1個開始) 20 * end -- 截至范圍(一般為數組中最后一個元素的索引) 21 */ 22 void maxHeapDown(int* a, int start, int end) 23 { 24 int c = start; // 當前(current)節點的位置 25 int l = 2*c + 1; // 左(left)孩子的位置 26 int tmp = a[c]; // 當前(current)節點的大小 27 for (; l <= end; c=l,l=2*l+1) 28 { 29 // "l"是左孩子,"l+1"是右孩子 30 if ( l < end && a[l] < a[l+1]) 31 l++; // 左右兩孩子中選擇較大者,即m_heap[l+1] 32 if (tmp >= a[l]) 33 break; // 調整結束 34 else // 交換值 35 { 36 a[c] = a[l]; 37 a[l]= tmp; 38 } 39 } 40 } 41 42 /* 43 * 堆排序(從小到大) 44 * 45 * 參數說明: 46 * a -- 待排序的數組 47 * n -- 數組的長度 48 */ 49 void heapSortAsc(int* a, int n) 50 { 51 int i,tmp; 52 53 // 從(n/2-1) --> 0逐次遍歷。遍歷之后,得到的數組實際上是一個(最大)二叉堆。 54 for (i = n / 2 - 1; i >= 0; i--) 55 maxHeapDown(a, i, n-1); 56 57 // 從最后一個元素開始對序列進行調整,不斷的縮小調整的范圍直到第一個元素 58 for (i = n - 1; i > 0; i--) 59 { 60 // 交換a[0]和a[i]。交換后,a[i]是a[0...i]中最大的。 61 tmp = a[0]; 62 a[0] = a[i]; 63 a[i] = tmp; 64 // 調整a[0...i-1],使得a[0...i-1]仍然是一個最大堆。 65 // 即,保證a[i-1]是a[0...i-1]中的最大值。 66 maxHeapDown(a, 0, i-1); 67 } 68 } 69 70 /* 71 * (最小)堆的向下調整算法 72 * 73 * 注:數組實現的堆中,第N個節點的左孩子的索引值是(2N+1),右孩子的索引是(2N+2)。 74 * 其中,N為數組下標索引值,如數組中第1個數對應的N為0。 75 * 76 * 參數說明: 77 * a -- 待排序的數組 78 * start -- 被下調節點的起始位置(一般為0,表示從第1個開始) 79 * end -- 截至范圍(一般為數組中最后一個元素的索引) 80 */ 81 void minHeapDown(int* a, int start, int end) 82 { 83 int c = start; // 當前(current)節點的位置 84 int l = 2*c + 1; // 左(left)孩子的位置 85 int tmp = a[c]; // 當前(current)節點的大小 86 for (; l <= end; c=l,l=2*l+1) 87 { 88 // "l"是左孩子,"l+1"是右孩子 89 if ( l < end && a[l] > a[l+1]) 90 l++; // 左右兩孩子中選擇較小者 91 if (tmp <= a[l]) 92 break; // 調整結束 93 else // 交換值 94 { 95 a[c] = a[l]; 96 a[l]= tmp; 97 } 98 } 99 } 100 101 /* 102 * 堆排序(從大到小) 103 * 104 * 參數說明: 105 * a -- 待排序的數組 106 * n -- 數組的長度 107 */ 108 void heapSortDesc(int* a, int n) 109 { 110 int i,tmp; 111 112 // 從(n/2-1) --> 0逐次遍歷每。遍歷之后,得到的數組實際上是一個最小堆。 113 for (i = n / 2 - 1; i >= 0; i--) 114 minHeapDown(a, i, n-1); 115 116 // 從最后一個元素開始對序列進行調整,不斷的縮小調整的范圍直到第一個元素 117 for (i = n - 1; i > 0; i--) 118 { 119 // 交換a[0]和a[i]。交換后,a[i]是a[0...i]中最小的。 120 tmp = a[0]; 121 a[0] = a[i]; 122 a[i] = tmp; 123 // 調整a[0...i-1],使得a[0...i-1]仍然是一個最小堆。 124 // 即,保證a[i-1]是a[0...i-1]中的最小值。 125 minHeapDown(a, 0, i-1); 126 } 127 } 128 129 int main() 130 { 131 int i; 132 int a[] = {20,30,90,40,70,110,60,10,100,50,80}; 133 int ilen = (sizeof(a)) / (sizeof(a[0])); 134 135 cout << "before sort:"; 136 for (i=0; i<ilen; i++) 137 cout << a[i] << " "; 138 cout << endl; 139 140 heapSortAsc(a, ilen); // 升序排列 141 //heapSortDesc(a, ilen); // 降序排列 142 143 cout << "after sort:"; 144 for (i=0; i<ilen; i++) 145 cout << a[i] << " "; 146 cout << endl; 147 148 return 0; 149 }

1 /** 2 * 堆排序:Java 3 * 4 * @author skywang 5 * @date 2014/03/11 6 */ 7 8 public class HeapSort { 9 10 /* 11 * (最大)堆的向下調整算法 12 * 13 * 注:數組實現的堆中,第N個節點的左孩子的索引值是(2N+1),右孩子的索引是(2N+2)。 14 * 其中,N為數組下標索引值,如數組中第1個數對應的N為0。 15 * 16 * 參數說明: 17 * a -- 待排序的數組 18 * start -- 被下調節點的起始位置(一般為0,表示從第1個開始) 19 * end -- 截至范圍(一般為數組中最后一個元素的索引) 20 */ 21 public static void maxHeapDown(int[] a, int start, int end) { 22 int c = start; // 當前(current)節點的位置 23 int l = 2*c + 1; // 左(left)孩子的位置 24 int tmp = a[c]; // 當前(current)節點的大小 25 26 for (; l <= end; c=l,l=2*l+1) { 27 // "l"是左孩子,"l+1"是右孩子 28 if ( l < end && a[l] < a[l+1]) 29 l++; // 左右兩孩子中選擇較大者,即m_heap[l+1] 30 if (tmp >= a[l]) 31 break; // 調整結束 32 else { // 交換值 33 a[c] = a[l]; 34 a[l]= tmp; 35 } 36 } 37 } 38 39 /* 40 * 堆排序(從小到大) 41 * 42 * 參數說明: 43 * a -- 待排序的數組 44 * n -- 數組的長度 45 */ 46 public static void heapSortAsc(int[] a, int n) { 47 int i,tmp; 48 49 // 從(n/2-1) --> 0逐次遍歷。遍歷之后,得到的數組實際上是一個(最大)二叉堆。 50 for (i = n / 2 - 1; i >= 0; i--) 51 maxHeapDown(a, i, n-1); 52 53 // 從最后一個元素開始對序列進行調整,不斷的縮小調整的范圍直到第一個元素 54 for (i = n - 1; i > 0; i--) { 55 // 交換a[0]和a[i]。交換后,a[i]是a[0...i]中最大的。 56 tmp = a[0]; 57 a[0] = a[i]; 58 a[i] = tmp; 59 // 調整a[0...i-1],使得a[0...i-1]仍然是一個最大堆。 60 // 即,保證a[i-1]是a[0...i-1]中的最大值。 61 maxHeapDown(a, 0, i-1); 62 } 63 } 64 65 /* 66 * (最小)堆的向下調整算法 67 * 68 * 注:數組實現的堆中,第N個節點的左孩子的索引值是(2N+1),右孩子的索引是(2N+2)。 69 * 其中,N為數組下標索引值,如數組中第1個數對應的N為0。 70 * 71 * 參數說明: 72 * a -- 待排序的數組 73 * start -- 被下調節點的起始位置(一般為0,表示從第1個開始) 74 * end -- 截至范圍(一般為數組中最后一個元素的索引) 75 */ 76 public static void minHeapDown(int[] a, int start, int end) { 77 int c = start; // 當前(current)節點的位置 78 int l = 2*c + 1; // 左(left)孩子的位置 79 int tmp = a[c]; // 當前(current)節點的大小 80 81 for (; l <= end; c=l,l=2*l+1) { 82 // "l"是左孩子,"l+1"是右孩子 83 if ( l < end && a[l] > a[l+1]) 84 l++; // 左右兩孩子中選擇較小者 85 if (tmp <= a[l]) 86 break; // 調整結束 87 else { // 交換值 88 a[c] = a[l]; 89 a[l]= tmp; 90 } 91 } 92 } 93 94 /* 95 * 堆排序(從大到小) 96 * 97 * 參數說明: 98 * a -- 待排序的數組 99 * n -- 數組的長度 100 */ 101 public static void heapSortDesc(int[] a, int n) { 102 int i,tmp; 103 104 // 從(n/2-1) --> 0逐次遍歷每。遍歷之后,得到的數組實際上是一個最小堆。 105 for (i = n / 2 - 1; i >= 0; i--) 106 minHeapDown(a, i, n-1); 107 108 // 從最后一個元素開始對序列進行調整,不斷的縮小調整的范圍直到第一個元素 109 for (i = n - 1; i > 0; i--) { 110 // 交換a[0]和a[i]。交換后,a[i]是a[0...i]中最小的。 111 tmp = a[0]; 112 a[0] = a[i]; 113 a[i] = tmp; 114 // 調整a[0...i-1],使得a[0...i-1]仍然是一個最小堆。 115 // 即,保證a[i-1]是a[0...i-1]中的最小值。 116 minHeapDown(a, 0, i-1); 117 } 118 } 119 120 public static void main(String[] args) { 121 int i; 122 int a[] = {20,30,90,40,70,110,60,10,100,50,80}; 123 124 System.out.printf("before sort:"); 125 for (i=0; i<a.length; i++) 126 System.out.printf("%d ", a[i]); 127 System.out.printf("\n"); 128 129 heapSortAsc(a, a.length); // 升序排列 130 //heapSortDesc(a, a.length); // 降序排列 131 132 System.out.printf("after sort:"); 133 for (i=0; i<a.length; i++) 134 System.out.printf("%d ", a[i]); 135 System.out.printf("\n"); 136 } 137 }
它們的輸出結果:
before sort:20 30 90 40 70 110 60 10 100 50 80
after sort:10 20 30 40 50 60 70 80 90 100 110