Bootstrap

< 数据结构 > 堆的实现

目录

1、前言

        堆的概念

        堆的结构

2、堆的实现

      2.1、准备工作

         创建堆结构

         初始化堆

         堆的打印

         堆的销毁

      2.2、堆调整

         堆的交换

         堆向上调整算法

         堆向下调整算法

      2.3、核心功能

         堆的插入

         堆的删除

         堆的判空

         获取堆的元素个数

         获取堆顶元素

3、总代码

        Heap.h 文件

        Heap.c 文件

        Test.c 文件


1、前言

  • 用一张图回顾下上篇博文最后讲解的知识点:

通过上篇博文的讲解我们得知完全二叉树和满二叉树是可以通过数组来进行存储的,它们间的父子关系可以通过下标来表示。这里再强调下物理结构是是在内存当中实实在在存储的,在物理上是数组,但是在逻辑上要把它看出二叉树。就好比牛奶里没有牛,矿泉水里没有矿,老婆饼里没老婆~

  • 既然完全二叉树和满二叉树适合用数组来存储,那普通的二叉树呢?画图理解:

普通的二叉树是不适合用数组来存储的,因为可能会存在大量的空间浪费。而完全二叉树更适合使用顺序结构存储。因为空间利用率高,不会造成浪费。现实中我们通常把堆(一种二叉树)使用顺序结构的数组来存储需要注意的是这里的堆和操作系统虚拟进程地址空间中的堆是两回事,一个是数据结构,一个是操作系统中管理内存的一块区域分段。如果不是完全二叉树或满二叉树的话,推荐用链式存储,这点在上篇博文已经讲解过,不再赘述。

而堆是一个完全二叉树,可以使用数组存储,接下来就详细讲解下:

堆的概念

由上文得知,堆是一个完全二叉树,它的所有元素按照完全二叉树的顺序存储方式存储在一个一维数组中。堆分为两种:小根堆、大根堆

  1. 小堆:每一个父结点的值均小于等于其对应的子结点的值,而根结点的值就是最小的。
  2. 大堆:每一个父结点的值均大于等于其对应的子结点的值,而根结点的值就是最大的。
  • 堆的性质:
  1. 堆中某个节点的值总是不大于或不小于其父节点的值。
  2. 堆总是一棵完全二叉树。

堆的结构

通过物理结构可以得知以下两点:

  1. 有序的一定是堆
  2. 无序的可能是堆

2、堆的实现

2.1、准备工作

创建堆结构

  • 思路:

由上文得知,堆的基本结构是数组,创建堆结构的时候就跟之前一样动态开辟即可,操作流程也是类似的,直接上代码。不过先以小根堆为例。

  • Heap.h 文件:
//创建堆结构
typedef int HPDataType; //堆中存储数据的类型
typedef struct Heap
{
	HPDataType* a; //用于存储数据
	size_t size; //记录堆中有效元素个数
	size_t capacity; //记录堆的容量
}HP;

初始化堆

  • 思路:

对堆进行初始化,那么传过来的结构体指针不能为空,首先要断言。剩下的操作跟之前顺序表,栈初始化没两样。

  • Heap.h 文件:
//初始化堆
void HeapInit(HP* php);
  • Heap.c 文件:
//初始化堆
void HeapInit(HP* php)
{
	assert(php);
	php->a = NULL;
	php->size = php->capacity = 0;
}

堆的打印

  • 思路:

其实堆的打印很简单,堆的物理结构就是数组,打印堆的实质不还是类似于先前顺序表的打印嘛,依次访问下标打印即可。

  • Heap.h 文件:
//堆的打印
void HeapPrint(HP* php);
  • Heap.c 文件:
//堆的打印
void HeapPrint(HP* php)
{
	assert(php);
	for (size_t i = 0; i < php->size; i++)
	{
		printf("%d ", php->a[i]);
	}
	printf("\n");
}

堆的销毁

  • 思路:

对于动态开辟的内存在使用完毕后要即使进行销毁

  • Heap.h 文件:
//堆的销毁
void HeapDestroy(HP* php);
  • Heap.c 文件:
//堆的销毁
void HeapDestroy(HP* php)
{
	assert(php);
	free(php->a);//释放动态开辟的空间
	php->a = NULL; //置空
	php->size = php->capacity = 0; //置0
}

2.2、堆调整

堆的交换

  • 思路:

堆的交换还是比较简单的,跟之前写的没什么区别,记得传地址。

  • Heap.h 文件:
//交换
void Swap(HPDataType* pa, HPDataType* pb);
  • Heap.c 文件:
//交换
void Swap(HPDataType* pa, HPDataType* pb)
{
	HPDataType tmp = *pa;
	*pa = *pb;
	*pb = tmp;
}

堆向上调整算法

  • 思路:

此算法是为了确保插入数据后的堆依然是符合堆的性质而单独封装出来的函数,就好比如我们后续要插入的数字10,画个图先

为了确保在插入数字10后依然是个小根堆,所以要将10和28交换,依次比较父结点parent和子结点child的大小,当父小于子结点的时候,就返回,反之就一直交换,直到根部。

由前文的得知的规律,parent = (child - 1) / 2,我们操控的是数组,但要把它想象成二叉树。画图演示调整过程:

  • Heap.c 文件:
//向上调整算法
void AdjustUp(HPDataType* a, size_t child)
{
	size_t parent = (child - 1) / 2;
	while (child > 0)
	{
		//if (a[child] > a[parent]) //大根堆
		if (a[child] < a[parent]) //小根堆
		{
			Swap(&a[child], &a[parent]);
			child = parent;
			parent = (child - 1) / 2;
		}
		else
		{
			break;
		}
	}
}

堆向下调整算法

  • 思路:

先用一张图作演示:

此时我们看到,这个二叉树整体上不符合堆的性质,但是其根部的左子树和右子树均满足堆的性质。 接下来,就要进行向下调整,确保其最终是个堆。只需三部曲。

  1. 找出左右孩子中最小的那个
  2. 跟父亲比较,如果比父亲小,就交换
  3. 再从交换的孩子位置继续往下调整

变化图如下:

  • Heap.c 文件:
//向下调整算法
void AdjustDown(HPDataType* a, size_t size, size_t root)
{
	int parent = root;
	int child = 2 * parent + 1;
	while (child < size)
	{
		//1、确保child的下标对应的值最小,即取左右孩子较小那个
		if (child + 1 < size && a[child + 1] < a[child]) //得确保右孩子存在
		{
			child++; //此时右孩子小
		}
		//2、如果孩子小于父亲则交换,并继续往下调整
		if (a[child] < a[parent])
		{
			Swap(&a[child], &a[parent]);
			parent = child;
			child = 2 * parent + 1;
		}
		else
		{
			break; //如果中途满足堆的性质,直接返回
		}
	}
}

2.3、核心功能

堆的插入

  • 注意:

堆的插入不像先前顺序表一般,可以头插,任意位置插入等等,因为是堆,要符合大根堆或小根堆的性质,不能改变堆原本的结构,所以尾插才是最适合的,并且尾插后还要检查是否符合堆的性质。

比如我们有一串数组,该数组是按照小根堆的性质存储的。现在想在数组尾部插入一个数字10,如图:

  • 思路:

这颗树在没插入数字10之前是个小堆,在插入后就不是了,改变了小根堆的性质了。因为子结点10小于其父结点28,那该怎么办呢?

首先最基本的,在插入之前就要先判断该堆的容量是否还够插入数据,先检查要不要扩容,扩容完毕后。我们可以发现,插入的10只会影响到从自己本身开始到根,也就是祖先,只要这条路上符合堆的性质,插入即成功。

核心思想:向上调整算法。当我们看到插入的10比父亲28小时,此时交换数字,但是此时10又要比18小,再次交换,最终发现10比15还小,再次交换,此时结束,到根部了。当然这是最坏的情况,如果在中间换的过程中满足了堆的性质,那么就不需要再换了,直接返回即可。这就叫向上调整算法,直接套用上面的函数即可。

  • Heap.h 文件:
//堆的插入
void HeapPush(HP* php, HPDataType x);
  • Heap.c 文件:
//堆的插入
void HeapPush(HP* php, HPDataType x)
{
	assert(php);
	//检测是否需要扩容
	if (php->size == php->capacity)
	{
		//扩容
		size_t newcapacity = php->capacity == 0 ? 4 : php->capacity * 2;
		HPDataType* tmp = (HPDataType*)realloc(php->a, sizeof(HPDataType) * newcapacity);
		if (tmp == NULL)
		{
			printf("realloc fail\n");
			exit(-1);
		}
		php->a = tmp;
		php->capacity = newcapacity;
	}
	php->a[php->size] = x;
	php->size++;
	//保持继续是堆,向上调整算法
	AdjustUp(php->a, php->size - 1);
}
  • Test.c 文件:
void TestHeap()
{
	HP hp;
	HeapInit(&hp);
	//插入数据
	HeapPush(&hp, 1);
	HeapPush(&hp, 5);
	HeapPush(&hp, 3);
	HeapPush(&hp, 0);
	HeapPush(&hp, 8);
	HeapPush(&hp, 9);
	//打印
	HeapPrint(&hp);
	//销毁
	HeapDestroy(&hp);
}
  • 效果如下:

符合堆的性质。

堆的删除

  • 如图:

  • 思路:

在上文堆的插入中,我们明确插完依旧是堆,而这里堆的删除同样也要确保删除后依旧是堆,注意:这里堆的删除是删除堆顶的数据。以小根堆为例,删除堆顶的数据,也就是把最小的数据删掉,那么还要保证依旧是堆,我给出的思路是:

  • 首先,把第一个数据和最后一个数据进行交换

交换后,此时的堆就不符合其性质了,因为原先最后一个数据肯定是比第一个数据大的,现在最后一个数据到了堆顶,就不是堆了,但是根结点的左子树和右子树不受影响,单独看它们依旧是堆

  • 接着,--size,确保删除堆顶数据

因为此时堆顶的数据已经到了堆尾,只需要像顺序表那样--size,确保有效数据减1也就是确保了堆顶的删除

  • 最后,运用向下调整算法,确保其是堆结构

变化图如下:

时间复杂度分析

第一个数据和最后一个数据交换是O(1),而向下调整算法的时间复杂度为O(logN),因为向下调整是调整高度次,根据结点个数N可以推出高度约为logN

  • Heap.h 文件:
//堆的删除  删除堆顶的数据
void HeapPop(HP* php);
  • Heap.c 文件:
//堆的删除  删除堆顶的数据
void HeapPop(HP* php)
{
	assert(php);
	assert(php->size > 0);//确保size>0
	Swap(&php->a[0], &php->a[php->size - 1]); //交换堆头和堆尾
	php->size--;
	//向下调整,确保仍然是堆结构
	AdjustDown(php->a, php->size, 0);
}
  • Test.c 文件:
void TestHeap2()
{
	HP hp;
	HeapInit(&hp);
	//插入数据
	HeapPush(&hp, 1);
	HeapPush(&hp, 5);
	HeapPush(&hp, 3);
	HeapPush(&hp, 0);
	HeapPush(&hp, 8);
	HeapPush(&hp, 9);
	HeapPrint(&hp);//打印
	//删除堆顶数据
	HeapPop(&hp);
	HeapPrint(&hp);//打印
	//销毁
	HeapDestroy(&hp);
}
  • 效果如下:

堆的判空

  • 思路:

堆的判空很简单,跟之前栈顺序表啥的没区别,若size为0,直接返回即可。

  • Heap.h 文件:
//堆的判空
bool HeapEmpty(HP* php);
  • Heap.c 文件:
//堆的判空
bool HeapEmpty(HP* php)
{
	assert(php);
	return php->size == 0; //size为0即为空
}

获取堆的元素个数

  • 思路:

直接返回size即可。

  • Heap.h 文件:
//堆的元素个数
size_t HeapSize(HP* php);
  • Heap.c 文件:
//堆的元素个数
size_t HeapSize(HP* php)
{
	assert(php);
	return php->size;
}

获取堆顶元素

  • 思路:

直接返回堆顶即可。前提是得断言size>0

  • Heap.h 文件:
//获取堆顶元素
HPDataType HeapTop(HP* php);
  • Heap.c 文件:
//获取堆顶元素
HPDataType HeapTop(HP* php)
{
	assert(php);
	assert(php->size > 0);
	return php->a[0];
}

3、总代码

Heap.h 文件

#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
#include<stdbool.h>

//创建堆结构
typedef int HPDataType; //堆中存储数据的类型
typedef struct Heap
{
	HPDataType* a; //用于存储数据
	size_t size; //记录堆中有效元素个数
	size_t capacity; //记录堆的容量
}HP;

//初始化堆
void HeapInit(HP* php);
//堆的销毁
void HeapDestroy(HP* php);
//堆的打印
void HeapPrint(HP* php);

//交换
void Swap(HPDataType* pa, HPDataType* pb);

//堆的插入
void HeapPush(HP* php, HPDataType x);
//堆的删除  删除堆顶的数据
void HeapPop(HP* php);

//堆的判空
bool HeapEmpty(HP* php);
//堆的元素个数
size_t HeapSize(HP* php);
//获取堆顶元素
HPDataType HeapTop(HP* php);

Heap.c 文件

#define _CRT_SECURE_NO_WARNINGS 1
#include"Heap.h"
//初始化堆
void HeapInit(HP* php)
{
	assert(php);
	php->a = NULL;
	php->size = php->capacity = 0;
}

//堆的销毁
void HeapDestroy(HP* php)
{
	assert(php);
	free(php->a);
	php->a = NULL; //置空
	php->size = php->capacity = 0; //置0
}
//堆的打印
void HeapPrint(HP* php)
{
	assert(php);
	for (size_t i = 0; i < php->size; i++)
	{
		printf("%d ", php->a[i]);
	}
	printf("\n");
}

//交换
void Swap(HPDataType* pa, HPDataType* pb)
{
	HPDataType tmp = *pa;
	*pa = *pb;
	*pb = tmp;
}

//向上调整算法
void AdjustUp(HPDataType* a, size_t child)
{
	size_t parent = (child - 1) / 2;
	while (child > 0)
	{
		//if (a[child] > a[parent]) //大根堆
		if (a[child] < a[parent]) //小根堆
		{
			Swap(&a[child], &a[parent]);
			child = parent;
			parent = (child - 1) / 2;
		}
		else
		{
			break;
		}
	}
}

//向下调整算法
void AdjustDown(HPDataType* a, size_t size, size_t root)
{
	int parent = root;
	int child = 2 * parent + 1;
	while (child < size)
	{
		//1、确保child的下标对应的值最小,即取左右孩子较小那个
		if (child + 1 < size && a[child + 1] < a[child]) //得确保右孩子存在
		{
			child++; //此时右孩子小
		}
		//2、如果孩子小于父亲则交换,并继续往下调整
		if (a[child] < a[parent])
		{
			Swap(&a[child], &a[parent]);
			parent = child;
			child = 2 * parent + 1;
		}
		else
		{
			break;
		}
	}
}

//堆的插入
void HeapPush(HP* php, HPDataType x)
{
	assert(php);
	//检测是否需要扩容
	if (php->size == php->capacity)
	{
		//扩容
		size_t newcapacity = php->capacity == 0 ? 4 : php->capacity * 2;
		HPDataType* tmp = (HPDataType*)realloc(php->a, sizeof(HPDataType) * newcapacity);
		if (tmp == NULL)
		{
			printf("realloc fail\n");
			exit(-1);
		}
		php->a = tmp;
		php->capacity = newcapacity;
	}
	php->a[php->size] = x;
	php->size++;
	//保持继续是堆,向上调整算法
	AdjustUp(php->a, php->size - 1);
}


//堆的删除  删除堆顶的数据
void HeapPop(HP* php)
{
	assert(php);
	assert(php->size > 0);//确保size>0
	Swap(&php->a[0], &php->a[php->size - 1]); //交换堆头和堆尾
	php->size--;
	//向下调整,确保仍然是堆结构
	AdjustDown(php->a, php->size, 0);
}


//堆的判空
bool HeapEmpty(HP* php)
{
	assert(php);
	return php->size == 0; //size为0即为空
}

//堆的元素个数
size_t HeapSize(HP* php)
{
	assert(php);
	return php->size;
}

//获取堆顶元素
HPDataType HeapTop(HP* php)
{
	assert(php);
	assert(php->size > 0);
	return php->a[0];
}

Test.c 文件

#define _CRT_SECURE_NO_WARNINGS 1
#include"Heap.h"
void TestHeap1()
{
	HP hp;
	HeapInit(&hp);
	//插入数据
	HeapPush(&hp, 1);
	HeapPush(&hp, 5);
	HeapPush(&hp, 3);
	HeapPush(&hp, 0);
	HeapPush(&hp, 8);
	HeapPush(&hp, 9);
	//打印
	HeapPrint(&hp);
	//销毁
	HeapDestroy(&hp);
}
void TestHeap2()
{
	HP hp;
	HeapInit(&hp);
	//插入数据
	HeapPush(&hp, 1);
	HeapPush(&hp, 5);
	HeapPush(&hp, 3);
	HeapPush(&hp, 0);
	HeapPush(&hp, 8);
	HeapPush(&hp, 9);
	HeapPrint(&hp);//打印
	//删除堆顶数据
	HeapPop(&hp);
	HeapPrint(&hp);//打印
	//销毁
	HeapDestroy(&hp);
}
int main()
{
	//TestHeap1();
	TestHeap2();
	return 0;
}
;