【数据结构初阶】之堆(C语言实现)

这篇具有很好参考价值的文章主要介绍了【数据结构初阶】之堆(C语言实现)。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

前言:在二叉树基础篇我们提到了二叉树的顺序实现,今天让我们来学习一下特殊的二叉树———堆的相关知识。

【数据结构初阶】之堆(C语言实现),数据结构与算法,数据结构,c语言,开发语言

📃博客主页: 小镇敲码人
💞热门专栏:数据结构与算法
🚀 欢迎关注:👍点赞 👂🏽留言 😍收藏
🌏 任尔江湖满血骨,我自踏雪寻梅香。 万千浮云遮碧月,独傲天下百坚强。 男儿应有龙腾志,盖世一意转洪荒。 莫使此生无痕度,终归人间一捧黄。🍎🍎🍎
❤️ 什么?你问我答案,少年你看,下一个十年又来了 💞 💞 💞

🌏 堆的概念

堆是一种完全二叉树,但是其节点值满足一些特定的规则,又分为小堆和大堆,小堆是任意根节点的值都小于等于它子树的节点值,大堆是任意根节点的值都大于等于它子树的值。

小堆:

【数据结构初阶】之堆(C语言实现),数据结构与算法,数据结构,c语言,开发语言

大堆:

【数据结构初阶】之堆(C语言实现),数据结构与算法,数据结构,c语言,开发语言

注意:不管是大堆还是小堆,其同一层的节点大小可任意。

🌏 堆的模拟实现

🐓 堆的结构和方法接口

在二叉树部分我们曾经提到,堆是一种完全二叉树,所以其使用顺序存储是不会浪费空间的,而顺序存储就是我们常说的动态顺序表。

#define  _CRT_SECURE_NO_WARNINGS 1
#include<stdbool.h>
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
#include<string.h>

typedef int HPDataType;
typedef struct Heap
{
	HPDataType* _a;
	int _size;
	int _capacity;
}Heap;

//堆的初始化
void HeapInit(Heap* hp);
// 堆的构建(直接给一个数组)
void HeapCreate(Heap* hp, HPDataType* a, int n);
// 堆的销毁
void HeapDestory(Heap* hp);
// 堆的插入
void HeapPush(Heap* hp, HPDataType x);
// 堆的删除
void HeapPop(Heap* hp);
// 取堆顶的数据
//HPDataType HeapTop(Heap* hp);
// 堆的数据个数
int HeapSize(Heap* hp);
// 堆的判空
int HeapEmpty(Heap* hp);
//向上调整建堆
void AdjustUp(HPDataType* a, int child);
//向下调整
void AdjustDown(HPDataType* a, int parent, int n);
//两数交换
void Swap(HPDataType* hp1, HPDataType* hp2);
//打印堆
void HeapPrint(Heap* hp);
//堆排序
void Heapsort(HPDataType* a, int n);

🐓 堆的方法的模拟实现

下面我们的方法我们都默认建一个小堆。

🙊 堆的初始化

堆的初始化很简单,我们将其对应的指针置空,元素个数和空间大小置0即可。

//堆的初始化
void HeapInit(Heap* hp)
{
	assert(hp);//hp不为空

	hp->_a = NULL;//指针置为空
	hp->_capacity = hp->_size = 0;//元素和空间大小置为0
}

🙊 堆的构建

给你一个数组,你应该如何建一个小堆呢?我们来画图分析:

// 堆的构建
void HeapCreate(Heap* hp, HPDataType* a, int n)
{
	assert(hp);//断言,防止hp为空

	hp->_a = (HPDataType*)malloc(sizeof(HPDataType) * n);//为存节点的数组申请空间
	hp->_size = hp->_capacity = n;//更新空间大小和元素大小

	memcpy(hp->_a, a, sizeof(HPDataType) * n);//将数组里的值copy到我们的数据域里面

	//向上调整建一个小堆
	for (int i = 1; i < n; i++)
	{
		AdjustUp(hp->_a,i);
	}
}

图解代码:
【数据结构初阶】之堆(C语言实现),数据结构与算法,数据结构,c语言,开发语言

向下调整建堆的时间复杂度分析:

【数据结构初阶】之堆(C语言实现),数据结构与算法,数据结构,c语言,开发语言

🙊 堆的插入

堆的插入应该如何实现呢?我们直接插入到线性表尾部就行,由于插入前是堆,所以我们走一个向上调整就可以将其调整为堆。

// 堆的插入
void HeapPush(Heap* hp, HPDataType x)
{
	assert(hp);//断言,防止hp为空

	if (hp->_size == hp->_capacity)//扩容
	{
		hp->_capacity = hp->_capacity == 0 ? 4 : hp->_capacity * 2;
		HPDataType* tmp = (HPDataType*)realloc(hp->_a,sizeof(HPDataType) * hp->_capacity);
		if (tmp == NULL)
		{
			perror("realloc failed\n");
			exit(-1);
		}
		hp->_a = tmp;
	}
	hp->_a[hp->_size] = x;//插入
	hp->_size++;

	//向上调整
	AdjustUp(hp->_a,hp->_size - 1);//在插入位置走一个向上调整
}

🙊 向上调整

上面已经分析过了。

//向上调整建小堆
void AdjustUp(HPDataType* a, int child)
{
	assert(a);//a不为空

	int parent = (child - 1) / 2;//找到child的父亲节点,和其比较看是否需要调整
	while (child > 0)
	{
		if (a[child] < a[parent])//建一个小堆,如果孩子节点比父亲节点的值小,需要调整
		{
			Swap(&a[child], &a[parent]);//交换
			child = parent;//将父亲节点的值给孩子,继续在这条路径上调整
			parent = (parent - 1) / 2;//更新父亲节点的下标
		}
		else
		{
			break;//孩子节点的值已经大于等于父亲节点的值了,不需要再继续调整
		}
	}
}

🙊 堆的删除

堆的删除我们是删除堆顶的数据,因为这个数据最值得去删,什么意思呢?举个例子1.因为删除最后一个节点不会改变堆的结构没什么意义,2.堆顶数据要么是最大值,要么是最小值,很特殊。

那我们应该如何实现堆的删除呢?我们还是画图来分析:

【数据结构初阶】之堆(C语言实现),数据结构与算法,数据结构,c语言,开发语言
代码实现:

// 堆的删除
void HeapPop(Heap* hp)
{
	assert(hp);//hp不为空
	assert(hp->_size > 0);//堆的元素个数要大于0
    
	Swap(&hp->_a[0], &hp->_a[hp->_size - 1]);//交换堆顶元素和最后一个元素的值
	hp->_size--;//_size--

	//向下调整
	AdjustDown(hp->_a, 0, hp->_size);//根节点左树和右树为小堆,可以向下调整
}

🙊 向下调整

这个我们上面也已经分析了,直接给代码:

//向下调整(小堆)
void AdjustDown(HPDataType* a, int parent, int n)
{
	assert(a);//a不为空
	int child = parent * 2 + 1;//先假设最小的孩子是左孩子
	while (child < n)
	{
		//找出最小的孩子
		if (child+1 < n && a[child] > a[child + 1])//如果右孩子存在且右孩子的值比左孩子更小
		{
			child = child + 1;
		}

		if (a[child] < a[parent])//最小的孩子比parent的值小,就交换
		{
			Swap(&a[child], &a[parent]);
			parent = child;//更新parent
			child = parent * 2 + 1;//更新child,也假设最小的孩子是左孩子
		}
		else//最小的孩子比父亲节点的值大,已经不需要调整,是小堆了。
		{
			break;
		}
	}
}

🙊 堆的数据的个数

直接返回_size的值。

// 堆的数据个数
int HeapSize(Heap* hp)
{
	assert(hp);//hp不为空

	return hp->_size;
}

🙊 堆的判空

如果_size不为0,堆就不为空。

// 堆的判空
int HeapEmpty(Heap* hp)
{
	assert(hp);

	if (hp->_size == 0)//为空返回1
		return 1;
	else//不为空返回0
		return 0;
}

🙊 堆顶元素

堆顶元素在下标为0的位置。

// 取堆顶的数据
HPDataType HeapTop(Heap* hp)
{
	assert(hp);
	assert(hp->_size > 0);

	return hp->_a[0];
}

🙊 堆的销毁

堆的销毁实际上主要要做的就是回收在堆上开的空间,将相应的指针置为空,空间和元素大小置为0。

// 堆的销毁
void HeapDestory(Heap* hp)
{
	assert(hp);//hp不为空
	assert(hp->_size > 0);//元素个数要大于0

	free(hp->_a);//释放空间
	hp->_a = NULL;//将动态数组的指针置为空
	hp->_capacity = hp->_size = 0;//将空间和元素大小置为0
}

🙊 堆排序

由于小堆和大堆的堆顶元素都是最小或者最大的,所以我们pop堆顶元素,然后再剩下的元素调整,就能得到一个有序的序列。

🌺 版本1 创建堆,通过push、pop进行操作 (异地操作)

那我们该如何实现这个堆排序呢?下面的代码可以吗为什么?

void Heapsort(HPDataType* a, int n)
{
	Heap hp1;//假设我们建一个小堆
	HeapInit(&hp1);
	for (int i = 0; i < n; ++i)
	{
		HeapPush(&hp1, a[i]);//插入对应元素
	}

	for (int i = 0; i < n; ++i)
	{
		a[i] = HeapTop(&hp1);//将最小的元素依次给数组
		HeapPop(&hp1);//pop堆顶元素
	}
}

效果演示:

【数据结构初阶】之堆(C语言实现),数据结构与算法,数据结构,c语言,开发语言

这种方法是不太好的,因为先暂且不谈时间复杂度的问题,我们为了去给数组排一个序,而建了一个堆,这个空间开销是没必要的,其实我们可以对数组原地建堆。

🌺 版本2 向上调整建堆(原地操作)
//堆排序,向上调整
void Heapsort2(HPDataType* a, int n)
{
	assert(a);


	for (int i = 1; i < n; i++)//我们向上调整,原地建一个小堆
	{
		AdjustUp(a,i);
	}

	for (int end = n - 1; end >= 0; --end)
	{
		Swap(&a[0], &a[end]);//将堆顶元素放到堆最后面去
		AdjustDown(a, 0, end);//此时的end就代表我们的元素个数
	}
}

原地建堆,如果你想排降序,就需要建一个小堆,因为小堆的堆顶元素是最小的,我们原地操作,就可以将最小的和最后一个元素交换,这样没有破坏原来的结构,走一个向下调整就可以恢复堆结构。

【数据结构初阶】之堆(C语言实现),数据结构与算法,数据结构,c语言,开发语言

时间复杂度:NlogN,向上调整建堆:NlogN,向下调整调整一次logN(一共调整了N次),所以总体的时间复杂度也是O(N*logN)。

🌺 版本3----向下调整建堆(原地操作)

但是这样就结束了吗,我们发现如果使用上面这种方法,既要写一个向上调整,也要写一个向下调整,我只是想排个序,需要这么复杂吗,难道就不能直接向下调整建堆吗?这样我就不用写两个了呀,调整那块肯定要用向下调整的,不然就破坏堆的结构了。

下面我们来介绍向下调整建堆

【数据结构初阶】之堆(C语言实现),数据结构与算法,数据结构,c语言,开发语言
代码实现:

//堆排序,向下调整
void Heapsort3(HPDataType* a, int n)
{
	assert(a);


	for (int i = (n-1-1)/2; i >= 0; i--)//我们向上调整,原地建一个小堆
	{
		AdjustDown(a,i,n);
	}

	for (int end = n - 1; end >= 0; --end)
	{
		Swap(&a[0], &a[end]);//将堆顶元素放到堆最后面去
		AdjustDown(a, 0, end);//此时的end就代表我们的元素个数
	}
}

运行结果:

【数据结构初阶】之堆(C语言实现),数据结构与算法,数据结构,c语言,开发语言

🍀 向下调整建堆的时间复杂度分析

【数据结构初阶】之堆(C语言实现),数据结构与算法,数据结构,c语言,开发语言

整体这种堆排序的时间复杂度也是O(N*logN),建堆的消耗是O(N),但是每一次调整的消耗还是logN,调整了次,量级还是和第二种一样,但是向下调整建堆只需要写一个向下调整,且向下调整建堆比向上调整建堆要更快。

🙊 topK问题

topK问题指的是让我们求第K大,第K小等问题,思考一下,在一个乱序的数组里面,如果我们想要这样来做,我们常规的解决办法是什么?排序!!!这样来做的时间复杂度是N*logN,我们只是求第K大或者第K小的数,有必要把这些数全部排一遍顺序吗,答案是不用,下面让我们来学习一下使用堆这种数据结构来解决经典的topK问题。

🌺 topK问题的分析

下面我们画图来分析一下topk问题具体的解决之道。

【数据结构初阶】之堆(C语言实现),数据结构与算法,数据结构,c语言,开发语言

🌺 topK问题的代码实现

我们使用C语言的文件操作,造了100w个小于100w的数据(随机值),然后我们求前5个大的数,我们这样来验证,把文件里任意5个数改为大于100w的数,如果最后打印出来是我们改的5个数,证明我们的topk问题得到解决。

【数据结构初阶】之堆(C语言实现),数据结构与算法,数据结构,c语言,开发语言

可以看到我们改了最后5个数,修改之后把创建数据的程序注释掉,防止它重新生成,我们的修改就没意义了。

代码实现:

void CreateNDate()
{
	// 造数据
	int n = 1000000;
	srand(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;//造随机数据,可以让我们的测试程序得到更好的检查
		fprintf(fin, "%d\n", x);//将数据放入文件
	}

	fclose(fin);
}

void PrintTopK(int k)
{
	const char* file = "data.txt";//定义文件
	FILE* flout = fopen(file, "r");
	if (flout == NULL)
	{
		perror("fopen failed");
		exit(-1);
	}

	//开一个数组存放k个数据,开一个小堆
	HPDataType* minheap = (HPDataType*)malloc(sizeof(HPDataType) * k);
	if (minheap == NULL)
	{
		perror("malloc failed");
		exit(-1);
	}
	for (int i = 0; i < k; i++)//将k个数放入数组中
	{
		fscanf(flout, "%d", &minheap[i]);
	}

	//向下调整建堆
	for (int i = (k - 1 - 1) / 2; i >= 0; i--)
	{
		AdjustDown(minheap, i, k);
    }
	//遍历,如果大于小堆堆顶的数据的话,就和它换,并向下调整
	int x = 0;
	while (fscanf(flout, "%d", &x) != EOF)
	{
		if (x > minheap[0])
		{
			Swap(&x, &minheap[0]);
			AdjustDown(minheap, 0,k);
		}
	}
	for (int i = 0; i < k; i++)//打印前k大的数
	{
		printf("%d ", minheap[i]);
	}
	fclose(flout);//关闭文件
}
int main()
{
	CreateNDate();//造10w个数据
	PrintTopK(5);
	return 0;
}

运行结果:

【数据结构初阶】之堆(C语言实现),数据结构与算法,数据结构,c语言,开发语言

可以看到预期结果和我们修改的数是一致的,说明我们的代码应该没啥问题。

🌺 top问题的时间复杂度和空间复杂度分析

【数据结构初阶】之堆(C语言实现),数据结构与算法,数据结构,c语言,开发语言

🌏 测试程序

下面我们来测试一下我们写的上述方法的正确性如何。

🐓 打印堆

我们可能需要把堆打印出来看结果。

//打印堆
void HeapPrint(Heap* hp)
{
	assert(hp);
	assert(hp->_size > 0);

	for (int i = 0; i < hp->_size; i++)
	{
		printf("%d ", hp->_a[i]);
	}
	printf("\n");
}

🐓 验证是否是一个堆

我们应该如何验证我们的数据是否是一个小堆呢?其实很简单,数组是顺序存储的,我们的数据一定是完全二叉树,这个不用验证,关键是验证我们的数据是否都满足小堆的定义,即所有的根节点的值都比它的左子树的和右子树的根节点的值要小,我们可以走一个前序遍历来验证。

//验证是否是一个小堆
int is_Heap(HPDataType* a, int n,int root)
{
	if (root >= n)
		return 1;
	int leftchild = root * 2 + 1;//左孩子的下标
	int rightchild = root * 2 + 2;//右孩子的下标
	if (leftchild < n && a[root] > a[leftchild])//左孩子存在且小于根,返回0,并打印相关信息
	{
		printf("%d\n位置不满足小堆", root);
		return 0;
	}
	if (rightchild < n && a[root] > a[rightchild])//右孩子存在且小于根,返回0,并打印相关信息
	{
		printf("%d\n位置不满足小堆", root);
		return 0;
	}
	return is_Heap(a, n, leftchild) && is_Heap(a, n, rightchild);//左树和右树都满足小堆就返回1
}

🐓 测试程序

void Test2()
{
	Heap hp1;//建堆
	HeapInit(&hp1);//初始化堆
	//造10w个数据进堆
	int N = 100000;
	srand(time(NULL));//通过时间来初始化随机数种子
	for (int i = 0; i < N; ++i)
	{
		HeapPush(&hp1, rand() % (10000000));//将数据存入堆里面
	}
	int i = is_Heap(hp1._a,N,0);//验证是否为堆
	if (i)//打印提示信息
		printf("是堆\n");
	else
		printf("不是堆\n");

	
	printf("堆的元素个数为%d\n", HeapSize(&hp1));//验证元素个数方法是否正确
	printf("堆顶元素为:%d\n", HeapTop(&hp1));//验证堆顶元素是否正确
	HeapPop(&hp1);//验证pop函数是否正确
	printf("堆的元素个数为%d\n", HeapSize(&hp1));
	printf("堆顶元素为:%d\n", HeapTop(&hp1));
	hp1._a[3] = -1;//破坏堆的结构
	i = is_Heap(hp1._a, N, 0);
	if (i)
		printf("是堆\n");
	else
		printf("不是堆\n");
	HeapDestory(&hp1);
	if (hp1._a == NULL)
		printf("销毁成功\n");
}

运行结果:

【数据结构初阶】之堆(C语言实现),数据结构与算法,数据结构,c语言,开发语言文章来源地址https://www.toymoban.com/news/detail-844821.html

到了这里,关于【数据结构初阶】之堆(C语言实现)的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 第11章:C语言数据结构与算法初阶之排序

    排序是一种非常重要的算法。 排序 :所谓排序,就是使一串记录,按照其中的某个或某些的大小,递增或递减的排列起来的操作。 稳定性 :假定在待排序的记录序列中,存在多个具有相同的的记录,若经过排序,这些记录的相对次序保持不变,即在原序列中,

    2024年02月12日
    浏览(47)
  • 『初阶数据结构 • C语言』③ - 算法分析专业工具——大O记法

    本文内容借鉴一本我非常喜欢的书——《数据结构与算法图解》。学习之余,我决定把这本书精彩的部分摘录出来与大家分享。        从之前的章节中我们了解到,影响算法性能的主要因素是其所需的步数。 然而,我们不能简单地把一个算法记为“22步算法”,把另一个算

    2024年02月16日
    浏览(48)
  • 数据结构初阶(用C语言实现简单数据结构)--栈和队列

    ✨✨欢迎来到T_X_Parallel的博客!!       🛰️博客主页:T_X_Parallel       🛰️专栏 : 数据结构初阶       🛰️欢迎关注:👍点赞🙌收藏✍️留言 这小猫真好看 言归正传,通过上篇有关顺序表和链表的博客,可以了解到线性表的一些大致特征,这篇博

    2024年02月08日
    浏览(41)
  • 数据结构之堆的结构与实现

    目录 一、堆的概念及结构 1.1堆的概念  1.2堆的性质 1.3堆的结构 二、堆的实现 2.1堆向下调整算法(父亲与孩子做比较)  2.2堆的向上调整算法(孩子与父亲做比较) 2.3堆的创建(向下建堆)  2.4向下建堆的时间复杂度 2.5堆的插入 2.6堆的删除 2.7堆的完整代码实现 三、堆的应

    2024年02月08日
    浏览(40)
  • 初阶数据结构之---导论,算法时间复杂度和空间复杂度(C语言)

    数据结构其实也学了挺长时间了,说着是要刷题所以才没怎么去写关于数据结构方面的内容。数据结构作为计算机中及其重要的一环,如果不趁着假期系统整理一下着实可惜,我这里构想的是将初阶数据结构和高阶数据结构,分别分成两个部分,初阶数据结构呢,大概有以下

    2024年02月22日
    浏览(57)
  • 数据结构初阶之顺序表(C语言实现)

    顺序表是数据结构里面很基础的一类,它是线性表的一种,其它线性表还有链表、栈和队列等,今天来和博主一起学习关于顺序表的知识吧。 顺序表,它分为两类: 动态顺序表 和 静态顺序表 ,这两个表的区别就是 前者的空间不固定 ,是 支持扩容 的,后者的 空间是固定

    2024年02月03日
    浏览(45)
  • 【数据结构之堆的实现】

    前言: 前篇学习了 数据结构之树和二叉树的基本概念知识,那么这篇继续学习怎么实现基本的操作。所以先建议看完上篇知识点,才有助于消化知识和理解。 / 知识点汇总 / 概念 :堆(Heap)是计算机科学中一类特殊的数据结构,是最高效的优先级队列。堆通常是一个可以被看

    2024年01月19日
    浏览(42)
  • C语言数据结构初阶(10)----二叉树的实现

    · CSDN的uu们,大家好。这里是C语言数据结构的第十讲。 · 目标:前路坎坷,披荆斩棘,扶摇直上。 · 博客主页: @姬如祎 · 收录专栏: 数据结构与算法     目录 1. 函数接口一览 2. 函数接口的实现 2.1 BTNode* BuyNode(BTDataType x) 的实现 2.2 BTNode* CreateTree() 的实现  2.3 void

    2023年04月08日
    浏览(43)
  • 数据结构初阶之基础二叉树(C语言实现)

    📃 博客主页: 小镇敲码人 💞 热门专栏:数据结构与算法 🚀 欢迎关注:👍点赞 👂🏽留言 😍收藏 🌏 任尔江湖满血骨,我自踏雪寻梅香。 万千浮云遮碧月,独傲天下百坚强。 男儿应有龙腾志,盖世一意转洪荒。 莫使此生无痕度,终归人间一捧黄。🍎🍎🍎 ❤️ 什么

    2024年03月19日
    浏览(43)
  • Leetcode刷题---C语言实现初阶数据结构---单链表

    删除链表中等于给定值 val 的所有节点 给你一个链表的头节点head和一个整数val,请你删除链表中所有满足Node.val==val的节点,并返回新的头节点 输入:head = [1,2,6,3,4,5,6], val = 6 输出:[1,2,3,4,5] 示例 2: 输入:head = [ ], val = 1 输出:[ ] 示例 3: 输入:head = [7,7,7,7], val = 7 输出:

    2024年02月15日
    浏览(56)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包