【数据结构 · 初阶】- 堆的实现

目录

一.初始化

二.插入

 三.删除(堆顶、根)

四.整体代码

Heap.h

Test.c

Heap.c


我们使用顺序结构实现完全二叉树,也就是堆的实现

以前学的数据结构只是单纯的存储数据。堆除了存储数据,还有其他的价值——排序。是一个功能性的数据结构

小根堆堆顶的数据一定是最小的,大根堆堆顶的数据一定是最大的
选出最大/最小,再选次大/次小……不断选最后就帮助排序。还可以解决取前几,后几的TOP-K 问题 

我们以建大堆为例。建小堆只需改变 爸 < 娃 即可


一.初始化

下面多次用到交换,将交换分装成函数

Heap.h

typedef int HPDataTypt;

typedef struct Heap
{
	HPDataTypt* a; // 数组指针,指向要开辟的存储数据的数组
	int size; // 当前已存储的有效数据个数
	int capacity; // 最大容量
}HP;

void HeapInit(HP* php); // 初始化
void HeapDestroy(HP* php); // 销毁

Heap.c 

void HeapInit(HP* php)
{
	assert(php);
	php->a = (HPDataTypt*)malloc(sizeof(HPDataTypt) * 4);
	if (php->a == NULL)
	{
		perror("malloc fail");
		return;
	}
	php->size = 0;
	php->capacity = 4;
}

void Swap(HPDataTypt* p1, HPDataTypt* p2)
{
	HPDataTypt tmp = *p1;
	*p1 = *p2;
	*p2 = tmp;
}

php 是指向主函数中,HP(结构体类型)的变量 hp 地址的指针。php 中存放的是 hp 的地址。若为空就说明结构体没有开好,所以一定不能为空,断言。

二.插入

堆的底层就是数组,可以插入数据。要把控制数组想象成控制树。原来是大根堆,插入后,要求还得是堆。
插入前是堆,插入后会影响部分祖先(跟祖先调整)

以大根堆为例,看最简单的情况:插入20,插入后不影响堆的性质。

    

再插入60,插入后要调整。 

为保证父亲 > 娃,要交换   

娃 还> 父亲,继续交换 父亲 > 娃,结束

上面的过程叫 向上调整 ,最多调整高度次,时间复杂度:O( log N )。插入一个数据,想让他再调整成堆只要 log N 次


堆的插入不像链表、顺序表,不能想往哪插就往哪插,要保持性质。

上面的尾插,如果堆原来是这样,就不能尾插20 

所以插入单纯的叫 Push 就好,因为不是由接口指定在哪个位置插入。


void AdjustUp(HPDataTypt* a, int child) // 从孩子(插入数据)位置向上调整
{
	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 HeapPush(HP* php, HPDataTypt x)
{
	assert(php);
	if (php->size == php->capacity)
	{
		HPDataTypt* tmp = (HPDataTypt*)realloc(php->a, sizeof(HPDataTypt) * php->capacity * 2);
		if (tmp == NULL)
		{
			perror("malloc fail");
			return;
		}
		php->a = tmp;
		php->capacity *= 2;
	}
	php->a[php->size] = x;
	php->size++;

	AdjustUp(php->a, php->size - 1); // 从孩子(插入数据)位置向上调整
}

 三.删除(堆顶、根)

堆删除,删尾轻松,但无意义。

为什么删堆顶、根才有意义?老大被干掉了,老二才能冒头。

堆实现的意义,无论是排序还是 top-k ,本质是在帮我选数,选出最大/最小数

删除后,也要保证是堆。要把最大的删掉,怎么搞?


不能挪动删除(直接删)!原因:1.效率低下   2.父子兄弟关系全乱了


正确方法:(间接删) 堆顶和最后的元素换一下;--size,使换下去的最后一个(原堆顶)元素失效
1.效率高   2.最大程度的保持了父子关系

单看左右子树依旧是大堆,换上去的原最后元素大概率是比较小的,就要向下调整


看下面的新场景:为保证换了之后父亲 > 娃,现在的堆顶(原最后一个元素)要跟大的娃换。

娃中大的 > 爸,把爸换下去 继续换     

最坏情况调到叶子结束。物理上是数组,怎么判断到叶子——没有娃,怎么判断没有娃呢?
把它当做爸,算左娃的下标,如果超出数组范围就没娃,所以参数要多给个数组的大小 size,用来判断 child 是否越界

最坏走高度 log N 次

void AdjustDown(HPDataTypt* a, int n, int parent)
{
	int child = parent * 2 + 1; // 默认左孩子大,将左孩子定为 child
	while (child < n)
	{
		// 选出左右孩子中大的那一个
		if (child + 1 < n && a[child] < a[child + 1]) // 防止无右娃的越界风险
		{
			child++; // 如果右孩子大,++后,child 就是右孩子
		}
		if (a[child] > a[parent])
		{
			Swap(&a[child], &a[parent]);
			parent = child;
			child = parent * 2 + 1;
		}
		else
		{
			break;
		}
	}
}

void HeapPop(HP* php)
{
	assert(php);
	assert(!HeapEmpty(php));
	
	Swap(&php->a[0], &php->a[php->size - 1]); // 交换堆顶、最后元素
	php->size--; // 删除换下来的原堆顶元素

	AdjustDown(php->a, php->size, 0); // 向下调整,0是开始调整位置的下标
    // n 是有效数据个数,作为下标,用来判断 child 是否越界
}

向上调整的前提:除了 child 这个位置,前面的数据构成堆
向下调整的前提:保证左右子树都是堆

四.整体代码

Heap.h

typedef int HPDataTypt;

typedef struct Heap
{
	HPDataTypt* a; // 数组指针,指向要开辟的存储数据的数组
	int size; // 当前已存储的有效数据个数
	int capacity; // 最大容量
}HP;

void HeapInit(HP* php); // 初始化
void HeapDestroy(HP* php); // 销毁

void HeapPush(HP* php, HPDataTypt x); // 插入
void HeapPop(HP* php);// 删除堆顶

HPDataTypt HeapTop(HP* php); // 堆顶的数据
bool HeapEmpty(HP* php); // 探空
int HeapSize(HP* php);

void AdjustUp(HPDataTypt* a, int child); // 向上调整
void AdjustDown(HPDataTypt* a, int n, int parent); // 向下调整

Test.c

void test1() // 排序
{
	HP hp;
	HeapInit(&hp);
	HeapPush(&hp, 2);
	HeapPush(&hp, 45);
	HeapPush(&hp, 76);
	HeapPush(&hp, 23);
	HeapPush(&hp, 5654);
	HeapPush(&hp, 24);
	HeapPush(&hp, 5);
	HeapPush(&hp, 242);
	HeapPush(&hp, 25);

	while (!HeapEmpty(&hp))
	{
		printf("%d ", HeapTop(&hp));
		HeapPop(&hp);// 选老二,必须干掉老大
	}

	HeapDestroy(&hp);
}

void test2() // top-k
{
	HP hp;
	HeapInit(&hp);
	HeapPush(&hp, 2);
	HeapPush(&hp, 45);
	HeapPush(&hp, 76);
	HeapPush(&hp, 23);
	HeapPush(&hp, 5654);
	HeapPush(&hp, 24);
	HeapPush(&hp, 5);
	HeapPush(&hp, 242);
	HeapPush(&hp, 25);
	HeapPush(&hp, 5);
	HeapPush(&hp, 5);

	int k = 0;
	scanf("%d", &k);
	while (!HeapEmpty(&hp) && k--)
	{
		printf("%d ", HeapTop(&hp));
		HeapPop(&hp);// 选老二,必须干掉老大
	}

	HeapDestroy(&hp);
}

Heap.c

void HeapInit(HP* php)
{
	assert(php);
	php->a = (HPDataTypt*)malloc(sizeof(HPDataTypt) * 4);
	if (php->a == NULL)
	{
		perror("malloc fail");
		return;
	}
	php->size = 0;
	php->capacity = 4;
}

void Swap(HPDataTypt* p1, HPDataTypt* p2)
{
	HPDataTypt tmp = *p1;
	*p1 = *p2;
	*p2 = tmp;
}

void AdjustUp(HPDataTypt* a, int child) // 从孩子(插入数据)位置向上调整
{
	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 HeapPush(HP* php, HPDataTypt x)
{
	assert(php);
	if (php->size == php->capacity)
	{
		HPDataTypt* tmp = (HPDataTypt*)realloc(php->a, sizeof(HPDataTypt) * php->capacity * 2);
		if (tmp == NULL)
		{
			perror("malloc fail");
			return;
		}
		php->a = tmp;
		php->capacity *= 2;
	}
	php->a[php->size] = x;
	php->size++;

	AdjustUp(php->a, php->size - 1); // 从孩子(插入数据)位置向上调整
}

void AdjustDown(HPDataTypt* a, int n, int parent)
{
	int child = parent * 2 + 1; // 默认左孩子大,将左孩子定为 child
	while (child < n)
	{
		// 选出左右孩子中大的那一个
		if (child + 1 < n && a[child] < a[child + 1]) // 防止无右娃的越界风险
		{
			child++; // 如果右孩子大,++后,child 就是右孩子
		}
		if (a[child] > a[parent])
		{
			Swap(&a[child], &a[parent]);
			parent = child;
			child = parent * 2 + 1;
		}
		else
		{
			break;
		}
	}
}

void HeapPop(HP* php)
{
	assert(php);
	assert(!HeapEmpty(php));
	
	Swap(&php->a[0], &php->a[php->size - 1]); // 交换堆顶、最后元素
	php->size--; // 删除换下来的原堆顶元素

	AdjustDown(php->a, php->size, 0); // 向下调整,0是开始调整位置的下标
    // n 是有效数据个数,作为下标,用来判断 child 是否越界
}

HPDataTypt HeapTop(HP* php)
{
	assert(php);
	return php->a[0];
}

bool HeapEmpty(HP* php)
{
	assert(php);
	return php->size == 0;
}

int HeapSize(HP* php)
{
	assert(php);
	return php->size;
}

void HeapDestroy(HP* php)
{
	assert(php);
	free(php->a);
	php->a = NULL;
	php->size = php->capacity = 0;
}

评论 28
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值