关于堆的知识

本文详细介绍了堆的概念,包括堆的向下排序算法、无规律数组建堆的实现、堆的向上调整算法,以及堆排序的完整过程。通过代码实例展示了如何在C语言中实现这些算法,同时提供了堆的初始化、销毁、打印、插入和删除等操作。堆排序利用小堆实现降序排列,避免了大堆导致的结构破坏问题,提高了效率。
摘要由CSDN通过智能技术生成

什么是堆

在这里插入图片描述

可能概念讲的不是很好理解,下面我们看两幅图。

在这里插入图片描述

那么问题来了?我们给的一组数据总不可能就那么巧吧,正好是有序的。这时就要介绍一个算法了:堆的向下排序算法。使一组不是那么有规律的数字也变成堆。

堆的向下排序算法

int array[] = {27,15,19,18,28,34,65,49,25,37};

在这里插入图片描述

从这组数据中你能发现什么特点吗?也不难发现,这组数据除了根节点,左右子树都是小堆。这也是使用向下调整算法的前提:
左右子树必须是一个堆,才能调整。
下面我们来看调整的过程:让根和它孩子中较小的孩子进行交换,然后孩子的位置换成父亲节点,父亲节点再找孩子节点,以此达到迭代的目的。

在这里插入图片描述

代码实现

void swap(int* children, int* gen)
{
	int temp = *children;
	*children = *gen;
	*gen = temp;
}

//建的小堆
void Heapdown(int *a, int n,int gen)
{
	//假设法:假设目标值是你想要的值
	int children = gen * 2 + 1;
	while (children<n)
	{
		if (children+1<n&&a[children + 1] < a[children])
		{
			children++;
		}
		if (a[children] <= a[gen])
		{
			//要传地址,不然影响不到外面
			swap(&a[children],&a[gen]);
		}
		//为什么可以直接break出去了,这是由使用堆的向下调整算法的前提条件决定的,因为
		//你只有满足左右子树都是小堆或大堆才能使用这个方法,所以这个时候你可以直击break
		//出去,不然你继续向下迭代也没有意义,因为它根本不会发生交换,最后会因为children>n
		//跳出循环。
		else
		{
			break;
		}
		gen = children;
		children = gen * 2 + 1;
	}
}

注意点:
1.巧用假设法,假设目标值为小的孩子,这样可以大大简化代码。
2.要保证children+1<n以防数组越界。
3.swap传参的时候要传地址,这样才能影响到外面。

如何对无规律的数组进行堆的向下排序算法使其成为堆

到这儿, 相信聪明的你一定会问了?如果给的一组数据完全没有规律可循了,那怎么办了?
下面我们就来看看堆的向下排序的妙处:从后往前排,来解决这个问题。

在这里插入图片描述

其实这种思想我是这样理解的:如果你想让整棵树都成为小堆,那么你就要让其中各个部分变成小堆,从最后一个父亲节点开始逐渐调整。

代码实现

void swap(int* children, int* gen)
{
	int temp = *children;
	*children = *gen;
	*gen = temp;
}

//建的小堆
void Heapdown(int *a, int n,int gen)
{
	//假设法:假设目标值是你想要的值
	int children = gen * 2 + 1;
	while (children<n)
	{
		if (children+1<n&&a[children + 1] < a[children])
		{
			children++;
		}
		if (a[children] <= a[gen])
		{
			//要传地址,不然影响不到外面
			swap(&a[children],&a[gen]);
		}
		else
		{
			break;
		}
		gen = children;
		children = gen * 2 + 1;
	}
}


int main()
{
	int a[] = { 27, 15, 18, 13, 19, 21, 28, 29, 1, 2 };
	int sz = sizeof(a) / sizeof(int);
	int gen = (sz - 1 - 1) / 2;
	int i = gen;
	for (i = gen; i >= 0; i--)
	{
		//向下调整算法,用来建堆
		Heapdown(a, sz, i);
	}
	for (i = 0; i < sz; i++)
	{
		printf("%d ", a[i]);
	}
	printf("\n");
}

在这里插入图片描述

注意点:这里要注意的就是,父亲节点与孩子节点之间的关系。

堆的向上调整算法

既然堆有向下调整算法,那么相对的也就有向上调整算法。
当我们在一个堆的末尾插入一个数据后,需要对堆进行调整,使其仍然是一个堆,这时需要用到堆的向上调整算法。
在这里插入图片描述
向上调整算法的基本思想(以建小堆为例):
1.将目标结点与其父结点比较。
2.若目标结点的值比其父结点的值小,则交换目标结点与其父结点的位置,并将原目标结点的父结点当作新的目标结点继续进行向上调整。若目标结点的值比其父结点的值大,则停止向上调整,此时该树已经是小堆了。
在这里插入图片描述

实现代码

void AdjustUp(int* a,int child)
{
	int father = (child - 1) / 2;
	while (child!=0)
	{
		if (a[child] > a[father])
		{
			swap(&a[child], &a[father]);
			child = father;
			father = (child - 1) / 2;
		}
		else
		{
			break;
		}
	}
}

注意点:while里面的判断条件最好不要写上father>=0,这样写只不过是碰巧能过而已,因为到最后一次的时候(0-1)/2等于0,进循环0和0比,走的是break,因此这样跳出了循环。

建堆的时间复杂度

到这人你可能会问了?那么建堆的时间复杂度是多少了?
下面我们来看看推导过程(可能字写的有些丑,大家请见谅):
在这里插入图片描述

注意上图是以向下调整算法为例的,其实向上调整算法建堆的时间复杂度也为O(N)。

堆排序

前面介绍的东西其实都是为了堆排序做铺垫的。
下面我们来想一个问题:如果我们把一组数据排成降序,应该是用小堆还是大堆了? 相信很多人肯定会首先回答:大堆。因为大堆的0号位数字为最大的数,然后再找次大的数字。但这样的话会有一个很严重的问题:那就是堆的结构被破坏了。
下面我们来看一个例子:
在这里插入图片描述

当我们再次找最大的数据时,我们发现这时堆的结构已经被破坏了,如果你要继续找的话,还要将他进行建堆,效率低。
正确的做法应该是建小堆:

在这里插入图片描述

代码实现

void swap(int* children, int* gen)
{
	int temp = *children;
	*children = *gen;
	*gen = temp;
}

//建的小堆
void Heapdown(int *a, int n,int gen)
{
	//假设法:假设目标值是你想要的值
	int children = gen * 2 + 1;
	while (children<n)
	{
		if (children+1<n&&a[children + 1] < a[children])
		{
			children++;
		}
		if (a[children] <= a[gen])
		{
			//要传地址,不然影响不到外面
			swap(&a[children],&a[gen]);
		}
		else
		{
			break;
		}
		gen = children;
		children = gen * 2 + 1;
	}
}

int main()
{
	int a[] = { 27, 15, 18, 13, 19, 21, 28, 29, 1, 2 };
	int sz = sizeof(a) / sizeof(int);
	int gen = (sz - 1 - 1) / 2;
	int i = gen;
	for (i = gen; i >= 0; i--)
	{
		//向下调整算法,用来建堆
		Heapdown(a, sz, i);
	}
	//堆排序,用小堆的话排的就是降序
	//用大堆的话排的就是升序
	int end = sz - 1;
	for (end = sz - 1; end > 0;)
	{
		swap(&a[0],&a[end]);
		end--;
		sz--;
		for (i = (sz - 1 - 1) / 2; i >= 0; i--)
		{
			Heapdown(a, sz, i);
		}
	}
	sz = sizeof(a) / sizeof(int);
	for (i = 0; i < sz; i++)
	{
		printf("%d ", a[i]);
	}
	printf("\n");
}

注意点:
1.这里要注意sz和end大小的调整。
2.这里要注意这种思想的运用,在之后的学习中,这种思想很重要。

堆的实现

初始化堆

首先,必须创建一个堆类型,该类型中需包含堆的基本信息:存储数据的数组、堆中元素的个数以及当前堆的最大容量。

typedef int HpDataType;
typedef struct Heap
{
	HpDataType* a;
	int size;
	int cap;
}Hp;

然后我们需要一个初始化函数,对刚创建的堆进行初始化,注意在初始化期间要将传入数据建堆。

void HpInit(Hp* php,int *b,int sz)
{
	assert(php);
	HpDataType* new = (HpDataType*)malloc(sizeof(HpDataType)*sz);
	if (new == NULL)
	{
		printf("malloc failed\n");
		exit(-1);
	}
	php->a = new;
	int i = 0;
	//for (i = 0; i < sz; i++)
	//{
	//	php->a[i] = b[i];
	//}
	//另外一种快速的传递方法
	//拷贝数据到堆中
	memcpy(php->a, b, sizeof(HpDataType)*sz);
	php->size = sz;
	php->cap = sz;
	int gen = (sz - 2) / 2;
	for (i = gen; i >= 0; i--)
	{
		Adjustdown(php->a, sz,i);
	}
}

销毁堆

为了避免内存泄漏,使用完动态开辟的内存空间后都要及时释放该空间,所以,一个用于释放内存空间的函数是必不可少的。

void HpDestory(Hp* php)
{
	assert(php);
	//因为是连续的空间,所以可以这样释放,跟顺序表一样
	free(php->a);
	php->a = NULL;
	php->size = php->cap = 0;
}

打印堆

把堆里面的数据打印出来,可以方便我们观察。

void HpPrint(Hp php)
{
	int i = 0;
	for (i = 0; i <php.size; i++)
	{
		printf("%d ", php.a[i]);
	}
	printf("\n");
}

注意:这里我们这按堆在内存中的物理结构打印的,并没有按逻辑结构(也就是数的形状来打印)。

堆的插入

数据插入时是插入到数组的末尾,即树形结构的最后一层的最后一个结点,所以插入数据后我们需要运用堆的向上调整算法对堆进行调整,使其在插入数据后仍然保持堆的结构。

void HpPush(Hp* php, HpDataType x)
{
	assert(php);
	//扩容
	if (php->size == php->cap)
	{
		HpDataType* new = (HpDataType*)realloc(php->a,sizeof(HpDataType)*(php->cap)*2);
		if (new == NULL)
		{
			printf("realloc failed\n");
			exit(-1);
		}
		php->a = new;
		php->cap *= 2;
	}
	//注意这儿写的时候的顺序,要先调整一下,再让size++,如果反过来的话,你传参进去的size就会多加了一下
	php->a[php->size] = x;
	AdjustUp(php->a, php->size);
	php->size++;
}

注意:写的时候要先写向上调整算法,再写size++,不然参进去的size就会多加了一下

堆的删除

堆的删除,删除的是堆顶的元素,但是这个删除过程可并不是直接删除堆顶的数据,而是先将堆顶的数据与最后一个结点的位置交换,然后再删除最后一个结点,再对堆进行一次向下调整。
原因:我们若是直接删除堆顶的数据,那么原堆后面数据的父子关系就全部打乱了,需要全体重新建堆,时间复杂度为O ( N ) O(N)O(N)。若是用上述方法,那么只需要对堆进行一次向下调整即可,因为此时根结点的左右子树都是小堆,我们只需要在根结点处进行一次向下调整即可,时间复杂度为O ( log ⁡ ( N ) )

void HpPop(Hp* php)
{
	assert(php);
	assert(!HpEmpty(*php));
	swap(&php->a[0], &php->a[php->size - 1]);
	php->size--;
	Adjustdown(php->a, php->size, 0);
}

获取堆顶的元素

HpDataType HpTop(Hp php)
{
	return php.a[0];
}

堆的判空

bool HpEmpty(Hp php)
{
	return php.size == 0;
}

堆中元素的个数

int HpSize(Hp php)
{
	return php.size;
}
  • 21
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 19
    评论
CTF入门基础知识是指在CTF(Capture The Flag)比赛中,掌握关于(heap)的一些基础知识是计算机内存的一部分,用于存储动态分配的数据。在中,数据可以通过malloc()和free()这样的函数进行分配和释放。 为了应对CTF相关的问题和挑战,以下是几个基础知识点: 1. 管理:了解内存的布局,包括段(heap segment)的起始地址、结束地址以及分配的内存块。此外,还需要了解块(heap chunk)的结构,包括块的头部和尾部。块中的元数据通常用于管理分配和释放。对于不同的管理器,其块结构可能有所不同。 2. 溢出漏洞:溢出是一种常见的漏洞类型。当程序没有正确地管理内存时,会导致溢出漏洞。攻击者可以通过溢出篡改重要的数据或劫持程序流程。学习如何利用溢出漏洞可以帮助我们理解程序的弱点以及如何加强安全性。 3. 分配技巧:在CTF中,有时需要进行分配,比如分配特定大小的块或者创建一定数量的块。掌握一些分配技巧可以帮助我们解决一些相关的CTF问题。 4. 利用技术:了解利用技术是掌握CTF基础的重要部分。常见的利用技术包括重叠块、fastbin攻击、unsorted bin攻击等。通过这些技术,攻击者可以在利用溢出漏洞时实现特定的攻击目标。 以上是CTF入门基础知识的一些关键点。通过学习和实践,逐渐掌握这些知识可以帮助我们在CTF比赛中更好地理解和解决相关的问题。
评论 19
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

一个数学不怎么好的程序员

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

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

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

打赏作者

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

抵扣说明:

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

余额充值