堆的c语言实现以及简单应用

目录

前言:(废话~😢)

一、堆的认知:

1.堆的存储方式:

2.二叉树父结点和子结点之间的关系(同样适用于堆)

二、堆的实现:

1.堆的结构和函数(Heap.h)

2.堆的函数实现(Heap.c):

(1).HeapInit初始化

(2).HeapDestroy摧毁堆

(3).HeapPush插入数据*

(4).AdjustUp向上检查*

 (5).HeapPop删除数据*

(6).AdjustDwon向下检查*

(7)判空HeapEmpty

 (8).HeapTop返回堆顶元素

(9)Swap交换函数

(10)综合代码

三、堆的应用HeapSort(目前不全,后续补充)


前言:(废话~😢)

嘿嘿,大家好啊,首先,非常感谢您能抽出宝贵时间来看我的文章,虽然你在看这句的时候已经在浪费了...┭┮﹏┭┮ 啊不是,欸嘿(●'◡'●)。

总之,堆是数据结构中我们接触二叉树遇到的第一个比较强劲的对手呢!哼哼<(  ̄^ ̄)( 所以,来和我一起,打败他,从这位身上吸取宝贵的经验吧~~

一、堆的认知:

 既然要打败对手,那么关于它的认知是必不可少的!它是一个完全二叉树关于二叉树以及树的介绍可以查询相关资料哦~这里我只介绍几种和堆相关的嘻嘻~😛。

注意,这里代码实现的时候物理空间是利用的连续的内存,也就是数组,但是这可不是线性表,这中间实现堆的意义就是在于里面所实现的逻辑,和数组元素相关联的规律!ಠಿ_ಠ

如下图所示,这就是一种完全二叉树,里面存储的数据是整型的,并且是小根堆存储~(拍案!下面会介绍的!😏)

1.堆的存储方式:

为了在排序或者取出删除方便的情况下,又或者指定堆的功能(纯属自己的理解嘿嘿😚,一切以官方为准~) 堆有两种储存的顺序:

(1).小根堆(所有父结点比孩子结点小 堆顶(根)是最小的)

如图所示: 也就是刚刚上面的那张图哦~ 15比18 19小 18比25 28...这样就组成了小根堆!(●'◡'●)

(2).大根堆 (所有父结点比孩子结点大 堆顶(根)是最大的)

如图所示:嘿嘿,完全和小堆储存反过来了嘛 65比49 34大 49比27 37大...这样就组成了大根堆!o((>ω< ))o

了解了堆的储存方式后,我们就要明确这些要怎么用代码实现出来,毕竟,这些都是实现出来的呀~话不多说,开始下一步~

2.二叉树父结点和子结点之间的关系(同样适用于堆)

 上面在对堆的总述中提到了,我们实现堆所利用的物理空间是数组,那么,为了能够实现上述两种逻辑储存结构,自然要在数组元素之间建立一种关系,好在数组中建立起一种非线性表出来,这个关系不仅仅是堆的关系,也是二叉树里的关系:

比如上面我们用过的一个完全二叉树的图,如果用对应的序号标出,我们很容易就能发现其中的规律:

(1)父结点下标 = (左子结点/右子节点 - 1) / 2;(比如:0 = (1- 1) /2 or (2 - 1)/2   2 = (5 - 1)/ 2 or (6 - 1)/ 2......)

(2)左子结点 = 父结点 * 2 + 1; (比如 1 = 0*2+1  9 = 4*2+1 .....)

(3)右子结点 = 父结点 * 2 + 2; (比如 2 = 0*2+2  8 = 3*2+2.....)

是不是得到这个结论非常惊喜,是的,这一重大发现将在我们之后实现堆里发挥重要作用,什么?准备工作已经完成?好!我们去攻克实现堆这一关!冲冲冲*(੭*ˊᵕˋ)੭*ଘ 

二、堆的实现:

上面相关关系我们准备好之后就可以直面堆了!哼哼(¬︿̫̿¬☆)既然是跟数组有关,那么我们就优先确定堆的结构以及实现功能吧!目标我们定在Heap.h文件中!

1.堆的结构和函数(Heap.h)

我们首先引用相关库函数,这里不必在多说。首先定义数据类型,为了简易实现堆的功能,我们数据类型自然是整型,将其定义成HPDataType,即:

typedef int HPDataType;//堆的数据类型

(1).堆的结构HP

然后,就像实现线性表一样,为了找到实时储存和为了扩展内存的关系,我们可以将堆的结构定义如下(HP):

typedef struct Heap{
    HPDataType* a;
    int size; //实时个数
	int capacity;//储存的最大个数
}HP;

里面一个数据a是用来存储连续内存开始的地址,之后根据实时个数和储存最大个数相等的时候需要动态扩展的,size就是实时个数,capacity就是储存的最大个数。

堆的大致结构完成后,我们就要实现堆的功能创建实现他们的函数或者接口。

(2)初始化

首先自然是初始化,将a置为NULL,其余置为0,以免创建时发生野指针引用。

//初始化是少不了的
void HeapInit(HP* php);

 传参自然是指针,这样才能通过地址修改在主函数里的数据,避免修改了个寂寞的错误。

(3).摧毁堆

防止内存泄漏等情况,就要在堆用完之后释放内存,所以肯定要写上一个摧毁堆的函数‘

//摧毁堆,释放内存
void HeapDestroy(HP* php);

(4).插入数据

接下来就是真正需要关注的功能,我们利用物理的连续内存,如何做到逻辑储存内存的那种二叉树呢?就是要实现插入向上进行调整的函数

//插入数据 开始向上调整
void HeapPush(HP* php, HPDataType x);

传入我们需要改的堆地址,然后插入一个数据,实现逻辑...

(5).删除数据

有插入自然会有删除,为了保持删除后依然有堆的结构(插入也一样)就看实现代码里的具体逻辑咯~

//删除数据 开始向下调整
void HeapPop(HP* php);

(6).取出数据

既然也是一个数据结构肯定会有和外界交互的接口啊

//获得堆顶元素
HPDataType HeapTop(HP* php);

(7).判空

判空条件的代码利用率大,所以就要实现一下

//判断是否为空
bool HeapEmpty(HP* php);

(8).返回储存个数

//返回当前储存个数
int HeapSize(HP* php);

 (9)向上检查

维持堆结构所需代码,用于插入

//向上检查 堆核心代码
void AdjustUp(HPDataType* a, int child);

(10).向下检查

维持堆结构所需代码,用于删除

//向下检查
void AdjustDwon(HPDataType* a, int size, int parent);

 (11).交换代码

为了使代码简洁,可以将数组两元素交换使用函数实现

//交换函数
void Swap(HPDataType* s1, HPDataType* s2);

(12).综合

//Heap.h
#pragma once
#define _CRT_SECURE_NO_WARNINGS 1

#include<stdio.h>
#include<assert.h>
#include<stdlib.h>
#include<stdbool.h>

typedef int HPDataType;//堆的数据类型

//定义堆的物理结构 -- 动态数组
typedef struct Heap
{
	HPDataType* a;
	int size; //实时个数
	int capacity;//储存的最大个数
}HP;

//初始化是少不了的
void HeapInit(HP* php);

//摧毁堆,释放内存
void HeapDestroy(HP* php);

//插入数据 开始向上调整
void HeapPush(HP* php, HPDataType x);

//删除数据 开始向下调整
void HeapPop(HP* php);

//获得堆顶元素
HPDataType HeapTop(HP* php);

//判断是否为空
bool HeapEmpty(HP* php);

//返回当前储存个数
int HeapSize(HP* php);

//向上检查 堆核心代码
void AdjustUp(HPDataType* a, int child);

//向下检查
void AdjustDwon(HPDataType* a, int size, int parent);

//交换函数
void Swap(HPDataType* s1, HPDataType* s2);

2.堆的函数实现(Heap.c):

确定了接口后,我们可以配合这在test(包含主函数的c文件)逐步完善这些功能,下面我提供一下我的思路,仅供参考哦~嘿嘿😚

(1).HeapInit初始化

传过来堆结构的地址,然后将里面的存数组地址置为空,其余为0

//初始化堆函数
void HeapInit(HP* php)
{
	assert(php);

	php->a = NULL;
	php->capacity = php->size = 0;

}

(2).HeapDestroy摧毁堆

对应的,因为数组时malloc过来的,自然需要free,然后置为空,其余置为0即可。

//释放内存
void HeapDestroy(HP* php)
{
	assert(php);

	free(php->a);
	php->a = NULL;
	php->capacity = php->size = 0;

}

(3).HeapPush插入数据*

插入函数,即每次输入数据时需要插入堆中的元素,如何将这些元素安排到合适的位置,正是这个函数里面应该要实现的功能:void HeapPush(HP* php, HPDataType x)

首先检查传进来的是否空指针,防止空指针引用 assert(php);然后判断是否已经储存满,储存满的话进入if语句,realloc解决问题,注意判断申请不到内存的情况,然后正常尾插,但是,到此之前我们进行的都是顺序表插入数据的步骤,之后我们需要将这个数组的地址和最后一个元素的下标传入向上检查的函数中AdjustUp,这样才能为我们创建堆的结构或者维持结构,随后size++即可。

那么,这里为什么使用向上检查呢?一开始从逻辑结构来说,从头往尾插入的,所以一开始是上面才有的数据,自然就是向上检查了。

//插入数据
void HeapPush(HP* php, HPDataType x)
{
	assert(php);

	//先正常尾插 即数组对应地方插入
	//先判断是否要扩展内存
	if (php->size == php->capacity)
	{
		int newcapacity = php->capacity == 0 ? 4 : php->capacity * 2;//判断是否初次进入
		HPDataType* tmp = (HPDataType*)realloc(php->a, sizeof(HPDataType) * newcapacity);//防止申请失败
		if (tmp == NULL)
		{
			printf("申请内存失败┭┮﹏┭┮\n");
			exit(-1);//申请失败没有继续下去的意义了 (正常情况下不会申请失败的)
		}
		php->a = tmp;
		php->capacity = newcapacity;
	}

	php->a[php->size] = x;
	//防止堆结构破坏或者组件堆结构 向上检查即可
	AdjustUp(php->a, php->size);
	php->size++;

}

(4).AdjustUp向上检查*

在上面的插入中少不了的核心代码,就是这里啦~看我看我看我!嘿嘿( ̄y▽, ̄)╭ 

因为传给我们的参数是子结点,物理结构上也就是最后一个元素,由我们上面得到的公式,我们可以推得父结点,然后两者比较,根据什么储存方式比较对应得符号(这样后面更换符号更加方便),由上面的图,比如我们给了这样的一个数组,然后将它转化为小根堆储存的堆结构,我们会怎么做呢?下面来演示一下下啦~嘿嘿😊

❤演示:

{27, 15, 19, 18}比如这样的一个数组,当我们把第一个数传入我们堆的物理结构也就是数组中时,不进行比较,随后当第二个数传进来时,进入向上比较函数里面,计算出它的父结点,发现其的值比自己大,说明就要调整了,通过Swap函数交换后,就可以得到一个初步的堆结构。

下面的图示分别时逻辑结构和物理结构:

 

 

 

 后面的依次这样进行比较,最终的逻辑结构和物理结构如下:

 那么,循环条件是什么,每次交换依次,子结点的位置和父结点的位置都要交换,然后新的子结点继续算到新的父结点以便向上检查。那么就说明到边界了,child>0显然是一个不错的选择,当child等于零的时候,说明上一个父结点和子结点交换位置了,既然第一个根结点都交换过了,那这不已经完了嘛,但是,parent>=0这个条件不行,因为当子结点等于0的时候,上一步父结点说明等于0了,算新的父结点的时候是(0-1)/2 = 0所以会多循环一次,显然错误。

下面代码进行详细说明:目前是小根堆储存

//向上检查
void AdjustUp(HPDataType* a, int child)
{
	assert(a);

	//向上检查即出父结点的下标
	int parent = (child - 1) / 2;
	//进入循环 更替
	while (child > 0)
	{
		//用小于符号,方便切换
		if (a[child] < a[parent])
		{
			//交换数据
			Swap(&a[child], &a[parent]);
			//向上检查
			child = parent;
			parent = (child - 1) / 2;
		}
		else//只要父不大于子即正确
		{
			break;//跳出循环即可
		}
	}

}

 (5).HeapPop删除数据*

void HeapPop(HP* php);

 想一下,我们要删除数据,还是在有一定逻辑得数据结构里面删除数据,那么删除的要么是最小要么是最大,删除其他数据就不存在意义了。

删除数据简单,我们只需要删除头即可....真的直接去头吗?如果利用顺序表里面的头删的话,后面的数据往前挪动,在耗大量时间复杂度的同时还把堆的结构彻底打乱了┭┮﹏┭┮!!这样讨好谁呢!? 为了能够保持一定的堆结构完整性,我们只需要最后一个数据和第一个数据交换,然后删除最后一个数据不久可以了嘛,但是!此时也只是下面保持着堆的结构,头并没有,所以接下来向下检查即可,便就实现了删除数据啦~~

上代码!

//删除数据 
void HeapPop(HP* php)
{
	assert(php);
	assert(!HeapEmpty(php));//防止没有元素便删

	//删除堆顶元素 是直接间头删吗?
	//不行这样顺序全部打乱,我们需要向下检查,那么至少要保证除开根结点外是正常的堆结构,所以便就可以和最后一个元素交换即可
	Swap(&php->a[0], &php->a[php->size - 1]);
	//交换之后删掉元素,然后向下检查即可
	php->size--;
	AdjustDwon(php->a, php->size, 0);//传长度的原因就是为了判断循环条件

}

(6).AdjustDwon向下检查*

void AdjustDwon(HPDataType* a, int size, int parent);

这里也是堆结构里面的第二个核心代码。

向下检查,也就是在除开被检查的父结点外,其他的结构应该均属于堆结构!这样才能保证一直都是修改不是堆的结构的数。

利用传过来的parent即父结点下标,可以计算出左右子节点 然后两个子结点进行比较,看谁大谁小,然后不满足和父结点的堆结构关系的话交换,然后将parent赋予child即可,child进行新的计算。

那么循环条件是什么呢?是parent < size - 1吗?不是,随便举一个例子,比如size = 8, 如果parent = 6,满足循环要求,但是child计算的是 6*2 + 1 = 13远超 8,这不就越界了嘛,所以,循环条件应该是child < size即可。

上代码!

//向下调整
void AdjustDwon(HPDataType* a, int size, int parent)
{
	assert(a);

	//首先得到左孩子,默认child是左孩子
	int child = parent * 2 + 1;
	//当子结点被传到超过范围便就可以停下来了
	while (child < size)
	{
		//看是左孩子大还是右孩子大 然后将小得下标给child就行,这样就不用创建两个child了
		if (child + 1 < size && a[child + 1] < a[child])//如果只有两个元素的情况下 这样child+1 =2但是下标2不存在,产生了越界,所以需要在增减一个条件 一定要谨慎
		{
			child++;
		}
		//注意大小于符号,更换得时候方便换
		if (a[child] < a[parent])
		{
			Swap(&a[child], &a[parent]);
			//其他不变,因为改变的只有这一个,其余是保持着堆结构的,所以只需将child赋给新的父即可
			parent = child;
			child = parent * 2 + 1;
		}
		else//和向上检查类似,一旦不存在即结构正确,无需循环了
		{
			break;
		}
	}
}

(7)判空HeapEmpty

这个在于实用性,比如在取出堆顶元素,然后pop掉后的循环条件,又或者是在取出堆顶元素判断是否存在元素和删除元素判断是否存在元素。

这个实现起来非常简单,正常思路下判断size是否等于0就可以了,但是通常是利用if语句或者是三目运算符来判断。这样都不简洁,其实只需要return一个逻辑语句 size == 0即可,若是真的就是真,假的返回假即可。

bool HeapEmpty(HP* php)
{
	assert(php);

	return php->size == 0;
}

 (8).HeapTop返回堆顶元素

在循环遍历输出时,需要使用的一种函数,这个直接输出第一个元素就行啦~

//获得堆顶元素
HPDataType HeapTop(HP* php)
{
	assert(php);
	assert(!HeapEmpty(php));

	return php->a[0];
}

(9)Swap交换函数

这个函数的实现是为了能够方便的交换两个数组的值,并且时实际的交换,所以传的是指针,具体实现如下:

//交换数据
void Swap(HPDataType* s1, HPDataType* s2)
{
	assert(s1 && s2);

	HPDataType temp = *s1;
	*s1 = *s2;
	*s2 = temp;

}

(10)综合代码

综上,在Heap.c文件里面实现的代码如下:🤭

//Heap.c
#include"Heap.h"

//初始化堆函数
void HeapInit(HP* php)
{
	assert(php);

	php->a = NULL;
	php->capacity = php->size = 0;

}

//交换数据
void Swap(HPDataType* s1, HPDataType* s2)
{
	assert(s1 && s2);

	HPDataType temp = *s1;
	*s1 = *s2;
	*s2 = temp;

}

//释放内存
void HeapDestroy(HP* php)
{
	assert(php);

	free(php->a);
	php->a = NULL;
	php->capacity = php->size = 0;

}

//插入数据
void HeapPush(HP* php, HPDataType x)
{
	assert(php);

	//先正常尾插 即数组对应地方插入
	//先判断是否要扩展内存
	if (php->size == php->capacity)
	{
		int newcapacity = php->capacity == 0 ? 4 : php->capacity * 2;//判断是否初次进入
		HPDataType* tmp = (HPDataType*)realloc(php->a, sizeof(HPDataType) * newcapacity);//防止申请失败
		if (tmp == NULL)
		{
			printf("申请内存失败┭┮﹏┭┮\n");
			exit(-1);//申请失败没有继续下去的意义了 (正常情况下不会申请失败的)
		}
		php->a = tmp;
		php->capacity = newcapacity;
	}

	php->a[php->size] = x;
	//防止堆结构破坏或者组件堆结构 向上检查即可
	AdjustUp(php->a, php->size);
	php->size++;

}

//向上检查
void AdjustUp(HPDataType* a, int child)
{
	assert(a);

	//向上检查即出父结点的下标
	int parent = (child - 1) / 2;
	//进入循环 更替
	while (child > 0)
	{
		//用小于符号,方便切换
		if (a[child] < a[parent])
		{
			//交换数据
			Swap(&a[child], &a[parent]);
			//向上检查
			child = parent;
			parent = (child - 1) / 2;
		}
		else//只要父不大于子即正确
		{
			break;//跳出循环即可
		}
	}

}

//删除数据 
void HeapPop(HP* php)
{
	assert(php);
	assert(!HeapEmpty(php));//防止没有元素便删

	//删除堆顶元素 是直接间头删吗?
	//不行这样顺序全部打乱,我们需要向下检查,那么至少要保证除开根结点外是正常的堆结构,所以便就可以和最后一个元素交换即可
	Swap(&php->a[0], &php->a[php->size - 1]);
	//交换之后删掉元素,然后向下检查即可
	php->size--;
	AdjustDwon(php->a, php->size, 0);//传长度的原因就是为了判断循环条件

}

//向下调整
void AdjustDwon(HPDataType* a, int size, int parent)
{
	assert(a);

	//首先得到左孩子,默认child是左孩子
	int child = parent * 2 + 1;
	//当子结点被传到超过范围便就可以停下来了
	while (child < size)
	{
		//看是左孩子大还是右孩子大 然后将小得下标给child就行,这样就不用创建两个child了
		if (child + 1 < size && a[child + 1] < a[child])//如果只有两个元素的情况下 这样child+1 =2但是下标2不存在,产生了越界,所以需要在增减一个条件 一定要谨慎
		{
			child++;
		}
		//注意大小于符号,更换得时候方便换
		if (a[child] < a[parent])
		{
			Swap(&a[child], &a[parent]);
			//其他不变,因为改变的只有这一个,其余是保持着堆结构的,所以只需将child赋给新的父即可
			parent = child;
			child = parent * 2 + 1;
		}
		else//和向上检查类似,一旦不存在即结构正确,无需循环了
		{
			break;
		}
	}
}


//获得堆顶元素
HPDataType HeapTop(HP* php)
{
	assert(php);
	assert(!HeapEmpty(php));

	return php->a[0];
}

//判空
bool HeapEmpty(HP* php)
{
	assert(php);

	return php->size == 0;
}

//返回当前储存个数
int HeapSize(HP* php)
{
	assert(php);

	return php->size;
}



三、堆的应用HeapSort(目前不全,后续补充)

上述代码帮我们实现了堆,那么现在我们就要落实一下相关堆结构的应用。

我们知道,在c语言写的几个排序代码,其时间复杂度最快也就是O(n^2) 但是如果我们能够利用堆来写出堆排序的话,最快时间复杂度达到了O(NlogN),这可是天差地别。(✿◕‿◕✿)

既然是堆排,那么我们肯定需要一个堆的,是重新创造一个堆吗,然后将数组导入,然后输出?这样的话造价太高,这不适得而反嘛,所以,我们只需要在原来堆的基础上,将数组转化为堆即可,即使用向上检查和向下检查即可,一个需要上面是堆,一个是需要下面是堆结构,这个好办,一个从头开始循环即可,另一个从尾开始循环。

重点来了,当将一个数组堆化后,如何将其按照从小到大或者从大到小呢?我们知道小根堆的头是最小的,大根堆的头是最大的,是不是每次我们将其头和尾交换,然后将除开尾巴之外视为一个除头外的堆进行向下检查法就可以了呢?那么,从小到大是不是用小跟头呢?自然不是,因为第一个元素就是小,拍到末尾不就是从大到小了嘛,所以应该反着来,利用一个变量控制循环即可!!

代码如下:

//堆排
void HeapSort(int* x, int n)
{
	//首先将这个数组变成堆
	//可以利用向上检查 或者向下检查的方式建立堆 但是两种的时间复杂度不同
	//向上检查从头开始 O(N*logN)
	//for (int i = 0; i < n; i++)
	//{
	//	AdjustUp(x, i);
	//}
	//向下检查从尾开始 O(N)
	for (int i = n - 1; i >= 0; i--)
	{
		AdjustDwon(x, n, i);
	}

	//然后开始排序
	//现在是从大到小 你以为是大根堆吗?不是,因为再次转换的话必须要从后往前 现在是小根堆 堆顶是最小的,排到最后  从小到大 使用大根堆即可
	int end = n - 1;
	while (end > 0)
	{
		//首先交换头和尾 类似于删除里
		Swap(&x[0], &x[end]);
		AdjustDwon(x, end, 0);//向下调整即可,并且不会调整到最后一个元素
		--end;
	}

}

上面的就是我的浅浅理解啦~希望大家给我指出错误,或者能够帮助到您!👄😍❤

  • 5
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值