从认识堆到堆排序一篇搞定(动图解释)

Posted Aline2021-yxz

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了从认识堆到堆排序一篇搞定(动图解释)相关的知识,希望对你有一定的参考价值。

什么是堆?



🔶像上面所展示的两个完全二叉树都称作为堆,第一个叫小堆,因为双亲结点的值都小于子节点,第二个叫大堆,因为双亲结点的值都大于子节点,如果一个二叉树不满足大堆或小堆的特征,就不叫堆。

堆满足的性质:
0️⃣:子节点都不大于或不小于父结点。
1️⃣:是一个完全二叉树。

数据结构在堆实现时用的是数组的形式,父节点和子节点之间下标的关系是:

左子结点下标=父节点下标*2+1;

右子节点下标=父节点下标*2+2;

➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖

如何建堆?

向下调整算法
 以建小堆为例:比较两个子节点,找到小的那个和父结点交换,重复此步骤直到小堆建成。
 思想就是把小的往上换。

❗❗❗注意: 根的左子树右子树必须是堆



代码实现:

void Swap(int *a, int *b)
{
	int tmp = *a;
	*a = *b;
	*b = tmp;
}

void Heapdown(int * p,int sz,int begin)//sz是数组大小,begin是开始调整的初始位置
{
	int parent = begin;
	int minson = parent * 2 + 1;//找到子节点,默认小的那个是左节点
	while (minson < sz)
	{
		if (minson+1<sz&&*(p + minson)>*(p + minson + 1))//如果右节点不存在就不需要比较,存在就比较,得到小的。
			minson++;
		if (*(p + minson) < *(p + parent))//把小的换上去
		{
			Swap(p + minson, p + parent);
			parent = minson;//迭代
		}
		else//如果较小的子节点比父节点还大,就不需要继续了,因为左右两边都是小堆,后面的还大。
			break;
	}

}

一般要建堆的数据可能不会那么美好,根的左右子树可能不是堆,向下调整算法就没办法了,例如:

如果使用向下调整算法得到的结果(建大堆):

可以看到得到的结果不是堆。

反向向下调整算法

为了解决上面的问题,我们的前辈想出来了这种方法。

🔶 算法思想如果把最后一个子结点(3)看为根,那么它没有子结点,可以将它看做大堆或小堆,其他没子节点的结点也一样不需要处理,第一个特殊的结点就是3的父节点,因为3可以看作大堆或小堆,满足向下调整算法的特点,直接调用即可,其他父节点也一样,这样当根使用向下调整算法后,堆自然也就形成了。
同样对于上面不能使用向下调整算法的数据,反向向下调整的顺序如下:


代码实现:

void Swap(int *a, int *b)
{
	int tmp = *a;
	*a = *b;
	*b = tmp;
}

void Heapdown(int * p,int sz,int begin)//sz是数组大小,begin是开始调整的初始位置
{
	int parent = begin;
	int minson = parent * 2 + 1;//找到子节点,默认小的那个是左节点
	while (minson < sz)
	{
		if (minson+1<sz&&*(p + minson)>*(p + minson + 1))//如果右节点不存在就不需要比较,存在就比较,得到小的。
			minson++;
		if (*(p + minson) < *(p + parent))//把小的换上去
		{
			Swap(p + minson, p + parent);
			parent = minson;//迭代
		}
		else//如果较小的子节点比父节点还大,就不需要继续了,因为左右两边都是小堆,后面的还大。
			break;
	}

}

void HeapCreat(int *p, int sz)//建堆
{
	assert(p);
	int parent = (sz -1- 1) / 2;//最后一个堆结点的父结点
	while (parent >= 0)
	{
		Heapdown(p, sz,parent);//注意传sz数据时仍然传的是原数组的大小
		parent--;
	}
}
向上调整算法

算法思想:先将一个结点看作为堆,将后面的结点插入堆,以建小堆为例,子节点比父的小就交换
,否则不然,重复步骤直到小堆建成。

还是以上面不能使用向下调整算法为例:

代码实现:

void Heapup(int *p,int sz)
{
	assert(p);

	int child = 1;
	int parent = (child - 1) / 2;

	while (child < sz)
	{
		int tmp = child;//代替child迭代
		while (tmp >= 0)
		{
			if (*(p + tmp) < *(p + parent))
			{
				Swap(p + tmp, p + parent);
				//向上走
				tmp = parent;
				parent = (tmp - 1) / 2;
			}
			else
			{
				break;
			}
		}
		//新子结点,新父节点
		child++;
		parent = (child - 1) / 2;
	}
}

➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖➖

堆排序

堆排序的时间复杂度是O(nlogn),空间复杂度是O(1),稳定。 算法稳定性介绍

要使用堆排序肯定要建一个堆,先来一个小思考:

如果让你将一组数据使用堆排序为升序,是建大堆还是小堆?

答案是最好建大堆!

以升序为例:
算法思想:大堆堆顶数据是最大的,升序应该在数组的最后一位,那么我们将堆顶元素和最后一一个元素交换就可以达到,因为堆顶元素已经到达它该在的位置了,不需要对其再操作,所以数组大小-1屏蔽掉它,堆顶元素换成了不知大小的数,但是因为根的左子树和右子树的大堆结构没被破坏,之间使用向下调整算法就可以构成新大堆,重复此操作直到数组大小为1结束。

代码实现:

void Swap(int *a, int *b)
{
	int tmp = *a;
	*a = *b;
	*b = tmp;
}

void Heapup(int *p, int sz)
{
	assert(p);

	int child = 1;
	int parent = (child - 1) / 2;

	while (child < sz)
	{
		int tmp = child;//代替child迭代
		while (tmp >= 0)
		{
			if (*(p + tmp) > *(p + parent))
			{
				Swap(p + tmp, p + parent);
				//向上走
				tmp = parent;
				parent = (tmp - 1) / 2;
			}
			else
			{
				break;
			}
		}
		//新子结点,新父节点
		child++;
		parent = (child - 1) / 2;
	}
}

void HeapCreat(int *p, int sz)//建堆
{
	assert(p);
	Heapup(p, sz);
}

void Heapsort(int *p, int sz)//排升序建大堆
{
	HeapCreat(p, sz);//建大堆,不能直接用向下调整算法!
	while (sz > 0)
	{
		Swap(p, p + sz - 1);//交换堆顶数据和最后一个数据
		sz--;//屏蔽最后一个数据
		Heapup(p, sz);//建大堆,上面介绍的三种方法都行
	}
}

关于堆的介绍就到这里了哈😀,如果有任何想法及博客中出现的问题欢迎再评论区留言哦,博主一定会回的!

创作不易,
赠人玫瑰,手留余香!
感谢支持!

以上是关于从认识堆到堆排序一篇搞定(动图解释)的主要内容,如果未能解决你的问题,请参考以下文章

图文最详细的堆解析:从二叉树到堆到解析大根堆小根堆,分析堆排序,最后实现topK经典面试问题

算法漫游指北(第十一篇):归并排序算法描述动图演示代码实现过程分析复杂度

排序算法总结

花一个晚上时间整理,十大经典排序算法(Python版本),拿起就用

经典排序算法一从简单选择排序到堆排序的深度解析

专门为学弟学妹写的十大排序算法(背诵版+动图+代码)