目录
🎈前言(树的基本概念及结构)
🌲树的概念
树型结构是一类重要的非线性数据结构,它是由n(n>=0)个有限结点组成一个具有层次关系的集合。把它叫做树是因为它看起来像一棵倒挂的树,也就是说它是根朝上,而叶朝下的。
🔷有一个特殊的结点,称为根结点,根结点没有前驱结点。
🔷当n>=1时,其余结点可分为m(m>0)个互不相交的有限集T1,T2,...,Tm,其中每一个集合本身又是一棵树,也就是根结点的子树。
例如:A是根结点,A的子树有T1={B},T2={C},T3={D,H},T4={E,I,J,P,Q},T5{F,K,L,M},T6={G,N}。T1是以B为根结点的子树,T2是以C为根结点的子树,T3是D为根结点的子树,T4是E为根结点的子树,T5是F为根结点的子树,T6是G为根结点的子树。他们都是根A的子树。像T41={I},T42={J,P,Q},T41和T42又是根E的子树。
树与非树的区别:
🔸子树与子树之间是不相交的,根A结点的子树T1,T2,T3,T4,T5,T6没有相交结点。
🔸除了根结点外,每个结点有且仅有一个根结点。
🔸一颗有N个结点的树,他们连接的边有N-1条。
🌲树的基本术语
🌍结点的度
结点拥有子树的数量,称为该结点的度。例如上图:A的度为6,B和C的度为0,D的度为1,E的度为2,F的度为3,G的度为1.
🌍叶结点 or 终端结点
度为0的结点称为叶子结点或终端结点。例如上图:叶子结点有B,C,H,I,P,Q,K,L,M,N。
🌍非终端结点 or 分支结点
度不为0的结点称为非终端结点或分支结点。例如上图:分支结点有A,D,E,J,F,G。
🌍双亲结点 or 父结点
结点的子树的根结点称为该结点的孩子,该结点称为孩子的双亲(父结点)例如上图:B,C,D,E,F,G的父结点为A。
🌍孩子结点 or 子结点
结点的子树的根称为孩子结点。例如上图:A的孩子结点有B,C,D,E,F,G。
🌍兄弟结点
具有共同父结点的结点称为兄弟结点。例如上图:B,C,D,E,F,G。互为兄弟结点
🌍 树的度
一颗树中,最大结点的度称为树的度。例如上图:A结点的度为6最大,所以树的度为6
🌍结点的层次
从根开始定义起,根结点为第一层,根的子结点为第二层,以此类推。
🌍树的 深度 or 高度
树中结点的最大层次。例如上图:树的高度为4
🌍结点的祖先
从根到该结点所经过的所有分支结点都是该结点的祖先。例如上图:A~P经过了A,E,J这些结点都是P的祖先结点。
🌍子孙结点
以某结点为根,该根的所以子树的结点都称为该结点的子孙。例如上图:以A为根,其他所有结点都是根A的子孙结点
🌍森林
由m(m>0)颗互不相交的多颗树的集合称为森林
🌲树的表示方式
双亲表示法
表达方式:使用一个结构体数组储存树的每一个结点,每一个结点储存数据域和该结点的双亲结点下标
#define int TREE_MAX_SIZE;
typedef int ElemData
typedef struct TreeNode
{
ElemData data;
int parent;
}TreeNode;
Typedef struct Tree
{
TreeNode nodes[TREE_MAX_SIZE];
int n;//结点数
}Tree;
弊端:涉及到孩子结点的操作,不容易找到其孩子。求结点的孩子,需要遍历整颗数。
孩子兄弟表示法
表达方式:每一个结点有两个指向域,一个指向其孩子,一个指向其兄弟。
typedef int ElemData;
typedef struct TreeNode
{
ElemData data;
struct TreeNode *child;
struct TreeNode *brother;
}
孩子表示法
表达方式:使用一个结构体数组储存树的每一个结点,每一个结点包含数据域,和孩子链表,用于存储孩子节点位于顺序表中的位置。如果没有孩子结点,则该孩子链表为空。
#define TREE_MAX_SIZE 100;
typedef int ElemType;
typedef struct LNode
{
int child;
struct LNode *next;
}LNode;
typedef struct TreeNode
{
ElemType data;
LNode *FirstChild;//孩子链表头指针
}TreeNode;
typedef struct Tree
{
TreeNode nodes[TREE_MAX_SIZE];
int n;//结点数量
}Tree;
🍍二叉树介绍
📖基本定义及性质
定义:二叉树(binary tree)是指树中节点的度不大于2的有序树,它是一种最简单且最重要的树。二叉树的递归定义为:二叉树是一棵空树,或者是一棵由一个根节点和两棵互不相交的,分别称作根的左子树和右子树组成的非空树;左子树和右子树又同样都是二叉树。--《百度百科》
二叉树的五种基本形态
特点:
💥每个结点最多有两颗子树,即每个结点的度不大于2。
💥二叉树的子树有左右之分,其子树的次序不能颠倒。
性质:
🔶在二叉树的第i层至多有个结点
🔶深度为k的二叉树至多有个结点
(每一层的最多结点情况个数相加)
🔶对任何一颗二叉树T,如果其总端结点数为,度为2的结点数为,则
证明:设树的总结点个数为n,分支条数为A,则,.
在二叉树中,度为1的结点会射出一条边,度为2的结点会射出两条边
特殊的二叉树:完全二叉树和满二叉树
🚶满二叉树:一个二叉树,如果每一层的结点都达到最大值,则这个二叉树就是满二叉树,满二叉树的结点个数为,如果一个满二叉树的层次为k,则其结点个数为
🚶完全二叉树:一棵深度为k的有n个结点的二叉树,对树中的结点按从上至下、从左到右的顺序进行编号,如果编号为i(1≤i≤n)的结点与满二叉树中编号为i的结点在二叉树中的位置相同,则这棵二叉树称为完全二叉树。(k层前每一层都是满结点,第k层依次从左到右连续有结点)
🛑满二叉树是特殊的完全二叉树
性质:
🔶n个结点的完全二叉树的深度为
证明:假设一个完全二叉树的深度为k,结点个数为n个,完全二叉树的结点个数
完全二叉树的结点个数k-1层满二叉树的结点个数:
🔶深度为k的完全二叉树的结点个数为()
📖二叉树的存储结构
顺序存储:二叉树的顺序存储是使用数组来存储的,一般使用数组比较适合完全二叉树,因为不是完全二叉树会有空间浪费,二叉树的顺序存储在物理上是一个数组,在逻辑上是一颗树。
typedef struct BinaryTree
{
ElemData nodes[Tree_MAX_SIZE];
int n;
}
链式存储 :用链表来表示一棵二叉树,即用链表来指示元素的逻辑关系。 通常的方法使用二叉链表示每一个结点,链表中每个结点由三个域组成,数据域和左右指针域,左右指针分别用来给出该结点左孩子和右孩子所在的链结点的存储地址 。链式结构又分为二叉链和三叉链,二叉链有两个指针域,三叉链有三个指针域,左右指针和双亲指针。
//二叉链表示结点
typedef struct BinaryTreeNode
{
struct BinaryTreeNode *pLeftChild;
struct BinaryTreeNode *prightChild;
ElemData data;
}BTNode;
//三叉链表示结点
typedef struct BinaryTreeNode
{
struct BinaryTreeNode *pLeftChild;
struct BinaryTreeNode *pRightChild;
struct BinaryTreeNode *pParent;
ElemData data;
}BTNode;
📖二叉树的遍历
先序遍历二叉树:若二叉树为空,则返回,否则访问根结点,先序遍历左子树,先序遍历右子树。
void PreOrderTraversal(Binadry *root)
{
if(root == NULL)
{
return;
}
else
{
printf("%d",root->data);
PreOrderTraversal(root->pLeftChild);
PreOrderTraVersal(root->pRightChild);
}
}
中序遍历二叉树:若二叉树为空,则返回,否则中序遍历左子树,访问根结点,中序遍历右子树。
void MidOrderTraversal(BTNode *root)
{
if(root == NULL)
{
return;
}
else
{
PreOrderTraversal(root->pLeftChild);
printf("%d",root->data);
PreOrderTraVersal(root->pRightChild);
}
}
后序遍历二叉树:若二叉树为空,则返回,否则后序遍历左子树,后序遍历右子树,访问根结点
void PreOrderTraversal(BTNode *root)
{
if(root == NULL)
{
return;
}
else
{
PreOrderTraversal(root->pLeftChild);
PreOrderTraVersal(root->pRightChild);
printf("%d",root->data);
}
}
层序遍历:设二叉树的 根节点所在层数为1,层序遍历就是从所在二叉树的根节点出发,首先访问第一层的树根节点,然 后从左到右访问第2层上的节点,接着是第三层的节点,以此类推,自上而下,自左至右逐层访树的结点的过程就是层序遍历。
void LevelOrder(BTNode *root)
{
Queue q;
InitQueue(&q);
if(root != NULL)
{
QueuePush(&q,root);
}
while(!QueueEmpty(&q) == 1)
{
BTNode *pNode=QueueFront(&q);
QueuePop(&q);
printf(PNode->data);//未知数据类型,以此来代替
if(pNode->pLeftChild != NULL)
{
QueuePush(pNode->pLeftChild);
}
if(PNode->pRightChild != NULL)
{
QueuePush(pNode->pRightChild);
}
}
return;
}
📃二叉搜索树的介绍
若它的左子树不空,则左子树上所有结点的值均小于它的根结点的值; 若它的右子树不空,则右子树上所有结点的值均大于它的根结点的值; 它的左、右子树也分别为二叉排序树。二叉搜索树作为一种经典的数据结构,它既有链表的快速插入与删除操作的特点,又有数组快速查找的优势;所以应用十分广泛,例如在文件系统和数据库系统一般会采用这种数据结构进行高效率的排序与检索操作
💡二叉搜索树的实现方式
二叉链式搜索树:通过使用链表的形式实现搜索树
逻辑关系:
物理关系:
💡二叉树的基本操作
✨创建新结点
根据二叉链结点的特点,初始化结点。
BTNode *NewNode(ElemData data)
{
BTNode *newNode=(BTNode *)malloc(sizeof(BTNode));
//初始化结点
newNode->data = data;
newNode->pLeftChild = NULL;
newNode->pRightChild = NULL;
return BTNode;
}
✨初始化二叉树
二叉搜索树的初始化,返回一颗空树。
BTNode* BinaryTreeInit()
{
BTNode* root = NULL;//初始化根结点
return root;
}
✨插入结点
插入的结点依次和树中的结点进行比较,通过比较的结果选择插入根或子根的左孩子或者右孩子
void BinaryTreePush(BTNode **root,ElemData data)
{
static BTNode* newNode = NewNode(data);
//找到插入的位置
if (*root == NULL)
{
*root= newNode;
}
else
{
if ((*root)->data > data)//小于根结点则递归根到左子树,否则递归根到右子树
{
BinaryTreePush(&(*root)->pLeftChild,data);
}
else
{
BinaryTreePush(&(*root)->pRightChild, data);
}
}
return;
}
✨二叉树中查找一个节点
通过查找结点的数值和(根或子树的根)进行比较,遍历查找。
BTNode* BinaryNodeFind(BTNode* root,ElemData data)
{
//走到空结点每找到则没有
if (root == NULL)
return NULL;
if (root->data > data)//遍历查找
{
return BinaryNodeFind(root->pLeftChild, data);
}
else if (root->data < data)
{
return BinaryNodeFind(root->pRightChild, data);
}
else
{
return root;
}
}
✨获取最小节点值
最小结点的值位于左子树中左孩子为空的结点
BTNode* GetMinTreeNode(BTNode* root)
{
if (root == NULL)
return NULL;
//最小的结点是左子树中左孩子为空的结点
while (root->pLeftChild != NULL)
{
root = root->pLeftChild;
}
return root;
}
✨获取最大结点值
最大的结点位于右子树中右孩子为空的结点
BTNode* GetMaxTreeNode(BTNode* root)
{
if (root == NULL)
return NULL;
//最大的结点是,右子树中右孩子结点为空的结点
while (root->pRightChild != NULL)
{
root = root->pRightChild;
}
return root;
}
✨删除节点
情况1:删除的是根结点
情况2:删除的是叶子结点
情况3:删除的是只有左子树的结点
情况4:删除的是只有右子树的结点
情况5:删除的是既有左子树又有右子树的结点
void TreeNodeEarse(BTNode** root, BTNode* n)
{
assert(n);
if (*root == NULL)
return;
//如果删除的结点为根节点时,改变根结点的值
if (*root == n)
{
if ((*root)->pLeftChild == NULL && (*root)->pRightChild == NULL)
*root = NULL;
else if ((*root)->pLeftChild == NULL && (*root)->pRightChild != NULL)
*root = (*root)->pRightChild;
else if ((*root)->pLeftChild != NULL && (*root)->pRightChild == NULL)
*root = (*root)->pLeftChild;
else
{
BTNode* maxLeft1 = GetMaxTreeNode((*root)->pLeftChild);
(*root)->data = maxLeft1->data;
TreeNodeEarse(root,maxLeft1);
}
return;
}
BTNode* parent = NULL;
if (n->pLeftChild == NULL && n->pRightChild == NULL)//删除结点为叶子结点时
{
//找到其父亲结点
parent = ParentNode(*root, n);
//将双亲结点的一个孩子置空
if (n == parent->pLeftChild)
{
parent->pLeftChild = NULL;
}
if (n == parent->pRightChild)
{
parent->pRightChild = NULL;
}
free(n);
return;
}
if (n->pLeftChild == NULL && n->pRightChild != NULL)//只有右结点的情况
{
//找到其父亲结点,和孩子结点
parent = ParentNode(*root, n);
if (parent->pLeftChild == n)
parent->pLeftChild = n->pRightChild;
if (parent->pRightChild == n)
parent->pRightChild = n->pRightChild;
free(n);
return;
}
if (n->pLeftChild != NULL && n->pRightChild == NULL)//只有左结点的情况
{
//找到其父亲结点,和孩子结点
parent = ParentNode(*root, n);
if (parent->pLeftChild == n)
parent->pLeftChild = n->pLeftChild;
if (parent->pRightChild == n)
parent->pRightChild = n->pLeftChild;
free(n);
return;
}
if (n->pLeftChild != NULL && n->pRightChild != NULL)//左右结点都存在的情况
{
//找到删除结点左子树中最大的结点
BTNode* maxLeft=GetMaxTreeNode(n->pLeftChild);
n->data = maxLeft->data;//将删除结点的值赋成删除结点左子树中最大的结点
//删除该结点左子树中最大的结点
TreeNodeEarse(&n, maxLeft);
return;
}
}
✨销毁树
使用后序遍历释放每一个结点
void DestroyBinaryTree(BTNode* root)
{
if (root == NULL)
{
return;
}
else
{
//后序遍历释放每个结点
DestroyBinaryTree(root->pLeftChild);
DestroyBinaryTree(root->pRightChild);
free(root);
}
}
✨计算树中结点的个数
int TreeSize(BTNode* root)
{
if (root == NULL)
{
return 0;
}
else
{
return TreeSize(root->pLeftChild) + TreeSize(root->pRightChild) + 1;
}
}
✨叶子结点的个数
当根或子树的根结点左右孩子都为空时,则为叶子结点
int LeafSize(BTNode* root)
{
if (root == NULL)
{
return 0;
}
if (root->pRightChild == NULL && root->pLeftChild == NULL)
{
return 1;
}
else
{
return LeafSize(root->pLeftChild) + LeafSize(root->pRightChild);
}
}
🔚总结
二叉树是我们学习数据结构的重难点,当我们在学习的过程中可以尝试多多画图,有了图像的直观表达,我们分析一个问题就会简单很多。本节中二叉搜索树的基本操作大部分是使用递归来实现。当然你也可以尝试使用非递归,递归的方法还是相对比较好理解的。在我们在学习二叉树时,务必掌握好递归,可能你并不是很理解递归,或许你会在学习二叉树之后有了对递归的更多看法
鄙人不才,文章有错误的地方还请高人指点指点🈯 。
感谢大家的支持!