【C语言】建堆算法与向上/下调整

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档


前言

带领大家简单了解一下堆,二叉树的部分有简单了解就可以看的懂


提示:以下是本篇文章正文内容,下面案例可供参考

一、堆

堆这种数据结构其实就是“更有规矩”一点的完全二叉树
在这里插入图片描述
如果在一个堆中,任意一个根节点都大于等于它的子节点,则称作这个堆为大堆。
如果在一个堆中,任意一个根节点都小于等于它的子节点,则称作这个堆为小堆。
在这里插入图片描述
一般做题的时候为了方便在本地调试,我们可能会一个一个的开辟出节点,然后手动把它们连接在一起,但实际上我们可以写出一个函数,让它根据数组创建堆

虽然堆中的数据是以“二叉树”的逻辑顺序存储的,但是实际上堆是以数组这样的物理顺序依次存储的

这样其实可以发现,设父节点的下标为parent,左子节点的下标为child,那么child=parent*2+1,这在后面是非常重要的一个点
下面是我自己写的堆中头文件的内容:

#pragma once
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
typedef int HpDataType;
typedef struct Heap
{
	HpDataType* a;
	int size;
	int capacity;
}Heap;
// 对数组进行堆排序
void HeapSort(int* a, int n);
//输入一个拥有n个元素的数组,实现堆的构建
void HeapCreate(Heap* hp, HpDataType* a, int n);
//堆的初始化
HpDataType* HeapInit(Heap* hp);
//堆的销毁
void HeapDestory(Heap* hp);
//打印堆中元素
void HeapPrintf(Heap* hp);
//插入新元素
void HeapPush(Heap* hp,HpDataType x);
//push过程中维持小堆形态
void AdjustUp(Heap* hp,int child);
//删除堆顶元素
void HeapPop(Heap* hp);
//删除堆顶元素过程中维持大堆
void AdjustDown(Heap* hp, int n,int parent);
//返回堆顶元素
HpDataType HeapTop(Heap* hp);
//返回堆大小
int HeapSize(Heap* hp);
//堆的判空
int HeapEmpty(Heap* hp);

下面建堆算法中,我以大堆来举例,对应的是函数“AdjustDown”

1、向上/下调整

先来看一些无关紧要的函数,对堆先有一点小了解

//堆的初始化
HpDataType* HeapInit(Heap* hp)
{
	assert(hp);
	hp->a = NULL;
	hp->size = 0;
	hp->capacity = 0;
}
//堆的销毁
void HeapDestory(Heap* hp)
{
	assert(hp);
	free(hp->a);
	hp->a = NULL;
	hp->capacity = hp->size = 0;
}
void HeapPrintf(Heap* hp)//方便调试
{
	assert(hp);
	int i = 0;
	for (i = 0; i < hp->size; i++)
	{
		printf("%d ", hp->a[i]);
	}
	printf("\n");
}

然后是一些小例子,这是一个大堆:
在这里插入图片描述
直接删除堆顶数据,再按原来的顺序构建二叉树,你会发现它已经不再是堆

在这里插入图片描述
按照正常规则,删除堆顶元素的以后的二叉树长这样:
在这里插入图片描述
所以在堆的增删过程中我们需要用函数来对整个堆进行调整

堆的删除,指的是堆顶元素的删除,也就是将数组中的数据用建堆算法排好序以后,删除堆这个二叉树“祖先节点”-也就是下标为0的数据,然后,我们需要将堆底的数据置于堆顶,(实现起来的话先”交换“后“删除”是一样的)最后需要用函数AdjustDown对堆进行调整

//删除堆顶元素,并将堆底元素置于堆顶
void HeapPop(Heap* hp)
{
	assert(hp);
	int tmp = hp->a[0];
	hp->a[0] = hp->a[hp->size - 1];
	hp->size--;
	AdjustDown(hp, hp->size, 0);
}

向下调整指的是把刚刚被挪到堆顶的数据向下调整

由于是从堆顶往堆底调整,所以首先要注意child不要超过数组的容量n;
parent这个参数主要是要在建(大)堆算法中复用,所以特意设置的

//删除堆顶元素过程中维持大堆
void AdjustDown(Heap* hp, int n, int parent)//参数parent是为了在建堆算法中复用
{
	int child = 2 * parent + 1;
	while (child < n)
	{
		 保证child指向大的那个孩子
		if ((child+1 < n) && (hp->a[child] < hp->a[child + 1]))
		{
			child++;
		}
		// 1、孩子大于父亲,交换,继续向下调整
		// 2、孩子小于父亲,则调整结束
		if (hp->a[parent] < hp->a[child])
		{
			int tmp = hp->a[parent];
			hp->a[parent] = hp->a[child];
			hp->a[child] = tmp;
			parent = child;
			child = 2 * parent + 1;
		}
		else
		{
			break;
		}
	}
}

插入数据后要进行调整,也是相似的道理
在插入新元素的时候我们选择直接在数组的后面直接插入这个数据,但如果我们想把它安放在逻辑结构的堆中的正确顺序,我们还需要把这个数组重新用函数AdjustUp调整

//插入新元素
void HeapPush(Heap* hp, HpDataType x)
{
	assert(hp);
	if (hp->capacity == hp->size)
	{
		hp->a = (HpDataType*)realloc(hp->a, 2 * (hp->capacity) * sizeof(HpDataType));
	}
	hp->a[hp->size] = x;
	hp->size++;
	AdjustUp(hp->a, hp->size, hp->size - 1);
}

向上调整指的是把新插入的元素向上调整到合适的位置

主要思想是从新插入的节点开始,如果它比父节点大,那么就交换两个节点的值,因为这个过程需要把节点和父节点“从上往下,从堆顶到堆底”进行比较,所以边界条件是child大于0

//push过程中维持小堆形态
void AdjustUp(Heap* hp, int child)
{
	assert(hp);
	int parent = (child - 1) / 2;
	while (child > 0)
	{
		if (hp->a[child] > hp->a[parent])
		{
			int tmp = hp->a[child];
			hp->a[child] = hp->a[parent];
			hp->a[parent] = tmp;
			child = parent;//确保循环持续运行
			parent = (child - 1) / 2;
		}
		else
		{
			break;
		}
	}
}

2、建堆算法

以建造大堆为例
建堆算法的规则就是:一个一个的把所有parent节点用adjustdown按照从堆底向堆顶的方向挨个跑一次

堆的构建(复杂度高)
//void HeapCreate(Heap* hp, HpDataType* arr, int n)
//{
//	assert(hp);
//	HeapInit(hp);
//	int i = 0;
//	Heap* tmp = (HpDataType*)malloc(n * sizeof(HpDataType));
//	if (!tmp)
//	{
//		perror("HeapInit:malloc\n");
//		exit(-1);
//	}
//	hp->a = tmp;
//	hp->capacity = n;
//	for (i = 0; i < n; i++)
//	{
//		HeapPush(hp, arr[i]);
//		AdjustUp(hp, (hp->size) - 1);
//	}
//}
//堆的构建
void HeapCreate(Heap* hp, HpDataType* arr, int n)
{
	assert(hp);
	HeapInit(hp);
	int i = 0;
	Heap* tmp = (HpDataType*)malloc(n * sizeof(HpDataType));
	if (!tmp)
	{
		perror("HeapInit:malloc\n");
		exit(-1);
	}
	hp->a = tmp;
	hp->capacity = hp->size = n;//注意n就是size,所以后面求父节点时要减2才是下标
	memcpy(hp->a, arr, n * sizeof(HpDataType));
	for (int i = (n - 2) / 2; i >=0; i--)
	{
		AdjustDown(hp, n, i);
	}


3.Topk问题

举个具体的例子就是“从N个数里面寻找最大的K个”这样的问题,我们既可以建大堆,取一次堆顶数据然后pop一次,
但真正高效的做法是:
建立K个数小堆,然后遍历这N个数的数组,如果遍历到的数据比堆顶的数据大,那么就把这个数据push进去,然后向下调整,遍历结束以后,留在这个小堆中的数据就是前k大的数据

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

Alexanderite

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

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

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

打赏作者

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

抵扣说明:

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

余额充值