堆的实现


在介绍堆之前,我们要先简单介绍一下二叉树,因为堆的结构就是一个二叉树。详细的二叉树我会在下一篇博客中给大家讲解。

二叉树

二叉树的概念

一棵二叉树是结点的一个有限集合,该集合或者为空,或者是由一个根节点加上两棵别称为左子树和右子树的二叉树组成。二叉树就像下图中的树倒过来的样子。

在这里插入图片描述
而数据结构中的二叉树如下图,可以是空树、可以是只有一个根节点、可以是只有一棵子树、也可以是有两棵子树,只要节点个数小于2即可
在这里插入图片描述
在二叉树中,还有两个很特殊的二叉树:
满二叉树:一个二叉树,如果每一个层的结点数都达到最大值,则这个二叉树就是满二叉树。也就是说,如果一个二叉树的层数为K,且结点总数是(2^k) -1 ,则它就是满二叉树。
在这里插入图片描述
完全二叉树:完全二叉树是效率很高的数据结构,完全二叉树是由满二叉树而引出来的。对于深度为K的,有n个结点的二叉树,当且仅当其每一个结点都与深度为K的满二叉树中编号从1至n的结点一一对应时称之为完全二叉树。 要注意的是满二叉树是一种特殊的完全二叉树。
在这里插入图片描述
而堆就是由完全二叉树所构成的。

堆的概念

如果有一个关键码的集合K = {k0,k1, k2,…,kn-1},把它的所有元素按完全二叉树的顺序存储方式存储
在一个一维数组中,并满足:Ki <= K2i+1 且 Ki<= K2i+2 (Ki >= K2i+1 且 Ki >= K2i+2) i = 0,1,2…,则称为小堆(或大堆)。将根节点最大的堆叫做最大堆或大根堆,根节点最小的堆叫做最小堆或小根堆。

堆的性质:
堆中某个节点的值总是不大于或不小于其父节点的值;
堆总是一棵完全二叉树。

堆分为两种:小根堆和大根堆

小根堆:根节点的值小于左右节点的值

在这里插入图片描述
大根堆:根节点的值大于左右节点的值
在这里插入图片描述

堆的代码实现

给定一个数组,逻辑上是看作一个完全二叉树,但是这并不符合堆的结构要求,所以我们首先需要一个算法,将每个元素调整到它正确的位置。

向下调整算法

在这里插入图片描述
假设给定这样一个数组,我们将它用二叉树的形式表示如下:
在这里插入图片描述
我们从上面可以看到,这个二叉树除了根节点之外,其他的子树已经是满足小根堆的结构,那么我们就需要将这个跟节点调整到它正确的位置上。我用一组动图来解释一下什么是向下调整算法:
在这里插入图片描述
要找到根节点对应的位置,那么先判断它的左孩子和右孩子的大小,将其与较小的孩子交换,第一次把27换到了15的位置;然后再次比较27的左右孩子,和较小的18交换;再次比较左右孩子,和较小的25交换,直到左右孩子的下标超出数组长度为止。

从上面的解释中,总结如下:建小根堆,如果某一个非叶子节点或者根节点的值比左右孩子最小的还要小,就不交换,说明节点位置是正确的;如果它比最小的孩子要大,那么就和最小的孩子交换,这样子可以保证,交换之后现在的非叶子节点或者根节点一定比它的孩子都要小,那么就满足小根堆的要求(如果建大根堆,调整的算法与此相反)。

void AdjustDown(HDataType* a, HDataType root, int size)
{

	HDataType parent = root;
	HDataType child = parent * 2 + 1;//假设第一个孩子是较小的那个
	if (child + 1 < size && a[child] > a[child + 1])
	{
		child++;
	}
	while (child < size && a[parent] > a[child])
	{

		Swap(&a[parent], &a[child]);
		parent = child;
		child = parent * 2 + 1;
		if (child + 1 < size && a[child] > a[child + 1])
		{
			child++;
		}
	}
}

堆的创建

讲解完向下调整算法之后,就可以进行堆的创建了,给定一个随机的数组
在这里插入图片描述
我们按照二叉树的结构画出示意图:
在这里插入图片描述

我们可以看出,这并不是一个小根堆,那么我们怎么样才能把它调整成小根呢?肯定是需要一个元素一个元素来进行调整的,那么是从上向下调整还是从下往上调整呢?答案是从下往上,因为如果是从上往下的话,并不能保证下面调整之后的元素一定比上面的大(大根堆相反)。因此,我们从下往上进行调整。

从下往上调整的起点应该是倒数第一个非叶子节点,因为叶子节点本身是没有子树的,不需要调整。
在这里插入图片描述
从倒数第一个非叶子开始按照向下调整的算法进行调整,直到调整到根节点为止,这样子就得到了小根堆。大根堆的建立和小根堆是刚好相反的。

//堆创建
void HeapCreate(Heap* hp, HDataType* a, int size)
{
	assert(hp);
	int i = 0;
	for (i = (size - 1 - 1) / 2; i >= 0; i--)
	{
		AdjustDown(a, i, size);
	}
	hp->_a = (HDataType*)malloc(sizeof(HDataType)*size);
	memcpy(hp->_a, a, sizeof(int)*size);
	hp->_size = size;
	hp->_capacity = size;
}

堆的插入

把堆建好之后,如何进行插入操作呢?
有两种方案,一种是插在堆的头,进行向下调整;一种是插在尾上,进行向上调整。

堆的插入一定是要用第二种方案的
第一种方案有两个缺点:一个是插在堆的头部,也就是插在数组的头部,那么必定是要移动数据的,会有很多的消耗,不如插在尾部方便;二是插在头部的话,插入的元素就要做根节点,会整体改变堆原本的结构,这是非常麻烦的。

那么我们用第二种方案,将新插入的元素放在尾部,但是并不能保证插入的元素满足小根堆或者大根堆的结构,因此这里要介绍一种向上调整的算法。

向上调整算法

向上调整算法和向下调整算法思路是基本相同的,用动图来解释一下:
比如我们插入一个1进去,很明显是不满足小根堆的结构;在这里插入图片描述
在这里插入图片描述
向上调整算法的思路是:建立小根堆,将插入的元素与它的父亲节点比较,如果比父亲小就交换;交换之后再次与其父亲节点比较,如果小于父亲节点继续交换,直到数组的头为止。

//向上调整算法
void AdjustUp(HDataType* a, HDataType Leap)
{
	HDataType child = Leap;
	HDataType parent = (child - 1) / 2;
	while (parent >= 0 && a[child] < a[parent])
	{
		Swap(&a[child], &a[parent]);
		child = parent;
		parent = (child - 1) / 2;
	}
}

//堆插入
void HeapPush(Heap* hp, HDataType x)
{
	assert(hp);
	//如果容量不够扩容
	if (hp->_size == hp->_capacity)
	{
		hp->_capacity *= 2;
		HDataType* ptr = (HDataType*)realloc(hp->_a, sizeof(HDataType)*hp->_capacity);
		if (ptr == NULL)
		{
			printf("realloc failed\n");
			exit(-1);
		}
		hp->_a = ptr;
	}
	hp->_a[hp->_size] = x;
	AdjustUp(hp->_a, hp->_size);//插入的时候把数插在最后一个,然后向上调整
	hp->_size++;
}

堆的删除

堆的删除也就是删除堆顶的数据,如果直接把堆顶的数据删除的话,不仅要移动数组的数据,同时会破坏整体的结构。可以看出,只有从堆的底部删除数据不会破坏堆的整体结构。

我们可以把堆顶的数据和堆的最后一个数据进行交换,然后删除最后一个数据,将根节点进行向下调整即可。
在这里插入图片描述

//堆的删除
void HeapPop(Heap* hp)
{
	assert(hp);
	Swap(&hp->_a[0], &hp->_a[hp->_size - 1]);
	hp->_size--;
	AdjustDown(hp->_a, 0, hp->_size);
}

堆排序

上面就是堆的一些基本操作,讲解完这些,我们就可以用堆来实现一下排序算法。
给定如下数组:
在这里插入图片描述
这里有一个比较关键的问题,如果要排升序的话,是建小堆来进行排序还是建大堆来进行排序呢?

用下面的动图来了解一下堆排序的思想,就能够得到答案。
在这里插入图片描述
这是上面数组的二叉树形式,我们先将其改成大根堆(具体的原因在下面进行阐述)。
在这里插入图片描述
通过动图来解释一下排序的思路:
在这里插入图片描述

通过建大根堆,最大的数一定在根节点的位置,要排升序,那么就把根节点和最后一个节点交换,如上图将20和3进行交换,此时3为根节点,但是这不满足大根堆的结构,因此需要向下调整使得堆依然是大根堆。之后将次小的和倒数第二个交换(交换后的最大的数此时不算在堆中),继续进行向下调整。以此类推,直到节点的下标等于0为止,此时堆一定是有序的。

这里就能够解释上面提出的问题,如果建小根堆的话,最小的数在根节点,那么我们找出次小的数,就需要将除根节点之外的数再次建一个小堆才能找到,因为最小的数的左孩子并不一定是次小的。这就非常的麻烦,所以排升序需要建大根堆;排降序需要建小根堆。

//堆排序
void HeapSort(HDataType* a, int size)
{
	int end =  size - 1;
	while (end > 0)
	{
		Swap(&a[0], &a[end]);
		size--;//每次交换完之后,用于向下调整的元素就少一个
		AdjustDown(a, 0, size);
		end--;
	}
}

完整的代码点开链接获取
链接:
https://gitee.com/cao-xudong/bit/tree/master/Heap_new/Heap_new

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值