数据结构 严薇敏 堆 的实现及其使用方法详解

本文详细介绍了堆的概念,包括最大堆和最小堆的定义。堆是一种特殊的完全二叉树,其中每个节点的值都大于或小于其子节点。接着,文章展示了堆的C语言实现,包括创建、调整、插入、删除、获取堆顶元素、检查容量、销毁等操作。堆的调整算法包括向上调整和向下调整,确保堆的性质得以维护。此外,文章还提供了堆的扩容方法。最后,通过一个示例展示了如何使用这些接口操作堆。
摘要由CSDN通过智能技术生成

1.堆的概念

将一个关键码的集合K = {k0 , k1,k2,k3……kn-1}把他所有元素按完全二叉树的存储方式放在一个一维数组中,并且满足双亲节点大于孩子节点,或者双亲节点小于孩子节点将根节点最大的堆叫做最大堆或大根堆,根节点最小的堆叫做最小堆或小根堆。

1.2堆的性质

  1. 堆中某个节点的值总是不大于或不小于其父节点的值;
  2. 堆总是一棵完全二叉树

2.堆的实现及其接口详解

2.1Heap.h

#pragma once
#include <stdio.h>
#include <assert.h>
#include <stdlib.h>
#include <string.h>
typedef int Datatype;

typedef int (*func)(Datatype left, Datatype right);

typedef struct Heap {
	Datatype* arr;
	int capacity;
	int size;
	func Comper;
}Heap;

//堆的创建 用回调函数判断是要创建小堆还是大堆
void HeapCreate(Heap* p, Datatype *arr, int size, func Comper);

//向上调整
void HeapAdjustup(Heap* p, int child);

//向下调整
void HeapAdjustDown(Heap* p, int parent);

//堆的插入
void HeapPush(Heap* p, Datatype data);

//堆的移除
void HeapErase(Heap* p);

//获取堆顶元素
Datatype HeapTop(Heap* p);

//获取堆的长度
int HeapSize(Heap* p);

//堆是否为空
int HeapEmpty(Heap* p);

//堆的销毁
void HeapDestroy(Heap* p);

//堆的扩容
void HeapCheakCapacity(Heap* p);

2.2Heap.c

#include "Heap.h"
//交换函数
void Swap(Datatype* left, Datatype* right) {
	Datatype temp = *left;
	*left = *right;
	*right = temp;

}

int Max(Datatype left, Datatype right)
{
	return left > right;
}

int Min(Datatype left, Datatype right)
{
	return left < right;
}
//只是在除了根节点以外 他的两个子树也是堆结构的情况下才成立
//而且你要小堆我得改一次大于号,远没有回调函数那样的便捷
//回调函数忘记了可以自己手动实现一次qsort

//冒泡排序
void BobSort(Datatype* arr,int size) {
	for (int i = 0; i < size - 1; i++) {
		for (int j = 0; j < size - 1 - i; j++) {
			if (arr[j - 1] > arr[j]) {
				Swap(&arr[j - 1], &arr[j]);
			}
		}
	}
}
//向下调整
void HeapAdjustDown(Heap* p, int parent)
{
	int child = (parent << 1) + 1;
	int size = p->size;
	while (child < size) {
		//因为我么对parent * 2 + 1找到的是这个节点对应的左孩子
		//要是我右孩子还小呢
		//利用回调函数找到更小的那一个将child指针移动到右孩子那块
		if (child + 1 < size && p->Comper(p->arr[child + 1], p->arr[child])) {
			child += 1;
		}
		//是否满足堆的特性
		if (p->Comper(p->arr[child], p->arr[parent])) {
			Swap(&p->arr[parent], &p->arr[child]);
			parent = child;
			child = (parent << 1) + 1;
		}
		else {
			return;
		}
	}
}

//向上调整
void HeapAdjustup(Heap* p, int child)
{
	int parent = (child - 1) >> 1;
	while (child) {
		if (p->Comper(p->arr[child], p->arr[parent])) {
			Swap(&p->arr[child], &p->arr[parent]);
			child = parent;
			parent = (child - 1) >> 1;
		}
		else {
			return;
		}
	}
}

//堆的扩容
void HeapCheakCapacity(Heap* p)
{
	assert(p);
	//判断是否需要扩容
	if (p->size == p->capacity) {
		//把容量变成原来的两倍
		int NewCapaty = p->capacity << 1;
		//从堆上开辟新的内存空间
		Datatype* temp = (Datatype*)malloc(sizeof(Datatype) * NewCapaty);
		if (NULL == temp) {
			assert(0);
			return;
		}

		//把原来的数据拷贝
		memcpy(temp, p->arr, sizeof(Datatype) * p->size);

		//释放旧空间改变指针指向
		free(p->arr);
		p->arr = temp; 
		p->capacity;
	}
}

//堆的创建
// 创建堆的时候需要用到向下调整
void HeapCreate(Heap* p, Datatype *arr, int size, func Comper)
{
	assert(p);
	//动态申请内存完成堆的初始化
	p->arr = (Datatype*)malloc(sizeof(Datatype) * size);
	//检测是否成功开辟空间
	if (NULL == p->arr) {
		assert(0);
		return;
	}
	//更新容量
	p->capacity = size;

	//把你要调整的数据放在你创建的堆里面
	memcpy(p->arr, arr, sizeof(Datatype) * size);
	p->size = size;

	p->Comper = Comper;
	for (int root = (size - 2) / 2; root >= 0; root--)
	{
		HeapAdjustDown(p, root);
	}
}

//插入
void HeapPush(Heap* p, Datatype data)
{
	HeapCheakCapacity(p);
	p->arr[p->size] = data;
	p->size++;
	HeapAdjustup(p, p->size - 1);
}

void HeapErase(Heap* p)
{
	if (HeapEmpty(p)) {
		return;
	}
	Swap(&p->arr[0], &p->arr[p->size - 1]);
	p->size--;
	HeapAdjustDown(p, 0);

}

//获取堆顶元素
//因为我们是顺序表构建的堆
//那他的0号下标必是root
Datatype HeapTop(Heap* p)
{
	assert(p);
	return p->arr[0];
}

//获取堆的长度
int HeapSize(Heap* p)
{
	assert(p);
	return p->size;
}

//检验堆是否为空
int HeapEmpty(Heap* p)
{
	assert(p);

	return 0 == p->size;
}

//堆的销毁
void HeapDestroy(Heap* p)
{
	assert(p);
	//看是否不为空 然后就直接free 最后更新堆的数据
	if (p->arr) {
		free(p->arr);
		p->arr = NULL;
		p->capacity = 0;
		p->size = 0;
	}
}



int main() {

	int arr[] = { 49, 27, 37, 65, 28, 34, 25, 15, 18, 19 };
	Heap p;
	HeapCreate(&p, arr, sizeof(arr) / sizeof(arr[0]), Min);
	printf("top = %d\n", HeapTop(&p));
	printf("size = %d\n", HeapSize(&p));

	HeapPush(&p, 10);
	printf("top = %d\n", HeapTop(&p));
	printf("size = %d\n", HeapSize(&p));

	HeapErase(&p);
	printf("top = %d\n", HeapTop(&p));
	printf("size = %d\n", HeapSize(&p));

	HeapDestroy(&p);
}

2.3接口实现以及算法详解

2.3.1向下调整算法

//向下调整
void HeapAdjustDown(Heap* p, int parent)
{
	int child = (parent << 1) + 1;
	int size = p->size;
	while (child < size) {
		//因为我么对parent * 2 + 1找到的是这个节点对应的左孩子
		//要是我右孩子还小呢
		//利用回调函数找到更小的那一个将child指针移动到右孩子那块
		if (child + 1 < size && p->Comper(p->arr[child + 1], p->arr[child])) {
			child += 1;
		}
		//是否满足堆的特性
		if (p->Comper(p->arr[child], p->arr[parent])) {
			Swap(&p->arr[parent], &p->arr[child]);
			parent = child;
			child = (parent << 1) + 1;
		}
		else {
			return;
		}
	}
}
  1. 我们知道对于二叉树双亲节点乘2+1就是本双亲节点的左孩子。
  2. 假如我现在要创建一个小堆结构,但是我的双亲节点比我两个子节点都大,那就应该有两个指针一个找到最小的孩子,一个指向双亲,然后交换。
  3. 先要判断参数合法性,也就是我有没有右孩子要是我的左孩子++,大于size了那就是没有右孩子了。
  4. 在创建堆的时候我们提供了一个函数指针这样我们就可以根据选用不同的函数进行大堆还是小堆的创建了,这就是一个回调函数,这里就是检验是否右孩子比我左孩子小,检测是然后将这个指针进行移动。
  5. 再往下要检验是否满足你要创建的堆的特性,假如我现在是要创建小堆双亲节点小于两个子节点,那就不交换了,但是发现我比你最小的都大那就你俩交换。
  6. 交换完成之后原来的parent指针移动到子节点,对这个子节点进行运算找到他的左孩子。
  7. 我们的一系列操作都是在循环中完成的,那什么时候结束循环呢,那就是child长于size就终止了被。

2.3.2向上调整算法 

//向上调整
void HeapAdjustup(Heap* p, int child)
{
	int parent = (child - 1) >> 1;
	while (child) {
		if (p->Comper(p->arr[child], p->arr[parent])) {
			Swap(&p->arr[child], &p->arr[parent]);
			child = parent;
			parent = (child - 1) >> 1;
		}
		else {
			return;
		}
	}
}
  1. 跟向下调整简直是异曲同工,你从传参就能看出来,向下调整穿的是parent,也就是你要通过基本性质,获得child这个刚好是反过来,通过child获得parent。
  2. 还是先判断是否满足堆,在交换,循环的出口就是root那块了

2.3.3堆的扩容 

//堆的扩容
void HeapCheakCapacity(Heap* p)
{
	assert(p);
	//判断是否需要扩容
	if (p->size == p->capacity) {
		//把容量变成原来的两倍
		int NewCapaty = p->capacity << 1;
		//从堆上开辟新的内存空间
		Datatype* temp = (Datatype*)malloc(sizeof(Datatype) * NewCapaty);
		if (NULL == temp) {
			assert(0);
			return;
		}

		//把原来的数据拷贝
		memcpy(temp, p->arr, sizeof(Datatype) * p->size);

		//释放旧空间改变指针指向
		free(p->arr);
		p->arr = temp; 
		p->capacity;
	}
}
  1. 这个就跟顺序表的扩容是一样的,就是开辟新空间释放旧空间,然后指针指向新空间
  2. 最后在进行数据更新。

2.3.4堆的创建

//堆的创建
// 创建堆的时候需要用到向下调整
void HeapCreate(Heap* p, Datatype *arr, int size, func Comper)
{
	assert(p);
	//动态申请内存完成堆的初始化
	p->arr = (Datatype*)malloc(sizeof(Datatype) * size);
	//检测是否成功开辟空间
	if (NULL == p->arr) {
		assert(0);
		return;
	}
	//更新容量
	p->capacity = size;

	//把你要调整的数据放在你创建的堆里面
	memcpy(p->arr, arr, sizeof(Datatype) * size);
	p->size = size;

	p->Comper = Comper;
	for (int root = (size - 2) / 2; root >= 0; root--)
	{
		HeapAdjustDown(p, root);
	}
}
  1. 这里就是用到了向下调整算法了,先开辟新空间,拷贝数据。
  2. 最后在进行数据调整,通过对size的改变进行对赋值改变。

这里用这个数组进行堆的创建

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

 2.3.5堆的插入

//插入
void HeapPush(Heap* p, Datatype data)
{
	HeapCheakCapacity(p);
	p->arr[p->size] = data;
	p->size++;
	HeapAdjustup(p, p->size - 1);
}
  1. 先查看是否要扩容之后在进行数据的放入。
  2. 放入数据都是在size位置放的,在进行向上调整

还是刚刚这个数组

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

我插入一个99 

 2.3.6堆的删除

void HeapErase(Heap* p)
{
	if (HeapEmpty(p)) {
		return;
	}
	Swap(&p->arr[0], &p->arr[p->size - 1]);
	p->size--;
	HeapAdjustDown(p, 0);

}
  1. 删除堆是删除堆顶的数据。
  2. 先检验是否为空为空就不进行操作了
  3. 堆顶跟最后一个进行交换
  4. 在向下调整算法。

还是刚刚的数组

2.3.7堆顶元素获取

//获取堆顶元素
//因为我们是顺序表构建的堆
//那他的0号下标必是root
Datatype HeapTop(Heap* p)
{
	assert(p);
	return p->arr[0];
}

2.3.8堆顶长度获取

//获取堆的长度
int HeapSize(Heap* p)
{
	assert(p);
	return p->size;
}

2.3.9堆是否为空

//检验堆是否为空
int HeapEmpty(Heap* p)
{
	assert(p);

	return 0 == p->size;
}

2.3.10堆的销毁

//堆的销毁
void HeapDestroy(Heap* p)
{
	assert(p);
	//看是否不为空 然后就直接free 最后更新堆的数据
	if (p->arr) {
		free(p->arr);
		p->arr = NULL;
		p->capacity = 0;
		p->size = 0;
	}
}

以上代码用的都是跟顺序表一个套路,我在此就不赘述了,顺序表还不会写的同学可以移步这篇文章

数据结构 严薇敏 顺序表的实现(增 删 改)及其使用方法详解

评论 19
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

五毛变向.

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

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

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

打赏作者

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

抵扣说明:

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

余额充值