堆排序的实现+应用

目录

1.堆的概念及结构

2.堆的实现

2.1堆的创建

 2.2开始排序

整体代码

3.堆排的时间复杂度

3.1建堆的时间复杂度

3.2堆正式排序过程的时间复杂度

4.堆的应用(Top-K问题)

Top-K问题的时间复杂度


1.堆的概念及结构

如果有一个关键码的集合K = { , , ,…, },把它的所有元素按完全二叉树的顺序存储方式存储 在一个一维数组中,并满足: = 且 >= ) i = 0,1, 2…,则称为小堆(或大堆)。将根节点最大的堆叫做最大堆或大根堆,根节点最小的堆叫做最小堆或小根堆。

堆的性质:

  • 堆中某个节点的值总是不大于或不小于其父节点的值;
  • 堆总是一棵完全二叉树。

2.堆的实现

2.1堆的创建

这里我们先给出一个数组和它的逻辑结构(我们要把数组看成一个完全二叉树)

接下来构建大堆

 

这里给出一些公式,可以自己看图理解:

leftchild = parent * 2 + 1;

rightchild = parent * 2 + 2;

parent = (child - 1)  / 2;   // child是leftchild或rightchild的下标

leftchild、rightchild、parent、child都是结点下标

先将数组构建成堆

向下调整算法:

这里用最后一步来解释,请看下图

parent(下标) = 0;

leftchild = parent * 2 + 1 = 0 * 2 + 1 = 1;rightchild = parent * 2 + 2 = 2;

先判断leftchild < 6 && rightchild < 6 ,(6是数组的元素个数)

满足条件,再比较a[leftchild]a[rightchild] 谁大,这里是a[leftchild]大,

然后再比较a[parent]a[leftchild]谁大

如果是a[parent](父结点)大的话就不用再向下调整了

这里是a[leftchild]大,于是要将两个值交换,同时parent = leftchild;接着就重复上面的过程


 2.2开始排序

现在我们已经建好大堆了,然后我们将第一个结点和最后一个结点交换,再进行调整

整体代码

         // a是数组  n是数组元素个数  parent是父结点
void AdjustDown(int* a,int n, int parent)
{
	// 找到左孩子结点的下标
	int child = parent * 2 + 1; 
	while (child < n) //下标要小于n
	{
		// 判断左孩子大还是右孩子大     同时右孩子的下标也要保持 < n
		if (a[child] > a[child + 1] && (child + 1) < n)
		{
			child = child + 1;
		}
		// 当父结点小于孩子时
		if (a[child] < a[parent])
		{
			swap(&a[child], &a[parent]);
			parent = child;
			child = parent * 2 + 1;
		}
		else // 说明该结点已经调好了
		{
			break;
		}
	}
}

// 对数组进行堆排序
void HeapSort(int* a, int n) // n为数组元素的个数
{
	// 先将数组构建成堆   (n - 1)为最后一个元素的下标
	int parent = ((n - 1) - 1) / 2; // 先找倒数第一个非叶子结点
	for (int i = parent; i >= 0; i--)
	{
		// 从倒数第一个非叶子节点开始调整
		AdjustDown(a, n, i); // 这里采取向下调整算法
	}

	// 开始排序
	// 将根节点元素与最后一个节点元素交换再进行调整
	int size = n - 1; 
	while (size > 0)
	{
		swap(&a[0], &a[size]); // 根节点和最后一个节点交换
		AdjustDown(a, size, 0); // 除去最后一个节点,数组长度 = n - 1
		size--;
	}
	
}

3.堆排的时间复杂度

3.1建堆的时间复杂度

因为堆是完全二叉树,而满二叉树也是完全二叉树,此处为了简化使用满二叉树来证明时间复杂度:

则结点总移动步数为:

因此:建堆的时间复杂度为O(N)


3.2堆正式排序过程的时间复杂度

排序是先第一个结点和最后一个结点交换,交换完后,再对堆顶结点进行向下调整


所以总的时间复杂度为:O(N*logN)


4.堆的应用(Top-K问题)

TOP-K问题:即求数据结合中前K个最大的元素或者最小的元素,一般情况下数据量都比较大
比如:专业前10名、世界500强、富豪榜、游戏中前100的活跃玩家等。
对于Top-K问题,能想到的最简单直接的方式就是排序,但是:如果数据量非常大,排序就不太可取了(可能数据都不能一下子全部加载到内存中)。最佳的方式就是用堆来解决,基本思路如下:
1. 用数据集合中前K个元素来建堆
前k个最大的元素,则建小堆(大的数据会往下沉)
前k个最小的元素,则建大堆(小的数据会往下沉)
2. 用剩余的N-K个元素依次与堆顶元素来比较,不满足则替换堆顶元素
将剩余N-K个元素依次与堆顶元素比完之后,堆中剩余的K个元素就是所求的前K个最小或者最大的元素。
void CreateNDate()
{
	// 造数据
	int n = 10000;
	srand((unsigned int)time(0));
	const char* file = "data.txt";
	FILE* fin = fopen(file, "w");
	if (fin == NULL)
	{
		perror("fopen error");
		return;
	}

	for (size_t i = 0; i < n; ++i)
	{
		int x = rand() % 1000000; // 让数据小于1000000,便于后面测试
		fprintf(fin, "%d\n", x);
	}

	fclose(fin);
}

这个是造数据的代码,数据存入data.txt这个文件中(这个利用文件指针写入数据,看不懂也没太大关系,知道这个函数的作用就行)


// 查找数据中最大的前K个数
void PrintTopK()
{
	int k = 0;
	printf("请输入k值");
	scanf("%d", &k);
	int* a = (int*)malloc(sizeof(int) * k);
	const char* file = "data.txt";
	FILE* fout = fopen(file, "r");
	// 读取文件前K个数据存入a数组中
	for (int i = 0; i < k; i++)
	{
		fscanf(fout, "%d", &a[i]);
	}
	// 现将文件中前k个元素构件成小堆
	for (int i = (k - 1 - 1) / 2; i >= 0; i--)
	{
		AdjustDown(a, k, i);
	}
	// 用剩余的N-K个元素依次与堆顶元素来比较,不满足则替换堆顶元素
	int temp = 0;
	while (fscanf(fout, "%d", &temp) != EOF)
	{
		if (temp > a[0])
		{
			swap(&temp, &a[0]);
		}
		AdjustDown(a, k, 0);
	}

	fclose(fout);

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

我们先调用CreateNDate()函数造数据,运行成功后,按照下面顺序,打开该项目下文件

这些是随机创建的数据,不过都比1000000这个小,于是我们修改5个数据让他们大于1000000以验证程序的准确性

修改这5个元素,修改后要记得保存

然后再调用PrintTopK()函数

1000000数据中最大的5个数,这里没进行排序,要排序就调用一下我们写的HeapSort(int* a, int n)这个堆排序函数。

Top-K问题的时间复杂度

建堆消耗时间K,后面剩余N-K个数据与堆顶进行比较,假设后面剩余的每一个元素都与堆顶进行交换,然后向下调整到完全二叉树的最后一层(向下调整到最后一层时间复杂度为logK(树的高度))

所以时间复杂度为K+ (N - K)*logK

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

wrf228

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

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

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

打赏作者

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

抵扣说明:

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

余额充值