目录
一、二叉树的存储结构
1.顺序结构
2.链式结构
二、堆的概念及结构
三、堆的实现
1.堆的定义及功能的确立
2.堆的初始化
3.插入数据
4.删除数据
5.其他操作
四、完整代码
二叉树一般可以使用两种结构存储,一种顺序结构,一种链式结构。
顺序结构存储就是使用数组来存储,一般使用数组只适合表示完全二叉树,因为不是完全二叉树会有空间的浪费。而现实中使用中只有堆才会使用数组来存储。二叉树顺序存储在物理上是一个数组,在逻辑上是一个二叉树。
二叉树的链式存储结构是指,用链表来表示一棵二叉树,即用链表来指示元素的逻辑关系。通常的方法是链表中每个结点由三个域组成,数据域和左右指针域,左右指针分别用来给出该结点左孩子和右孩子所在的链结点的存储地址。链式结构又分为二叉链和三叉链。
这里我们来主要讲解二叉树的顺序结构。
普通的二叉树是不适合用数组来存储的,因为可能会存在大量的空间浪费。而完全二叉树更适合使用顺序结构存储。现实中我们通常把堆(一种二叉树)使用顺序结构的数组来存储,需要注意的是这里的堆和操作系统虚拟进程地址空间中的堆是两回事,一个是数据结构,一个是操作系统中管理内存的一块区域分段
概念:如果有一个关键码的集合k = {k_0,k_1,k_2,……,k(n-1)},把它的所有元素按完全二叉树的顺序存储方式存储在一个一维数组中,并满足:k_i <= k(2i+1) 且 k_i <= k(2i+2) (或者k_i >= k(2i+1)且k_i >= k(2i+2)) (i = 0,1,2...),则称为小堆(或大堆)。将根结点最大的堆叫做大根堆,根结点最小的堆叫做小根堆。
堆的性质:
1. 堆中某个结点的值总是不大于或不小于其父结点的值;
2. 堆总是一棵完全二叉树。
3. 已知某一结点在数组中下标为n,其父节点下标为 (n-1) / 2,其左右子结点下标分别为 n*2+1,n*2+2。
大根堆逻辑示意图如下:
大堆和小堆的实现方式类似,大小堆的转换只需要做一点小改动即可。
这里我们以大堆的实现为例:
堆的实现由数组完成,确切的说是以顺序表的方式完成,堆在逻辑上是一棵数,而现实中是一个顺序表。顺序表中定义的结构通常由一个存放数据的数组,两个分别表示当前存放的位置以及容量的整形组成。当然我们要把存放的数据类型进行重命名。
typedef int HeapDateType;
typedef struct Heap {
HeapDateType* date;
int size;
int capacity;
}Hp;
结点定义好之后我们就要想堆该具备的功能。
首先定义完一个堆之后需要将堆中的元素进行初始化,所以我们需要一个初始化函数。其次我们需要两个插入数据、删除数据操作的函数,在删除数据时还要对堆进行空的判断,考虑到后续要对堆的数据进行进一步操作,还需要读取堆顶数据的函数,最后就是对堆进行销毁。这样堆的功能就初步确立了。
//堆的初始化
void HeapInit(Hp* php);
//堆的插入
void HeapPush(Hp* php,HeapDateType x);
//堆的删除
void HeapPop(Hp* php);
//堆的为空判断
bool HeapEmpy(Hp* php);
// 取堆顶的数据
HeapDateType HeapTop(Hp* php);
// 堆的数据个数
int HeapSize(Hp* php);
//堆的销毁
void HeapDestory(Hp* php);
我们在调用堆的时候,首先要定义一个堆的结构体。
int main()
{
Hp heap;
return 0;
}
我们要对这个结构体内的三个元素进行初始化,也就是要改变结构体内的元素,传参时传入的是一个结构体指针。传入之后我们用malloc对数组元素进行空间开辟,并确定初始容量。
//堆的初始化
void HeapInit(Hp* php)
{
assert(php);
php->date = (HeapDateType*)malloc(sizeof(HeapDateType) * 4);
if (!php->date)
{
perror("malloc error");
return;
}
php->size = 0;
php->capacity = 4;
}
大堆的结构决定了我们插入数据的方式只有一种,不存在头插尾插之分,插入的数据比它的父结点大则取代它,直到出现比它大或相等的父结点。这里我们用 (child-1) / 2的方式找到父结点
我们把这个操作称作堆的向上调整,由于这个向上调整操作在对堆进行排序时也能用到,所以我们把它分装成一个单独的函数。在插入函数中对它进行调用即可。传参我们用HeapDateType*类型而不是结构体指针。
//堆的向上调整
void AdjustUp(HeapDateType* php, int child);
我们在向上调整的过程中需要反复进行数据的交换,后续功能的实现中也同样要用到,所以我们不妨把它分装成一个函数。
//结点数据的交换
void Swap(HeapDateType* A, HeapDateType* B)
{
HeapDateType tmp = *A;
*A = *B;
*B = tmp;
}
再回到插入操作,数据的插入是插入在数组最后一个元素的后面,也就是size处,而这里就面临一个容量问题,如果容量不足就会出现数组越界,所以要对容量进行一个判定,容量不足则对数组进行realloc扩容。
//堆的插入
void HeapPush(Hp* php, HeapDateType x)
{
assert(php);
if (php->size == php->capacity)
{
HeapDateType* tmp = (HeapDateType*)realloc(php->date,sizeof(HeapDateType) * php->capacity * 2);
if (!tmp)
{
perror("HeapPush::realloc");
return;
}
php->date = tmp;
php->capacity *= 2;
}
php->date[php->size] = x;
php->size++;
AdjustUp(php->date, php->size - 1);
return;
}
扩容的空间一般是原空间的两倍,扩容完、插入数据后都要记得修改参数。
在向上调整时,我们需要对子节点和父节点进行比较,子大于父则交换数据,反之返回值,比较不一定只有一次,所以外头使用一个循环语句,循环结束条件是子结点下标来到了0。
//堆的向上调整
void AdjustUp(HeapDateType* php, int child)
{
int parent = (child - 1) / 2;
while (child)
{
if (php[parent] < php[child])
{
Swap(&php[parent], &php[child]);
child = parent;
parent = (child - 1) / 2;
}
else
break;
}
}
我们对插入函数进行验证:
#include"Heap.h"
int main()
{
Hp heap;
HeapInit(&heap);
HeapPush(&heap, 3);
HeapPush(&heap, 9);
HeapPush(&heap, 7);
HeapPush(&heap, 12);
HeapPush(&heap, 16);
for (int i = 0; i < heap.size; i++)
printf("%d ", heap.date[i]);
return 0;
}
结果正确。
在堆上删除数据时我们需要考虑一个问题,是该删除堆顶、堆底还是堆中任意位置呢,我们再来看看大堆的结构:
我们可以看到,堆底的数据是8,那么这个8有什么特殊之处?没有,虽然它处于堆的底部,但是数值大小并不是垫底的,当然它也可能是数值最大的,这么一个既不是最大也不是最小的数据对它进行删除好像并没有什么实际用途,堆中的数据亦是同理,反观堆顶,在大堆中,堆顶的数据是整个堆当中最大的,删除最大值也相当于拿取了最大值,然后再让老二顶替,再是老三、老四,如此一来不就可以对堆中的数据进行排序了吗。
所以我们选择删除堆顶数据。但是删除的方式也有讲究。我们可以马上想到的是挪动删除法
从最后一个元素开始,到第二个元素为止,将数据赋值给前一个元素,但是这样一来会面临一个很大的问题:树的紊乱。
可以看到,首结点的子结点1的值超过了它,包括结点2的子结点5的值也超过了它,这已经不符合大堆的结构要求了,因此挪动删除不可取。我们再来看看正确的删除操作:
首先我们让堆顶与堆底数据互换,再删除堆底元素,具体操作就是让参数size--即可。
由于堆顶的数据不再是最大值,所以我们需要进行堆的向下调整操作。
堆顶位置看作parent,通过parent*2+1,parent*2+2可以找到其左右子结点位置,将左右子结点的数据大小进行比较,较大值与parent结点数据进行交换,parent来到交换后的位置处……依次循环,直到parent结点数据不小于左右子结点数据或parent来到叶子结点处为止。
需要注意的是,虽然在逻辑上原堆底数据已经被删除了,但实际上原堆底数据依然存放在原堆底处,我们在进行向下调整操作时一定要注意这点,这时候就要用到我们的参数size进行判定。当parent结点只有左子结点时,也就是parent*2+1 = size - 1时,子结点数据大则交换并终止循环,否则就直接终止循环。
//堆的向下调整
void AdjustDown(Hp* php)
{
assert(php);
if (!HeapisEmpty(php))
return;
int i = 0;
while (i * 2 + 1 < php->size)
{
if (i * 2 + 1 == php->size - 1 && php->date[i] < php->date[i * 2 + 1])
{
Swap(&php->date[i * 2 + 1], &php->date[i]);
return;
}
if (i * 2 + 1 == php->size - 1 && php->date[i] >= php->date[i * 2 + 1])
return;
if (php->date[i] > php->date[i * 2 + 1] && php->date[i] > php->date[i * 2 + 2])
return;
if (php->date[i * 2 + 1] <= php->date[i * 2 + 2] && php->date[i] < php->date[i * 2 + 2])
{
Swap(&php->date[i * 2 + 2], &php->date[i]);
i = i * 2 + 2;
continue;
}
if(php->date[i * 2 + 1] > php->date[i * 2 + 2] && php->date[i] < php->date[i * 2 + 1])
{
Swap(&php->date[i * 2 + 1], &php->date[i]);
i = i * 2 + 1;
}
}
return;
}
剩下的空堆判断,取堆顶数据,堆的数据个数,堆的销毁操作都是线性表常规操作了,这里不过多赘述,直接看代码:
// 空堆判断
bool HeapisEmpy(Hp* php)
{
if (php->size == 0);
return 1;
return 0;
}
// 取堆顶的数据
HeapDateType HeapTop(Hp* php)
{
HeapDateType ret = php->date[0];
php->size--;
AdjustDown(php);
return ret;
}
// 堆的数据个数
int HeapSize(Hp* php)
{
int flag = 0;
while (php->size)
{
HeapTop(php);
flag++;
}
return flag;
}
//堆的销毁
void HeapDestory(Hp* php)
{
assert(php);
free(php->date);
php->date = NULL;
php->capacity = php->size = 0;
return;
}
如此一来堆的实现就告成了,
下面献上完整代码:
//Heap.h
#pragma once
#include
#include
#include
#include
typedef int HeapDateType;
//堆的定义
typedef struct Heap {
HeapDateType* date;//数组
int size;//当前存放的空间
int capacity;//可存放的最大空间
}Hp;
//堆的初始化
void HeapInit(Hp* php);
//结点的交换
void Swap(HeapDateType* A, HeapDateType* B);
//堆的向上调整
void AdjustUp(Hp* php, int child);
//堆的插入
void HeapPush(Hp* php,HeapDateType x);
//堆的向下调整
void AdjustDown(Hp* php);
//堆的删除
void HeapPop(Hp* php);
//堆的为空判断
bool HeapEmpy(Hp* php);
// 取堆顶的数据
HeapDateType HeapTop(Hp* php);
// 堆的数据个数
int HeapSize(Hp* php);
//堆的销毁
void HeapDestory(Hp* php);
//Heap.c
#define _CRT_SECURE_NO_WARNINGS 1
#include"Heap.h"
//堆的初始化
void HeapInit(Hp* php)
{
assert(php);
php->date = (HeapDateType*)malloc(sizeof(HeapDateType) * 4);
if (!php->date)
{
perror("malloc error");
return;
}
php->size = 0;
php->capacity = 4;
}
//结点的交换
void Swap(HeapDateType* A, HeapDateType* B)
{
HeapDateType tmp = *A;
*A = *B;
*B = tmp;
}
//堆的向上调整
void AdjustUp(Hp* php, int child)
{
int parent = (child - 1) / 2;
while (child)
{
if (php->date[parent] < php->date[child])
{
Swap(&php->date[parent], &php->date[child]);
child = parent;
parent = (child - 1) / 2;
}
else
break;
}
}
//堆的插入
void HeapPush(Hp* php, HeapDateType x)
{
assert(php);
if (php->size == php->capacity)
{
HeapDateType* tmp = (HeapDateType*)realloc(php->date,sizeof(HeapDateType) * php->capacity * 2);
if (!tmp)
{
perror("HeapPush::realloc");
return;
}
php->date = tmp;
php->capacity *= 2;
}
php->date[php->size] = x;
php->size++;
AdjustUp(php, php->size - 1);
return;
}
//堆的向下调整
void AdjustDown(Hp* php)
{
assert(php);
if (!HeapisEmpty(php))
return;
int i = 0;
while (i * 2 + 1 < php->size)
{
if (i * 2 + 1 == php->size - 1 && php->date[i] < php->date[i * 2 + 1])
{
Swap(&php->date[i * 2 + 1], &php->date[i]);
return;
}
if (i * 2 + 1 == php->size - 1 && php->date[i] >= php->date[i * 2 + 1])
return;
if (php->date[i] > php->date[i * 2 + 1] && php->date[i] > php->date[i * 2 + 2])
return;
if (php->date[i * 2 + 1] <= php->date[i * 2 + 2] && php->date[i] < php->date[i * 2 + 2])
{
Swap(&php->date[i * 2 + 2], &php->date[i]);
i = i * 2 + 2;
continue;
}
if(php->date[i * 2 + 1] > php->date[i * 2 + 2] && php->date[i] < php->date[i * 2 + 1])
{
Swap(&php->date[i * 2 + 1], &php->date[i]);
i = i * 2 + 1;
}
}
return;
}
//堆的删除
void HeapPop(Hp* php)
{
assert(php);
Swap(&php->date[0], &php->date[php->size-1]);
php->size--;
AdjustDown(php);
}
//堆的为空判断
bool HeapisEmpy(Hp* php)
{
if (php->size == 0);
return 1;
return 0;
}
// 取堆顶的数据
HeapDateType HeapTop(Hp* php)
{
HeapDateType ret = php->date[0];
php->size--;
AdjustDown(php);
return ret;
}
// 堆的数据个数
int HeapSize(Hp* php)
{
int flag = 0;
while (php->size)
{
HeapTop(php);
flag++;
}
return flag;
}
//堆的销毁
void HeapDestory(Hp* php)
{
assert(php);
free(php->date);
php->date = NULL;
php->capacity = php->size = 0;
return;
}