原理
堆排序(从小到大)的实现主要利用大顶堆的特征,取全堆最大值(即根节点即根节点与最后的叶节点的值做交换)放置到有序的序列中,然后,除根节点的其余节点继续建立大顶堆,直到所有节点都被取走。
大顶堆的特征
- 是一棵近似的完全二叉树,除了最底层,其它是全满,且从左向右填充。
- 树的每个节点对应数组一个元素,根节点对应数组下标为0的元素。
- 对于下表i,它的父节点下表为(i+1)/2 -1, 左孩子节点下标为 i * 2 + 1, 右孩子节点下标为i * 2 + 2.
- 最大堆中,每个节点都必须大于等于左右孩子节点。
建大顶堆的过程
堆由数组这种来存储,其本质是完全二叉树。
设堆a[n] = {a1, a2, … an}; 注:索引从0开始
由二叉树的性质可知:
- 索引为(n-1)/2 的节点为二叉树最后一个父节点。
- 所有的父节点都在索引为[0~(n-1)/2]的数组中
因此依次(先由最后一个父节点开始依次向前)调整每个子树
for (i = (n-1)/2; i >= 0; i--)
由大顶堆的特征调整每个子树。
// 父节点索引i,即左孩子节点索引为2*i+1,右孩子为2*i+2(注:索引从0开始)
i = (n-1)/2
l = 2*i+1, r = l+1;
若a[i]<max(a[l],a[r]),则a[i]与左右节点中最大的节点交换值;
若a[i] 与 a[l]交换,那么继续l为父节点调整该子树为大顶堆!!!
同理:若a[i]与a[r]交换,那么继续以r为父节点调整该子树为大顶堆!!!
若a[i] >max(a[l],a[r]),则什么也不做
实现
// 堆排序直接版
// 大顶堆、从小到大排序
void swap(int *pa, int *pb) {
int tmp = *pa;
*pa = *pb;
*pb = tmp;
}
// 向下调整
// a[]:表示存放堆的数组
// i:表示要调整堆中父节点的索引
// n-1:表示要调整堆的最大索引
void ajustDown(int a[], int i, int n ) {
int l = 2*i +1; // l 表示左孩子索引,l+1 表示右孩子索引
if (l < n) {
int max = i; // max 保存要调整的父节点的索引
if (l+1 < n) { // 完全二叉树性质可知:若右孩子存在,则左孩子必然存在
if (a[l] > a[l+1])
max = l;
else
max = l+1;
if (a[max] > a[i]) {
swap(a+max, a+i);
ajustDown(a, max, n); // 以a[max]元素为父节点继续调整
}
} else {
if (a[l] > a[i]) {
swap(a+l, a+i);
ajustDown(a, l, n); // 以a[l]元素为父节点继续调整
}
}
}
}
// 建大顶堆
void makeHeap(int a[], int n) {
// 保持大顶堆的特性
for (int i = (n-1)/2; i >= 0; i--) {
ajustDown(a, i, n);
}
}
// 堆排序-从小到大排序;数组下标范围[0-n-1]
void heapSort(int a[], int n) {
for (int i = 0; i < n; ++i) {
makeHeap(a, n-i);
// 最大值放到数组末尾
swap(a, a+n-1-i);
}
}
// heapSort第二种版本
void heapSort2(int a[], int n) {
// 建大顶堆
makeHeap(a, n);
// 交换最大元素到数组末尾,调整大顶堆
while (n > 0) {
swap(a[0], a[--n]);
ajustDown(a, 0, n);
}
}
时空复杂度分析
时间复杂度:
heapSort的中makeHeap的时间复杂度为logn, 调用makeHeap的次数为n次,那么heapSort的时间复杂度为O(nlogn)。
空间复杂度:很显然堆排序是原地排序,所以空间复杂度为O(1)。
堆排序的缺点
- 堆排序是不稳定的排序算法。
- 不能有效的利用缓存(缓存只能一次加载部分数组元素,需要调整的元素可能并不在加载到缓存的元素)
- 内部的循环比快排花费的时间更长。
堆排序的应用
操作系统中可以利用大顶堆实现最大优先队列来实现共享计算机系统的作业调度。最大优先队列记录个个作业之间的相对优先级,当某个作业中断后选出具有最高优先级的队列来执行。
最大优先级队列应该支持如下操作:
- maximum(): 返回堆得最大值
- extractMax(): 返回堆的最大值并从堆中删除
- heapIncreaseKey(i, key): 将下标为i 的元素增大为key
- maxHeapInsert(key): 将元素 key 插入队中。
maximum()实现
int maximum() {
return a[0];
}
extractMax(),取出第一个后,只需要把最后一个元素放到第一个,然后对第一个元素进行维护大顶堆即可
int size = n; // 当前最大优先队列的大小
int extractMax() {
if (size >= 1) {
int max = a[0];
a[0] = a[--size];
ajustDown(a, 0, size);
} else {
// 最大优先队列为空时的处理
...
}
}
heapIncreaseKey(i, key)会增大下标为i的元素为key。首先将a[i]的值更新为key,因为增大的a[i]关键字可能违背大顶堆得性质,因此需要对a[i]进行逐级上升调整。即当前元素逐级与父节点比较如果大于父节点,则与父节点进行交换,一直到当前元素小于父节点为止。
int parent(int i) {
return (i+1)/2 - 1;
}
void heapIncreaseKey(int i, key) {
if (key > a[i]) {
a[i] = key;
while (i > 0 && a[parent(i)] < a[i]) {
swap(a[i], a[parent(i)])
i = parent(i);
}
} else {
// key 小于 a[i]的处理
}
}
maxHeapInsert(key),等价于数组长度加一,然后最后一个元素设置为-∞, 然后把它增大为key的操作:
int capacity = ....; // 当前最大队列的容量
void maxHeapInsert(int key) {
if (size + 1 > capacity) {
resize(size+1);
}
a[size++] = INT_MIN;
ajustDown(a, 0, size);
}