堆的实现(数据结构)

 一、问题描述

堆也叫做优先队列,为什么需要堆?在现实生活中,存在许多需要从一群人、一些任务或一些对象中找出“下一位最重要”目标的情况。例如:我们平时在处理事情的时候我们总会先把“下一个最重要”的事情取出来先处理。在处理完这个事情后我们需要继续从剩下的事情中找出“下一个最重要”的事情,取出来处理。

定义:一些按照重要性或优先级来组织的对象称为优先队列。那我们如何去实现优先队列呢?通常来说,我们容易想到以下的方法:

通过普通队列排序实现。但实际上这种方法并不是很好。因为其实我可能只要取一个最大值就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
*/

运行结果


 

至此我们已经成功构建了最大值堆。最小值堆只需要做一些小修改即可。

猜你喜欢

转载自blog.csdn.net/alexwym/article/details/80663275