【数据结构入门指南】二叉树顺序结构: 堆及实现(全程配图,非常经典)
- 一、前言:二叉树的顺序结构
 - 二、堆的概念及结构
 - 三、堆的实现(本篇博客以实现小堆为例)
 - 3.1 准备工作
 - 3.2 初始化
 - 3.3 堆的插入
 - 3.3.1 向上调整算法
 
- 3.4 堆的删除
 - 3.4.1 向下调整算法
 
- 3.5 堆的判空(<font color=orange>接下的过于简单直接给出带啊吗)
 - 3.6 取堆顶的数据
 - 3.7 堆的个数
 - 3.8 堆的销毁
 
- 四、所有代码
 

一、前言:二叉树的顺序结构
普通的二叉树是不适合用数组来存储的,因为可能会存在大量的空间浪费。而完全二叉树更适合使用顺序结构存储。
现实中我们通常把堆(一种二叉树)使用顺序结构的数组来存储,需要注意的是这里的堆和操作系统虚拟进程地址空间中的堆是两回事,一个是数据结构,一个是操作系统中管理内存的一块区域分段。

二、堆的概念及结构
堆是一种特殊的数据结构,可以将堆分为大顶堆和小顶堆两种形式。堆中的每个节点都有一个值,并且满足以下两个条件:
①:堆的父节点的值总是大于或等于其子节点的值(大顶堆)或者小于或等于其子节点的值(小顶堆)。
②:堆是完全二叉树,即除了最底层外,其他层的节点都是满的,并且最底层的节点都尽量靠左排列。
大(小)堆示意图:

 
三、堆的实现(本篇博客以实现小堆为例)
3.1 准备工作
由于堆是通过数组来实现的,所以我们也和顺序表一样,首先要创建一个结构体来存储:数组指针 + 容量 + 存储数据大小。
代码实现:
typedef int HPDataType;
typedef struct Heap
{
	HPDataType* a;//数组指针
	int size;//存储数据大小
	int capacity;//容量
}HP;
 
3.2 初始化
初始化有两种方式:
①:初始化时为数组开辟一定大小空间。②:直接将数组指针置为NULL,插入数据过程中在进一步处理。(本篇博客采用第二种)
代码实现:
void HPInit(HP* php)
{
	assert(php);
	php->a = NULL;
	php->capacity = 0;
	php->size = 0;
}
 
3.3 堆的插入
【代码思路】:
①:在插入数据前,我们首先要判断是否要扩容的问题。由于前面初始化时我们直接置空,所以我们先判断容量是否为空。如果为空开4个空间,否则空间扩大到原来的2倍。(为空时,第一次具体开辟多少空间读者可自行选择,本篇博客开4)
②:接下来就是插入数据了!但有一个问题,直接插入数据可能会破坏堆的结构,所以我们采用了向上调整算法。
代码:
void HPPush(HP* php, HPDataType x)
{
	assert(php);
	//扩容
	if (php->size == php->capacity)
	{
		int newcapacity = php->size == 0 ? 4 : php->capacity * 2;
		HPDataType* tmp = (HPDataType*)realloc(php->a,sizeof(HPDataType) * newcapacity);
		if (tmp == NULL)
		{
			perror("malloc fail");
			exit(-1);
		}
		php->a = tmp;
		php->capacity = newcapacity;
	}
	
	//插入数据
	php->a[php->size] = x;
	php->size++;
	//向上调整
	AdjustUp(php->a, php->size - 1);
}
 
3.3.1 向上调整算法
【代码思路】:由于插入数据时,影响的只是插入数据到根节点间的关系。所以我们只需将插入数据通过双亲节点调到合适位置即可。
Tips:父节点和子节点关系
- leftchild = parent *2 + 1
 - rightchild = parent * 2 + 2
 - parent = (child - 1)/2
 

 代码:
void Swap(HPDataType* p1, HPDataType* p2)
{
	HPDataType tmp = *p1;
	*p1 = *p2;
	*p2 = tmp;
}
void AdjustUp(HPDataType* a, int child)
{
	assert(a);
	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;
		}		
	}
}
 
3.4 堆的删除
【代码思路】:堆的删除默认是头删。删除有两种思路:
①:将根节点删除,再将其余数据全部向前移动。但这样做会造成一个问题:挪动覆盖导致父子节点的关系全乱了,需要重新建堆。为了删个数据,重新建堆,得不偿失,所以我们采用下面这种方法。
②:将堆顶的1数据和最后一个数据交换,在删除最后一个数据。堆顶数据在通过向下调整算法调到合适位置即可。

代码:
void HPPop(HP* php)
{
	assert(php);
	assert(!HPEmpty(php));//非空,还有数据可删。该接口后面实现
	
	Swap(&php->a[0], &php->a[php->size - 1]);//交换
	php->size--;//删除
	//向下调整
	AdjustDown(php->a, php->size, 0);
}
 
3.4.1 向下调整算法
【代码思路】:向下调整算法有一个前提:左右子树必须是一个堆,才能调整。同时还要注意是调大堆还是小堆。
调小堆:堆顶元素和孩子中最小的节点比较,如果父节点大于较小的子节点子,两者交换。不断向下调整到合适位置。(调大堆,和较大孩子比较)

代码:
void AdjustDown(HPDataType* a, int n, int parent)
{
	assert(a);
	int child = parent * 2 + 1;//假设左孩子更小
	while (child < n)
	{
		//如果有孩子存在,且有孩子更小,则左孩子加1移到右孩子
		if ((child + 1) < n && a[child] > a[child + 1])
		{
			child++;
		}
		if (a[parent] > a[child])//交换
		{
			Swap(&a[parent], &a[child]);
			parent = child;
			child = parent * 2 + 1;
		}
		else
		{
			//父节点小于较小的子节点,说明已经调到合适位置,此时break跳出
			break;
		}
	}
}
 
3.5 堆的判空(接下的过于简单直接给出带啊吗)
bool HPEmpty(HP* php)
{
	return php->size == 0;
}
 
3.6 取堆顶的数据
	assert(php);
	assert(!HPEmpty(php));//断言堆中还有元素
	return php->a[0];
 
3.7 堆的个数
int HPSize(HP* php)
{
	assert(php);
	return php->size;
}
 
3.8 堆的销毁
void HPDestory(HP* php)
{
	assert(php);
	php->a = NULL;
	php->capacity = php->size = 0;
}
 
四、所有代码
Heap.h
#pragma once
#include <stdio.h>
#include <assert.h>
#include <stdlib.h>
#include <stdbool.h>
typedef int HPDataType;
typedef struct Heap
{
	HPDataType* a;
	int size;
	int capacity;
}HP;
void HPInit(HP* php);//初始化
void HPPush(HP* php, HPDataType x);//插入数据
void HPPop(HP* php);//删除数据
int HPTop(HP* php);//堆顶元素
bool HPEmpty(HP* php);//判空
int HPSize(HP* php);//数据个数
void HPDestory(HP* php);//销毁
 
Heap.c
#include "Heap.h"
void HPInit(HP* php)
{
	assert(php);
	php->a = NULL;
	php->capacity = 0;
	php->size = 0;
}
void Swap(HPDataType* p1, HPDataType* p2)
{
	HPDataType tmp = *p1;
	*p1 = *p2;
	*p2 = tmp;
}
void AdjustUp(HPDataType* a, int child)
{
	assert(a);
	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 AdjustDown(HPDataType* a, int n, int parent)
{
	assert(a);
	int child = parent * 2 + 1;
	while (child < n)
	{
		if ((child + 1) < n && a[child] > a[child + 1])
		{
			child++;
		}
		if (a[parent] > a[child])
		{
			Swap(&a[parent], &a[child]);
			parent = child;
			child = parent * 2 + 1;
		}
		else
		{
			break;
		}
	}
}
void HPPush(HP* php, HPDataType x)
{
	assert(php);
	//扩容
	if (php->size == php->capacity)
	{
		int newcapacity = php->size == 0 ? 4 : php->capacity * 2;
		HPDataType* tmp = (HPDataType*)realloc(php->a,sizeof(HPDataType) * newcapacity);
		if (tmp == NULL)
		{
			perror("malloc fail");
			exit(-1);
		}
		php->a = tmp;
		php->capacity = newcapacity;
	}
	php->a[php->size] = x;
	php->size++;
	//向上调整
	AdjustUp(php->a, php->size - 1);
}
bool HPEmpty(HP* php)
{
	return php->size == 0;
}
void HPPop(HP* php)
{
	assert(php);
	assert(!HPEmpty(php));
	
	Swap(&php->a[0], &php->a[php->size - 1]);
	php->size--;
	//向下调整
	AdjustDown(php->a, php->size, 0);
}
int HPTop(HP* php)
{
	assert(php);
	assert(!HPEmpty(php));
	return php->a[0];
}
int HPSize(HP* php)
{
	assert(php);
	return php->size;
}
void HPDestory(HP* php)
{
	assert(php);
	php->a = NULL;
	php->capacity = php->size = 0;
}
 

 



















