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

文章目录


前言

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


一、堆的概念与结构

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

大根堆:

堆的特点:

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

从二叉树的性质讨论出

对于具有 n 个结点的完全二叉树,如果按照从上至下从左至右的数组顺序对所有结点从
0 开始编号,则对于序号为 i 的结点有:
1. 若 i>0 , i 位置结点的双亲序号: (i-1)/2 ; i=0 , i 为根结点编号,无双亲结点
2. 若 2i+1<n ,左孩子序号: 2i+1 , 2i+1>=n 否则无左孩子
3. 若 2i+2<n ,右孩子序号: 2i+2 , 2i+2>=n 否则无右孩子

二、堆的实现

堆的定义

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

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

1.初始化堆

代码解析:

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

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

2.堆的销毁

代码解析:

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

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

//销毁
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 函数对堆进行调整

//往堆中插入数据(以建小堆为例)
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);
}


总结

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值