堆(数据结构)的定义:
wiki百科中对堆的定义是
既然堆是一棵树,那么其特点也应该是递归的了。继续wikipedia:
1.父节点的键值总是大于等于(或小于等于)任何子节点的键值
2.每个节点的左右子树都是一个二叉堆(具有树的标志性的递归定义)
其中父节点的键值比子节点大的叫做大根堆,反之则是小根堆。
小根堆例子: 大根堆:(同样数据)
1 9
/ \ / \
4 6 7 8
/ \ / \ / \ / \
5 7 8 9 6 5 4 1
可能大家建的堆跟图中的不一样,但是也符合堆的定义,因此可以看到对于同样一组数据,堆的构建不是唯一的,因此堆排序的不稳定的也不难理解了。
堆排序的时间复杂度是O(NlogN),这个后面会介绍到。
因为其他几种堆(二项式堆,斐波那契堆)用的较少,因此通常来讲,我们习惯将堆默认为二叉堆。
堆是用数组来存储的,采取的是树的双亲存储结构(一种顺序存储结构),原因:堆是一颗完全二叉树,用下标即可表达父子关系,而数组具有操作简单,速度更快的优点。
以上图中的小根堆为例:
1 | 4 | 6 | 5 | 7 | 8 | 9 |
i 节点的孩子下标应该是2 * i + 1(左孩子)和 2 * i + 2(右孩子),父节点的下标应该是(i - 1)/ 2【下标从0开始】
堆排序的过程:(堆的基本操作:插入与删除 包含其中,不再单独介绍)
1)建堆(以大根堆为例)(图源来自http://www.java3z.com/cwbwebhome/article/article1/1362.html?id=4745感谢原博主)
该完全二叉树中,叶节点为30,48,93,15,35,显然,叶节点是满足堆的要求的,因此我们应该从第一个非叶节点 72 开始调整。
72比35大,因此不需要做处理,再看53,比左孩子小,将其与左孩子交换;再看18,比两个孩子都小,应该跟大的换,如果跟30换,那么30还要继续跟48换,从而才能保证根最大;
以此类推...直到根节点
2)排序
建堆工作已经完毕,我们将最大的元素放在了根节点,首先我们将根节点与最后一个节点(35)作交换。第一趟排序完成。93到达了最终位置。将剩余部分继续调整为堆即可,现在堆中只有35一个数字不满足堆的定义。其他记录都满足,因此只需要调整35即可。
具体的步骤就是35一直往下沉,直到满足堆的定义。不再赘述。
经过第二趟排序,我们可以得到次大的元素72,再将72与最后一个节点交换,依照以上处理方法继续处理,直到树中只有一个元素位置,排序结束。
算法思想如下:
public void heapSort(int[] a){
//1.build the heap
//2.exchange the first node with the last one,heapLength--;
//3.split the biggest node(the last node after changed) with left ones,
//4.adjust the first node(after changed)to fit in heap defination
}
下面来看代码,首先我们知道,图例介绍中,堆排序主要分为两部分:建堆 & 调整;我们可以看到建堆的过程其实也是在调整,刚好符合树的递归定义,因此,我们先介绍如何调整。
static void ajustHeap(int[] heap, int length, int i) {
int left = 2 * i + 1;//左孩子
int right = 2 * i + 2;//右孩子
int big = i;//较大的节点下标
int tem;
while (left < length || right < length) {//循环直到确定最终位置
if (left < length && heap[left] > heap[big]) {
big = left;
}
if (right < length && heap[right] > heap[big]) {
big = right;
}//确定较大键值的下标
if (i == big) {//如果该节点满足要求,则跳出循环
break;
} else {//否则与较大键值的孩子交换,并递归往下
tem = heap[i];
heap[i] = heap[big];
heap[big] = tem;
i = big;
left = 2 * i + 1;
right = 2 * 2 + 2;
}
}
}
配合上图中建堆过程中的调整理解。
static void buildHeap(int[] heap, int length) {
//从第一个非叶结点开始调整
//由于堆是完全二叉树,因此如果堆的总节点个数是偶数,则最后一个叶节点一定是其父节点的左孩子
//如果堆的总结点数是奇数,则非叶节点均包含两个孩子(扯远了)
int begin = length % 2 == 0 ? length / 2 : (length - 1) / 2;
for(int i = begin; i >= 0;i--){
ajustHeap(heap, length, i);//建堆的过程就是逐个调整的过程
}
}
public static void main(String[] args) {
int[] heap = { 1, 2, 3, 4, 5, 6, 7, 8, 9 };
int length = heap.length;
buildHeap(heap, length);//建堆
System.out.println(Arrays.toString(heap));
while (length > 1) {
int tem = 0;
tem = heap[length - 1];
heap[length - 1] = heap[0];
heap[0] = tem;//将收尾交换
length--;//将最大节点从堆中删除
ajustHeap(heap, length, 0);//调整堆,只需调整第一个节点即可
}
System.out.println(Arrays.toString(heap));
}
}
打印结果是:
[1, 2, 3, 5, 4, 6, 7, 8, 9]
从代码中可以看出,调整每个节点的时间复杂度是树的高度logN,因此简化后的时间复杂度为O(NlogN)
空间复杂度,由于存在交换键值,因此需要一个额外空间,空间复杂度为O(1)。
堆排序适合记录数很多的情况,比如从100000个记录中选出最小的前10个,用堆排序最好。如果记录数较少,则不提倡。