从认识堆到堆排序一篇搞定(动图解释)
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经典面试问题
算法漫游指北(第十一篇):归并排序算法描述动图演示代码实现过程分析复杂度