堆结构的两个重要应用(topk问题,堆排序)

目录

写在前面的话

一,topk 问题

1.1思路一实现

步骤一

步骤二

代码实现

1.2思路二实现

代码实现

二,堆排序

2.1堆排序思路

2.2思路实现


写在前面的话

友友们大家好啊!今天的主题是关于堆排序的两个应用:topk问题以及堆排序问题。topk问题在日常生活中也是很常见的,比如我们需要在一个几万甚至几十万的数中找出最大的前十个数,那么如果将他们全部加载到内存中去排序的话,明显是不行的,因为内存没有那么大。

                                       

所以这里我们就用到了堆结构的的特性来解决这个问题。接下来我们一起看看吧。

一,topk 问题

那么对于堆如何实现一组很大的数中选出最大或者最小的前 k 个数呢?

1.1思路一实现

首先我们的思路是这样的,将这组数中的前面 k 个数建堆,这里我们以选出前 k 个最大的数为例。

步骤一

那么对于建堆操作,我们一定需要用到的是向上或者向下调整 ,这里为了统一,我们统一用向下调整,然后将数组中下标为 0~9 的十个数依次建堆。

同时我们考虑到的一个问题是,如果我们想要选出前 k 个最大的数,那么一定需要建小堆,这样我们能保证堆顶的元素一定是最小的,而其他的元素一定比它大,如下图所示:

注意在建小堆的过程中,因为用的是向下调整建小堆,所以最后一个参数传递的应该是孩子节点的下标;如果这里我们用的是向上调整的话,则需要传递的是双亲节点的下标。(如有小伙伴对于向下调整和向上调整有疑虑,请移步上篇文章哦!) 

步骤二

接下来我们将剩余的元素依次与堆顶元素比较,如果比堆顶元素大,那么则交换两个值,然后再调用一次向下调整。直到所有元素比较完,最后得到的便是一个最大的前 k 个数的一个小堆。

代码实现

//找出前 k 个最大数
void test_topk()
{
	int k =8;
	int a[] = {12,34,65,67,1,4,135,234,567,3444,5685};
	int sz = sizeof(a) / sizeof(a[0]);
	//1.将前 k 个数建小堆
	for (int i = 0; i < k; i++)
	{
		ADJustDown(a, k, (i - 1 - 1) / 2);
	}
    //2.将剩余的数依次与堆顶的数比较,如果大就换,最后得到的便是最大的前 k 个数
	for (int i = k; i < sz; i++)
	{
		if (a[i] > a[0])
		{
			Swap(&a[i],&a[0]);
			ADJustDown(a,k,0);
		}
	}
	for (int i = 0; i < k; i++)
	{
		printf("%d ",a[i]);
	}
}

1.2思路二实现

那么因为我们是通过堆实现的,所以我们可以通过堆的一些接口实现。

其实大概思路是相同的,首先我们还是需要建小堆(或大堆),但是不是通过向下调整或者向上调整建的。而是通过堆尾插入操作实现的,当然,在堆尾插入的时候还是需要调堆。

其次,剪完堆之后的第二步,我们是在比较完了之后,如果堆顶元素小于当前位置元素,首先将堆顶元素删除,然后再将该元素插入,那么同样的,因为在插入元素的函数中已经实现了调堆操作,所以这里我们不需要过多的其他操作。

这样两步之后,就实现了 topk 问题。

tips:我们需要知道的是,对于数组而言,其实就相当于是一个堆了,只是是一个顺序按照数组元素顺序存放的堆,所以我们可以不用进行通过堆的接口函数去实现建堆的操作了,但是向上调整和向下调整依旧是进行了建堆操作,所以其实道理是一样的。

代码实现

//堆排序
//堆排序一般不用创建堆。数组就可以当做堆,只需要进行调整即可。
void HeapSort(int* a, int sz)
{
	HP hp;
	HeapInit(&hp);
	//1.先建一个大堆
	for (int i = 0; i<sz; i++)
	{
		ADJustDown(a, sz, (i-1)/2);
	}
	for (int i = sz - 1; i > 0; i--)
	{
		//2.将最大的那个元素,也即堆顶元素与最后一个节点交换。
		Swap(&a[0], &a[i]);
		ADJustDown(a, i, 0);
	}

	for (int i = 0; i < sz; i++)
	{
		printf("%d ", a[i]);
	}
}

二,堆排序

那么上面我们通过对 topk 问题的实现,我们大概理解了对于建堆以及调堆的操作。接下来我们实现一下堆排序。

2.1堆排序思路

这里我们以升序做例子。

主要思路:

首先,如果我们想排升序,那么我们需要创建一个大堆,因为此时堆顶的元素一定是最大的(小堆的话,就是最小的),然后我们将堆尾元素与堆顶元素互换。最后我们再将不包括堆尾元素(该元素此时是最大值)的大堆再进行一次调堆。

然后重复上面的步骤,直到最后只剩一个元素。最后得到的便是一个升序的小堆,也就完成了我们的排序操作。

2.2思路实现

根据上述思路,这里我们依旧不打算采用堆的一些接口,因为那样做的代价是比较简单的,所以我们依旧只是用向下调整去实现。

//堆排序
//堆排序一般不用创建堆。数组就可以当做堆,只需要进行调整即可。
void HeapSort(int *a,int sz)
{
	//1.先建一个大堆
	for (int i = 0; i <sz ; i++)
	{
		ADJustDown(a,sz,(i-1)/2);
	}
	for (int i = sz-1; i > 0 ; i--)
	{
		//2.将最大的那个元素,也即堆顶元素与最后一个节点交换。
		Swap(&a[0],&a[i]);
		ADJustDown(a, i,0);
	}

	for (int i = 0; i < sz; i++)
	{
		printf("%d ",a[i]);
	}
}

那么对于这里的第二个循环,因为每一次交换堆顶元素和堆尾元素之后,需要将不包括最后一个元素的堆再次进行一次调堆操作。所以这里每次传递给向下调整函数的数组元素个数是变化的,所以循环变量 i 是倒着变得。

2.3复杂度及稳定性 

因为堆排序是通过堆来选数的,所以效率会高很多。这里们求出其时间复杂度为O(N^logN),同样是没有开辟空间的,所以空间复杂度是O(1)。

那么因为在建完堆之后,调堆操作会将最大数或者最小数移动到堆尾,所以每个元素的相对位置都可能发生变化,所以是不稳定的。

好的,那么本文到此就结束啦!如有问题,还请指正呀!

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值