
本篇文章将进行图文讲述该种数据结构!看完一定不会让你失望,好的文章不需要过多的浮夸,质量就是深得人心的砝码!下面我总结了最形象的趣味理解方法,一遍看完终身不忘!制作不易,能否一键三连呢!话不多说,正文开始!
树
数据结构中的树其实与生活中下面这种树很像!只是将它倒过来,存储的结构像一种树状结构!

树的基本概念
树的概念:是N(>=0)个数据的集合,但是我个人认为这样很抽象,为哈有限集的数据就是树?因此我觉得可以这样理解:具有层次关系的N(>=0)个数据的集合。比如:学校校长,他下面有年级主任,主任下面又有班主任,班主任管理学生,这样来理解层次感!(层的开始由自己设置)
N等于0就是空树 ,N大于等于0就是非空树(主要研究),下面我们来看树的组成:
树是由节点(Node)与边(Edge)组成的集合。下面我们看看哈是节点?哈是边?
(节点:树的组成基本单元,包含数据与指针,既可以存储数据,节点就是一个个数据节点)
(边:边就是连接节点的线,用来表示节点之间的关系)
这里需要注意:在上面对"边"的定义也很抽象。边是该节点到它下一层子节点的连线,该节点有多少子树就有多少边!如下图:

A的边有:1、2、3、4。C的边有:5、6,只计算该节点到它临近子节点的连线个数!
树需要满足以下条件:(非空树)
1:有且只有一个根节点(Root)
2:除了根节点外,每个节点有且只有一个父节点
3:从根节点到任意一个节点,只有唯一的路径,而每个集合本身又构成了 一个树,我们叫它子树,而子树之间是不相交的。比如下图的:H、L、 M、N构成的一个集合,下图3个颜色的方框构成了3个子树(部分子树)

在上图中: (最形象的理解方式就是当成父子关系去解读)A是唯一的根节点我们叫他老祖,B是F的父亲,F就是B的儿子,同时F也是K的父亲。再比如H,H是L、M、N的父亲,它们都是H的子儿子。咱们可以这么简单又趣味的理解:根节点就是老祖,然后一代一代传下来!一个儿子不能有多个父亲吧!而"唯一路径"就理解为从根节点到该节点需要满足从父节点 或者 前辈节点到该节点一线到底,不能乱了父子长辈关系(仅趣味理解哈)!比如下面这幅图不满足条件,就不属于树:

树的基本术语
在树的交流中,我们可能听见别人说一些专有名词,比如下面这些比较常见的:
根节点(Root):树的顶层节点(有且只有一个,空树除外),我们称他为老祖!
子节点(Child):一个节点的直接下层节点,该节点的下层且具有连线关系的都是子节点!(也叫孩子节点)
父节点(Parent):一个节点且具有连线关系的上一层节点,也叫双亲节点,其它叫母节点的也是说的是父节点
叶子节点(Leaf):没有其它分支的节点,代表这颗树的最后一个节点
子树(Subtree):以某个节点为根的树的分支,也就是上文说的其它集合所形成的新树,不相交。对于任意的树,其子树总量等于树中节点的数量,因为每个节点都可以作为一个子树的根
深度(Depth):根节点到该节点的边数
高度(Height):节点到最深叶子节点的路径中边的数量,如:空树的高度为-1,只有根节点的树的高度为0,根节点到下一层子节点树的高度为1
度(Degree):该节点的子节点数量(也就是该节点边的数量)
兄弟节点(Brother):在同一层的节点
有序树/无序树:各节点的子树从左至右有/无次序
下面我们将进行图文用例子讲解这些术语:

在上图中,首先有唯一的根节点11,树中的每个元素叫节点 。我们首先来看看内部节点 、叶节点:
内部节点就是有子节点 的节点,比如11、7、15、5、9、13、20都是内部节点,而像3、6、8、10、12、14、18、25这些没有子节点的就称为叶节点,也叫外部节点 。父节点其实也就是这些儿子的长辈,比如3号节点,3的父节点有5、7、11。而关于子树 :就是由节点和它的子节点组成的一个集合,比如图中的13、14、12就组成了子树。下面我们了解深度 :比如25号节点,由根节点11到该节点,总共经过了3个节点,分别是:11、15、20,那么它的深度就是3。下面是高度 :我们在上图已经抽象的对这个树进行了分层,那么从根节点到最后一层的叶节点,总共经历了3层,因此这个树的高度就是3。最后是度:比如7,它的孩子节点有:5、9,因此7的度就是2。树的概念以及术语理解其实并不难,用这种趣味关系去解读,更效率!
树的性质

**1:**节点数=总度数+1,节点的度=节点孩子个数
**2:**度为m的树:各节点度的最大值为m,任意节点的度<=m,至少有一个节点的度=m
**3:**m叉树的区别:每个节点最多有m个孩子,任意节点的度<=m,允许所有节点的度都<m
**4:**度为m的树(m叉树)
第 i 层最多有m的(i -1)次方个节点(i>=1)
例如:上面的第一层到第二层,有3个边,属于3叉树,那么m=3,第一层就有1个节点,第二层就最多有3的(2-1)次方个节点,以此类推!
**5:**高度为h的m叉树最多有(m-1)分之(m的h次方-1)个节点,高度为h的m叉树最少有h个节点
这个公式就是由第4个累加得来的,,比如:第一层有1个节点,第二层有m个节点,以此类推,累加就得到公式
6:有n个节点的m叉树最小高度:所有节点都有m个孩子,高度h=log以m为底的(n(m-1)+1)的对数,这个性质用来求最小树的高度。
树的分类
树也有分类,咱们今天学习的二叉树就是其中的一种,一起来简单了解一下!
二叉树(Binary Tree):
定义:每个节点最多2个子节点(左节点与右节点)
遍历方式:前序遍历(根->左->右)、中序遍历(左->根->右)、后序遍历(左->右->根)、层序 遍历(根据层级,从上到下,从左到右)
二叉搜索树(Binary Search Tree):
定义:左子树所有节点的值<根节点值<右节点所有节点的值
操作复杂度:查找、插入、删除,一般为O(n),应用于:快速查找、排序
AVL树:
定义:一种自平衡的二叉搜索树,可以保证任意节点的左右子树高度不超过1
复杂度:所有操作几乎都是O(logn),应用于频繁插入/删除的场景
红黑树(Red---Black Tree):
定义:通过颜色标记和规则保持平衡的二叉搜索树
复杂度:几乎也均为O(logn),应用于:Java的TreeMap、C++的std::map等
B树(B Tree):
定义:多路平衡搜索树,每个节点包含多个子节点和键值
应用于:数据库索引、文件系统
.........
树的存储结构
前面我们已经知道了树的一些基本术语以及概念,下面咱们来简单看看树的存储结构
树有2大类存储结构:顺序存储 链式存储
顺序存储其实也就是数组存储,掌握起来并不难,树的链式存储又分为二叉树的链式存储与普通树的链式存储。咱们先看二叉树的链式存储:每个节点包含数据域和两个指针域(左子节点,右子节点),如下:
struct Tree
{
int data;//数据域
struct Tree* left;//左子节点
struct Tree* right;//右子节点
};
普通树的链式存储需有3种表示方法,下面我们来进行初步理解:
(1)孩子表示法
结构设计:每个节点的子节点组织成一个链表,每个节点包含2部分:数据域(存储数据) 子节点链表(指向该节点的第一个子节点,后续子节点可以通过链表连接)
定义两个结构体:树节点(TreeNode) 表示树中的下一个节点和**子节点链表节点(ChildNode)**用于链表父节点的所有子节点

理解:大家看, 将R作为节点,那么它的孩子节点有A、B、C。将A作为节点,那么它的孩子节点有D、E。同理将F作为节点,那么它的孩子节点有G、H、K,每个节点将它的孩子节点连接起来形成链表,注意节点与孩子节点应该是相邻的层次,比如第0层只能找第1层的孩子节点,不能跨层
cs
//树节点
typedef struct TreeNode
{
int data;
struct ChildNode* First_Child;//指向第一个子节点
}TreeNode;
//子节点链表节点
typedef struct ChildNode
{
struct TreeNode* child;//子节点指针
struct ChildNode* next;//指向下一个子节点
}ChildNode;
cs
存储后的链表关系
R.First_Child->A->B->C->NULL
A.First_Child->D->E->NULL
F.First_Child->G->H->K->NULL
(2)孩子兄弟表示法
结构设计:每个节点包含三个部分,数据域、左孩子指针(指向该节点的第一个子节点)、右孩子指针(指向该节点的下一个兄弟节点),因此只需要一个结构体即可
孩子兄弟表示法也叫二叉链表示法,以二叉链表作为树的存储结构。链表中节点的两个链域分别指向该节点的 第一个孩子 和 下一个兄弟,遍历的顺序:先遍历树对应的前序遍历,再进行中序遍历(下文有讲到!)
cs
//孩子兄弟表示法
typedef struct CsNode
{
int data;
struct CsNode* First_child;//指向第一个节点
struct CsNode* Next_sibling;//指向下一个兄弟节点
}CsNode;
(3)双亲表示法


cs
#define MAX_TREE_SIZE 100
typedef struct PTNode { // 结点结构
int data; //数据域
int parent; // 双亲位置域
}PTNode;
typedef struct { // 树的结构
PTNode nodes[MAX_TREE_SIZE]; // 结点数组
int size; // 结点数
}PTree;
我们先来了解双亲,双亲节点其实也就是父节点。树是由几个节点集合组成的,因此我们需要定义一个节点结构,还有一个树结构,树结构里面的数组用来存储根节点的指向,用来表示父节点在节点数组的位置。核心思想就是通过数组记录每个节点的父节点位置,然后定位层级关系。
树的应用场景
树的应用主要在下面这5个场所,总体来说,还是很广泛的!树结构通过层次化的数据组织方式,在高效搜索、动态更新、数据关联等诸多场景中运用广泛。因此我们需要选择正确的树去针对问题
文件系统:目录结构是树形层次
数据库索引:B+树加速查询
编译器:语法树表示代码结构
网络路由:决策树优化路径选择
游戏开发:场景管理、行为树AI
二叉树
二叉树基本概念
学完前面的树,我们已经有了一个大概的了解,下面我们来学习树的一种特殊结构:二叉树。
二叉树(Binary Tree)是一种特殊的树形数据结构,它需要满足以下2个要求:
(1):除了最后一层,每层节点的子节点数量不能超过2个(因此被称为:"二叉",大家Get到了吗!)
(2):子树有左右节点之分,不可以颠倒

二叉树的主要性质
1:每层节点的数量是有限的
第 i 层最多有2的(i-1)个节点。例如:第一层只有一个根节点,那么可表示为2的(1-1)个节点
二叉树第二层最多有2个节点,可表示为2的(2-1)个节点,以此类推
2:叶子节点与度为2的节点关系:
叶子节点数n与度为2的节点数k满足:n=k+1,例如:一个二叉树,它度为2的节点总共有9个,那么它的叶子节点数就有9+1=10个
3:完全二叉树的深度:
具有n(n>=0)个节点的完全二叉树的深度为log以2为底的n的对数+1,对数运算只取整,如:完全二叉树有5个节点,那么n=5,深度就为:2+1=3。如:完全二叉树有10个节点,那么它的深度为3+1=4。对数运算只取整数
4:满二叉树与完全二叉树的区别:
满二叉树是指每一层都是满的二叉树,而完全二叉树是指除了最后一层外,其它层都是满的,并且最后一层的节点都集中在左边,可以参考下面的分类
二叉树的几大分类
完全二叉树:
除了最后一层外,其它层的节点数达到最大,最后一层节点从左到右连续填充,**从左至右的叶子节点依次减少,**比较适合数组存储

二叉排序树:
任意节点与其左右节点的关键字大小关系都满足:左<中<右
例如:3在最左边,3小于它右边的每一个关键字。9在偏中间,9小于它右边所有的关键字

平衡二叉树:
**任意节点的左右子树高度差都不超过1,**看起来比较的工整的一种树!

满二叉树:
每层的节点都是满的,除了最后一层的叶子节点,每个父节点都有2个孩子节点,如下:

二叉树的顺序存储
二叉树的顺序存储就是将二叉树节点按照完全二叉树的层次顺序存储到一维数组,从左到右、从上到下。适用于完全二叉树、满二叉树。存储的规则:根节点可以存在数组下标0或者1,如果下标从1开始:第n个子节点的左子节点为2n+1,其右子节点为2n+2。如果下标从0开始:第n个子节点的左子节点为2n,其右子节点为2n+1。空节点可以用特殊符号来进行占位,图例+结构体定义如下:

cpp
#define MAX_size 10
//二叉树的顺序存储结构
typedef struct OlderTree
{
int size;//实际节点数量
int Tree[MAX_size];//存储数组
}OlderTree;
特点比较:需要通过下标来访问某个节点,空间是连续的,因此比较紧凑,用来存储满二叉树时空间不造成浪费,但是存储完全二叉树时,会形成空间浪费,在进行插入删除某个树节点时,会整体移动,比较麻烦
二叉树的链式存储
二叉树的链式存储无疑是使用链表来完成存储操作,链式结构适用于各种二叉树,因此咱们重点来讲解它的逻辑思路以及代码实现!
首先它的基本存储结构是一个二叉链表,每个节点包含3个部分:数据域、左孩子指针、右孩子指针,如下:
cs
typedef struct Treenode
{
struct Treenode* light;//左孩子指针
struct Treenode* right;//右孩子指针
int data;//数据
}Treenode;
**思路:**咱们开始肯定要开辟一个根节点!那么此时它的左右孩子指针初始应该指向NULL,随着后面孩子节点的开辟之后涉及到连接,我们应该加一个判断,是否需要根节点以及之后开辟的父节点里面的左右孩子指针指向它对应的孩子节点。不存在的孩子节点我们之间让它指向NULL。如果要删除某个节点,可以直接跳过该节点,相较于数组方便很多。其次是层次关系,通过递归和迭代遍历二叉链表,来实现打印数据!下面我们来分布实现
(1)二叉树开辟节点
节点开辟几乎都是统一的,设置好节点的类型、大小,然后初始化,再进行返回空间指针就行
cs
//开辟节点
ChainTree* Perliminery(int data)
{
ChainTree* newnode = (ChainTree*)malloc(sizeof(ChainTree));
//判断空间有效性
if (newnode == NULL)
{
printf("开辟失败,空间无效\n");
return NULL;
}
//初始化节点
newnode->data = data;
newnode->left = NULL;
newnode->right = NULL;
return newnode;
}
(2)二叉树插入节点
在前面我们设置了开辟节点的函数,那么我们根据需要,会在外面定义一个空指针(TreeNode),这个TreeNode可以理解为父节点指针(空树时是根节点指针),将这个空指针进行传参进插入函数里面,再设置2个判断,如果这个参数是空,那么直接返回指向开辟好的节点指针,此时外面的这个TreeNode就不是空指针了,它指向了这个返回的空间,因此TreeNode指针是一直被更新的。如果这个参数非空,那么通过递归来判断节点值的比较,来考虑插入的位置,然后更新TreeNode指针指向,若两值相等,那么直接返回,不更新父节点指针TreeNode。代码如下:

cs
//插入节点
ChainTree* Insert(ChainTree* TreeNode, int data)
{
//如果是空树,那么作为根节点
if (TreeNode == NULL)
{
return Perliminery(data);
}
//如果是非空树,根据传的值与当前节点的值比较
if (data < TreeNode->data)
{
TreeNode->left = Insert(TreeNode->left, data);
}
else
TreeNode->right = Insert(TreeNode->right, data);
return TreeNode;
}
(3)二叉树的遍历
二叉树的遍历就是通过特定的顺序来访问节点的数据,以下就是几种常见的遍历逻辑(特别声明:图片来自大数据!仅为方便理解!)
先序遍历:
前序遍历就是先访问根节点,再以递归的方式来处理左子树与右子树,顺序是先访问根节点,然后访问左子树,最后访问右子树:

cs
//先序遍历
void Preorder(ChainTree* TreeNode)
{
//判断是否是空树
if (TreeNode == NULL)
{
return;
}
printf("%d ", TreeNode->data);
//遍历左子节点
Preorder(TreeNode->left);
//遍历右子节点
Preorder(TreeNode->right);
}
中序遍历:
顺序就是先递归左子树,再访问根节点,最后访问右子树,只是换个顺序而已!

cs
//中序遍历
void Middle(ChainTree* TreeNode)
{
//判断是否是空树
if (TreeNode == NULL)
{
return;
}
//遍历左子节点
Preorder(TreeNode->left);
printf("%d ", TreeNode->data);
//遍历右子节点
Preorder(TreeNode->right);
}
后序遍历:
顺序:先递归左子树,再递归右子树,再访问根节点

cs
//后序遍历
void Postorder(ChainTree* TreeNode)
{
//判断是否是空树
if (TreeNode == NULL)
{
return;
}
//遍历左子节点
Preorder(TreeNode->left);
//遍历右子节点
Preorder(TreeNode->right);
printf("%d ", TreeNode->data);
}
层序遍历:
按照字面意思,就是一层一层的去遍历

二叉树的链式存储效果展示+完整代码

cs
#define _CRT_SECURE_NO_WARNINGS 1
#include"text.h"
int main()
{
//数据
int data = 0;
ChainTree* TreeNode = NULL;
//开辟节点
data = 40;
TreeNode = Insert(TreeNode, data);
data = 20;
TreeNode = Insert(TreeNode, data);
data = 30;
TreeNode = Insert(TreeNode, data);
data = 10;
TreeNode = Insert(TreeNode, data);
//先序遍历
printf("先序遍历: ");
Preorder(TreeNode);
printf("\n");
//中序遍历
printf("中序遍历: ");
Middle(TreeNode);
printf("\n");
//后序遍历
printf("后序遍历: ");
Postorder(TreeNode);
printf("\n");
return 0;
}
头文件:
cs
#pragma once
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
#include<stdlib.h>
typedef struct ChainTree
{
struct ChainTree* left;//左孩子节点
struct ChainTree* right;//右孩子节点
int data;//存储数据
}ChainTree;
//开辟节点
ChainTree* Perliminery(int data);
//插入节点
ChainTree* Insert(ChainTree* TreeNode, int data);
//先序遍历
void Preorder(ChainTree* TreeNode);
//中序遍历
void Middle(ChainTree* TreeNode);
//后序遍历
void Postorder(ChainTree* TreeNode);
函数实现:
cs
#define _CRT_SECURE_NO_WARNINGS 1
#include"text.h"
//开辟节点
ChainTree* Perliminery(int data)
{
ChainTree* newnode = (ChainTree*)malloc(sizeof(ChainTree));
//判断空间有效性
if (newnode == NULL)
{
printf("开辟失败,空间无效\n");
return NULL;
}
//初始化节点
newnode->data = data;
newnode->left = NULL;
newnode->right = NULL;
return newnode;
}
//插入节点
ChainTree* Insert(ChainTree* TreeNode, int data)
{
//如果是空树,那么作为根节点
if (TreeNode == NULL)
{
return Perliminery(data);
}
//如果是非空树,根据传的值与当前节点的值比较
if (data < TreeNode->data)
{
TreeNode->left = Insert(TreeNode->left, data);
}
else
TreeNode->right = Insert(TreeNode->right, data);
return TreeNode;
}
//先序遍历
void Preorder(ChainTree* TreeNode)
{
//判断是否是空树
if (TreeNode == NULL)
{
return;
}
printf("%d ", TreeNode->data);
//遍历左子节点
Preorder(TreeNode->left);
//遍历右子节点
Preorder(TreeNode->right);
}
//中序遍历
void Middle(ChainTree* TreeNode)
{
//判断是否是空树
if (TreeNode == NULL)
{
return;
}
//遍历左子节点
Preorder(TreeNode->left);
printf("%d ", TreeNode->data);
//遍历右子节点
Preorder(TreeNode->right);
}
//后序遍历
void Postorder(ChainTree* TreeNode)
{
//判断是否是空树
if (TreeNode == NULL)
{
return;
}
//遍历左子节点
Preorder(TreeNode->left);
//遍历右子节点
Preorder(TreeNode->right);
printf("%d ", TreeNode->data);
}
二叉树的存储结构比较
链式存储相较于顺序存储咱们都很清楚的一个特点,就是链式存储空间利用率更高,但是采用链式存储的话,代码的难度也是相较较大的,非常考察代码能力以及逻辑思维能力,在此篇文章中,多次使用了递归的逻辑去实现二叉树的遍历,因为递归可以大大提高代码的简洁能力,因此递归有很强大的工具价值,我们需要了解递归的思路,为了以后的问题可以快速的想到递归,下面是补充的一个小节,有关递归的逻辑思路讲解,希望可以完全拿下二叉树的遍历!
递归的逻辑讲解
递归可以大大简化代码,提高工程效率,下面我们主要讲递归的逻辑思路与机制!
递归的实现机制:
递归以来栈来多次调用函数,每次递归会创建新的栈帧空间,返回时从开辟好的空间末尾逐渐释放空间的初始位置,因此递归可以结合数据结构栈来想象,递归如果没有结束标志,那么会形成栈空间溢出
因此递归必须有一个限制条件来作为结束标志,每次递归函数会调用自身,更新参数,当到达限制条件返回时,栈帧开始逐渐出栈,并开始计算结果,先递推到限制条件,然后带值回归到初始条件,得到最终结果:
