数据结构之堆和堆排序(C语言版)

前言

各位读者朋友们大家好!这篇博客我们来讲一下数据结构的堆和堆排序以及代码的实现。

一. 堆的概念

堆是一种满足特定条件的完全二叉树,主要可分为两种类型:大顶堆和小顶堆。

  • 大顶堆: 父亲节点的值 >= 子节点的值。
  • 小顶堆: 父亲节点的值 <= 子节点的值。

在这里插入图片描述
堆还有以下性质:

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

二. 堆的实现

这里我们建大堆,如果要建小堆只需要调整修改大小逻辑判断(例如将<改为>)。

2.1 堆的存储与表示

因为堆总是一棵完全二叉树,所以我们用数组来存储堆会更加方便。当我们使用数组来实现堆的时候,很容易通过下标来找到某个节点的父亲节点和子节点。
在这里插入图片描述

  • 父亲节点:(i - 1) / 2
  • 子节点:2i + 1(左孩子节点)2i + 2(右孩子节点)

2.2 元素入堆

给定元素 val ,我们首先将其添加到堆底。添加之后,由于 val 可能大于堆中其他元素,堆的成立条件可能已被破坏,因此需要修复从插入节点到根节点的路径上的各个节点。
在这里插入图片描述
在对堆进行修复的时候,我们需要用到向上调整算法,即将比父亲节点大的节点的元素换到父亲节点的位置,父亲节点的元素跟该子节点交换。

向上调整算法
void AdjustUp(HPDataType* a, int child)
{
	int parent = (child - 1) / 2;
	while (child > 0)
	{
		if (a[parent] < a[child])
		{
			Swap(&a[parent], &a[child]);
			child = parent;
			parent = (child - 1) / 2;
		}
		else
		{
			break;
		}
	}
}

在这里插入图片描述

2.3 删除堆顶元素

堆顶元素是二叉树的根节点,即数组首元素。如果我们直接从数组中删除首元素,那么堆的所有节点的索引都会发生变化,这将使得后续进行修复变得困难。为了尽量减少元素索引的变动,我们将堆顶元素与最后一个元素进行交换,然后删除最后一个元素,再进行堆的调整。
在这里插入图片描述
这里需要使用向下调整算法,我们在删除堆顶数据的时候,并没有破坏左子树和右子树的堆的属性,因此我们在向下调整时,只需比较根节点的值与左右子节点的值,如果小于子节点的值只需与子节点中较大的一个进行交换,直到调整到叶子节点即可。

向下调整算法
//parent是父亲节点的下标,n是数组的元素个数
void AdjustDown(HPDataType* a, int parent, int n)
{
	int child = 2 * parent + 1;
	while (child < n)//child>=n就说明到叶子节点了
	{
		//找左右孩子中较大的一个
		if (child + 1 < n && a[child] < a[child + 1])//防止越界
		{
			++child;
		}
		if (a[parent] < a[child])
		{
			Swap(&a[parent], &a[child]);
			parent = child;
			child = parent * 2 + 1;
		}
		else
		{
			break;
		}
	}
}

在这里插入图片描述
在这里插入图片描述

三. 建堆算法

3.1 向上调整建堆

将数组的第一个元素作为堆顶元素,随后将数组的后续元素依次进行向上调整建堆。

void HPCreat(HPDataType* a, int n)
{
	for (int i = 1; i < n; i++)
	{
		AdjustUp(a, i);
	}
}

要将下面数组中的元素建一个大堆按照上面的算法,步骤如下:
在这里插入图片描述

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

根据上面图示分析,当插入一个节点时,要向上调整层数次,一共有N个节点,因此向上调整建堆的时间复杂度是:O(Nlog N)

3.2 向下调整建堆

在向下调整算法中,左子树和右子树都是大(小)堆,我们调整的时候,只需直接向下调整即可,但是在建堆的过程中我们不能保证左右子树都是大(小)堆,在这种情况下就引出了向下调整建堆算法。我们要使每一个父亲节点的左右子树都是大(小)堆,所以我们在最后一个非叶子结点的位置向下调整,这样就可以保证在进行向下调整时,左右子树都是大(小)堆了。

在这里插入图片描述

void HPCreat(HPDataType* a, int n)
{
	for (int i = (n - 1 - 1) / 2; i >= 0; i--) //n - 1是最后一个子节点
	{
		AdjustDown(a, i, n);
	}
}

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

在这里插入图片描述
向下调整建堆的时间复杂度:O(n)

所以在实际应用中,我们使用向下调整建堆。

四. 堆排序

  • 排升序,建大堆
  • 排降序,建小堆
    堆排序的思想是将最大(小)的元素调整到堆顶,然后将其换到数组末尾对数组的元素个数-1(忽略换过去的所有元素),再继续进行向下调整,将次大(小)的元素调整到堆顶。
void HeapSort(HPDataType* a, int n)
{
	//对数组进行建堆
	for (int i = (n - 1 - 1) / 2; i >= 0; i--) 
	{
		AdjustDown(a, i, n);
	}
	int end = n - 1;
	while (end > 0)
	{
		Swap(&a[0], &a[end]);
		AdjustDown(a, 0, end);//这里的end是新数组的元素个数(原数组元素个数-1),
		//即忽略了换到后面的元素
		end--;
	}
}

4.1 堆排序的时间复杂度

在这里插入图片描述

时间复杂度:O(NlogN)

五. TOP-K问题

我们可以基于堆更加高效地解决 Top-k 问题:

  • 1.初始化一个小堆,其堆顶元素最小。
  • 2.先将数组的前 k 个元素依次入堆。
  • 3.从第 k 个元素开始,若当前元素大于堆顶元素,则将堆顶元素出堆,并将当前元素入堆。
  • 4.遍历完成后,堆中保存的就是最大的 k 个元素。
    我们基于文件操作来实现TOP-K问题:
#include<stdio.h>
#include<time.h>
#include<stdlib.h>
void CreateNDate()
{
	// 造数据
	int n = 100000;
	srand(time(0));
	const char* file = "data.txt";
	FILE* fin = fopen(file, "w");
	if (fin == NULL)
	{
		perror("fopen error");
		return;
	}
	for (int i = 0; i < n; ++i)
	{
		int x = rand() % 10000000;
		fprintf(fin, "%d\n", x);
	}
	fclose(fin);
}

void AdjustDown(int* arr, int n, int src)
{
	int child = 2 * src + 1;
	while (child < n)
	{
		if (child + 1 < n && arr[child] > arr[child + 1])
		{
			child++;
		}
		if (arr[child] < arr[src])
		{
			int tmp = arr[child];
			arr[child] = arr[src];
			arr[src] = tmp;
			src = child;
			child = src * 2 + 1;
		}
		else
		{
			break;
		}
	}
}
void PrintTopK()
{
	int k = 0;
	scanf("%d", &k);
	int* kminheap = (int*)malloc(sizeof(int) * k);
	if (kminheap == NULL)
	{
		perror("malloc fail");
		return;
	}
	const char* file = "data.txt";
	FILE* fout = fopen(file, "r");
	if (fout == NULL)
	{
		perror("fopen error");
		return;
	}
	// 读取文件中前k个数
	for (int i = 0; i < k; i++)
	{
		fscanf(fout, "%d", &kminheap[i]);
	}
	//建小堆
	for (int i = (k - 1 - 1) / 2; i >= 0; i--)
	{
		AdjustDown(kminheap, k, i);
	}
	int x = 0;
	while (fscanf(fout, "%d", &x) > 0)
	{
		if (x > kminheap[0])
		{
			kminheap[0] = x;
			AdjustDown(kminheap, k, 0);
		}
	}
	for (int i = 0; i < k; i++)
	{
		printf("%d ", kminheap[i]);
	}
}

结语

以上我们就讲完了堆的基本内容,希望对大家有所帮助,欢迎大家批评指正!

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值