【初阶数据结构】树——二叉树——堆(中)

文章目录

前言

一、堆的概念与结构

二、堆的实现

堆的定义

1.初始化堆

2.堆的销毁

3.堆的插入

3.1向上调整算法

4.堆的判空

5.求有效个数

6.删除堆顶数据

6.1向下调整算法

7.获取栈顶数据

三、完整源码

总结


前言

上篇了解树和二叉树相关的概念,这篇学习一种特殊的二叉树--堆,通过认识堆来实现堆和堆的应用。


一、堆的概念与结构

如果有一个关键码的集合 K = { k 0 , k 1 , k 2 , ..., k n −1 } ,把它的所有元素按完全二叉树的顺序存储方式存储,在一个一维数组中,并满足: K i <= K 2∗ i +1 ( K i >= K 2∗ i +1 且 K i <= K 2∗ i+2 ), i = 0、1、2... ,则称为小堆(或大堆)。将根结点最大的堆叫做最大堆或大根堆,根结点最小的堆叫做最小堆或小根堆。
小根堆:

大根堆:

堆的特点:

  1. 堆中某个结点的值总是不大于或不小于其父结点的值;
  2. 堆总是一棵完全二叉树。
    从二叉树的性质讨论出
    对于具有 n 个结点的完全二叉树,如果按照从上至下从左至右的数组顺序对所有结点从
    0 开始编号,则对于序号为 i 的结点有:
  3. 若 i>0 , i 位置结点的双亲序号: (i-1)/2 ; i=0 , i 为根结点编号,无双亲结点
  4. 若 2i+1<n ,左孩子序号: 2i+1 , 2i+1>=n 否则无左孩子
  5. 若 2i+2<n ,右孩子序号: 2i+2 , 2i+2>=n 否则无右孩子

二、堆的实现

堆的定义

由此,我们可知堆的底层是数组来实现的,则堆的结构是顺序结构,可写出堆的结构定义

cpp 复制代码
typedef int HPDatatype;
//堆的结构
typedef struct Heap
{
	HPDatatype* arr;
	int size;      //有效数据个数
	int capacity; //容量
}HP;

1.初始化堆

代码解析:

先对未开辟空间的数组指针置为空,再对有效个数和容量大小都置为0.

cpp 复制代码
//初始化
void HPIint(HP* php)
{
	assert(php);
	php->arr = NULL;
	php->size = php->capacity = 0;
}

2.堆的销毁

代码解析:

先判断堆是否为空,不为空先对数组进行释放再置为NULL,再对有效个数和容量大小都置为0

注:对堆开辟空间使用完后就要对堆进行销毁,避免造成空间浪费,因此要对堆进行销毁

cpp 复制代码
//销毁
void HPDesTroy(HP* php)
{
	//判断堆是否为空,不为空就直接free再置空
    assert(php);
	if (php->arr)
		free(php->arr);
	php->arr = NULL;
	php->size = php->capacity = 0;
}

3.堆的插入

代码解析:

堆的插入就是在尾部进行数据插入。先判断堆是否已满,堆已满就进行 realloc 增容并更新capacity。增容后,把插入进来的数据进行重新调整,用 AdjustUp 函数对堆进行调整

cpp 复制代码
//往堆中插入数据(以建小堆为例)
void HPPush(HP* php, HPDatatype x)
{
	assert(php);
	if (php->size == php->capacity)
	{
		//增容
		int newCapacity = php->capacity == 0 ? 4 : 2 * php->capacity;
		HPDatatype* tmp = (HPDatatype*)realloc(php->arr, sizeof(HPDatatype) * newCapacity);
		if (tmp == NULL)
		{
			perror("realloc fail!\n");
			exit(1);
		}
		php->arr = tmp;
		php->capacity = newCapacity;
	}
	//插入数据
	php->arr[php->size] = x;
	//插入数据后用向上调整方法来调整成小堆或者大堆
	AdjustUp(php->arr, php->size);
	++php->size;
}

3.1向上调整算法

接下来给大家介绍AdjustUp 函数

代码解析:
先将元素插入到堆的末尾,即最后一个孩子之后
插入之后如果堆的性质遭到破坏,将新插入结点顺着其双双亲往上调整到合适位置即可

由二叉树的性质,我们可知已知孩子结点可求父结点:Parent =(child-1)/2。在这里我们建小堆,要求孩子结点大于父结点,如果不满足就对进行交换,在让child 走到parent ,parent 走到parent 的父结点;如果满足不用交换直接跳出循环。

复制代码
//向上调整算法:
void AdjustUp(HPDatatype* arr, int child)
{
	//已知子节点下标,来求父节点下标
	int parent = (child - 1) / 2;
	while (child>0)
	{
		if (arr[child] > arr[parent])//建大堆就大于
		{
			Swap(&arr[child], &arr[parent]);
			//交换后,子节点会跑到旧位置的父节点,则再求新位置的父节点
			child = parent;
			parent = (child - 1) / 2;
		}
		else {
			break;//如果子节点大于父节点,则不需要变化直接跳出循环
		}
	}
}

时间复杂度:
因为堆是完全⼆叉树,⽽满⼆叉树也是完全⼆叉树,此处为了简化使⽤满⼆叉树来证明(时间复杂度本来看的就是近似值,多⼏个结点不影响最终结果)
分析:
第1层, 2 0 个结点,需要向上移动0层
第2层, 2 1 个结点,需要向上移动1层
第3层, 2 2 个结点,需要向上移动2层
第4层, 2 3 个结点,需要向上移动3层
......
第h层, 2 h −1 个结点,需要向上移动h-1层
则需要移动结点总的移动步数为:每层结点个数 * 向上调整次数(第⼀层调整次数为0)

4.堆的判空

代码解析:用bool函数来判断堆是否为空

复制代码
bool HPEmpty(HP* php)
{
	assert(php);
	return php->size == 0;
}

5.求有效个数

代码解析:直接返回有效个数即可

复制代码
int HPSize(HP* php)
{
	assert(php);
	return php->size;
}

6.删除堆顶数据

代码解析:

先判断堆是否为空堆,是就返回0,不是返回有效数据;让根结点和最后一个元素进行交换,把交换后的最后一个元素删除后,再进行向下调整算法

复制代码
//删除堆顶数据(以建小堆为例)
void HPPop(HP* php)
{
	assert(!HPEmpty(php));
	//交换根节点和最后一个节点,再对新的最后一个节点进行删除
	Swap(&php->arr[0], &php->arr[php->size - 1]);
	--php->size;
	//使用向下调整算法
	AdjustDown(php->arr, 0, php->size);

}

6.1向下调整算法

代码解析:
将堆顶元素与堆中最后一个元素进行交换
删除堆中最后一个元素
将堆顶元素向下调整到满足堆特性为止

由二叉树的性质,我们可知,已知parent 结点的下标,就可求左,右孩子结点的下标,左下标:2(parent )+1=child, 右下标:2parent +2=child 。把最后一个元素删除后,这里需要建小堆,先找到孩子结点中较小的结点,把父结点和较小的孩子结点进行交换,再让父结点走到较小的孩子结点的交换前的位置,再更新孩子结点的下标;如果父结点小于孩子结点就不用交换,直接跳出循环。

复制代码
//向下调整算法
void AdjustDown(HPDatatype* arr, int parent, int n)
{
	//已知父节点,求子节点又可求左右子节点
	int child = parent * 2 + 1;//左子节点
	while (child<n)
	{
		//找到最小的节点,让其与父节点进行交换,谁小谁往上调(如果是大堆,谁大谁往上调)
		if (child + 1 <n && arr[child] > arr[child + 1])
		{
			child++;
		}
		if (arr[child] < arr[parent])
		{
			Swap(&arr[child], &arr[parent]);
			//父节点走到旧的字节点下标,再求新子节点下标
			parent = child;
			child = parent * 2 + 1;
		}
		else {
			break;
		}
	}
}

时间复杂度:
第1层, 2 0 个结点,需要向下移动h-1层
第2层, 2 1 个结点,需要向下移动h-2层
第3层, 2 2 个结点,需要向下移动h-3层
第4层, 2 3 个结点,需要向下移动h-4层
......
第h-1层, 2 h −2 个结点,需要向下移动1层

则需要移动结点总的移动步数为:每层结点个数 * 向下调整次数

注:堆的向上调整算法和向下调整算法都可以建大堆和小堆。向上调整算法主要用于堆插入,向下调整算法主要用于堆应用和堆排序。通过两者得出的时间复杂度,可知向下调整算法时间复杂度比向上调整算法复杂度好。

7.获取栈顶数据

代码解析:直接返回栈顶的数据

复制代码
HPDatatype HPTop(HP* php)
{
	assert(!HPEmpty(php));
	return php->arr[0];
}

三、完整源码

Heap,h:

复制代码
#include<stdio.h>
#include<assert.h>
#include<stdlib.h>
#include<stdbool.h>

//一、用顺序结构实现完全二叉树,底层是数组
typedef int HPDatatype;
//堆的结构
typedef struct Heap
{
	HPDatatype* arr;
	int size;      //有效数据个数
	int capacity; //容量
}HP;

//初始化
void HPIint(HP* php);
//销毁
void HPDesTroy(HP* php);
//往堆中插入数据
void HPPush(HP* php, HPDatatype x);
//删除堆顶数据
void HPPop(HP* php);
//判空
bool HPEmpty(HP* php);
//求size
int HPSize(HP* php);
//获取栈顶数据
HPDatatype HPTop(HP* php);

//向上调整算法:
void AdjustUp(HPDatatype* arr, int child);
//向下调整算法
void AdjustDown(HPDatatype* arr, int parent, int n);

Heap,c:

复制代码
#include"Heap.h"

//初始化
void HPIint(HP* php)
{
	assert(php);
	php->arr = NULL;
	php->size = php->capacity = 0;
}
//销毁
void HPDesTroy(HP* php)
{
	assert(php);
	//判断堆是否为空,不为空就直接free再置空
	if (php->arr)
		free(php->arr);
	php->arr = NULL;
	php->size = php->capacity = 0;
}


//交换:父节点与子节点比较,谁小谁往上调(谁大谁往上调)
void Swap(int* x, int* y)
{
	int temp = *x;
	*x = *y;
	*y = temp;
}

//向上调整算法:
void AdjustUp(HPDatatype* arr, int child)
{
	//已知子节点下标,来求父节点下标
	int parent = (child - 1) / 2;
	while (child>0)
	{
		if (arr[child] > arr[parent])//建大堆就大于
		{
			Swap(&arr[child], &arr[parent]);
			//交换后,子节点会跑到旧位置的父节点,则再求新位置的父节点
			child = parent;
			parent = (child - 1) / 2;
		}
		else {
			break;//如果子节点大于父节点,则不需要变化直接跳出循环
		}
	}
}

//往堆中插入数据(以建小堆为例)
void HPPush(HP* php, HPDatatype x)
{
	assert(php);
	if (php->size == php->capacity)
	{
		//增容
		int newCapacity = php->capacity == 0 ? 4 : 2 * php->capacity;
		HPDatatype* tmp = (HPDatatype*)realloc(php->arr, sizeof(HPDatatype) * newCapacity);
		if (tmp == NULL)
		{
			perror("realloc fail!\n");
			exit(1);
		}
		php->arr = tmp;
		php->capacity = newCapacity;
	}
	//插入数据
	php->arr[php->size] = x;
	//插入数据后用向上调整方法来调整成小堆或者大堆
	AdjustUp(php->arr, php->size);
	++php->size;
}

///
//对堆进行判断是否为空
bool HPEmpty(HP* php)
{
	assert(php);
	return php->size == 0;
}
//求size
int HPSize(HP* php)
{
	assert(php);
	return php->size;
}

//向下调整算法
void AdjustDown(HPDatatype* arr, int parent, int n)
{
	//已知父节点,求子节点又可求左右子节点
	int child = parent * 2 + 1;//左子节点
	while (child<n)
	{
		//找到最小的节点,让其与父节点进行交换,谁小谁往上调(如果是大堆,谁大谁往上调)
		if (child + 1 <n && arr[child] > arr[child + 1])
		{
			child++;
		}
		if (arr[child] < arr[parent])
		{
			Swap(&arr[child], &arr[parent]);
			//父节点走到旧的字节点下标,再求新子节点下标
			parent = child;
			child = parent * 2 + 1;
		}
		else {
			break;
		}
	}
}

//删除堆顶数据(以建小堆为例)
void HPPop(HP* php)
{
	assert(!HPEmpty(php));
	//交换根节点和最后一个节点,再对新的最后一个节点进行删除
	Swap(&php->arr[0], &php->arr[php->size - 1]);
	--php->size;
	//使用向下调整算法
	AdjustDown(php->arr, 0, php->size);

}

//获取栈顶数据
HPDatatype HPTop(HP* php)
{
	assert(!HPEmpty(php));
	return php->arr[0];
}

test,c:

复制代码
#include"Heap.h"

void test()
{
	HP hp;
	HPIint(&hp);
	HPPush(&hp, 6);
	HPPush(&hp, 4);
	HPPush(&hp, 8);
	HPPush(&hp, 10);
	
	//HPPop(&hp);
	//HPPop(&hp);
	//HPPop(&hp);
	//HPPop(&hp);

    while (!HPEmpty(&hp))
	{
		printf("%d ", HPTop(&hp));
		HPPop(&hp);//取栈顶就要删除栈顶,不然会一直循环
	}
	HPDesTroy(&hp);
}

总结

非常感谢大家阅读完这篇博客。希望这篇文章能够为您带来一些有价值的信息和启示。如果您发现有问题或者有建议,欢迎在评论区留言,我们一起交流学习。

相关推荐
PixelMind6 分钟前
【LUT技术专题】基于扩展卷积的极快速LUT算法
算法
彷徨而立7 分钟前
【C/C++】errno/strerror 和 GetLastError()/FormatMessage 的区别
c语言·windows api
jie1889457586625 分钟前
python--------修改桌面文件内容
java·数据库·python
caihuayuan426 分钟前
【Azure Redis 缓存】关于Azure Cache for Redis 服务在传输和存储键值对(Key/Value)的加密问题
java·大数据·sql·spring·课程设计
末央&36 分钟前
【数据结构】手撕二叉搜索树
开发语言·数据结构·c++
SeasonedDriverDG43 分钟前
GEC6818蜂鸣器驱动开发
算法
珹洺1 小时前
C++从入门到实战(十二)详细讲解C++如何实现内存管理
c语言·开发语言·数据结构·c++
iuyou️1 小时前
Java泛型
java·开发语言·javase·范型
悦悦子a啊1 小时前
第八章--图
数据结构·c++·算法