1. 树
1.1 树的概念与结构
树是⼀种非线性的数据结构,它是由 n(n>=0) 个有限结点组成⼀个具有层次关系的集合。把它叫做树是因为它看起来像⼀棵倒挂的树,也就是说它是根朝上,而叶朝下的。
• 有⼀个特殊的结点,称为根结点,根结点没有前驱结点。
• 除根结点外,其余结点被分成 M(M>0) 个互不相交的集合 T1、T2、......、Tm ,其中每⼀个集合 Ti(1 <= i <= m) 又是⼀棵结构与树类似的子树。每棵子树的根结点有且只有⼀个前驱,可以有 0 个或多个后继。因此,树是递归定义的。
树形结构中,子树之间不能有交集,否则就不是树形结构。
非树形结构:
• 子树是不相交的(如果存在相交就是图了)(除了根节点之外,有其它的集合,这些集合就是树)
• 除了根结点外,每个结点有且仅有一个父结点
• ⼀棵N个结点的树有N-1条边
1.2树相关术语
**父结点/双亲结点:**若⼀个结点含有子结点,则这个结点称为其子结点的父结点; 如上图:A是B的父结点。
子结点/孩子结点:⼀个结点含有的子树的根结点称为该结点的子结点; 如上图:B是A的孩子结点。
结点的度:⼀个结点有几个孩子,他的度就是多少;比如A的度为6,F的度为2,K的度为0。
树的度:⼀棵树中,最大的结点的度称为树的度; 如上图:树的度为 6。
**叶子结点/终端结点:**度为 0 的结点称为叶结点; 如上图: B 、 C 、 H 、 I... 等结点为叶结点。
**分支结点/非终端结点:**度不为 0 的结点; 如上图: D 、 E 、 F 、 G... 等结点为分支结点。
**兄弟结点:**具有相同父结点的结点互称为兄弟结点(亲兄弟); 如上图: B 、 C 、D、E、F等 是兄弟结点。(H、I是表兄弟节点)。
**结点的层次:**从根开始定义起,根为第 1 层,根的子结点为第 2 层,以此类推。
**树的高度或深度:**树中结点的最大层次; 如上图:树的高度为 4。
**结点的祖先:**从根到该结点所经分支上的所有结点;如上图: A 是所有结点的祖先。比如P的祖先节点是(A、E、J)。
路径:⼀条从树中任意节点出发,沿父节点------子节点连接,达到任意节点的序列;比如A到Q的路径为: A-E-J-Q;H到Q的路径H-D-A-E-J-Q。
**子孙:**以某结点为根的子树中任⼀结点都称为该结点的子孙。如上图:所有结点都是A的子孙。
**森林:**由 m ( m>0 )棵互不相交的树的集合称为森林。一棵树也可以称为森林。
1.3 树的表示
孩子兄弟表示法:
树结构相对线性表就比较复杂了,要存储表示起来就比较麻烦了,既然保存值域,也要保存结点和结点之间的关系,实际中树有很多种表示方式如:双亲表示法,孩子表示法、孩子双亲表示法以及孩子兄弟表示法等。我们这里就简单的了解其中最常用的孩子兄弟表示法。
1.4 树形结构实际运用场景
文件系统是计算机存储和管理文件的⼀种方式,它利用树形结构来组织和管理文件和文件夹。在文件 系统中,树结构被广泛应用,它通过父结点和子结点之间的关系来表示不同层级的文件和文件夹之间的关联。
2. 二叉树
2.1 概念与结构
在树形结构中,我们最常用的就是⼆叉树,⼀棵⼆叉树是结点的⼀个有限集合,该集合由⼀个根结点加上两棵别称为左子树和右子树的⼆叉树组成或者为空。二叉树是树形结构的一种,也可以说是对树的结构加以限制形成二叉树。
从上图可以看出二叉树具备以下特点:
- ⼆叉树不存在度大于 2 的结点。(二叉树只存在度为0、1、2的节点)
- ⼆叉树的子树有左右之分,次序不能颠倒,因此二叉树是有序树。(这里的有序是指左右孩子是有区分的)
注意:对于任意的二叉树都是由以下几种情况复合而成的。
第一个是空树(度为0),第二个叫只有根节点的二叉树,第三个叫做只有左子树的二叉树,第四个叫做只有右子树的二叉树,第五个叫做左右子树都存在的二叉树。
2.2 特殊的二叉树
2.2.1 满二叉树
⼀个二叉树,除了叶子节点外,如果每⼀个层的结点数都达到最大值,则这个二叉树就是满二叉树。也就是说,如果一个二叉树的层数为 K ,且结点总数是 2 k − 1 ,则它就是满二叉树。
2.2.2 完全⼆叉树
完全⼆叉树是效率很高的数据结构,完全二叉树是由满二叉树引出来的。对于深度为 K 的,有 n 个结点的二叉树,当且仅当其每⼀个结点都与深度为K的满二叉树中编号从 1至 n 的结点⼀⼀对应时称 之为完全二叉树。要注意的是满二叉树是⼀种特殊的完全二叉树。
假设二叉树层次为K,除了第K层外,每层结点的个数达到最大结点数,第K层结点个数不一定达到最大节点数。
这种就不是完全二叉树(完全二叉树结点的顺序是从左到右的)。
总结:
满二叉树一定是完全二叉树,但是完全二叉树不一定是满二叉树。
💡 ⼆叉树性质
根据满二叉树的特点可知:
1)若规定根结点的层数为 1 ,则⼀棵非空二叉树的第i层上最多有 2^i −1 个结点
2)若规定根结点的层数为 1 ,则深度为 h 的二叉树的最大结点数是 2^h − 1
3)若规定根结点的层数为 1 ,具有 n 个结点的满⼆叉树的深度 ( log以2为底, n+1 为对数)
(由2^h-1 = n演变而来)
2.3 ⼆叉树存储结构
二叉树⼀般可以使用两种结构存储,⼀种顺序结构,⼀种链式结构。
2.3.1 顺序结构
顺序结构存储就是使用数组来存储,一般使用数组只适合表示完全二叉树,因为不是完全二叉树会有 空间的浪费,完全二叉树更适合使用顺序结构存储。
现实中我们通常把堆(⼀种二叉树)使用顺序结构的数组来存储,需要注意的是这里的堆和操作系统虚拟进程地址空间中的堆是两回事,⼀个是数据结构,⼀个是操作系统中管理内存的⼀块区域分段。
2.3.2 链式结构
二叉树的链式存储结构是指,用链表来表示⼀棵⼆叉树,即用链来指示元素的逻辑关系。 通常的方法是链表中每个结点由三个域组成,数据域和左右指针域,左右指针分别用来给出该结点左孩子和右孩子所在的链结点的存储地址 。链式结构又分为二叉链和三叉链,当前我们学习中一般都是二叉链。后面课程学到高阶数据结构如红黑树等会用到三叉链。
3. 实现顺序结构二叉树
一般堆使用顺序结构的数组来存储数据,堆是⼀种特殊的二 叉树,具有二叉树的特性的同时,还具备其他的特性。
3.1 堆的概念与结构
堆具有以下性质:
• 堆中某个结点的值总是不大于或不小于其父结点的值;
• 堆总是⼀棵完全二叉树。
• 小堆堆顶是堆最小值
• 大 堆堆顶是堆最大值
• 存储在数组中的元素不一定是有序的
💡 二 叉树性质
• 对于具有 n 个结点的完全二叉树,如果按照从上至下从左至右的数组顺序对所有结点从
0 开始编号,则对于序号为 i 的结点有:
- 若 i>0 , i 位置结点的双亲序号: (i-1)/2 ; i=0 , i 为根结点编号,无双亲结点
- 若 2i+1<n ,左孩子序号: 2i+1 , 2i+1>=n 否则无左孩子
- 若 2i+2<n ,右孩子序号: 2i+2 , 2i+2>=n 否则无右孩子
//定义堆的结构------数组 堆的底层是使用顺序结构数组来实现的
typedef int HPDataType;
typedef struct Heap
{
HPDataType* arr;
int size;//有效的数据个数
int capacity;//空间大小
}HP;
//堆的初始化
void HPInit(HP* php);
堆的销毁
void HPDestroy(HP* php);
堆数据的插入
void HPPush(HP* php, HPDataType x);
//判断链表是否为空
//判断空间是否足够
实现到堆的数据插入之后,不确定是不是真的符合大、小堆存储,这时候就要进行堆的向上调整算法。在这之间,还要用到两个变量交换的函数Add。(只需要比较父结点和左孩子结点,若父结点大于左孩子结点,就交换位置)。
接下来是出堆,而出堆指的就是删除堆顶数据,当我们直接删除堆顶数据时,会导致堆乱套(后一个位置移动到前一个位置处,堆的底层是顺序表),所以不能直接删除堆顶数据。因此,我们必须采取其他的办法。
方法:
最后一个结点的数据和堆顶数据交换,这时让size--;而堆顶数据(交换后为最后一个结点)能直接被删除,而删除后的堆顺序不一定符合大、小堆,所以我们要用到向下调整算法。
//去堆顶
void HPPop(HP* php);
在出堆中,我们必须要保证父结点的值和左右孩子中最小的值去交换(向下调整算法)。
//出堆顶
HPDataType HPTop(HP* php);
//打印元素判断代码是否正确
附源代码:
Heap.h
#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
#include<stdbool.h>
//定义堆的结构------数组 堆的底层是使用顺序结构数组来实现的
typedef int HPDataType;
typedef struct Heap
{
HPDataType* arr;
int size;//有效的数据个数
int capacity;//空间大小
}HP;
void HPInit(HP* php);
void HPDestroy(HP* php);
void HPPush(HP* php, HPDataType x);
//去堆顶
void HPPop(HP* php);
//判空
bool HPEmpty(HP* php);
//出堆顶
HPDataType HPTop(HP* php);
void Swap(int* x, int* y);
Heap.c#define _CRT_SECURE_NO_WARNINGS 1
#include"Heap.h"
void HPInit(HP* php)
{
assert(php);
php->arr = NULL;
php->capacity = php->size = 0;
}
void HPDestroy(HP* php)
{
assert(php);
if (php->arr)
{
free(php->arr);
}
php->arr = NULL;
php->capacity = php->size = 0;
}
void Swap(int* x, int* y)
{
int tmp = *x;
*x = *y;
*y = tmp;
}
void AdjustUp(HPDataType *arr, int child)
{
int parent = (child - 1) / 2;
while (child > 0)//不需要等于,child只要走到根节点的位置,根节点没有父节点不需要交换
{
if (arr[child] < arr[parent])
{
Swap(&arr[parent], &arr[child]);
child = parent;
parent = (child - 1) / 2;
}
else
{
break;
}
}
}
void HPPush(HP* php, HPDataType x)
{
assert(php);
//判断空间是否足够
if (php->capacity == php->size)
{
//扩容
int newCapacity = php->capacity == 0 ? 4 : 2 * php->capacity;
HPDataType* tmp = (HPDataType*)realloc(php->arr, newCapacity * sizeof(HPDataType));
if (tmp == NULL)
{
perror("realloc fail!");
exit(1);
}
php->arr = tmp;
php->capacity = newCapacity;
}
php->arr[php->size] = x;
AdjustUp(php->arr, php->size);
php->size++;
}
void AdjustDown(HPDataType* arr, int parent, int n)
{
int child = 2 * parent + 1;
while (child < n)
{
//找左右孩子中最小的
if (child +1 < n && arr[child] > arr[child + 1])
{
child++;
}
if (arr[parent] > arr[child])
{
Swap(&arr[child], &arr[parent]);
parent = child;
child = 2 * parent + 1;
}
else
{
break;
}
}
}
void HPPop(HP* php)
{
assert(php && php->size);
Swap(&php->arr[0], &php->arr[php->size - 1]);
--php->size;
AdjustDown(php->arr, 0, php->size);
}
//判空
bool HPEmpty(HP* php)
{
assert(php);
return php->size == 0;
}
HPDataType HPTop(HP* php)
{
assert(php && php->size);//堆顶的元素不能为空
return php->arr[0];
}
text.c#define _CRT_SECURE_NO_WARNINGS 1
#include"Heap.h"
void text01()
{
HP hp;
HPInit(&hp);
int arr[] = { 17,20,10,13,19,15 };
for (int i = 0; i < 6; i++)
{
HPPush(&hp, arr[i]);
}
while (!HPEmpty(&hp))
{
printf("%d ", HPTop(&hp));
HPPop(&hp);
}
HPDestroy(&hp);
}
int main()
{
text01();
return 0;
}