前言

在了解二叉堆之前,先来说一下优先队列

优先队列是允许至少下列操作的数据结构:插入删除最小者,它的工作是找出,返回,删除优先队列中的最小者

要实现这种数据结构,除了二叉堆外还有许多方法。

1,使用一个链表存储这个序列,插入直接在表头或者表尾插入既可,时间复杂度为o(1),删除最小元操作可以遍历链表,找到最小的元素,然后用链表的删除操作将其删除,时间复杂度为o(n),这样做的弊端是如果我们要频繁地删除最小元,这个办法就相对来说不太好了。

2,使用二叉查找树存储这个序列,我们知道对二叉查找树的操作平均情况是o(logn),这样无论是插入和删除都能以理想的时间去运行,但是我们删除操作并不是随机删除,我们每次都删除最小元,这样会导致左边的树的深度越来越小,树会越来越趋近于不平衡,那么使用二叉平衡树又如何呢,如果使用二叉平衡树,我们调整树的平衡会发生额外的开销,特别是二叉平衡树的删除操作,我在二叉平衡树(AVL树)的基本操作(c语言)这篇文章里面已经提到过了,二叉平衡树的删除操作开销很大。而我们最核心的操作就是删除最小元操作,用二叉平衡树也是不合理的。

我们将要使用的这种工具叫二叉堆,它可以仅用一个数组存储起来,不像树还要运用指针,它也已最坏情况o(logn)支持插入和删除最小元操作,除此之外,它还支持合并操作(虽然本文不会提到)。

二叉堆分为大根堆小根堆,本文以小根堆的为例。

预备知识

完全二叉树:若设二叉树的深度为h,除第h 层外,其它各层 (1~h-1) 的结点数都达到最大个数,第h层所有的结点都连续集中在最左边,这就是完全二叉树。下面是一颗完全二叉树。
二叉堆(优先队列)的基本操作(c语言)-LMLPHP
观察这颗完全二叉树可以发现,完全二叉树是绝对平衡的,这样我们就彻底不用担心,插入和删除操作会影响树的平衡。其次,完全二叉树很有规律,我们可以只用一个数组去表示它,例如,上图可以用A,B,C,D,E,F,G,H,I,J来表示这颗树。

二叉堆的结构性:二叉堆有两个性质,结构性堆序性。它的结构性是指堆是一颗完全二叉树,因此一个堆将由一个数组来存储。如下面所示,与上面的完全二叉树的图是对应的。该数组有一个0位置处有一个哨兵元素,哨兵元素(也称为哨兵节点)是一个特殊的元素或节点,用于简化边界条件、减少循环中的判断,从而提高代码效率。这里不详细说明,但是在堆排序(不会在本文提到)里面这个哨兵元素可以没有。
二叉堆(优先队列)的基本操作(c语言)-LMLPHP

规律揭示:对于任意位置i上的元素,其左儿子在位置2i上,其右儿子在位置2i + 1上,它的父亲位置则在i / 2上(整数除法)。例如B结点的位置是2,它的左孩子就是4对应D,右孩子就是E,父亲结点就是1对应A。这个规律本文不会去证明,直接使用

二叉堆的堆序性:二叉堆有两个性质,结构性堆序性。我们以小根堆为例,它的堆序性是指,对于每一个节点XX的父亲节点中的关键词小于或者等于X中的关键词,根节点除外,它没有父节点。与二叉查找树不同的是,它对一个节点的左孩子和右孩子之间的大小没有要求。下面就是一个例子。

二叉堆(优先队列)的基本操作(c语言)-LMLPHP

只要满足二叉堆的两个性质,便是一个二叉堆

定义

我们规定二叉堆里面存储的是整数值

它有一个maxsize,用于表示这个堆的最大大小

它还有一个size,用来表示这个堆的实际大小

最后它还有一个整形数组arr,用来存储二叉堆。

二叉堆的初始化可以看下面的其他代码

struct heap{
    int maxsize;
    int size;
    int* arr;
};//by wqs

二叉堆的插入操作

首先规定如果已经有了待插入的元素,我们便什么都不做

二叉堆的插入操作2个步骤:

1,给二叉堆的实际大小加1,将待插入元素直接插入到二叉堆的最后面,这个时候堆很有可能二叉堆已经不满足堆序性

2,我们下面的操作便是通过上滤操作,重新调整堆,上滤操作的定义是:新元素在堆中上滤直到找出正确的位置。主要是通过不断交换父节点和子节点来实现的,下面举个例子。

二叉堆(优先队列)的基本操作(c语言)-LMLPHP

在上面这个例子中,我们要插入14,我们先size1,并让14直接插入到二叉堆的最后面,也就是过程1,然后我们要不断比较14父节点14,如果14父节点竟然比14大。说明14更应该为父节点,故交换14父节点,如果最后14大于了父节点,说明已经满足了堆序性。我们比较14与它的父节点31,31>14,所以交换,到了过程2,接着我们比较14与它的父亲节点2121>14,所以交换到了过程3,接着我们比较14和它的父亲节点13,13<14,所以调整完成,结束循环。这样来看二叉堆的插入还是很简单的,一个循环就可以解决。

下面是代码实现,接下来的这个函数接受一个已经创建好的堆p,以及一个要插入的数x,函数直接返回插入好的堆。

struct heap* insert(struct heap* p, int x){
    int i;
    for (i = ++p->size; p->arr[i / 2] > x; i = i / 2){//让i为初始化为最后一个元素,如果满足循环条件则一直更新父节点。
        p->arr[i] = p->arr[i / 2];
    }
    p->arr[i] = x;//此时父节点小于待插入节点,直接插入x就可以满足堆序性。
    return p;
}//by wqs

二叉堆的删除最小元操作

首先我们规定,如果原来的二叉堆中没有一个元素,我们便什么都不做

二叉堆的删除最小元操作分为2个步骤:

1,让第一个节点等于最后一个节点,并且让size1,这个时候二叉堆已经不满足堆序性

2,然后通过下滤操作不断调整堆。至于下滤操作与上滤操作的区别就是调整的方向不一样,上滤是向上调整,而下滤是向下调整,由于我们在第一个节点就失去了堆序性,故要从上往下去调整。但是下滤调整有一个问题,一个节点有两个孩子,我们应该要与最小的孩子去比较,直到父节点比最小的孩子还小,我们才停止循环

还是举一个例子,在下面这个例子中,我们要删除最小元,也就是13,对应下面第一个堆

二叉堆(优先队列)的基本操作(c语言)-LMLPHP

我们先让第一个节点13等下31这个节点,同时size减了1(少了一根线),这时已经失去了堆序性,对应上面第二个堆

接下来31有两个孩子1416,最小的孩子是14,发现31>14,所以交换3114,就到了下面的第一个堆

二叉堆(优先队列)的基本操作(c语言)-LMLPHP

接着我们对比3131的两个孩子1921,较小的是19,我们交换3119,得到了上面的第二个堆

二叉堆(优先队列)的基本操作(c语言)-LMLPHP

最后我们比较31的两个孩子6526,较小的是26,31>26,所以我们交换3126,得到了上面这个堆,31已经没有孩子节点了,所以我们的堆也调整完毕了,这样堆中删除了最小元13,并且重新满足了堆序性。可以看出删除操作也是比较简单。

下面是代码实现,接下来的函数接受一个已经创建好的堆p,函数直接返回删除最小元后的堆。

struct heap* deletemin(struct heap* p){
    p->arr[1] = p->arr[p->size--];//让第一个等于最后一个,同时size减1
    int i, child, temp;
    for (i = 1; i * 2 <= p->size; i = child){//如果i * 2 > p->size,说明没有孩子,则不用接下来的调整,一定是满足堆序性
        child = i * 2;//假设最小孩子是左孩子,如果假设不成立,则child++,变成右孩子。详情见下。
        if (i * 2 != p->size && p->arr[child + 1] < p->arr[child]) child++;//防止数组越界,并找出最小孩子
        if (p->arr[i] > p->arr[child]){//交换父节点和子节点
            temp = p->arr[i];
            p->arr[i] = p->arr[child];
            p->arr[child] = temp;
        }
        else break;
    }
    return p;
}//by wqs

其他代码

堆的初始化操作

也就是定义它的maxsize,size,给arr分配空间。

struct heap* initial(int maxsize)
{
	struct heap* p = (struct heap*)malloc(sizeof(struct heap));
	p->maxsize = maxsize;
	p->size = 0;
	p->arr = (int*)malloc((maxsize + 1) * sizeof(int));
	p->arr[0] = -1;
	return p;
}//by wqs

判断一个序列是不是堆

int isheap(struct heap* p)
{
	int i = 1;
	for (i = 1; i <= p->size; i++)
	{
		if (2 * i <= p->size && p->arr[i] > p->arr[2 * i]) return 0;
		if (2 * i + 1 <= p->size && p->arr[i] > p->arr[2 * i + 1]) return 0;
	}
	return 1;
}//by wqs

打印堆的遍历结果

void jiegou(struct heap* p)
{
	int i = 1;
	printf("堆的遍历结果为");
	for (i = 1; i <= p->size; i++)
	{
		printf("%d ", p->arr[i]);
	}
	printf("\n");
}//by wqs

总代码

#include<stdio.h>
#include<stdlib.h>
struct heap {
	int maxsize;
	int size;
	int* arr;
};
struct heap* initial(int);
int isheap(struct heap*);
struct heap* insert(struct heap*, int);
struct heap* deletemin(struct heap*);
void jiegou(struct heap*);
int main()
{
	int maxsize, i = 1, x;
	printf("输入堆的最大空间:");
	scanf("%d", &maxsize);
	struct heap* p = initial(maxsize);
	printf("堆初始化成功\n");
	int size;
	printf("请输入堆的元素个数:");
	scanf("%d", &size);
	printf("请依次输入堆序列:\n");
	for (i = 1; i <= size; i++)
	{
		scanf("%d", &p->arr[i]);
		p->size++;
	}
	jiegou(p);
	if (isheap(p) == 0)
	{
		printf("不是堆\n");
		return 0;
	}
	else printf("这是堆\n");
	printf("接下来进入操作集\n输入1插入,输入2删除,输入3免费打印一次堆的结构,输入0退出");
	while (1) {
		int key;
		printf("输入:");
		scanf("%d", &key);
		if (key == 0) break;
		else if (key == 1) {
			printf("插入:");
			scanf("%d", &x);
			p = insert(p, x);
			printf("插入成功\n");
		}
		else if (key == 2) {
			p = deletemin(p);
			printf("删除最小元成功\n");
		}
		else if (key == 3) {
			jiegou(p);
		}
		else printf("你的输入有误");
	}
	return 0;
}
struct heap* initial(int maxsize)
{
	struct heap* p = (struct heap*)malloc(sizeof(struct heap));
	p->maxsize = maxsize;
	p->size = 0;
	p->arr = (int*)malloc((maxsize + 1) * sizeof(int));
	p->arr[0] = -1;
	return p;
}
int isheap(struct heap* p)
{
	int i = 1;
	for (i = 1; i <= p->size; i++)
	{
		if (2 * i <= p->size && p->arr[i] > p->arr[2 * i]) return 0;
		if (2 * i + 1 <= p->size && p->arr[i] > p->arr[2 * i + 1]) return 0;
	}
	return 1;
}
struct heap* insert(struct heap* p, int x) {
	int i;
	for (i = ++p->size; p->arr[i / 2] > x; i = i / 2) {
		p->arr[i] = p->arr[i / 2];
	}
	p->arr[i] = x;
	return p;
}
struct heap* deletemin(struct heap* p) {
	p->arr[1] = p->arr[p->size--];
	int i, child, temp;
	for (i = 1; i * 2 <= p->size; i = child) {
		child = i * 2;
		if (i * 2 != p->size && p->arr[child + 1] < p->arr[child]) child++;
		if (p->arr[i] > p->arr[child]) {
			temp = p->arr[i];
			p->arr[i] = p->arr[child];
			p->arr[child] = temp;
		}
		else break;
	}
	return p;
}
void jiegou(struct heap* p)
{
	int i = 1;
	printf("堆的遍历结果为");
	for (i = 1; i <= p->size; i++)
	{
		printf("%d ", p->arr[i]);
	}
	printf("\n");
}//by wqs

效果展示

给你一个免费的二叉堆,其他输入自行发挥。

11
13 14 16 19 21 19 68 65 26 32 31

二叉堆(优先队列)的基本操作(c语言)-LMLPHP

03-21 13:44