C语言数据结构初阶(11)----堆

· CSDN的uu们,大家好。这里是C语言数据结构的第十讲。
· 目标:前路坎坷,披荆斩棘,扶摇直上。
· 博客主页: @姬如祎
· 收录专栏: 数据结构与算法

 

目录

​编辑 

1. 堆的概念及性质

 2. 函数接口一览

3. 函数接口的实现

3.1 void HeapInit(Heap* pHeap) 的实现

3.2 void HeapDestroy(Heap* pHeap) 的实现

3.3 bool HeapEmpty(Heap* pHeap) 的实现

3.4 void HeapPush(Heap* pHeap, HeapDataType x) 的实现

3.5 void HeapPop(Heap* pHeap) 的实现

 3.6 HeapDataType HeapTop(Heap* pHeap) 的实现

 3.7 int HeapSize(Heap* pHeap) 的实现

4. 堆的应用

4.1 Top-K问题

 4.2 堆排序


1. 堆的概念及性质

堆是具有下列性质的完全二叉树:每个节点的值都大于或等于其左右孩子节点的值,称为:大顶堆(大根堆,大堆)(左图);或者每个节点的值都小于或等于其左右孩子节点的值,称为小顶堆(小根堆,小堆)(右图)。

堆的逻辑结构是一棵完全二叉树,但是他的物理结构是内存空间连续的数组。

 

根据堆这种数据结构的存储结构,我们可以得出父节点与左右孩子之间的下标关系:

先看公式: parent = (child - 1) / 2 (这里的除法为整除) ;

                   leftchild = parent * 2 + 1 ;

                   rightchild = parent * 2 + 2 ;

                   rightchild = leftchild + 1;

如果有不理解的地方,可以画图举例哦!

 我们可以看到 70 这个节点的下标为 0,根据公式:他的左孩子的下标应该为:0 * 2 + 1 = 1

依次类推哈。

 下面是一些有关堆的基础知识的选择题,加深大家对堆这种数据结构的理解。

1.下列关键字序列为堆的是:()
A 100,60,70,50,32,65
B 60,70,65,50,32,100
C 65,100,70,32,50,60
D 70,65,100,32,50,60
E 32,50,100,70,65,60
F 50,100,70,65,60,32
2.已知小根堆为8,15,10,21,34,16,12,删除关键字 8 之后需重建堆,在此过程中,关键字之间的比较次
数是()。
A:1
B:2
C:3
D:4
3.一组记录排序码为(5 11 7 2 3 17),则利用堆排序方法建立的初始堆为
A:(11 5 7 2 3 17)
B:(11 5 7 2 17 3)
C:(17 11 7 2 3 5)
D:(17 11 7 5 3 2)
E:(17 7 11 3 5 2)
F:(17 7 11 3 2 5)
4.最小堆[0,3,2,5,7,4,6,8],在删除堆顶元素0之后,其结果是()
A:[3,2,5,7,4,6,8]
B:[2,3,5,7,4,6,8]
C:[2,3,4,5,7,8,6]
D:[2,3,4,5,6,7,8]

答案:
1.A
2.C
3.C
4.C

 2. 函数接口一览

堆的实现与顺序表的实现差不多,只是增删改查的逻辑不同。这里的实现是以构建大堆为准的哦!

这一点请牢记,非常重要!!!

typedef int HeapDataType;
typedef struct Heap
{
	HeapDataType* a;
	int size;
	int capacity;

} Heap;

//堆的初始化
void HeapInit(Heap* pHeap);

//堆的销毁
void HeapDestroy(Heap* pHeap);

//堆插入数据
void HeapPush(Heap* pHeap, HeapDataType x);

//堆删除数据
void HeapPop(Heap* pHeap);

//查看堆顶的数据
HeapDataType HeapTop(Heap* pHeap);

//判断堆是否为空
bool HeapEmpty(Heap* pHeap);

//堆的大小
int HeapSize(Heap* pHeap);

3. 函数接口的实现

3.1 void HeapInit(Heap* pHeap) 的实现

 该函数用于初始化堆,为堆的实现的数组分配内存空间,并初始化一些变量。我们初始为数组分配四个整型的空间,如果空间不够了,我们再选择扩容。扩容的大小为原 capacity 的两倍。

//堆的初始化
void HeapInit(Heap* pHeap)
{
	assert(pHeap);
	HeapDataType* tmp = (HeapDataType*)malloc(sizeof(HeapDataType) * 4);
	if (tmp == NULL)
	{
		perror("HeapInit::malloc");
		exit(-1);
	}
	else
	{
		pHeap->a = tmp;
		pHeap->capacity = 4;
		pHeap->size = 0;
	}
}

3.2 void HeapDestroy(Heap* pHeap) 的实现

  这个函数用于销毁我们在内存的堆区上申请的空间,即堆的实现所需的数组。

//堆的销毁
void HeapDestroy(Heap* pHeap)
{
	assert(pHeap);
	free(pHeap->a);
	pHeap->a = NULL;
}

3.3 bool HeapEmpty(Heap* pHeap) 的实现

这个函数用来判断堆是否为空。注意到我们的 Heap 结构体中维护了 size 这个变量。因此我们只需要判断 size 是否为 0 即可。

//判断堆是否为空
bool HeapEmpty(Heap* pHeap)
{
	assert(pHeap);
	return pHeap->size == 0;
}

3.4 void HeapPush(Heap* pHeap, HeapDataType x) 的实现

这个函数用于向堆中插入数据。插入数据的逻辑是:向数组的末尾插入数据,然后用向上调整的算法将整个数组重新调整为堆。

如下图:我们在 [ 70, 56, 30, 25, 15, 10 ] 的大堆中插入 90 这个元素。

 

向上调整的步骤为:将插入元素与他的父节点进行比较,如果插入的元素大于父节点交换两个元素,直到与根结点完成比较,或者插入的元素小于他的父节点,退出循环。

还有一点值得注意的是:插入数据时我们首先得判断堆的大小与容量之间的关系,如果堆满了,需要扩容。

void Swap(int* a, int* b)
{
	int tmp = *a;
	*a = *b;
	*b = tmp;
}

//向上调整,child表示要向上调整的元素的下标
void AdjustUp(int* a, int child)
{
	//根据公式找到父节点下标
	int parent = (child - 1) / 2;
	//child == 0 表明已经与根结点完成比较,所以循环条件为child > 0
	while (child > 0)
	{
		//如果插入的元素大于父节点,交换
		if (a[child] > a[parent])
		{
			Swap(&a[child], &a[parent]);
			child = parent;
			parent = (child - 1) / 2;
		}
		else //否则的话,退出循环,完成建大堆
		{
			break;
		}
	}
}

//堆插入数据
void HeapPush(Heap* pHeap, HeapDataType x)
{
	assert(pHeap);
	//堆是否满了
	if (pHeap->size == pHeap->capacity)
	{
		HeapDataType* tmp = (HeapDataType*)realloc(pHeap->a, sizeof(HeapDataType) * pHeap->capacity * 2);
		if (tmp == NULL)
		{
			perror("HeapPush::realloc");
			exit(-1);
		}
		else
		{
			pHeap->a = tmp;
			pHeap->capacity *= 2;
		}
	}
	//向数组末端插入数据
	pHeap->a[pHeap->size++] = x;
	//向下调整
	AdjustUp(pHeap->a, pHeap->size - 1);
}

3.5 void HeapPop(Heap* pHeap) 的实现

这个函数用来删除堆顶数据,删除的方法就是:将堆顶数据与数组最后面的那个数据进行交换,然后将堆顶数据进行向下调整算法即可。

如下图,我们想要删除堆顶的 70 这个元素。

 删除的方法是:我们将堆顶的 70 与数组末尾的 10 进行交换,然后对堆顶的数据(下标为0的元素,父节点)进行向下调整算法。根据公式找到下标为 0 的节点(父节点),的左右孩子中的那个较大的元素。如果下标为 0 的这个元素(父节点),小于左右孩子中较大的那个元素,交换他们的值。直到与叶子节点完成比较(交换)。如果比较的过程中父节点均大于他的左右孩子,那么结束循环即可。

//n表示数组的大小(堆的节点个数),parent 表示要向下调整的元素的下标
void AdjustDown(int* a, int n, int parent)
{
	//假设左孩子是父节点的左右孩子较大的
	int child = parent * 2 + 1;
	while (child < n)
	{
		//如果右孩子存在,并且右孩子比左孩子大,假设错误,进行纠正
		if (child + 1 < n && a[child + 1] > a[child])
			child++;
		//如果比父节点大,交换并更新parent,child
		if (a[child] > a[parent])
		{
			Swap(&a[child], &a[parent]);
			parent = child;
			child = parent * 2 + 1;
		}
		else //否则退出循环,重新构建大堆完成
		{
			break;
		}
	}
}

//删除堆顶数据
void HeapPop(Heap* pHeap)
{
	assert(pHeap);
	assert(!HeapEmpty(pHeap));
	Swap(&pHeap->a[0], &pHeap->a[pHeap->size - 1]);
	pHeap->size--;
	AdjustDown(pHeap->a, pHeap->size, 0);
}

 3.6 HeapDataType HeapTop(Heap* pHeap) 的实现

这个函数用于查看堆顶的元素,根据堆的物理结构与逻辑结构的关系,在堆不为空的前提下我们只要返回数组中下标为 0 的的元素即可。

//查看堆顶的数据
HeapDataType HeapTop(Heap* pHeap)
{
	assert(pHeap);
	assert(!HeapEmpty(pHeap));
	return pHeap->a[0];
}

 3.7 int HeapSize(Heap* pHeap) 的实现

这个函数用于返回堆的元素个数,注意到我们在结构体中维护了一个变量size,显然,我们只需要返回size即可。

//堆的大小
int HeapSize(Heap* pHeap)
{
	assert(pHeap);
	return pHeap->size;
}

4. 堆的应用

4.1 Top-K问题

TOP-K问题:即求数据集合中前 K 个最大的元素或者最小的元素,一般情况下数据量都比较大。
比如:专业前10名、世界500强、富豪榜、游戏中前100的活跃玩家等。
对于Top-K问题,能想到的最简单直接的方式就是排序,但是:如果数据量非常大,排序就不太可取了(可能数据都不能一下子全部加载到内存中)。最佳的方式就是用堆来解决,基本思路如下:

1. 用数据集合中前K个元素来建堆
        前k个最大的元素,则建小堆
        前k个最小的元素,则建大堆
2. 用剩余的 N - K 个元素依次与堆顶元素来比较,满足一定条件则替换堆顶元素。
将剩余 N - K 个元素依次与堆顶元素比完之后,堆中剩余的K个元素就是所求的前K个最小或者最大的元素。

例题:我们随机生成10万个(或者更多)整型数据,将其保存在文件中(因为如果数据量太大内存无法保存这么多数据),然后我们要从中选出前十大的数据。

思路分析:

①:我们利用 rand() 函数生成 10 万个堆积数,将生成的随机数对1万取模,方便我们进行数据的精确选择(取模的原因后面会提到)。

②:将生成的数据逐个保存到文件中,生成完毕后我们将文件中的数据修改10个,要求修改后的值大于1万,这样我们就能够很轻松的验证我们的 Top-10 是否正确了。这里就是随机数对1万取模的原因。

③:然后我们开始读取文件,取前十个数据,构建一个小堆,为什么是小堆呢?因为小堆的堆顶元素是堆中数据最小的那个,一个数想要排进前十,那么你首先得比前十中第十名的那个数大吧。通俗的理解:一个房子里面有十个美女,而看门的是这十个美女中排名第十的那个,你想要进入这个房子的前提就是要比看门的那个美女漂亮。堆顶元素就相当于是一个门槛。

④:继续读取文件,直到将文件读完,我们就选出了全部数据的Top-10。

注意点:

我们的例题是选出前十大的元素因此需要构建小堆,上面的堆实现我们构建的是大堆。不可混用。

改文件的数据可以打断点,将文件的数据改出10个大与等于1万的即可。

//n表示数组的大小(堆的节点个数),parent 表示要向下调整的元素的下标
//构建小堆
void AdjustDown(int* a, int n, int parent)
{
	//假设左孩子是父节点的左右孩子较小的
	int child = parent * 2 + 1;
	while (child < n)
	{
		//如果右孩子存在,并且右孩子比左孩子小,假设错误,进行纠正
		if (child + 1 < n && a[child + 1] < a[child])
			child++;
		//如果比父节点小,交换并更新parent,child
		if (a[child] < a[parent])
		{
			Swap(&a[child], &a[parent]);
			parent = child;
			child = parent * 2 + 1;
		}
		else //否则退出循环,重新构建小堆完成
		{
			break;
		}
	}
}

//向上调整,构建小堆
void AdjustUp(int* a, int child)
{
	//根据公式找到父节点下标
	int parent = (child - 1) / 2;
	//child == 0 表明已经与根结点完成比较,所以循环条件为child > 0
	while (child > 0)
	{
		//如果插入的元素小于父节点,交换
		if (a[child] < a[parent])
		{
			Swap(&a[child], &a[parent]);
			child = parent;
			parent = (child - 1) / 2;
		}
		else //否则的话,退出循环,完成建小堆
		{
			break;
		}
	}
}

//将Top-K的数据打印
void PrintTopK(int* a, int n)
{
	for (int i = 0; i < n; i++)
	{
		printf("%d ", a[i]);
	}
	printf("\n");
}

//随机生成count个数据到文件
void GenerateNum(const char* file, int count)
{
	FILE* pf = fopen(file, "w");
	if (pf == NULL)
	{
		perror("GenerateNum::fopen");
		exit(-1);
	}
	else
	{
		for (int i = 0; i < count; i++)
		{
			int randNum = rand() % 10000; //对生成的随机数对1完取模,方便后续的操作
			fprintf(pf, "%d\n", randNum); //输出到文件
		}
		fclose(pf);
		pf = NULL;
	}
}

//求文件中数据的前k大的元素
void TopK(const char* file, int k)
{
	FILE* pf = fopen(file, "r");
	if (pf == NULL)
	{
		perror("TopK::fopen");
		exit(-1);
	}
	else
	{
		int* topK = (int*)malloc(sizeof(int) * k);
		if (topK == NULL)
		{
			perror("TopK::malloc");
			fclose(pf);
			pf = NULL;
			exit(-1);
		}
		else
		{
			int num;
			for (int i = 0; i < k; i++)
			{
				fscanf(pf, "%d", &topK[i]); //先从文件中读取k个数据构建小堆
				AdjustUp(topK, i); //这里使用向上调整构建小堆
			}
			while (fscanf(pf, "%d", &num) != EOF) //读取文件中的剩余数据
			{
				if (num > topK[0]) //如果该数据比堆顶元素大,那么替换堆顶元素,向下调整
				{
					topK[0] = num;
					AdjustDown(topK, k, 0);
				}
			}
			PrintTopK(topK, k); //打印数据
			fclose(pf);
			pf = NULL;
		}
	}
}

int main()
{
	//设置随机数的种子
	srand((unsigned int)time(NULL));
	//文件名以及数据的总量
	const char* fileName = "data.txt";
	const int N = 100000;
	//生成数据到问价
	GenerateNum(fileName, N);
	//选出Top-K的数据
	TopK(fileName, 10);

	system("pause");
	return 0;
}

 4.2 堆排序

堆排序相关请戳我->-> 堆排序详解

 

  • 7
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

姬如祎

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

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

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

打赏作者

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

抵扣说明:

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

余额充值