【数据结构详解】——堆的应用

📖 前言:本期我们将详解堆相关的应用,如堆排序,Top-K问题


🎓 作者:HinsCoder
📦 作者的GitHub:代码仓库
📌 往期文章&专栏推荐:

  1. 【C语言详解】专栏
  2. 【数据结构详解】专栏
  3. 【C语言详解】——函数栈帧的创建与销毁(动图详解)
  4. 【数据结构详解】——线性表之顺序表(多图详解)
  5. 【数据结构详解】——线性表之单链表(动图详解)
  6. 【数据结构详解】——线性表之双向链表(动图详解)

🕒 1. 堆的应用

🕘 1.1 堆排序

堆排序相关知识点:

  • 选择排序的一种

  • 是利用这种数据结构的思想进行排序

💡 思路1:插入 - 删除堆

假如排升序,我们利用小堆进行排序的话:

我们建好一个N个数的小堆后,堆顶肯定是最小值,把这个最小值放到数组里,然后pop一下栈顶,再取下一个次小值,重复上述步骤即可。

void HeapSort(int* a, int n)
{
	HP hp;
	HeapInit(&hp);
	for (int i = 0; i < n; i++)
	{
		HeapPush(&hp, a[i]);
	}
	for (int i = 0; i < n; i++)
	{
		a[i] = HeapTop(&hp);
		HeapPop(&hp);
	}
	HeapDestroy(&hp);
}

这样有什么问题吗?
空间复杂度为O(N),因为你又开辟了一个小堆,并且你这样为了排序,还得写一个堆实现,这里能不能有更优的方法呢?

💡 思路2:模拟插入 - 向上/下调整建堆 - 依次选数调整

每一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,直到全部待排序的数据元素排完

我们可以把数组直接当成一个堆,以小堆为例,这里排的是升序,由于小堆的堆顶元素是最小值,这样我们就选出来最小值了,那怎么选出次小值呢?

我们只能把最小的那个值放到数组首,然后剩下的再看成一个堆
在这里插入图片描述
但是这样的问题很明显,我们之前建立好的小堆被打乱了,我们现在只能把剩下的再重新建小堆,这时的时间复杂度变成了O(N2) ,还不如直接遍历选数。

所以我们不妨来试试排升序建大堆
建大堆后,堆顶是最大值,然后让堆顶和最后的交换,向下调整,这样最大的值就被放到最后了,然后最后一个数不看成堆里面的元素,现在堆顶又是次大值了,再和后面交换,向下调整。以此类推最后就排序好了。

我们用向下调整法建堆,上面说过了,建堆的时间复杂度为O(N)
然后再选堆顶的数放后面,向下调整,每一次时间复杂度为O(logN)
总共N个数,所以是NlogN
所以最终的堆排序的时间复杂度为O(NlogN)。

大堆排升序动画:

请添加图片描述

升序:建大堆
降序:建小堆

void HeapSort2(int* a, int n)
{
	//建堆 --- 时间复杂度:O(N)
	//大堆
	for (int i = (n - 1 - 1) / 2; i >= 0; i--)
	{
		ADjustDown(a, n, i);
	}

	int end = n - 1;

	while (end > 0)		//end = 0的时候就截止:即 长度为0就截止
	{
		Swap(&a[0], &a[end]);
		//选出次大值
		ADjustDown(a, end, 0);
		end--;
	}
}

🕘 1.2 Top-K问题

问题大意:在N个数中找出最大/小的前K个数(一般K远小于N)
比如:在高考出成绩时,需要对全省考生的成绩进行排序并取出前50名考生的成绩进行屏蔽,此时就涉及Top-K问题了

🔎 例题: 剑指 Offer 40. 最小的k个数

接下来以找最大的前k个数举例

💡 思路1:堆排序

将所有数据进行排序,取前K个元素即可

void PrintTopKSort(int *a,int n,int k)
{
    HeapSort(a,n);
    for(int i=0;i<k;i++)
    {
        printf("%d ",a[i]);
    }
    printf("\n");
}

时间复杂度是O(N*logN)

💡 思路2:将N个数建堆,取出前K个

建好堆后,将堆顶的数与最后一个数交换位置,向下调整一次找次大的数(注意向下调整时不要包含交换后的最后一个位置),重复这样的操作,直到找完K个数

注意:
需要前K个最大的数时,建小堆
需要前K个最小的数时,建大堆

void PrintTopKBuildN(int* a, int n, int k)
{
	//建堆
	for (int i = (n - 1 - 1) / 2; i >= 0; i--)
	{
		AdjustDown(a, n, i);
	}
	int end = n - 1;
	for (int i = 0; i < n-k; i++)
	{
		
		Swap(&a[0], &a[end]);//交换最值和最后一个位置的数
		end--;//不包含最后一个位置
		AdjustDown(a, end, 0);//调整
	}
	for (int i = 0; i < k; i++)
	{
		printf("%d ", a[i]);
	}
	printf("\n");
}

N个数建堆时间复杂度是O(N),Pop K次的时间复杂度O(KlogN)
总时间复杂度为O(N+KlogN)

上面两种方法是有缺点的,假设N非常大,N是100亿(约40G),堆是在内存操作的,内存就存不下这些数据,他们只能存在文件中。

💡 思路3:建前K个数的堆(最优)

  1. 用前K个数建立一个K个数的小堆
  2. 依次遍历后续N-K个数,比堆顶的数据大,就替换堆顶数据,向下调整进堆。
  3. 然后再插入新数(因为是降序找最大的K个数,我们建的是小堆,所以堆顶是最小的数,当有比它大的数就替换它进堆)
  4. 最后堆里的K个数就是最大的前K个数
    在这里插入图片描述
void PrintTopKBuildK(int* a, int n, int k)
{
	//建k个数的堆
	for (int i = (k - 1 - 1) / 2; i >= 0; i--)//O(k)
	{
		AdjustDown(a, k, i);//取最大的需要建小堆,取最小的需要建大堆
	}
	for (int i = k; i < n; i++)//O(n*logk)
	{
		if (a[0] < a[i])//比堆顶大的就入堆
		{
			Swap(&a[0], &a[i]);
			AdjustDown(a, k, 0);
		}
	}
	for (int i = 0; i < k; i++)
	{
		printf("%d ", a[i]);
	}
	printf("\n");
}

建立一个K个数的堆时间复杂度是O(K)
剩下N-K个数向下调整时间复杂度O( (N-K)*logK )
总时间复杂度为O(K+(N-K)*logK)≈O(N)

💡 思路4:建前K个数的堆(文件版)

void CreateDataFile(const char* filename, int N)
{
	FILE* fin = fopen(filename, "w");
	if (fin == NULL)
	{
		perror("fopen fail");
		return;
	}
	srand(time(0));

	for (int i = 0; i < N; ++i)
	{
		fprintf(fin, "%d\n", rand() % 1000000);
	}

	fclose(fin);
}

void PrintTopKByFile(const char* filename, int k)
{
	assert(filename);

	FILE* fout = fopen(filename, "r");
	if (fout == NULL)
	{
		perror("fopen fail");
		return;
	}

	int* minHeap = (int*)malloc(sizeof(int) * k);
	if (minHeap == NULL)
	{
		perror("malloc fail");
		return;
	}
	// 如何读取前K个数据
	for (int i = 0; i < k; ++i)
	{
		fscanf(fout, "%d", &minHeap[i]);
	}

	// 建k个数小堆
	for (int j = (k - 2) / 2; j >= 0; --j)
	{
		AdjustDown(minHeap, k, j);
	}

	// 继续读取后N-K
	int val = 0;
	while (fscanf(fout, "%d", &val) != EOF)
	{
		if (val > minHeap[0])
		{
			minHeap[0] = val;
			AdjustDown(minHeap, k, 0);
		}
	}

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

	free(minHeap);
	fclose(fout);
}

int main()
{
	const char* filename = "Data.txt";
	int N = 10000;
	int K = 10;
	CreateDataFile(filename, N);
	PrintTopKByFile(filename, K);

	return 0;
}

OK,以上就是本期知识点“堆的应用”的知识啦~~ ,感谢友友们的阅读。后续还会继续更新,欢迎持续关注哟📌~
🎉如果觉得收获满满,可以点点赞👍支持一下哟~

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值