堆
二叉堆是一个数组,可以看做一个近似完全二叉树,对于一个长度为
的数组给定一个数组下标
,则:
最大堆
除根节点外,所有节点满足:
最小堆
除根节点外,所有节点满足:
堆的高度
定义:叶节点到根的最短路径上边的长度为堆的高。
定理6.1
对于节点数为 的堆,其所有节点的可能取值高度(即,以节点 为根节点的子树的高)为: ,且高度为 的节点,至多有 个。
堆排序
以最大堆为例:
排序的整个过程可以看做如下:
1. 建堆 BUILD_MAX_HEAP
2. 堆维护 MAX_HEAPITY
3. 断尾重构 HEAPSORT
堆维护 MAX_HEAPITY
MAX_HEAPITY堆维护是建堆过程中至关重要的一步,它负责维护最大堆的性质不变,即根节点一定大于等于子节点。
代码思想(伪代码)
MAX_HEAPITY(A,i):
left=i*2+1;
right=i*2+2;
//假设以left和right均为最大堆
//判断left和right的合理性 < heap_size
//比较节点与left、right的大小,
//将大的节点与i交换,并递归调用MAX_HEAPITY(A,max_index)
复杂度分析
从上可知,对于节点 至多交换 次,即被从堆顶被交换到堆底。所以时间复杂度为 。
也可用递推公式 ,通过主定理求解得 。
建堆 BUILD_MAX_HEAP
建堆主要是将无序数组按照树顺序,建立成最大(小)堆的过程。
已知对于节点数为 的二叉树,其叶子节点的序号一定从 开始,则在建堆初始化时,可以把第 个叶子节点看做单节点最大堆。故建堆循环应从后往前,直到根节点 。
代码思想(伪代码)
BUILD_MAX_HEAP(A):
for i=(n+1)/2-1 to 0:
MAX_HEAPITY(A,i)
复杂度分析
结论: 的线性复杂度。
分析:由伪代码可知,时间复杂度的上界为 ,由于MAX_HEAPITY的复杂度与节点i所处的高度有关,我们可以利用定理6.1得到一个更紧确的界:即把要经过MAX_HEAPIFY的节点复杂度按其高度进行复杂度求和。通过变换和得线性复杂度 。详见算法导论p88。
堆排序算法
用断尾重构的方法:每次把堆顶元素放至数组尾部,相应的堆size减小1
代码思想(伪代码)
HEAPSORT(A):
for i=A.length to 1:
exchange A[0],A[i]
--A.heap_size;
MAX_HEAPIFY(A,0);//对新的堆顶进行排序维护
复杂度分析
结论: 。
完整代码
//MaxHeap.h
#ifndef LINTCODE_MAXHEAP_H
#define LINTCODE_MAXHEAP_H
#include <vector>
using namespace std;
//原地排序算法
class MaxHeap {
private:
void exchangeNum(vector<int> &A, int i, int j);
void maxHeapity(vector<int> &A, int index, int &heap_size);
void buildMaxHeap(vector<int>& A, int &heap_size);
public:
void heapSort(vector<int>& A);
};
#endif //LINTCODE_MAXHEAP_H
//MaxHeap.cpp
#include "MaxHeap.h"
using namespace std;
void MaxHeap::exchangeNum(vector<int> &A, int i, int j) {
int temp = A[i];
A[i] = A[j];
A[j] = temp;
}
//数组下标从0开始
void MaxHeap::maxHeapity(vector<int> &A, int index, int &heap_size) {
int left = index * 2 + 1;
int right = index * 2 + 2;
int i = index;
i = (left < heap_size && A[left] > A[i]) ? left : i;
i = (right < heap_size && A[right] > A[i]) ? right : i;
if (i != index) {
exchangeNum(A, i, index);
//递归调用
maxHeapity(A, i, heap_size);
}
}
void MaxHeap::buildMaxHeap(vector<int> &A, int &heap_size) {
int n = A.size();
int leaf_num = (n + 1) / 2; //节点数为n的堆,的叶子节点号为(n+1)/2,(n+1)/2+1,...,n
for (int i = leaf_num - 1; i >= 0; --i) {
maxHeapity(A, i, heap_size);
}
}
//断尾法,得排序数组
void MaxHeap::heapSort(vector<int> &A) {
int heap_size = A.size();
buildMaxHeap(A, heap_size);
for (int i = A.size() - 1; i > 0; --i) {
exchangeNum(A, 0, i); //交换尾部和当前堆顶元素
--heap_size;
maxHeapity(A, 0, heap_size);
}
}
优先队列
我们常用最大堆和最小堆来维护一个优先队列。
总结
- 堆排序是基于比较的排序,性能稳定,排序不稳定。
- 性能稳定:堆排序的(最好/平均/最差)时间复杂度均为 。
- 只需要O(1)的辅助空间,既最高效率又节省空间
- 缺点:堆维护比较频繁,对于数组元素经常变动的情况下,不适合采用堆排序