📖 前言:树结构是不同于线性表、队列和树的一种“非线性”结构,它是对自然界中“树”的结构仿生。在生物学中对物种进行分类,国家治理中划分行政管理区域,甚至在读心术游戏中,都会用到树结构。当然,信息技术中的很多问题,也是通过各种各样的树结构来解决的。只要你愿意,还可以尝试用树结构来创作计算机艺术作品。
🎓 作者:HinsCoder
📦 作者的GitHub:代码仓库
📌 往期文章&专栏推荐:
🕒 1. 树概念及结构
🕘 1.1 树的概念
树是一种非线性
的数据结构,它是由n个有限结点组成的一个具有层次关系的集合。把他叫做树是因为看起来像一个倒挂的树,也就是说它根朝上,叶朝下
。
- 树根处的结点称为
根结点
,根结点没有前驱点。 - 除根结点外,其余结点被分成M(M>0)个互不相交的集合T1,T2……,Tm,其中每一个集合Ti(1<=i<=m)又是一个结构与树类似的结构。每棵树的根结点有且只有一个前驱,可以有0个或多个后继。
- 树是递归定义的。
⚡ 注意:
- 子树之间是
不相交
的。 - 除了根结点,每个结点有且只有
一个父节点
。 一颗N个结点的树有N-1条边
。
🕘 1.2 树的相关概念
🕘 1.3 树的表示
树结构相对线性表就比较复杂了,要存储表示起来就比较麻烦了,既要保存值域,也要保存结点和结点之间的关系
,实际中树有很多种表示方式如:双亲表示法,孩子表示法、孩子双亲表示法以及孩子兄弟表示法等。我们这里就简单的了解其中最常用的孩子兄弟表示法(左孩,右兄)。
typedef int DataType;
struct Node
{
struct Node* firstChild1; // 左边第一个孩子结点
struct Node* pNextBrother; // 指向其下一个兄弟结点
DataType data; // 结点中的数据域
};
void PrintTree(TreeNode*parent)
{
if (parent == NULL)
return;
printf(parent);
TreeNode*cur = parent->child;
while (cur)
{
PrintTree(cur);
cur=cur->brother;
}
}
🕘 1.4 树在实际中的运用
🕒 2. 二叉树概念及结构
🕘 2.1 概念
一棵二叉树是结点的一个有限集合,该集合为空
或者由一个根节点加上两棵被称为左子树和右子树的二叉树
组成
🕘 2.2 现实中的二叉树
🕘 2.3 特殊的二叉树
- 满二叉树:一个二叉树,如果每一个层的结点数都达到最大值,则这个二叉树就是满二叉树。也就是说,如果一个二叉树的层数为k,且结点总数是2(k-1) ,则它就是满二叉树。
- 完全二叉树:完全二叉树是效率很高的数据结构,完全二叉树是由满二叉树而引出来的。对于深度为K的,有n个结点的二叉树,当且仅当其每一个结点都与深度为K的满二叉树中编号从1至n的结点一一对应时称之为完全二叉树。 要注意的是满二叉树是一种特殊的完全二叉树。
🕘 2.4 二叉树的性质
-
规定根结点的层数为1,则一颗非空二叉树第i层最多有2(i-1)个。
-
规定根节点的层数为1,则深度为h的二叉树的最大节点数为2h−1.
-
任何一个二叉树,如果度为0其叶节点的个数时𝑛0,度为2的节点个数是𝑛2,则有𝑛0 = 𝑛2+1
-
若规定根结点的层数为1,具有n个节点的满二叉树的深度,h = log2(n+1)
-
对于具有n个节点的完全二叉树,如果按照从上至下从左至右的数组顺序从0开始编号,则对与序号为i的节点有:
-
若i>0,i位置节点的双亲序号为: ( i − 1 ) 2 {(i-1)} \over {2} 2(i−1),i为根结点编号,无双亲结点。
-
若2i+1<n,左孩子序号:2i+1,2i+1>=n否则无左孩子。
-
若2i+2<n,右孩子序号:2i+2,2i+2>=n否则无右孩子。
🕒 3. 二叉树的存储结构
二叉树一般可以使用两种结构存储,一种顺序结构,一种链式结构。
1、顺序存储
顺序结构存储就是使用数组来存储,一般使用数组只适合表示完全二叉树,因为不是完全二叉树会有空间的浪费。而现实中使用中只有堆才会使用数组来存储,关于堆我们后面会讲到。二叉树顺序存储在物理上是一个数组,在逻辑上是一颗二叉树。
2、链式存储
二叉树的链式存储结构是指,用链表来表示一棵二叉树,即用链来指示元素的逻辑关系。 通常的方法是链表中每个结点由三个域组成,数据域和左右指针域,左右指针分别用来给出该结点左孩子和右孩子所在的链结点的存储地址 。链式结构又分为二叉链和三叉链,当前我们学习中一般都是二叉链。
typedef int BTDataType;
// 二叉链
struct BinaryTreeNode{
struct BinTreeNode* Left; // 指向当前节点左孩子
struct BinTreeNode* Right; // 指向当前节点右孩子
BTDataType _data; // 当前节点值域
};
🕒 4. 二叉树的顺序结构及实现
🕘 4.1 二叉树的顺序结构
普通的二叉树是不适合用数组来存储的,因为可能会存在大量的空间浪费。而完全二叉树更适合使用顺序结构存储。现实中我们通常把堆
(一种二叉树)使用顺序结构的数组来存储,需要注意的是这里的堆和操作系统虚拟进程地址空间中的堆是两回事,一个是数据结构,一个是操作系统中管理内存的一块区域分段。
🕘 4.2 堆的概念及结构
- 堆的逻辑结构为
完全二叉树
,物理结构为数组
🕘 4.3 堆的性质
在堆中: 某个节点的值总是不大于或不小于其父节点的值
大根堆
:即当每个父亲结点的值总是≥孩子结点的值
小根堆
:即当每个父亲结点的值总是≤孩子结点的值
⚡ 注意:虽然根结点的值一定是全部结点的值中最大
或最小
的,但大根堆
、小根堆
并不代表说数组元素是按照降序
、升序
排放的,这两者没有任何关系
💡 重点规律:
通过数组
的特性随机访问
,如何用下标
去找到父亲结点或孩子结点
-
假设某一个父亲结点下标为parent
所以此父亲结点的左孩子结点为:leftchild = parent*2 + 1
所以此父亲结点的右孩子结点为:leftchild = parent*2 + 2
所以:我们可以通过+1
、+2
的下标调整找到左孩子
、右孩子
-
那此时我们就可以反推出:parent = (child - 1)/ 2
这是因为计算的是整型计算
,即使是右孩子
去用此算式虽然计算出来的下标是带有小数的,但下标的类型为整型
,就会自动抹去小数,那此时的整数也就为右孩子
的父亲结点的下标了
建堆的时间复杂度:
因为堆是完全二叉树,而满二叉树也是完全二叉树,此处为了简化使用满二叉树来证明(时间复杂度本来看的就是近似值,多几个节点不影响最终结果)
注:T(n) = 每一层结点个数 × 这一层结点最坏向下调整次数
🕘 4.4 堆的实现
以下以建小根堆
为例
typedef struct Heap
{
HPDataType* a;
int size; //记录目前数组内有几个数据
int capacity;
}HP;
🕤 4.4.1 初始化堆
void HeapInit(HP* php)
{
assert(php);
php->a = NULL;
php->size = php->capacity = 0;
}
🕤 4.4.2 销毁堆
void HeapDestroy(HP* php)
{
assert(php);
php->a = NULL;
php->capacity = php->size = 0;
}
🕤 4.4.3 打印堆
void HeapPrint(HP* php)
{
for (int i = 0; i < php->size; i++)
{
printf("%d ", php->a[i]);
}
printf("\n");
}
🕤 4.4.4 入堆
💡 思路:对数组尾插
后进行向上调整
⚡ 注意:并不是插入堆就直接满足小堆
,需要将刚插进来的数据经过比较放到能使整棵树满足小堆
的位置,此时就涉及另外一个算法:向上调整算法
-
对插入的数据进行向上调整,仅会对插入数据所在的路径产生影响,并不像向下调整算法会影响到整棵树
-
插入时完全二叉树已经满足
小堆
,所以向上调整算法将插入的结点
直接与父亲结点
的值进行比较即可(无需与自己的兄弟结点比较)
-
若
小于
父亲结点的值,则不满足小堆
,需要将插入进来的结点与其父亲结点交换,并继续向上与新的父亲结点进行比较,直至插入的结点已经到达根部
(即数组下标为0
处) ,就结束调整,表示已到达合适的位置 -
若
大于
父亲结点的值,则表明此时也已满足小堆
,插入进来的结点已经到达合适的位置了
写代码之前要记住一个核心公式:parent = (child - 1)/ 2
void Swap(HPDataType* p1, HPDataType* p2)
{
HPDataType tmp = *p1;
*p1 = *p2;
*p2 = tmp;
}
void AdjustUp(HPDataType* a, int child)
{
int parent = (child - 1) / 2;
//while (parent >= 0) //不对,会死循环,万一parent = (0 - 1) / 2就不行了
while (child > 0)
{
if (a[child] < a[parent]) //小堆
//if (a[child] > a[parent]) //大堆
{
Swap(&a[child], &a[parent]);
child = parent;
parent = (child - 1) / 2;
}
else
{
break;
}
}
}
// 插入x继续保持堆形态
void HeapPush(HP* php, HPDataType x)
{
assert(php);
if (php->size == php->capacity) //检查容量
{
int newCapacity = php->capacity == 0 ? 4 : php->capacity * 2;
HPDataType* tmp = realloc(php->a, newCapacity * sizeof(HPDataType));
if (tmp == NULL) //担心返回空指针导致原数组数据丢失
{
perror("realloc fail");
exit(-1);
}
php->a = tmp;
php->capacity = newCapacity;
}
php->a[php->size] = x;
php->size++;
AdjustUp(php->a, php->size - 1); //向上调整
}
🕤 4.4.5 出堆(删除堆顶元素)
💡 思路:交换堆顶
和堆尾
的值,后进行向下调整
⚡ 注意:如果直接删除堆顶的数据的,那就需要后面的整体数据往前挪动(O ( N ) ),树的结构也发生改变,需要重新建堆,才能再次满足堆为小堆
void AdjustDown(HPDataType* a, int n, int parent)
{
int minChild = parent * 2 + 1;
while (minChild < n)
{
if (minChild + 1 < n && a[minChild + 1] < a[minChild])
{
minChild++;
}
if (a[minChild] < a[parent])
{
Swap(&a[minChild], &a[parent]);
parent = minChild;
minChild = parent * 2 + 1;
}
else
{
break;
}
}
}
// 删除堆顶元素 -- 找次大或者次小
// O(logN)
void HeapPop(HP* php)
{
assert(php);
assert(!HeapEmpty(php));
Swap(php->a[0], php->a[php->size - 1]);
php->size--;
AdjustDown(php->a, php->size, 0);
}
🕤 4.4.6 取堆顶元素
HPDataType HeapTop(HP* php)
{
assert(php);
assert(!HeapEmpty(php));
return php->a[0];
}
🕤 4.4.7 判空
bool HeapEmpty(HP* php)
{
assert(php);
return php->size == 0;
}
🕤 4.4.8 获取元素个数
int HeapSize(HP* php)
{
assert(php);
return php->size;
}
🕒 5. 完整源码
// Heap.h
#define _CRT_SECURE_NO_WARNINGS 1
#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 HeapPrint(HP* php);
void HeapInit(HP* php);
void HeapDestroy(HP* php);
// 插入x继续保持堆形态
void HeapPush(HP* php, HPDataType x);
// 删除堆顶元素
void HeapPop(HP* php);
// 返回堆顶的元素
HPDataType HeapTop(HP* php);
bool HeapEmpty(HP* php);
int HeapSize(HP* php);
// Heap.c
#include"Heap.h"
void HeapPrint(HP* php)
{
for (int i = 0; i < php->size; i++)
{
printf("%d ", php->a[i]);
}
printf("\n");
}
void HeapInit(HP* php)
{
assert(php);
php->a = NULL;
php->size = php->capacity = 0;
}
void HeapDestroy(HP* php)
{
assert(php);
php->a = NULL;
php->capacity = php->size = 0;
}
void Swap(HPDataType* p1, HPDataType* p2)
{
HPDataType tmp = *p1;
*p1 = *p2;
*p2 = tmp;
}
void AdjustUp(HPDataType* a, int child)
{
int parent = (child - 1) / 2;
//while (parent >= 0) //不对,会死循环
while (child > 0)
{
if (a[child] < a[parent]) //小堆
//if (a[child] > a[parent]) //大堆
{
Swap(&a[child], &a[parent]);
child = parent;
parent = (child - 1) / 2;
}
else
{
break;
}
}
}
// 插入x继续保持堆形态
void HeapPush(HP* php, HPDataType x)
{
assert(php);
if (php->size == php->capacity) //检查容量
{
int newCapacity = php->capacity == 0 ? 4 : php->capacity * 2;
HPDataType* tmp = realloc(php->a, newCapacity * sizeof(HPDataType));
if (tmp == NULL) //担心返回空指针导致原数组数据丢失
{
perror("realloc fail");
exit(-1);
}
php->a = tmp;
php->capacity = newCapacity;
}
php->a[php->size] = x;
php->size++;
AdjustUp(php->a, php->size - 1); //向上调整
}
void AdjustDown(HPDataType* a, int n, int parent)
{
int minChild = parent * 2 + 1;
while (minChild < n)
{
if (minChild + 1 < n && a[minChild + 1] < a[minChild])
{
minChild++;
}
if (a[minChild] < a[parent])
{
Swap(&a[minChild], &a[parent]);
parent = minChild;
minChild = parent * 2 + 1;
}
else
{
break;
}
}
}
// 删除堆顶元素 -- 找次大或者次小
// O(logN)
void HeapPop(HP* php)
{
assert(php);
assert(!HeapEmpty(php));
Swap(php->a[0], php->a[php->size - 1]);
php->size--;
AdjustDown(php->a, php->size, 0);
}
// 返回堆顶的元素
HPDataType HeapTop(HP* php)
{
assert(php);
assert(!HeapEmpty(php));
return php->a[0];
}
bool HeapEmpty(HP* php)
{
assert(php);
return php->size == 0;
}
int HeapSize(HP* php)
{
assert(php);
return php->size;
}
// test.c
#include"Heap.h"
//int main()
//{
// int a[] = { 15,18,19,25,28,34,65,49,27,37,10 };
// HP hp;
// HeapInit(&hp);
// for (int i = 0; i < sizeof(a) / sizeof(int); i++)
// {
// HeapPush(&hp, a[i]);
// }
// HeapPush(&hp, 10);
// HeapPrint(&hp);
//
// HeapPop(&hp);
// HeapPrint(&hp);
//
// while (!HeapEmpty(&hp))
// {
// printf("%d ", HeapTop);
// }
//
// return 0;
//
//}
// 空间复杂度O(N)
//void HeapSort(int* a, int n)
//{
// HP php;
// HeapInit(&php);
//}
void HeapSort(int* a, int n)
{
// 建堆 -- a
// 模拟插入 - 向上调整建堆 - 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 main()
{
//int a[] = { 65,100,70,32,50,60 };
//int a[] = { 65,100,60,32,50,70 };
int a[] = { 15,1,19,25,8,34,65,4,27,7 };
HeapSort(a, sizeof(a) / sizeof(int));
return 0;
}
OK,以上就是本期知识点“堆”的知识啦~~ ,感谢友友们的阅读。后续还会继续更新,欢迎持续关注哟📌~
🎉如果觉得收获满满,可以点点赞👍支持一下哟~