堆是一颗完全二叉树,树中每个结点的值都不小于(或不大于)其左右孩子结点的值,其中,如果父亲结点的值大于或等于孩子结点的值,那么称这样的堆为大顶堆,这时每个结点的值都是以它为根结点的子树的最大值;如果父亲结点的值小于或等于孩子结点的值,那么称这样的堆为小顶堆,这时每个节点的值都是以它为根结点的子树的最小值。堆一般用于优先队列的实现,而优先队列的实现默认情况下使用的是大顶堆。
那么到底如何来建立堆呢?对完全二叉树来说,比较简洁的实现方法是用数组来存储完全二叉树,这样结点就按照层序存储于数组中,其中第一个结点将存储于数组中的1号位,并且数组i号位表示的结点的左孩子就是2i号位,而右孩子则是(2i+1)位。于是可以像下面这样定义数组来表示堆。
const int maxn=100;
//heap为堆,n为元素个数
int heap[maxn],n=10;
在建堆的过程中,每次调整都是把结点从上往下的调整。针对这种向下调整,调整方法是这样的:总是将当前结点V与它的左右孩子比较(如果有的话),假如孩子中存在权值比结点V的权值大的,就将其中权值最大的那个孩子节点与结点V交换;交换完毕后继续让结点V和孩子比较,直到结点V的孩子的权值都比结点V的权值小或是结点V不存在孩子结点。
于是可以很容易的写出向下调整的代码,显然时间复杂度为O(logn)
//对heap数组在[low,high]范围内进行调整
//其中low为欲调整结点的数组下标,high一般为堆的最后一个元素的数组下标
void downAdjust(int low,int high){
int i=low,j=i*2;//i为欲调整的结点,j为其左孩子
while(j<=high){
//存在孩子节点
//如果右孩子存在,且右孩子的值大于左孩子
if(if(j+1<=high&&heap[j+1]>heap[j])){
j=j+1;//让j存储右孩子下标
}
//如果孩子中最大的权值比欲调整结点i大
if(heap[j]>heap[i]){
swap(heap[j],heap[i]);//交换最大权值的孩子与欲调整结点i
i=j;
j=i*2;
}else{
break;//孩子的权值均比欲调整结点i小,调整结束
}
}
}
那么建堆的过程也就很容易了。假设序列中元素的个数为n,由于完全二叉树的叶子结点个数为[n/2],因此数组下标在[1,[[n/2]]范围内的结点都是非叶子节点,于是可以从[n/2]号为开始倒着枚举结点,对每个遍历到的结点i进行[i,n]范围的调整。为什么要倒着枚举呢?这是因为每次调整完一个结点后,当前子树中权值最大的结点就会处在根结点的位置,这样当遍历到其父亲结点时,就可以直接使用这个结果,也就是说,这种做法保证每个结点都是以其为根结点的子树中权值最大的结点。
建堆的代码如下,时间复杂度为O(n)
//建堆
void createHeap(){
for(int i=n/2;i>=1;i--){
downAdjust(i,n);
}
}
另外,如果要删除堆中的最大元素(也就是删除堆顶元素),并让其仍然保持堆的结构,那么只需要最后一个元素覆盖堆顶元素,然后对根结点进行调整即可。代码如下,时间复杂度为O(logn)'
//删除堆顶元素
void deleteTop(){
heap[1]=heap[n--];//用最后一个元素覆盖堆顶元素 ,并让元素个数减1
downAdjust(1,n);//向下调整堆顶元素
}
那么,如果想要往堆里添加一个元素,可以把想要添加的元素放在数组最后(也就是完全二叉树的最后一个结点后面),然后进行向上调整操作。向上调整总是把欲调整结点与父亲结点比较,如果权值比父亲结点大,那么就交换其与父亲结点,这样反复比较,直到堆顶或是父亲结点的权值较大为止。向上调整的代码如下,时间复杂度为O(logn)
//对heap数组在[low,high]范围进行向上调整
//其中low一般设置为1,high表示欲调整的结点的数组下标
void upAdjust(int low,int high){
int i=high,j=i/2;//i为欲调整的结点,j为其父亲
while(j>=low){
//父亲在[low,high]的范围内
//父亲权值小于欲调整的结点的权值
if(heap[j]<heap[i]){
swap(heap[j],heap[i]);//交换父亲结点和欲调整的结点i
i=j;
j=i/2;
}else{
break;//父亲权值比欲调整结点i的权值大,调整结束
}
}
}
在此基础上就很容易实现添加元素的代码了
//添加元素x
void insert(int x){
heap[++n]=x;//让元素个数加1,然后将数组末位赋值为x
upAdjust(1,n);//向上调整新加入的结点
}