🌈个人主页:Yui_
🌈C语言笔记专栏:C语言笔记
🌈数据结构专栏:数据结构
文章目录
1.二叉树的顺序结构及实现
1.1 二叉树的顺序结构
普通的二叉树是不适合用数组存储的,因为可能会存在大量的空间浪费。而完全二叉树更适合使用顺序结构存储。实现中我们通常把堆(一种二叉树)使用顺序结构的数组来存储,需要注意的是这里的堆和操作系统的虚拟进程地址空间的堆是两回事,一个是数据结构,一个是操作系统中管理内存的一块区域分段。
1.2 堆的概念及结构
如果有一个关键码的集合K = {k0,k1,k2,…,kn-1},把它的所有元素按完全二叉树的顺序存储方式存储在一个一维数组中,并满足:Ki<=K2*i+1且Ki<=k2*+2(Ki>=K2*i+1且Ki>=K2\2+2)i = 0,1,2…,则称为小堆(或大堆),将根节点最大的堆叫最大堆或大根堆,根节点最小的堆叫做最小堆或小根堆。
堆的性质:
- 堆中的某个节点的值总是不大于或不小于其父节点的值。
- 堆总是一颗完全二叉树。
1.3堆的实现
1.3.1 准备工作
定义一个堆的结构体,结构体中维护着,堆的数据域,堆的有效数据个数,堆的空间大小。
#define DataType int
typedef struct Heap
{
DataType* a;
int size;//有效数据个数
int capacity;//堆的空间大小
}Heap;
1.3.2 堆的初始化
初始化时先不给空间。
//堆的初始化
void InitHeap(Heap* ph)
{
assert(ph);
ph->a = NULL;
ph->capacity = ph->size = 0;
}
1.3.3 堆的销毁
只要使用的动态内存就要释放
//堆的销毁
void DestoryHeap(Heap* ph)
{
assert(ph);
free(ph->a);
ph->a = NULL;
ph->capacity = ph->size = 0;
}
1.3.4 堆的插入(重点)
在向调整前的代码都是写过好几遍的代码了,大家一看就懂了。
堆的插入后的向上调整是大家可能不熟悉的,堆在插入数据时会有2种情况(以小堆为例):
- 插入的数据刚好大于其父节点,无需调整。
- 插入的数据小于其父节点,需要调整。
了解完情况后,要怎么向上调整呢?
为了让这个堆回归正常,就必须人父节点小于子节点,那么就让新插入的子节点与父节点比较,如果子节点小于父节点,就交换它们的值,交换后再更新子节点的坐标继续向上比较,直到子节点更新到根节点的时候。注意:求父节点坐标的公式为(child-1)/2
为此我们需要传给向上调整函数的就有数组,数组元素个数,子节点坐标。
向上调整代码
//向上调整
void Adjustup(DataType* a, int n, int child)
{
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 PushHeap(Heap* ph, DataType x)
{
assert(ph);
if (ph->size == ph->capacity)//空间不够,开始扩容
{
int newcapacity = ph->capacity == 0 ? 4 : ph->capacity * 2;//注意初始时capacity的大小为0,特判一下。
DataType* tmp = (DataType*)realloc(ph->a, sizeof(DataType) * newcapacity);
if (tmp == NULL)//防止扩容失败
{
perror("realloc");
exit(-1);
}
ph->a = tmp;
ph->capacity = newcapacity;
}
ph->a[ph->size] = x;//插入数据
ph->size += 1;
//向上调整
Adjustup(ph->a, ph->size, ph->size - 1);
}
1.3.5 堆的删除(非常重要)
堆的删除是删根节点
提问:为什么需要交换首尾数据呢?
回答:堆的删除是删除根节点,可是当根节点被删除时,剩下的数据要怎么办呢,难道把就直接把15放到前面充当新的根节点吗,显然是不行的。
有关很棒的办法就是交换首尾数据然后再删除堆尾数据,这样就把根节点删除了,虽然此时并不满足小堆的特征,但是只要执行了向下调整函数就可以变为正常的小堆了。
提问:为什么会有向下调整呢?
回答:上图中的首尾数据交换然后再删除堆尾数据后,此时结构已经不在为小堆了,为了让其变回笑堆,需要执行向下调整
因为向下调整的目的是为了把值比根节点小的值往上推,为了实现这个目的,就需要让父节点和子节点比较,并且还有是子节点中最小的节点比较,如果子节点小于父节点就与父节点交换数据,然后更新父节点,重复执行直到子节点超出数组范围时,循环结束。
向下调整
//向下调整
void AdjustDown(DataType* a, int n, int parent)
{
int child = parent * 2 + 1;
while (child<n)
{
//child+1<n的目的是防止数组越界
if (child+1 < n && a[child] > a[child + 1])//找子节点最小的
{
child = child + 1;
}
if (a[child] < a[parent])
{
swap(&a[child], &a[parent]);
parent = child;
child = parent * 2 + 1;
}
else
{
break;
}
}
}
//堆的删除
void PopHeap(Heap* ph)
{
assert(ph);
assert(ph->size > 0);//为空就不能删
swap(&ph->a[0], &ph->a[ph->size - 1]);
ph->size -= 1;
AdjustDown(ph->a, ph->size, 0);
}
1.3.6 返回堆顶元素
唯一要注意的就是当堆为空时就不要返回了。
//返回堆顶元素
DataType TopHeap(Heap* ph)
{
assert(ph);
if(!EmptyHHeap(ph))
return ph->a[0];
}
1.3.7 判断堆是否为空
//判断堆是否为空
bool EmptyHHeap(Heap* ph)
{
assert(ph);
return ph->size == 0;
}
1.3.8 打印堆数据
//打印堆数据
void PrintHeap(Heap* ph)
{
assert(ph);
for (int i = 0; i < ph->size; ++i)
{
printf("%d ", ph->a[i]);
}
printf("\n");
}
1.4 代码整合
//heap.h
#include <stdio.h>
#include <assert.h>
#include <stdlib.h>
#include <stdbool.h>
#define DataType int
typedef struct Heap
{
DataType* a;
int size;
int capacity;
}Heap;
//堆的初始化
void InitHeap(Heap* ph);
//堆的销毁
void DestoryHeap(Heap* ph);
//堆的插入
void PushHeap(Heap* ph, DataType x);
//向上调整
void Adjustup(DataType* a, int n, int child);
//堆的删除
void PopHeap(Heap* ph);
//向下调整
void AdjustDown(DataType* a, int n, int parent);
//找堆顶元素
DataType TopHeap(Heap* ph);
//判断堆是否为空
bool EmptyHHeap(Heap* ph);
//打印堆数据
void PrintHeap(Heap* ph);
//heap.c
#include "heap.h"
//堆的初始化
void InitHeap(Heap* ph)
{
assert(ph);
ph->a = NULL;
ph->capacity = ph->size = 0;
}
//堆的销毁
void DestoryHeap(Heap* ph)
{
assert(ph);
free(ph->a);
ph->a = NULL;
ph->capacity = ph->size = 0;
}
void swap(DataType* a, DataType* b)
{
DataType tmp = *a;
*a = *b;
*b = tmp;
}
//向上调整
void Adjustup(DataType* a, int n, int child)
{
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 PushHeap(Heap* ph, DataType x)
{
assert(ph);
if (ph->size == ph->capacity)//空间不够,开始扩容
{
int newcapacity = ph->capacity == 0 ? 4 : ph->capacity * 2;//注意初始时capacity的大小为0,特判一下。
DataType* tmp = (DataType*)realloc(ph->a, sizeof(DataType) * newcapacity);
if (tmp == NULL)//防止扩容失败
{
perror("realloc");
exit(-1);
}
ph->a = tmp;
ph->capacity = newcapacity;
}
ph->a[ph->size] = x;//插入数据
ph->size += 1;
//向上调整
Adjustup(ph->a, ph->size, ph->size - 1);
}
//向下调整
void AdjustDown(DataType* a, int n, int parent)
{
int child = parent * 2 + 1;
while (child<n)
{
if (child+1 < n && a[child] > a[child + 1])//找子节点最小的
{
child = child + 1;
}
if (a[child] < a[parent])
{
swap(&a[child], &a[parent]);
parent = child;
child = parent * 2 + 1;
}
else
{
break;
}
}
}
//堆的删除
void PopHeap(Heap* ph)
{
assert(ph);
assert(ph->size > 0);//为空就不能删
swap(&ph->a[0], &ph->a[ph->size - 1]);
ph->size -= 1;
AdjustDown(ph->a, ph->size, 0);
}
//找堆顶元素
DataType TopHeap(Heap* ph)
{
assert(ph);
if(!EmptyHHeap(ph))
return ph->a[0];
}
//判断堆是否为空
bool EmptyHHeap(Heap* ph)
{
assert(ph);
return ph->size == 0;
}
//打印堆数据
void PrintHeap(Heap* ph)
{
assert(ph);
for (int i = 0; i < ph->size; ++i)
{
printf("%d ", ph->a[i]);
}
printf("\n");
}
//test.c
#include "heap.h"
//测试
void test1()
{
Heap h;
InitHeap(&h);
PushHeap(&h, 9);
PushHeap(&h, 8);
PushHeap(&h, 7);
PushHeap(&h, 6);
PushHeap(&h, 1);
PopHeap(&h);
PrintHeap(&h);
printf("%d", TopHeap(&h));
DestoryHeap(&h);
}
int main()
{
test1();
return 0;
}