堆排序(选择排序)-八大排序汇总(2)
二叉堆的定义
二叉堆是完全二叉树或者是近似完全二叉树。
二叉堆满足二个特性:
1.父结点的键值总是大于或等于(小于或等于)任何一个子节点的键值。
2.每个结点的左子树和右子树都是一个二叉堆(都是最大堆或最小堆)。
当父结点的键值总是大于或等于任何一个子节点的键值时为最大堆。当父结点的键值总是小于或等于任何一个子节点的键值时为最小堆。下图展示一个最小堆:
堆的存储
一般都用数组来表示堆,i结点的父结点下标就为(i – 1) / 2。它的左右子结点下标分别为2 * i + 1和2 * i + 2。如第0个结点左右子结点下标分别为1和2。
堆的操作——插入删除
下面先给出《数据结构C++语言描述》中最小堆的建立插入删除的图解。
堆的插入
每次插入都是将新数据放在数组最后。可以发现从这个新数据的父结点到根结点必然为一个有序的数列,现在的任务是将这个新数据插入到这个有序数据中——这就类似于直接插入排序中将一个数据并入到有序区间中。
1 // 新加入i结点 其父结点为(i - 1) / 2 2 void MinHeapFixup(int a[], int i) 3 { 4 int j, temp; 5 6 temp = a[i]; 7 j = (i - 1) / 2; //父结点 8 while (j >= 0 && i != 0) 9 { 10 if (a[j] <= temp) 11 break; 12 13 a[i] = a[j]; //把较大的子结点往下移动,替换它的子结点 14 i = j; 15 j = (i - 1) / 2; 16 } 17 a[i] = temp; 18 }
更简短的表达为:
1 void MinHeapFixup(int a[], int i) 2 { 3 for (int j = (i - 1) / 2; (j >= 0 && i != 0)&& a[i] > a[j]; i = j, j = (i - 1) / 2) 4 Swap(a[i], a[j]); 5 }
插入时:
1 //在最小堆中加入新的数据nNum 2 void MinHeapAddNumber(int a[], int n, int nNum) 3 { 4 a[n] = nNum; 5 MinHeapFixup(a, n); 6 }
堆的删除
按定义,堆中每次都只能删除第0个数据。为了便于重建堆,实际的操作是将最后一个数据的值赋给根结点,然后再从根结点开始进行一次从上向下的调整。调整时先在左右儿子结点中找最小的,如果父结点比这个最小的子结点还小说明不需要调整了,反之将父结点和它交换后再考虑后面的结点。相当于从根结点将一个数据的“下沉”过程。下面给出代码:
1 // 从i节点开始调整,n为节点总数 从0开始计算 i节点的子节点为 2*i+1, 2*i+2 2 void MinHeapFixdown(int a[], int i, int n) 3 { 4 int j, temp; 5 6 temp = a[i]; 7 j = 2 * i + 1; 8 while (j < n) 9 { 10 if (j + 1 < n && a[j + 1] < a[j]) //在左右孩子中找最小的 11 j++; 12 13 if (a[j] >= temp) 14 break; 15 16 a[i] = a[j]; //把较小的子结点往上移动,替换它的父结点 17 i = j; 18 j = 2 * i + 1; 19 } 20 a[i] = temp; 21 } 22 //在最小堆中删除数 23 void MinHeapDeleteNumber(int a[], int n) 24 { 25 Swap(a[0], a[n - 1]); 26 MinHeapFixdown(a, 0, n - 1); 27 }
堆化数组
有了堆的插入和删除后,再考虑下如何对一个数据进行堆化操作。要一个一个的从数组中取出数据来建立堆吧,不用!先看一个数组,如下图:
很明显,对叶子结点来说,可以认为它已经是一个合法的堆了即20,60, 65, 4, 49都分别是一个合法的堆。只要从A[4]=50开始向下调整就可以了。然后再取A[3]=30,A[2] = 17,A[1] = 12,A[0] = 9分别作一次向下调整操作就可以了。下图展示了这些步骤:
写出堆化数组的代码:
1 //建立最小堆 2 void MakeMinHeap(int a[], int n) 3 { 4 for (int i = n / 2 - 1; i >= 0; i--) 5 MinHeapFixdown(a, i, n); 6 }
至此,堆的操作就全部完成了(注1),再来看下如何用堆这种数据结构来进行排序。
堆排序
首先可以看到堆建好之后堆中第0个数据是堆中最小的数据。取出这个数据再执行下堆的删除操作。这样堆中第0个数据又是堆中最小的数据,重复上述步骤直至堆中只有一个数据时就直接取出这个数据。
由于堆也是用数组模拟的,故堆化数组后,第一次将A[0]与A[n - 1]交换,再对A[0…n-2]重新恢复堆。第二次将A[0]与A[n – 2]交换,再对A[0…n - 3]重新恢复堆,重复这样的操作直到A[0]与A[1]交换。由于每次都是将最小的数据并入到后面的有序区间,故操作完成后整个数组就有序了。有点类似于直接选择排序。
1 void MinheapsortTodescendarray(int a[], int n) 2 { 3 for (int i = n - 1; i >= 1; i--) 4 { 5 Swap(a[i], a[0]); 6 MinHeapFixdown(a, 0, i); 7 } 8 }
注意使用最小堆排序后是递减数组,要得到递增数组,可以使用最大堆。
基本思想
稳定性
堆排序是不稳定的排序方法
时间复杂度:
调整堆的时间复杂度是lgn,调用了n-1次,所以堆排序过程的时间复杂度是O(nlgn)。
空间复杂度
堆排序是就地排序,辅助空间为O(1)
比较
用小根堆排序与利用大根堆类似,只不过其排序结果是递减有序的。堆排序和直接选择排序相反:在任何时刻堆排序中无序区总是在有序区之前,且有序区是在原向量的尾部由后往前逐步扩大至整个向量为止
注意
大根堆输出的是降序排列的数组,小根堆输出的是升序排列的数组。
代码
1 #include "stdafx.h" 2 #include <iostream> 3 using namespace std; 4 5 inline void Swap(int &a, int &b) 6 { 7 int c = a; 8 a = b; 9 b = c; 10 } 11 12 inline void print(int a[], int n) 13 { 14 for (int i = 0; i < n; i++) 15 { 16 if (i != n - 1) 17 { 18 cout << a[i] << ","; 19 } 20 else 21 cout << a[i]; 22 } 23 cout << endl; 24 } 25 26 // 从i节点开始调整,n为节点总数 从0开始计算 i节点的子节点为 2*i+1, 2*i+2 27 void MinHeapFixdown(int a[], int i, int n) 28 { 29 int j, temp; 30 temp = a[i]; 31 j = 2 * i + 1; 32 while (j < n) 33 { 34 if (j + 1 < n && a[j + 1] < a[j]) //在左右孩子中找最小的 35 j++; 36 if (a[j] >= temp) 37 break; 38 a[i] = a[j]; //把较小的子结点往上移动,替换它的父结点 39 i = j; 40 j = 2 * i + 1; 41 } 42 a[i] = temp; 43 } 44 45 //建立最小堆 46 void MakeMinHeap(int a[], int n) 47 { 48 for (int i = n / 2 - 1; i >= 0; i--) 49 MinHeapFixdown(a, i, n); 50 print(a,10); 51 } 52 53 //最小堆排序 54 void MinheapsortTodescendarray(int a[], int n) 55 { 56 for (int i = n - 1; i >= 1; i--) 57 { 58 Swap(a[i], a[0]); 59 MinHeapFixdown(a, 0, i); 60 } 61 print(a, 10); 62 } 63 64 // 堆排序 65 void HeapSort(int a[],int n) 66 { 67 MakeMinHeap(a, n); 68 MinheapsortTodescendarray(a, n); 69 } 70 71 int _tmain(int argc, _TCHAR* argv[]) 72 { 73 int a[20] = {43,123,42,12,65,44,88,37,93,32}; 74 HeapSort(a, 10); 75 system("pause"); 76 return 0; 77 }
比如数组a[]={6,5,4,3,2,1,0}
6
5 4
3 2 1 0
第一轮:i = n/2-1 = 2, a[2]=4,子节点为1和0,与较小的交换,i--
6
5 0
3 2 1 4
第二轮:i = i-1 = 1, a[1]=5,子节点为3和2,与较小的交换
6
2 0
3 5 1 4
第三轮:i = i-1 =0,a[0]=6,子节点为2和0,与较小的交换
0
2 6
3 5 1 4
j = 2i+1=1,a[j]=6,子节点1和4,与较小的交换
0
2 1
3 5 6 4
这时完成堆的构建,接下来进行堆的排序
第一轮:交换a[0]和a[i-1]
4
2 1
3 5 6 0
从a[0]开始,与较小子节点交换
1
2 4
3 5 6 0
第二轮:a[0]和a[i-2]交换
6
2 4
3 5 1 0
恢复堆,从a[0]开始,与较小子节点交换
2
6 4
3 5 1 0
继续
2
3 4
6 5 1 0
第三轮:a[0]与a[i-3]交换
5
3 4
6 2 1 0
恢复堆,从a[0]开始,与较小节点交换
3
5 4
6 2 1 0
第四轮:a[0]与a[i-4]交换
6
5 4
3 2 1 0
恢复堆,从a[0]开始,与较小节点交换
4
5 6
3 2 1 0
第五轮:a[0]与a[i-5]交换
6
5 4
3 2 1 0
恢复堆,从a[0]开始,与较小节点交换
5
6 4
3 2 1 0
第六轮:a[0]与a[i-6]交换
6
5 4
3 2 1 0
从而完成堆的排序