http://bubkoo.com/2014/01/14/sort-algorithm/heap-sort/
1 什么是堆?
堆(二叉堆)可以视为一棵完全的二叉树,完全二叉树的一个“优秀”的性质是,除了最底层之外,每一层都是满的,这使得堆可以利用数组来表示(普通的一般的二叉树通常用链表作为基本容器表示),每一个结点对应数组中的一个元素。
对于给定的某个结点的下标 i,可以很容易的计算出这个结点的父结点、孩子结点的下标:
- Parent(i) = i/2,i 的父节点下标
- Left(i) = 2i,i 的左子节点下标
- Right(i) = 2i + 1,i 的右子节点下标
当下标从0开始,几个计算公式也要作出相应调整:
- Parent(i) = (i-1)/2,i 的父节点下标
- Left(i) = 2i + 1,i 的左子节点下标
- Right(i) = 2(i + 1),i 的右子节点下标
2 最大堆与最小堆
- 最大堆中的最大元素值出现在根结点(堆顶)
- 堆中每个父节点的元素值都大于等于其孩子结点(如果存在)
- 最小堆中的最小元素值出现在根结点(堆顶)
- 堆中每个父节点的元素值都小于等于其孩子结点(如果存在)
3 堆排序原理
1 插入
每次插入都是将新数据放在数组最后,接着从下至上进行交换。
i为插入的结点索引,也是堆数组插入后的有效长度-1
void MinHeapFixup(int a[], int i)
{
for (int j = (i - 1) / 2; (j >= 0 && i != 0)&& a[i] > a[j]; i = j, j = (i - 1) / 2)
int tmp = a[i];
a[i] = a[j];
a[j] = tmp;
}
2 删除
或称重排。按定义,堆中每次都只能删除第0个数据。
为了便于重建堆,实际的操作是将最后一个数据的值赋给根结点,
然后再从根结点开始进行一次从上向下的调整。
i为需要调整的结点,n为结点总数
正常删除时,首先把最后的元素覆盖掉第0个元素,数组长度n-1,i=0
void HeapDelete(int a[], int i, int n){
while(i < n){
int max = i;
int left = 2 * max + 1;
int right = 2 * (max + 1);
if(a[max] < a[left]){
max = left;
}
if(a[max] < a[right]){
max = right;
}
if(max != i){
int tmp = a[i];
a[i] = a[max];
a[max] = tmp;
i = max;
}else {
break;
}
}
}
3 创建堆
创建堆是将一个数组想象成完全二叉树,从倒数第一个父节点开始做堆重排(即堆删除后的重排操作),然后当前结点不断往上移动直到根节点
void HeapBuild(int[] a, int n) {
int i;
int parent = ((n - 1) / 2);
for (i = parent; i >= 0; i--) {
HeapDelete(a, i, n);
}
}
4 堆排序
使用最小堆排序后是递减数组,要得到递增数组,可以使用最大堆。
第一次将A[0]与A[n - 1]交换,再对A[0…n-2]重新恢复堆。第二次将A[0]与A[n – 2]交换,再对A[0…n - 3]重新恢复堆,重复这样的操作直到A[0]与A[1]交换。由于每次都是将最小的数据并入到后面的有序区间,故操作完成后整个数组就有序了。有点类似于直接选择排序。
void HeapSort(int[] a, int n) {
for (int i = n - 1; i >= -1; i++){
int tmp = a[i];
a[i] = a[0];
a[0] = tmp;
HeapDelete(a,0,i);
}
}
由于每次重新恢复堆的时间复杂度为O(logN),共N - 1次重新恢复堆操作,再加上前面建立堆时N / 2次向下调整,每次调整时间复杂度也为O(logN)。二次操作时间相加还是O(N * logN)。故堆排序的时间复杂度为O(N * logN)。