【堆的应用】堆排序

本篇总结堆排序是如何实现的,以及各种细节问题,两种不同的建堆方法以及各自的使用场景,注意排序利用堆删除思想进行处理

⏰堆排序

🕚建堆

建什么样的堆根据需求来决定
如果要排升序则建大堆
如果要排降序则建小堆
在这里插入图片描述
而建堆的方法有两种方法可以使用,一种是利用向上调整法建堆,即模拟插入建堆,一种是向下调整法建堆。不过通常堆排序中使用的建堆方法是向下调整建堆,因为向下调整建堆的方法效率要高。

并且在排序中也是使用向下调整思想进行排序,所以我们如果掌握了向下调整的思想,也就可以学会堆排序了。
向上调整建堆通常使用在堆中一开始没有数据,后来数据才进堆中,需要不断的往堆中插入,这样使用向上调整建堆很方便。因为数据只有在堆上面,下面也没有数据,使用向下调整很困难,而且低效。

而向下调整建堆长使用在已有数据,对已有数据进行建堆,也就是给你一个数组,让你进行排序就是很适合利用向下调整法进行建堆。
在这里插入图片描述

◽向上调整建堆

如何使用向上调整法建堆呢?
首先我们要理解向上调整的思想,即堆最后一个元素,顺着双亲往上调整即可
【建大堆】如果要建大堆则要求父结点元素大于子结点元素,如果不满足则两个结点值交换。
【建小堆】如果要建小堆则要求父节点元素小于子结点元素,如果不满足则两个结点值交换。

所以如果给定一个数组,我们如何使用向上调整法对这个数组建堆呢?
我们可以默认让数组第一个元素为堆顶元素,从数组第二个元素开始进行向上调整,调整完再调整数组第三个元素,……就这样调整n-1个元素就可以了。

void AdjustUp(int* a, int child)//向上调整的前提就是child之前的数是堆
{

	int parent = (child - 1) / 2;
	while (child > 0)
	{
		if (a[child] > a[parent])//这里决定建什么样的堆,大堆还是小堆
		{
			Swap(&a[child], &a[parent]);
			child = parent;
			parent = (child - 1) / 2;
		}
		else
		{
			break;
		}
	}

}

建堆:

//向上调整建堆--模拟插入建堆,时间复杂度为N*logN

for (int i = 1; i <10; i++)//从第二个元素开始调整,默认第一个元素为堆顶元素
	{
		AdjustUp(a, i);//i就是每次要调整的元素下标
	}

◽向下调整建堆

如何使用向下调整法进行建堆呢?
首先还是要理解向下调整法的思想
从堆顶元素开始往下调整,如果不满足要调整的堆性质,就要交换。

在已有数据的基础上我们进行建堆

下面我们给出一个数组,这个数组逻辑上可以看做一颗完全二叉树,但是还不是一个堆,现在我们通过算
法,把它构建成一个堆。根节点左右子树不是堆,我们怎么调整呢?这里我们从倒数的第一个非叶子节点的
子树
开始调整,一直调整到根节点的树,就可以调整成堆。

int a[] = {1,5,3,8,7,6}; 

倒数第一个非叶子,或者最后一个叶子的父亲。
为什么这样调?
我们知道,向下调整有前提条件,要求左右子树都为大堆或小堆。
我们从后面开始调,从最后一个叶子的父亲开始调,调完父亲,再调父亲的兄弟,那父亲和兄弟都调整完,父亲的父亲就可以调整了,因为父亲和兄弟那串子树都为堆了已经。 只要左右子树为堆我们就可以用向下调整。

在这里插入图片描述

要从最后一个子叶的父节点开始调整,首先我们需要找到它
对于数组来说,最后一个子叶的父亲的位置在哪呢?
最后一个结点的位置是n-1,而它的父节点的位置是((n-1)-1)/2 ,即(n-2)/2.
从这个位置开始向下调整,直到堆顶元素调整完即可

void AdjustDown(int* a, int n, int parent)//实现的前提是左右子树都是堆
{
	int child = parent * 2 + 1;
	while (child < n)
	{
		//选出左右孩子中比较大的孩子,假设child为左边,假设左边孩子比较大
		if (child + 1 < n && a[child] < a[child + 1])//不过这里存在越界的风险,不能保证右边的孩子一定存在
		{//右边的孩子要存在的话也需要小于n才可以所以我们再加上去
			++child;//让右边的孩子成为比较大的child
		}
		//然后让根(父亲)与较大儿子比较,这里是大堆,父亲要大于儿子的
		if (a[parent] >a[child])
		{
			Swap(&a[parent], &a[child]);
			//交互完后,让parent跳到儿子位置上去,儿子继续往下找
			parent = child;
			child = parent * 2 + 1;
		}
		else
		{
			break;
		}


	}
}

建堆:

    //通常建堆是使用向下调整建堆--怎么使用向下调整建堆呢?
	//从最后一个非空子叶开始向下调整,调整完后,再调整它的兄弟,再调整它们的父辈
	for (int i = (n -2) / 2; i >= 0; i--)
	{
		AdjustDown(a, n, i);
	}

◽两者区别:时间复杂度不同

向上调整的时间复杂度是O(N*logN)

而向下调整的时间复杂度是O(N)

也正是它们效率的不同,通常建堆常使用向下调整建堆,效率高。

🕐排序

利用堆删除思想进行排序
建完堆后,就要对堆中数据进行排序了。
那怎么排序呢?

比如我们排升序,我们要求的是第一个元素应该是最小的才对。
最后一个元素才是最大的。但我们建的是大堆,而大堆的堆顶数据是最大的,也就是数组第一个元素最大,完全不符合排序嘛。
我们是这样做:将堆顶数据与最后一个数据交换,并不删除最后一个元素,只是将堆的范围缩小,从堆顶开始往下调整使它的性质恢复。
然后再接着将堆顶与倒数第二个交换,交换完后,堆的范围缩小,从堆顶向下调整。

在这里插入图片描述

//将堆顶数据与堆尾部数据交换,然后不要删除最后这个数据,让剩下的n-1个结点继续变成堆,然后继续交换
	int end = n - 1;//堆尾数据
	for (int i = 0; i < n; i++)
	{
		Swap(&a[0], &a[end]);
		AdjustDown(a, end, 0);//end就是每次要调整堆的大小,每次都会缩小1
		end--;
	}

⏰整体代码

void HeapSort(int* a, int n)//堆排序
{
	//建堆
	for (int i = (n - 1 - 1) / 2; i >= 0; i--)
	{
		AdjustDown(a, n, i); 
	}
    //排序
	int end = n - 1;
	for (int i = 0; i < n; i++)
	{
		Swap(&a[0], &a[end]);
		AdjustDown(a, end, 0);
		end--;
	}
}
int main()
{
	int a[20] = { 9,5,2,3,6,7,8,4,1,0 };
	HeapSort(a, 10);
	return 0;
}

在这里插入图片描述

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小陶来咯

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值