数据结构(堆)的代码实现和测试——堆的原理及代码实现

一、堆的性质

堆的本质是用完全二叉树来进行存储,而完全二叉树是可以用数组实现。

所以,堆的存储用数组来实现,而其左右子树以及父亲节点的位置关系依靠数学计算来推导

在数学推导过程中,有一点必须要牢记:数组的索引是从0开始的

数学关系如下

假设一个节点在数组当中的索引为x

x为叶子节点:n/2 <= x < n   

                        (对于完全二叉树而言,n/2及以后的节点全部为叶子节点)

                        为什么这里n取不到呢?因为数组的索引是从0开始

x的左子节点:2*x + 1  (这里+1是因为数组的索引是从0开始的)

x的右子节点2*x + 2     

x的父亲节点:(x-1) / 2

知道了这些以后,下面就是堆的原理了:

在计算机应用中,我们往往需要在许多任务中挑选出最优先的那一个,依照优先度依次执行

可以考虑用BST来完成这个任务,但是BST可能会变得不平衡导致效率变差,因此,我们发明了堆。

最大堆:任意一个节点存储的值都大于等于其任意一个子节点的值

最小堆:任意一个节点存储的值都小于等于其任意一个子节点的值

因此,最大堆的根节点是整个堆中最大的,最小堆的根节点是整个堆中最小的

堆的原理已经清楚了,那要怎样维持一个堆呢?

下面是堆对于元素的基本操作

上推:将此元素与父节点进行比较,如果该元素比父节点优先级更大,则与父节点交换

下推:将此元素与左右子节点中优先级较大的进行比较,如果该节点优先级更小,则交换

下推操作的要求:节点的左右子树都为堆

下推操作的代码:

	while (!isLeaf(pos))//当pos节点是叶子节点时停止
	{
		int j = leftchild(pos);
		int rc = rightchild(pos);
		if ((rc < n) && comp(heap[rc], heap[j]))//如果rc在堆中并且 rc比j大 或rc比j小
		{
			j = rc; //那么就将索引j移动到rc(即左右子树中较大或较小的位置)
			//如果不符合要求,这步不执行,那么索引j的元素依然是左右子树较大或较小的位置
			//这个if的妙处就在于:会保持j始终为左右子树较突出的位置,下面只需要将pos位置和j位置比较即可
		}
		if (comp(heap[pos], heap[j]))//这里j已经是左右子树中较为突出的位置
			//如果pos还是排在j的前面,那么不需要进行下推了
		{
			return;
		}
		swap(heap, pos, j);//将pos节点的元素值与j的元素值进行交换,完成一次下推操作
		pos = j;//将完成下推的元素的索引更新,以便进行下一次的下推操作

	}

上推操作的代码:

while ((curr != 0) && (comp(heap[curr], heap[parent(curr)]))) {	//上推操作
	//将该节点与其父亲节点相比较,如果curr位置确实应该排到它的父节点前面,继续循环
	swap(heap, curr, parent(curr));//将curr与其父节点进行交换
	curr = parent(curr);		   //更新上推后的节点值,以便后面继续进行上推

}

在堆中,元素的移动无非两个方向:向上(上推)或者向下(下推)。

二、接下来介绍堆作为优先队列的基本操作:

1.建堆

建堆的基本原理就是从低到高全部下推,就像从地板上一点一点往上垒砖块

因为叶子节点无法下推、只能交换,所以我们只需将所有的非叶子节点下推完毕,即完成建堆操作

代码:

template<typename E>
inline void Heap<E>::buildHeap()
{
	for (int i = n / 2 - 1; i >= 0; i--)//由于完全二叉树的前n/2个节点全部不是叶子节点,那么,最后一个不是叶子节点的节点
	{							   //的序号为n/2-1,这个减一是因为数组是从零开始数的。
								   //如此以来,我们只要将所有不是叶子节点的节点全部下推完毕,即完成了一个堆的构建
								   //for循环中的--操作 实现了操作节点向前的移动
		siftdown(i);
	}
}

注意:由于完全二叉树的特性,前 n / 2 个节点全部为非叶子节点,所以我们只需对n/2 个节点全部进行下推操作完毕即可。

n / 2  - 1  减一是因为数组的索引是从0开始的,而倒着向前进行是因为:

对堆的元素进行下推操作的要求是:左右子树都为标准的堆

2.插入元素

由于堆是由数组实现的完全二叉树,因此我们每次插入新的元素只能将其按顺序放在数组的末尾,然后再进行上推操作将其放到合适的位置(因为此时新元素处于叶子节点,只能上推)

代码:

template<typename E>
inline void Heap<E>::insert(const E& it)
{
	Assert(n < maxsize, "Heap is full");//判断堆是否为满
	int curr = n++;	//该语句的执行顺序是:先赋值,再自增,取数组最后的位置给curr赋值
	heap[curr] = it;//将该元素放到堆的最后一个位置
	while ((curr != 0) && (comp(heap[curr], heap[parent(curr)]))) {	//上推操作
		//将该节点与其父亲节点相比较,如果curr位置确实应该排到它的父节点前面,继续循环
		swap(heap, curr, parent(curr));//将curr与其父节点进行交换
		curr = parent(curr);		   //更新上推后的节点值,以便后面继续进行上推

	}
}

注意这里 int curr = n++; 语句的顺序是:先赋值,再自增。

3.取根节点并移除(取队首元素)

由于堆是起到优先队列的作用,因此,取根节点并移除的操作等同于从队列中取队首元素

我们需要将根节点与数组的最后一个元素位置交换,交换后将数组的n--,再将交换到根节点的元素进行下推操作:

代码:

template<typename E>
inline E Heap<E>::removefirst()
{
	Assert(n > 0, "Heap is empty");//检查堆是否为空堆
	swap(heap, 0, --n);//该语句的执行顺序为:n先--,再执行swap中的内容,因为n表示元素个数,而数组从0开始
	//那么n-1刚好为堆中的最后一个元素的位置,此时的n的值已经-1
	if (n != 0)
	{
		siftdown(0);//如果删除后数组元素仍不为空,对刚换到堆顶的元素进行下推操作
	}
	return heap[n]; //将换下来的第n个位置的元素返回,注意此时n已经完成自减

}

注意:这里 swap(heap, 0, --n); 语句执行顺序是:n先自减,再交换。自减后的n

           刚好是堆中最后一个元素的位置(因为数组的索引是从0开始的)

           一系列操作进行完后,此时数组前 n - 1 个元素构成堆,第 n 个元素为原来的根节点

4.删除特定位置的值

堆还有一个实现的功能是删除特定位置的元素值

基本原理是:将这个元素与堆中最后一个元素交换,再对这个最后换过来的新元素进行位置调整

类比移除根节点的操作,我们可以得出一个结论:

在堆中删除元素的基本原理:与最后一个节点交换后再调整位置

而此时交换后的节点在堆的中间,可上可下。

那到底是上推还是下推呢?

代码:

template<typename E>
inline E Heap<E>::remove(int pos)
{
	Assert((pos >= 0) && (pos < n), "Bad position");//判断pos位置是否在堆中
	if (pos == n - 1)//删除堆中最后一个元素,不需要做任何处理
	{
		n--;
	}
	else {
		swap(heap, pos, --n);//将pos元素与 n-1的位置(即堆中最后一个元素的位置)交换,并删除交换后的最后一个元素
		//注意:此时n已经完成自减操作,也就是说已经完成了删除
		while (pos != 0 && comp(heap[pos], heap[parent(pos)]))//上推操作
		{
			swap(heap, pos, parent(pos));//交换pos和他的父亲节点
			pos = parent(pos);		   //更新pos的索引,以便进行下一次上推操作
		}
		if (n != 0)	//如果堆为非空,对其进行下推操作
			//如果上一步上推成立,则这一步下推不下去
			//如果上一步上推不成立,则这一步可能会下推下去
			//反正中间的元素无非两个方向移动,向上或者向下
		{
			siftdown(pos);
		}
		return heap[n];//返回这个被删除的元素
	}
}

这段代码的基本逻辑就是:

1.交换

2.尝试上推

3.尝试下推

他没有用条件来判断到底是进行上推还是进行下推,因为上推或者下推操作自带判断。

如果上推成功,那么下推自然推不下去,如果上推不成功,也不影响后面下推的操作

三、完整代码

注意:在堆的构造函数有四个参数

分别为(建堆数组的指针数组中存的元素个数数组最大空间排序函数指针

排序函数是用户自己定义的函数,是bool类型的函数,两个参数(a,b),如果a排在b前面则返回true,否则返回false

例:

bool comp(int a,int b) {

	return a >= b;
}

这个函数用于构建最大堆

注:在shuffer的书中,比较的函数是用一个comp类来作为模板参数传入

原型是这样 template < typename E , typename comp > class Heap

但是我在实际实验时发现,这样的方法在使用堆的过程中较为麻烦,因此,我将第二个比较类删除,改为由函数指针传入排序函数,将排序函数作为Heap类的一个成员。

完整代码:

heap.h

#pragma once
/*堆的代码实现,由于堆是一个完全二叉树的结构,因此用数组来实现*/
#include<bits/stdc++.h>
	
template<typename E> class Heap
{
private:
	void Assert(bool a, std::string b);//断言函数
	typedef bool(*FUNC_POINTER)(E a,E b);//声明一个函数指针
private:
	E* heap;//指向堆存储数据的数组
	int maxsize;//堆的最大存储空间
	int n;//当前堆中存储的元素个数
	FUNC_POINTER comp = NULL;//用于排序的函数
	
	void siftdown(int pos);//进行下推的函数
	void swap(E* heap,int a,int b);//交换符合要求的元素,是下推和上推所需要用到的工具函数
	

public:
	Heap(E* h, int num, int max,const FUNC_POINTER pot);//构造函数,初始化三个成员
	int size() const;//读取当前堆的元素个数
	bool isLeaf(int pos) const;//判断pos位置的节点是否是叶子节点
	int leftchild(int pos)const;//找到pos节点的左孩子的位置
	int rightchild(int pos)const;//找到pos节点右孩子的位置
	int parent(int pos)const;//返回pos节点的父亲节点
	void buildHeap();//建堆函数
	void insert(const E& it);//将it元素插入堆中
	E removefirst();//删除堆顶的元素
	E remove(int pos);//删除特定位置的元素
	void print();//打印堆中的元素用于测试
};


//=========================================================================

template<typename E>
inline void Heap<E>::Assert(bool a, std::string b)
{
	if (!a)
	{
		std::cout << b << std::endl;
	}
}

template<typename E>
inline void Heap<E>::siftdown(int pos)
{
	while (!isLeaf(pos))//当pos节点是叶子节点时停止
	{
		int j = leftchild(pos);
		int rc = rightchild(pos);
		if ((rc < n) && comp(heap[rc], heap[j]))//如果rc在堆中并且 rc比j大 或rc比j小
		{
			j = rc; //那么就将索引j移动到rc(即左右子树中较大或较小的位置)
			//如果不符合要求,这步不执行,那么索引j的元素依然是左右子树较大或较小的位置
			//这个if的妙处就在于:会保持j始终为左右子树较突出的位置,下面只需要将pos位置和j位置比较即可
		}
		if (comp(heap[pos], heap[j]))//这里j已经是左右子树中较为突出的位置
			//如果pos还是排在j的前面,那么不需要进行下推了
		{
			return;
		}
		swap(heap, pos, j);//将pos节点的元素值与j的元素值进行交换,完成一次下推操作
		pos = j;//将完成下推的元素的索引更新,以便进行下一次的下推操作

	}
}

template<typename E>
inline Heap<E>::Heap(E* h, int num, int max, const FUNC_POINTER pot)//这里留一点问题:为什么初始化要初始化n?
{												  //初始化n,我们可以在已知一个数组元素时直接进行建堆操作,而无需一个一个插入
	heap = h;
	n = num;
	maxsize = max;
	comp = pot;
	buildHeap();
}

template<typename E>
inline int Heap<E>::size() const
{
	return n;
}

template<typename E>
inline bool Heap<E>::isLeaf(int pos) const
{
	return (pos >= n / 2) && (pos < n);//对于完全二叉树而言,n/2之后,n之前的节点都是叶子节点
									   //至于最后的 pos < n 为什么不是 <= n,因为数组是从0开始数的 	
}

template<typename E>
inline int Heap<E>::leftchild(int pos) const
{
	return 2 * pos + 1;
}

template<typename E>
inline int Heap<E>::rightchild(int pos) const
{
	return 2 * pos + 2;
}

template<typename E>
inline int Heap<E>::parent(int pos) const
{
	return (pos -1)/2;
}

template<typename E>
inline void Heap<E>::buildHeap()
{
	for (int i = n / 2 - 1; i >= 0; i--)//由于完全二叉树的前n/2个节点全部不是叶子节点,那么,最后一个不是叶子节点的节点
	{							   //的序号为n/2-1,这个减一是因为数组是从零开始数的。
								   //如此以来,我们只要将所有不是叶子节点的节点全部下推完毕,即完成了一个堆的构建
								   //for循环中的--操作 实现了操作节点向前的移动
		siftdown(i);
	}
}

template<typename E>
inline void Heap<E>::insert(const E& it)
{
	Assert(n < maxsize, "Heap is full");//判断堆是否为满
	int curr = n++;	//该语句的执行顺序是:先赋值,再自增,取数组最后的位置给curr赋值
	heap[curr] = it;//将该元素放到堆的最后一个位置
	while ((curr != 0) && (comp(heap[curr], heap[parent(curr)]))) {	//上推操作
		//将该节点与其父亲节点相比较,如果curr位置确实应该排到它的父节点前面,继续循环
		swap(heap, curr, parent(curr));//将curr与其父节点进行交换
		curr = parent(curr);		   //更新上推后的节点值,以便后面继续进行上推

	}
}

template<typename E>
inline E Heap<E>::removefirst()
{
	Assert(n > 0, "Heap is empty");//检查堆是否为空堆
	swap(heap, 0, --n);//该语句的执行顺序为:n先--,再执行swap中的内容,因为n表示元素个数,而数组从0开始
	//那么n-1刚好为堆中的最后一个元素的位置,此时的n的值已经-1
	if (n != 0)
	{
		siftdown(0);//如果删除后数组元素仍不为空,对刚换到堆顶的元素进行下推操作
	}
	return heap[n]; //将换下来的第n个位置的元素返回,注意此时n已经完成自减

}

template<typename E>
inline E Heap<E>::remove(int pos)
{
	Assert((pos >= 0) && (pos < n), "Bad position");//判断pos位置是否在堆中
	if (pos == n - 1)//删除堆中最后一个元素,不需要做任何处理
	{
		n--;
	}
	else {
		swap(heap, pos, --n);//将pos元素与 n-1的位置(即堆中最后一个元素的位置)交换,并删除交换后的最后一个元素
		//注意:此时n已经完成自减操作,也就是说已经完成了删除
		while (pos != 0 && comp(heap[pos], heap[parent(pos)]))//上推操作
		{
			swap(heap, pos, parent(pos));//交换pos和他的父亲节点
			pos = parent(pos);		   //更新pos的索引,以便进行下一次上推操作
		}
		if (n != 0)	//如果堆为非空,对其进行下推操作
			//如果上一步上推成立,则这一步下推不下去
			//如果上一步上推不成立,则这一步可能会下推下去
			//反正中间的元素无非两个方向移动,向上或者向下
		{
			siftdown(pos);
		}
		return heap[n];//返回这个被删除的元素
	}
}

template<typename E>
inline void Heap<E>::print()
{
	for (int i = 0; i < n; i++)
	{
		std::cout << heap[i] << " ";
	}
	std::cout << std::endl;
}

template<typename E>
inline void Heap<E>::swap(E* heap, int a, int b)
{
	int temp = heap[b];
	heap[b] = heap[a];
	heap[a] = temp;
}

测试代码

test.cpp

#include"Heap.h"

bool comp(int a,int b) {

	return a >= b;
}

int main() {
	int b[10] = {1,2,3,4,5,6,7}; //测试原数组
	Heap<int> c(b, 7, 10, comp);
	c.print();

	std::cout << c.removefirst() << std::endl;//提取队首元素
	c.print();

	c.insert(8);//插入元素8
	c.print();

	c.remove(6);//这个remove函数有个缺点,就是只能删除指定位置的元素,而不能删除指定值的元素
	c.print();
	
	return 0;
}

测试结果:

通过此图来看符合堆的要求

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值