【数据结构与算法】堆排序、TopK问题详解

小编个人主页详情<—请点击
小编个人gitee代码仓库<—请点击
数据结构与算法系列专栏<—请点击
倘若命中无此运,孤身亦可登昆仑,送给屏幕面前的读者朋友们和小编自己!
在这里插入图片描述



前言

在学习完堆的实现的知识后,那么小编要继续讲解两个有关堆的应用,即堆排序、TopK问题
如果对堆不了解的读者朋友们可以点击此处了解<—

一、堆排序

对于一组数据我们要进行排序,使用冒泡排序的时间复杂度为O(N^2),然而使用堆排序的时间复杂度为O(N*logN),时间复杂度的提升不可谓不大,那么堆排序具体是怎么样实现的呢?跟随小编的节奏往下读吧

  1. 给你n个数据的数组,如果你要将其排序成为升序,那么我们就建大堆

  2. 建大堆我们采用可以采用从第二个位置开始进行逐个的向上调整算法(时间复杂度O(N*logN)),也可以采用向下调整算法(时间复杂度O(N)),这里从效率时间复杂度的角度考虑我们采用向下调整算法

  3. 在进行向下调整算法的时候是由下往上进行调整,从下面倒数第一个父亲节点开始调整,为什么不从叶子节点开始调整呢?因为叶子节点本身就是最小节点了,它可以看作大堆,也可以看作小堆,而我们要使用向下调整算法的首要条件就是要保证根节点的左右子树都为大堆,或者小堆,这里的叶子节点我们就将其看作大堆,那么倒数第一个父亲节点的左右子树就是大堆,所以我们可以使用向下调整算法,进行调整建立大堆

  4. 即给出一个乱序数组,我们算出倒数第一个父亲节点开始向下调整,调整完之后,由于数组中元素的存储是连续的,要找到父亲节点的前一个进行调整,此时前一个节点的左右子树已经是被调整完的大堆了,满足使用向下调整算法的使用条件,那么继续调整迭代直到调整完根节点即可完成我们的建大堆操作
    在这里插入图片描述

  5. 由于我们排序的是大堆,要实现升序,建大堆的目的就是要选出这n个数据中的最大值,这个最大值在数组的首项,即数组下标为0的位置

  6. 升序,排完序之后,最大值肯定是放到数组的末尾,最大值由数组末尾到数组的首项元素大小依次减小

  7. 那么我们选处这n个数据中的最大值之后,最大值应该放到数组的末尾,但是直接放到数组的末尾,原数组末尾的数据又被覆盖,所以我们先将数组末尾的元素和我们选出的数组的最大值下标为0的位置的元素换位,并且向下调整算法,那么这个最大值就应该被固定在了数组的末尾,所以我们采用一个中间变量作为数组的元素个数,在传入向下调整算法的时候,将数组个数传入的数组个数小一个,那么就相当于在进行向下调整算法的时候访问不到我们固定的最大值的位置,于是调整完之后,我们的排除了最大值的大堆就又建好了,由于下一次要换位的位置是数组中倒数第二个位置与数组的移除最大值之后的第二大的位置,所以采用tmp减一就可以定位到数组中倒数第二个位置进行换位

  8. 此时排在数组首位的就为第二大的元素,将第二大的元素换到倒数第二个元素的位置上,循环重复,直到堆的个数为1结束即可

void Swap(HPDataType* x, HPDataType* y)
{
    
    
	HPDataType tmp = *x;
	*x = *y;
	*y = tmp;
}

void AdjustDown(HPDataType* a, int n, int parent)
{
    
    
	int child = parent * 2 + 1;

	while (child < n)
	{
    
    
		if (child + 1 < n && a[child + 1] > a[child])
		{
    
    
			child++;
		}

		if (a[parent] < a[child])
		{
    
    
			Swap(&a[parent], &a[child]);
			parent = child;
			child = parent * 2 + 1;
		}
		else
		{
    
    
			break;
		}
	}
}

void HeapSort(int* a, int n)
{
    
    
	for (int i = (n - 1 - 1) / 2; i >= 0; i--)
	{
    
    
		AdjustDown(a, n, i);
	}

	int end = n - 1;
	while (end)
	{
    
    
		Swap(&a[0], &a[end]);
		AdjustDown(a, end, 0);
		end--;
	}
}

int main()
{
    
    
	int a[10] = {
    
     5,2,6,3,1,0,9,4,8,7 };
	int n = sizeof(a) / sizeof(a[0]);

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

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

	return 0;
}

这里运行程序,使用了堆排序按照我们所想,进行排了升序
在这里插入图片描述

向上调整建堆的时间复杂度计算

在这里插入图片描述

向下调整建堆的时间复杂度计算

在这里插入图片描述

交换确定元素位置的循环的时间复杂度

在这里插入图片描述

所以向下调整的两个部分的时间复杂度的和为O(N+N*logN),采用大O渐进法这里的N和NlogN不在同一个数量级,所以忽略N,所以堆排序的时间复杂度为O(NlogN)

二、TopK问题

如果小编给你100亿个int类型的整数让你选出最大的前K个数字,那么你如何选呢?
在这里插入图片描述

有的读者朋友们可能会想,这简单,排序不就好了吗,可是你要想,要对100亿整数进行排序电脑内存中无法加载40G那么大内存,所以这种思路不行,那么接下来小编就要引出一种建立个数为K个的堆就可以选出最大的前K个数字,这种方法仅仅需要开辟K个空间为int的空间,就可以直接对存放数据的位置进行查找最大的前K个数,这种建立小堆的方法解决TopK问题不需要电脑内存加载40G那么大内存

  1. 原理:你要在100亿中个数据中找到最大的前K个数,明确第一点,这最大的K个数一定是比其它的剩余的100亿-K个数据永远要大,那么我们就可以利用这个性质进行建立一个小堆,先使用100亿的前K个数字,使用malloc函数开辟K个大小为int的空间,使用向下调整将这K个数字建立成为一个小堆
  2. 建立完后,小堆的堆顶一定是这K个数字中的最小的数字,我们可以以此为依据建立去余下的100亿-K个数据中比较,假设有一个A就为这100亿-K个中的最大的数据,此时A一定大于堆顶的数据,那么我们A这个元素去覆盖堆顶元素,再使用向下调整算法进行调整建立小堆,幻想一下由于是建立小堆,然而此时堆顶的元素是最大的数A,那么其经过向下调整算法一定一定一定会沉下去到堆的最下面,无法维持在堆顶,此时堆顶为当前K个数中最小的数,那么连最大的值A都能进得去,其它的最大的前K个数也一定可以进去,并且大的数不会堵在堆顶,会沉下去这就是原理,所以我们的建立K个数据的小堆是一定可以找出最大的前K个数的

如果对文件操作不了解的读者朋友们点击这里了解详情<—

#include <stdio.h>
#include <stdlib.h>
#include <time.h>

typedef int HPDataType;

//Top-K问题
void Swap(HPDataType* x, HPDataType* y)
{
    
    
	HPDataType tmp = *x;
	*x = *y;
	*y = tmp;
}

void AdjustDown(HPDataType* a, int n, int parent)
{
    
    
	int child = parent * 2 + 1;

	while (child < n)
	{
    
    
		if (child + 1 < n && a[child + 1] < a[child])
		{
    
    
			child++;
		}

		if (a[parent] > a[child])
		{
    
    
			Swap(&a[parent], &a[child]);
			parent = child;
			child = parent * 2 + 1;
		}
		else
		{
    
    
			break;
		}
	}
}

void CreatNTopK()
{
    
    
	srand((unsigned int)time(NULL));
	int n = 10000;
	FILE* pf = fopen("test.txt", "w");
	if (pf == NULL)
	{
    
    
		perror("fopen error");
		return;
	}
	//写入文件数据
	for (int i = 0; i < n; i++)
	{
    
    
		int tmp = rand()%10000;

		fprintf(pf, "%d\n", tmp);
	}

	fclose(pf);
	pf = NULL;
}

void PrintKTopK(int k)
{
    
    
	FILE* pf = fopen("test.txt", "r");
	if (pf == NULL)
	{
    
    
		perror("fopen error");
		return;
	}

	int* arr = (int*)malloc(sizeof(int) * k);
	if (arr == NULL)
	{
    
    
		perror("malloc error");
		return;
	}
	//读取前k个数
	for (int i = 0; i < k; i++)
	{
    
    
		fscanf(pf, "%d ", arr + i);
	}

	for (int j = 0; j < k; j++)
	{
    
    
		printf("%d ", arr[j]);
	}
	printf("\n");
	//找最大的前k个数,建小堆即可
	for (int m = (k - 2) / 2; m >= 0; m--)
	{
    
    
		AdjustDown(arr, k, m);
	}
	//比较
	int tmp = 0;
	while (fscanf(pf, "%d", &tmp) != EOF)
	{
    
    
		if (tmp > arr[0])
		{
    
    
			arr[0] = tmp;
		}
		AdjustDown(arr, k, 0);
	}

	for (int j = 0; j < k; j++)
	{
    
    
		printf("%d ", arr[j]);
	}
	printf("\n");
}

int main()
{
    
    
	//CreatNTopK();

	int k = 10;
	PrintKTopK(k);

	return 0;
}

三、TopK问题的测试

  1. 下面我们进行使用文件测试随机生成10000个小于10000的值,生成完后我们再修改其中的值,修改10个超过10000最大值,再使用我们的建小堆选出最大的10个数字进行测试

进行观察我们的随机数已经生成好了,小编再进行随机修改10个地方位置的数据进行测试
在这里插入图片描述
如下完成了修改
在这里插入图片描述
在这里插入图片描述
运行程序打印最大的前K个数验证得,是我们所修改的最大的10个数,TopK问题讲解正确
在这里插入图片描述


总结

以上就是今天的博客内容啦,希望对读者朋友们有帮助
水滴石穿,坚持就是胜利,读者朋友们可以点个关注
点赞收藏加关注,找到小编不迷路!