目录
书接上回,我们继续学习二叉树的知识
2.5 二叉树的存储结构
二叉树一般可以使用两种数据结构,一种顺序结构,一种链式结构。
顺序储存
顺序结构存储就是使用 数组来存储,一般使用数组 只适合表示完全二叉树,因为不是完全二叉树会有空间的浪费。而现实中使用中只有堆才会使用数组来存储,关于堆我们后面的章节会专门讲解。 二叉树顺序存储在物理上是一个数组,在逻辑上是一颗二叉树。
表示二叉树的值在数组位置中父子下标关系
parent = (child - 1) \ 2
leftchild = parent * 2 + 1
rightchild = parent * 2 + 2
由图可知:将非完全二叉树存储在数组中,会浪费很多的空间
链式存储
- 二叉树的链式存储结构是指,用 链表来表示一棵二叉树,即用链来指示元素的逻辑关系。 通常的方法是链表中每个结点由三个域组成,数据域和左右指针域,左右指针分别用来给出该结点左孩子和右孩子所在的链结点的存储地址 。链式结构又分为二叉链和三叉链,当前我们学习中一般都是二叉链,后面课程学到高阶数据结构如红黑树等会用到三叉链
typedef int BTDataType;
// 二叉链
struct BinaryTreeNode
{
struct BinTreeNode* LeftChild; // 指向当前节点左孩子
struct BinTreeNode* RightChild; // 指向当前节点右孩子
BTDataType data; // 当前节点值域
}
// 三叉链
struct BinaryTreeNode
{
struct BinTreeNode* Parent; // 指向当前节点的双亲
struct BinTreeNode* LeftChild; // 指向当前节点左孩子
struct BinTreeNode* RightChild; // 指向当前节点右孩子
BTDataType _data; // 当前节点值域
};
这里作了解就可以了,后面高阶数据结构会用到
3.二叉树的顺序结构及实现
3.1二叉树的顺序结构
普通的二叉树是不适合用数组来存储的,因为可能会存在大量的空间浪费。而完全二叉树更适合使用顺序结构存储。 现实中我们通常把堆 ( 一种二叉树 ) 使用顺序结构的数组来存储,需要注意的是这里的堆和操作系统 虚拟进程地址空间中的堆是两回事,一个是数据结构,一个是操作系统中管理内存的一块区域分段。
3.2堆的概念以及结构
3.3堆的实现
->1.堆向下调整算法
现在我们给出一个数组,逻辑上看做一颗完全二叉树,我们通过从根节点开始的向下调整算法可以把它调整成一个小堆/大堆。向下调整有一个前提:左右子树必须是一个堆,才能调整
int array[] = {27, 15, 19, 18, 28, 34, 65, 49, 25, 37, };
->2.堆向上调整算法
例:尾插一个10,再进行向上调整算法,直到满足堆,以小根堆为例
->3.堆的删除
删除堆是删除堆顶数据,将堆顶的数据和最后一个数据一换,然后删除最后一个数据,再向下调整算法
3.4堆的代码实现
堆需要实现的功能
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include <assert.h>
typedef int HDataType;
typedef struct Heap
{
HDataType* a;
int size;
int capacity;
}Heap;
//初始化
void HeapInit(Heap* ps);
//销毁堆
void HeapDestroy(Heap* ps);
//向堆插入数据
void HeapPush(Heap* ps, HDataType x);
//向上调整,以大根堆为例
void AdjustUp(HDataType* ps, int child);
//向下调整,以大根堆为例
void AdjustDown(Heap* hp, int n, int parent);
//判断是否有数据
bool HEmpty(Heap* hp);
//获取元素个数
int HeapSize(Heap* hp);
//获取堆头元素
HDataType HeapTop(Heap* hp);
//删除堆尾元素
void HeapPop(Heap* hp);
实现堆最核心的两段代码
1.向上调整
//向上调整,以大根堆为例
void AdjustUp(HDataType* 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;
}
}
}
2.向下调整
//向下调整,以大根堆为例
void AdjustDown(HDataType* hp, int n, int parent)
{
assert(hp);
int child = parent * 2 + 1;
while (child < n)
{
if ((child + 1) < n && hp[child] < hp[child + 1])
{
++child;
}
if (hp[child] > hp[parent])
{
swap(&hp[child], &hp[parent]);
parent = child;
child = parent * 2 + 1;
}
else
{
break;
}
}
}
其他代码
删除堆尾元素
//删除堆尾元素
void HeapPop(Heap* hp)
{
assert(hp);
assert(!HEmpty(hp));
swap(&hp->a[0], &hp->a[hp->size - 1]);
hp->size--;
AdjustDown(hp->a, hp->size, 0);
}
这里为什么不使用挪动直接删除?
1.效率低
2.父子关系全乱了
如图
这里我们用的是另一种方法,来间接删除
1.效率高
2.父子关系没有很乱
//初始化
void HeapInit(Heap* hp)
{
assert(hp);
hp->a = (HDataType*)malloc(sizeof(HDataType) * 4);
if (NULL == hp->a)
{
perror("HeapInit::malloc");
return;
}
hp->size = 0;
hp->capacity = 4;
}
//交换
void swap(HDataType* x, HDataType* y)
{
HDataType tmp = *x;
*x = *y;
*y = tmp;
}
//销毁堆
void HeapDestroy(Heap* hp)
{
assert(hp);
assert(!HEmpty(hp));
free(hp->a);
hp->a = NULL;
hp->size = hp->capacity = 0;
free(hp);
hp = NULL;
}
//向堆插入数据
void HeapPush(Heap* hp, HDataType x)
{
assert(hp);
if (hp->size == hp->capacity)
{
HDataType* tmp = (HDataType*)realloc(hp->a, sizeof(HDataType) * hp->capacity * 2);
if (NULL == tmp)
{
perror("HeapPsuh::malloc");
return;
}
hp->a = tmp;
hp->capacity *= 2;
}
hp->a[hp->size] = x;
hp->size++;
AdjustUp(hp->a, hp->size-1);
}
//获取堆头元素
HDataType HeapTop(Heap* hp)
{
assert(hp);
assert(!HEmpty(hp));
return hp->a[0];
}
//判断是否有数据
bool HEmpty(Heap* hp)
{
assert(hp);
return hp->size == 0;
}
//获取元素个数
int HeapSize(Heap* hp)
{
assert(hp);
return hp->size;
}
功能测试:
#include "Heap.h"
int main()
{
Heap st;
HeapInit(&st);
HeapPush(&st, 21);
HeapPush(&st, 32);
HeapPush(&st, 3);
HeapPush(&st, 42);
HeapPush(&st, 15);
HeapPush(&st, 3);
HeapPush(&st, 5);
int k = 0;
scanf("%d", &k);
while (!HEmpty(&st) && k--)
{
printf("%d ", HeapTop(&st));
HeapPop(&st);
}
printf("\n");
return 0;
}
3.5堆的应用
堆排序,即:利用堆的思想来进行排序,总共分为两个步骤
1.建堆
- 升序:建大堆
- 降序:建小堆
升序为什么不建小堆?
如果排升序建的是小堆,根结点的数据是最小的,剩下的做堆,选次小的,与上面删除堆尾元素一样,这样做会导致后面父子关系全乱了,得重新排序
如图:
所以排升序,建大堆,反之,排降序,建小堆
2.利用堆删除的思想来进行排序
代码实现:
#define N 10
//排升序建大堆
void Heap_Sort(int* a, int n)
{
//建堆,向上调整建堆 时间复杂度为:O(N*logN)
/*for (int i = 1; i < n; ++i)
{
AdjustUp(a, i);
}*/
//向下调整,建大堆比上面那个快一点,这个的实际复杂度为:O(N)
for (int i = (n - 1 - 1) / 2; i >= 0; --i)
{
AdjustDown(a, n, i);
}
int end = n - 1;
while(end > 1)
{
swap(a[0], a[end]);
AdjustDown(a, end, end - 1);
--end;
}
}
int main()
{
int a[N] = {2, 1, 5, 7, 6, 8, 0, 9, 4, 3};
Heap_Sort(a, 10);
return 0;
}
测试结果: