一、问题描述
堆也叫做优先队列,为什么需要堆?在现实生活中,存在许多需要从一群人、一些任务或一些对象中找出“下一位最重要”目标的情况。例如:我们平时在处理事情的时候我们总会先把“下一个最重要”的事情取出来先处理。在处理完这个事情后我们需要继续从剩下的事情中找出“下一个最重要”的事情,取出来处理。
定义:一些按照重要性或优先级来组织的对象称为优先队列。那我们如何去实现优先队列呢?通常来说,我们容易想到以下的方法:
通过普通队列排序实现。但实际上这种方法并不是很好。因为其实我可能只要取一个最大值就OK了,但是你却画蛇添足地帮我把所有元素都排好了。很浪费时间。排序的时间复杂度最少为O(nlogn),插入操作和删除操作的时间复杂度为O(n)。理论上我们所要实现的优先队列的时间复杂度是可以比这个更优的。
因此就出现了一个新的数据结构——堆。我们先来看一下堆的定义。
1.它是一棵完全二叉树,所以往往用数组来实现这棵二叉树。
2.堆中的数据是局部有序的。也就是节点储存的值和它的子节点之间存在某种关系。
堆又分为两种,最大值堆和最小值堆。
最大值堆的性质:任意一个节点的值都大于它的子节点的值;这样子根节点储存的就是这组数据的最大值。
最小值堆的性质:任意一个节点的值都小于它的子节点的值;这样子根节点储存的就是这组数据的最小值。
注意点:堆的兄弟节点之间没有必然联系。
接下来我们来实现一个最大值堆。
二、详细设计
A.首先我们需要构建一个堆类。
这个类需要包含以下的属性和方法
属性:指向堆数组的指针、堆的最大元素个数、当前堆的元素个数
方法:
构造函数(设置初始值)
返回当前元素个数
判断当前节点是否是叶节点
返回当前节点的左孩子节点位置
返回当前节点的右孩子节点位置
返回当前节点的父节点位置
建堆函数
下拉函数
插入函数
移除函数
B.算法思想
a.判断当前函数是否是叶节点
由于我们的树是存放在数组中的,我们可以通过下标来判断。如果下标的值大于等于n/2并且小于n,就说明它是一个叶节点。
b.返回左右孩子及父节点的位置
我们可以通过当前节点的下标来计算。
完全二叉树在数组中的储存如下:
P1 |
LC1 |
RC1 |
|
|
|
|
根据二叉树的结构和数组下标的信息,我们可以推断出一下结论:
左孩子节点:2*pos+1;
右孩子节点:2*pos+2;
父节点:(pos-1)/2;
c.建堆函数
有两种方法,一种是利用插入函数,逐个插入数据。另一种是对已经存有数据的数组进行堆排序。我们这里采用的是第二种方法。基本思路:依次从树的倒数第二层往上遍历节点。如果当前节点的值小于它的某一个叶节点,我们调用下拉函数进行下拉操作。而由于叶节点不可能再往下走,所以我们直接从倒数第二层开始遍历即可。倒数第二层的位置:n/2-1。
d.下拉函数
判断当前节点和它两个子节点的大小关系,如果当前节点小于它的子节点。那么就将该节点往下拉。与较大的子节点交换位置。
e.插入函数
首先将要插入的数据加到堆的一个叶节点中,也就是当前数组的尾部。然后判断该节点和其父节点的大小关系,如果该节点大于其父节点,就把其上拉,和父节点交换位置,重复该过程直到该节点到了正确的位置。
e.移除函数
先把根节点和最后一个叶节点交换位置,把堆的元素大小减1。对改变后的根节点进行下拉操作,直到正确的位置。最后再返回被替换的那个叶节点的值。
C.源码分析
#include<iostream>
using namespace std;
template<class E>
void swap(E *Heap,int p,int j){
E zj=Heap[p];
Heap[p]=Heap[j];
Heap[j]=zj;
}
//Heap class
template<class E>
class heap{
private:
E *Heap;//指向Heap数组的指针
int maxsize;//堆的最大空间
int n;//堆的当前元素个数
//下拉函数
void siftdown(int pos){
while(!isLeaf(pos)){//如果已经拉到叶节点了就停止
int j=leftchild(pos);
int rc=rightchild(pos);//获取左右孩子节点的pos
if((rc<n)&&(Heap[rc]>Heap[j]))
j=rc;//把j指向较大的子节点的位置
if(Heap[pos]>Heap[j]) return;
swap(Heap,pos,j);//如果根节点的值大于较大的子节点的值,就交换位置
pos=j;//继续往原本值较大的节点走
}
}
public:
//创建构造函数
heap(E *h,int num,int max){
Heap=h; n=num; maxsize=max; buildHeap();
}
//返回当前堆的大小
int size() const{ return n;}
//判断当前节点是否是叶节点
bool isLeaf(int pos) const{ return (pos >= n/2) && (pos<n);}
//返回左孩子节点的位置
int leftchild(int pos) const{ return 2*pos + 1;}
//返回右孩子节点的位置
int rightchild(int pos) const{ return 2*pos + 2;}
//返回父母节点的位置
int parent(int pos) const{ return (pos-1)/2;}
//建堆函数
void buildHeap() {
for(int i=n/2-1;i>=0;i--) siftdown(i);//从倒数第二层开始往上遍历,依次调用下拉函数,建立最大值堆
}
//构建插入函数
void insert(const E it){
if(n>=maxsize){
cout<<"Heap is full!"<<endl;
return;
}
int curr = n++;//开辟一个位置
Heap[curr]=it;//在数组的最后加入数据
//加入之后开始将这个值往上拉
while((curr!=0)&&(Heap[curr]>Heap[parent(curr)])){
swap(Heap,curr,parent(curr));
curr = parent(curr);//往上走
}
}
//移除首个元素,也就是最大值
E removefirst(){
if(n<=0){
cout<<"Heap is empty!"<<endl;
return 0;
}
swap(Heap,0,--n);//把最后一个元素和第一个元素换位置,将堆的空间减1
if(n!=0) siftdown(0);//如果堆元素不为0,就对该元素进行下拉操作直到合适的位置
return Heap[n];//返回最大值
}
};
int main(){
int n,maxsize=256;
cin>>n;
int h[n];
for(int i=0;i<n;i++) cin>>h[i];
heap<int>a(h,n,maxsize);//数据类型属于模板的对象实例化要带上具体的数据类型
//检测建堆函数
for(int i=0;i<n;i++){
cout<<h[i]<<" ";
}
cout<<endl;
//检测出堆函数
int max=a.removefirst();
cout<<max<<endl;
for(int i=0;i<n;i++){
cout<<h[i]<<" ";
}
cout<<endl;
//检测插入函数
a.insert(15);
for(int i=0;i<n;i++){
cout<<h[i]<<" ";
}
cout<<endl;
return 0;
}
/*
8
5 3 21 6 9 4 5 12
*/
运行结果
至此我们已经成功构建了最大值堆。最小值堆只需要做一些小修改即可。