前言
此文章主要讲解二叉树的顺序结构,堆的实现。在阅读本文之前,希望大家可以对二叉树进行一定程度了解。
数据结构——树与二叉树_Massachusetts_11的博客-CSDN博客
1. 二叉树的顺序结构
普通的二叉树是不适合用数组来存储的,因为可能会存在大量的空间浪费。而完全二叉树更适合使用顺序结构存储。现实中我们通常把堆(一种二叉树)使用顺序结构的数组来存储,需要注意的是这里的堆和操作系统虚拟进程地址空间中的堆是两回事,一个是数据结构,一个是操作系统中管理内存的一块区域分段。
2.堆的概念及结构
以上是堆的定义,看不懂不重要,我们接下来慢慢解释:
1.首先堆必须时一棵完全二叉树
2.堆一般分为两种:
- 小根堆:任意一个节点的两个子节点都比自己的值大或相等,也就是根最小,整个二叉树从上到下递增。
- 大根堆: 任意一个节点的两个子节点都比自己的值小或相等,也就是根最大,整个二叉树从上到下递减。
转换原理:
那么问题来了,这里的存储结构是如何向逻辑结构去实现的呢?
不知大家是否还记得树与二叉树一文中二叉树性质的这几条
也就是说:对于任意一个节点,我们设它的下标为i,则它的左孩子节点下标就是2*i +1 ,右孩子就是2*i + 2,父节点下标就是(i - 1) / 2。
(可能大家也发现了,这里的父节点如果用两个子节点去倒推应该有两个不同的结果,但我们所求的下标必须是一个整型,由于c语言向零取整的原则,两个表达式可合成一个 (i - 1) / 2 )
我们不妨来验证一下:
这里元素28的下标是4,它的父节点下标计算后是(4-1)/ 2 = 1,恰好是18元素
它的左子节点2*4+1 = 9,恰好是37元素,它的右子节点2*4+2 = 10,恰好是10元素。
有了以上这些基础,我们也可以进行一下堆的实现了。
3. 堆的实现
与顺序表相同,我们同样也实现一下堆的增删查改
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 HeapPush(HP* php, HPDataType x);
//头删元素
void HeapPop(HP* php);
//判断是否为空
bool HeapEmpty(HP* php);
//计算节点个数
size_t HeapSize(HP* php);
//返回堆头节点
HPDataType HeapTop(HP* php);
3.1初始化
既然要实现,我们就需要从物理存储角度去思考。
与动态顺序表相同,我们为堆设置一个数组,同时为它附加两个记录:元素个数和容量大小(方便扩容)
在没有元素之前,数组置空,元素个数和容量都为0,后续在尾插元素时,再对堆进行判断,进行扩容
void HeapInit(HP* php)
{
assert(php);
php->a = NULL;
php->capacity = php->size = 0;
}
3.2销毁
对静态开辟的数组进行释放;
元素个数和容量都置零;
void HeapDestroy(HP* php)
{
assert(php);
assert(php->a);
free(php->a);
php->a = NULL;
php->capacity = php->size = 0;
}
3.2打印
可以选择打印出二叉树的外形,但太复杂了🥶,我们这里仅仅遍历打印了数组,如果各位大神有实现树状打印的可以在讨论区分享一下哈哈哈哈哈
void HeapPrint(HP* php)
{
assert(php);
for (int i = 0; i < php->size; i++)
{
printf("%d ", php->a[i]);
}
printf("\n");
}
3.3尾插
我们的重头戏终于来了。
这里我们尾插后一定要保证它依旧还是大堆或者小堆,所以我们要先实现一下向上调整的算法,用来保证我们每次插入的元素都能向上插入自己该去的地方。
向上调整算法
以下讲解都以小堆为例,首先上个图免得想不到
这里,我们想把元素10向上调整,在看这个具体案例之前,我们先了解一下原则:
让当前节点与父节点进行比较,若小于父节点,则交换两节点,接下来判断父节点与父节点的父节点,以此类推,直至出现当前节点不大于父节点。
我们来看一下它的可行性:
每次交换后都能保证父节点为首的这棵子树是堆,如这一次交换就保证了下面这个红圈是个堆
当下一次交换:10位置与18位置交换,说明原来10位置的数比18位置的小,也一定比25位置的小,也就是左右都比10大,这时又保证了大三角是个堆
以此类推,直至大三角框住整棵树,此时整棵树就成堆了。
看一下代码:
这里这个交换可以写个函数,方便表示,以后也能用:
void Swap(HPDataType* pa, HPDataType* pb)
{
assert(pa && pb);
HPDataType tmp = *pa;
*pa = *pb;
*pb = tmp;
}
向上调整:
void AdjustUp(HPDataType* a, size_t child)
{
assert(a);
size_t 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 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 failed\n");
exit(-1);
}
php->a = tmp;
php->capacity = newCapacity;
}
php->a[php->size] = x;
AdjustUp(php->a, php->size);
php->size++;
}
3.4头删
首先,我们想一下,可以把头元素删掉,后面的元素向前移动吗?
答案肯定是不行的,且不说这个O(N)的算法有点蠢,当我们把所有元素往前挪,这时节点之间的父子关系都被破坏了,十有八九它就不是个堆了,那我们就看看这种方法:
1.第一个数(根位置)和最后一个数进行交换。
2.删除最后一个数(size--就行)
3.把头顶的那个大数字再用一定的方式往下调,调成一个堆。
这时我们就需要一个下调整的算法。
向下调整算法
同样摆个图先看着,这里我们要把28向下调.
向下调整原则:
1.找出左右孩子中小的那个;
2.跟父亲比较,如果父亲小,交换;
3.从交换的孩子位置继续往下调。
可行性分析:
其实向下调整就是为了保证头插的元素通过向下调可以让这个二叉树又形成堆,
调整过程中,挑个小的换,就能保证红圈以外是个堆
再换一次,红圈继续缩:
直到最后没有子节点,也就是圈缩成是一个元素,这时就成堆了
向下调整代码:
void AdjustDown(HPDataType*a,size_t size,size_t root)
{
assert(a);
size_t parent = root;
size_t child = parent * 2 + 1;
while (child < size)
{
if (child+1<size&& a[child + 1] < a[child] )
{
child++;
}
if (a[child] < a[parent])
{
Swap(&a[child], &a[parent]);
parent = child;
child = parent * 2 + 1;
}
else
{
break;
}
}
}
头删代码;
void HeapPop(HP* php)
{
assert(php);
Swap(php->a, &php->a[php->size - 1]);
php->size--;
AdjustDown(php->a, php->size, 0);
}
3.5剩余
剩余函数较简单,相信大家一看就明白了
bool HeapEmpty(HP* php)
{
assert(php);
return php->size == 0;
}
size_t HeapSize(HP* php)
{
assert(php);
return php->size;
}
HPDataType HeapTop(HP* php)
{
assert(php);
return php->a[0];
}
4.堆——总代码
4.1 头文件:Heap.h
注:这里默认是小堆,如果对头文件BIGHEAP的宏进行解引用可实现大堆
#pragma once
#include<stdio.h>
#include<assert.h>
#include<stdlib.h>
#include<stdbool.h>
#include<math.h>
//#define BIGHEAP//若大堆,则解除注释,否则加注释
#define SIGN <
#ifdef BIGHEAP
#undef SIGN
#define SIGN >
#endif
// 小堆
typedef int HPDataType;
typedef struct Heap
{
HPDataType* a;
size_t size;
size_t capacity;
}HP;
void Swap(HPDataType* pa, HPDataType* pb);
void HeapInit(HP* php);
void HeapDestroy(HP* php);
void HeapPrint(HP* php);
// 插入x以后,保持他依旧是(大/小)堆
void HeapPush(HP* php, HPDataType x);
// 删除堆顶的数据。(最小/最大)
void HeapPop(HP* php);
bool HeapEmpty(HP* php);
size_t HeapSize(HP* php);
HPDataType HeapTop(HP* php);
4.2源文件:Heap.c
#include"Heap.h"
void Swap(HPDataType* pa, HPDataType* pb)
{
assert(pa && pb);
HPDataType tmp = *pa;
*pa = *pb;
*pb = tmp;
}
void HeapInit(HP* php)
{
assert(php);
php->a = NULL;
php->capacity = php->size = 0;
}
void HeapDestroy(HP* php)
{
assert(php);
assert(php->a);
free(php->a);
php->a = NULL;
php->capacity = php->size = 0;
}
void HeapPrint(HP* php)
{
assert(php);
for (int i = 0; i < php->size; i++)
{
printf("%d ", php->a[i]);
}
printf("\n");
}
void AdjustUp(HPDataType* a, size_t child)
{
assert(a);
size_t parent = (child - 1) / 2;
while (child > 0)
{
if (a[child] SIGN a[parent])
{
Swap(&a[child], &a[parent]);
child = parent;
parent = (child - 1) / 2;
}
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 failed\n");
exit(-1);
}
php->a = tmp;
php->capacity = newCapacity;
}
php->a[php->size] = x;
AdjustUp(php->a, php->size);
php->size++;
}
void AdjustDown(HPDataType*a,size_t size,size_t root)
{
assert(a);
size_t parent = root;
size_t child = parent * 2 + 1;
while (child < size)
{
if (child+1<size&& a[child + 1] SIGN a[child] )
{
child++;
}
if (a[child] SIGN a[parent])
{
Swap(&a[child], &a[parent]);
parent = child;
child = parent * 2 + 1;
}
else
{
break;
}
}
}
void HeapPop(HP* php)
{
assert(php);
Swap(php->a, &php->a[php->size - 1]);
php->size--;
AdjustDown(php->a, php->size, 0);
}
bool HeapEmpty(HP* php)
{
assert(php);
return php->size == 0;
}
size_t HeapSize(HP* php)
{
assert(php);
return php->size;
}
HPDataType HeapTop(HP* php)
{
assert(php);
return php->a[0];
}
4.3测试文件:test.c
#include"Heap.h"
int main()
{
HP h;
HeapInit(&h);
HeapPush(&h, 2);
HeapPush(&h, 5);
HeapPush(&h, 7);
HeapPush(&h, 1);
HeapPush(&h, 4);
HeapPrint(&h);
HeapPop(&h);
HeapPrint(&h);
printf("%d", HeapTop(&h));
}
4.4测试结果