树
树的定义
树(Tree):N(N>=0)个结点构成的有限集合。
当N=0时,称为空树;
对于任一棵非空树,具备以下性质:
- 树中有一个称为“根(Root)”的特殊结点,可以用r表示
- 其余结点可分为m(m>0)个互不相交的有限集T1,T2,…Tm,其中每个集合本身又是一棵树,称为原来树的“子树(SubTree)”。
注意:
- 子树是不相交的
- 除了根结点外,每个结点有且仅有一个父结点
- 一棵N个结点的树有N-1条边
树的一些基本术语
- 结点的度(Degree):结点的子树个数。如根结点A的度为3,即有3棵子树,B的度为2。
- 树的度:树的所有结点中最大的度数。如图为3。
- 叶结点(Leaf):度为0的结点。如图为F,L,H,M,J,K。
- 父结点(Parent):有子树的结点是其子树的根结点的父结点。如图B为F,G的父结点。
- 子结点(Child):若A结点是B结点的父结点,则称B结点是A结点的子结点;子节点也称为孩子结点。如图F,G也是B的子结点。
- 兄弟结点(Sibling):具有同一父结点的各结点彼此是兄弟结点。如图I,J,K称为兄弟结点。
- 路径和长度:从结点n1到nk的路径为一个结点序列n1,n2,…,nk,ni是ni+1的父结点。路径所包含边的个数为路径的长度。如图的一条路径是A->D->I->M,其长度为3。
- 祖先结点(Ancestor):沿树根到某一结点路径上的所有结点都是这个结点的祖先结点。如图A,C都是H的祖先结点。
- 子孙结点(Descendant):某一结点的子树中所有结点是这个结点的子孙。H也称为A、C的子孙结点。
- 结点的层次(Level):规定根结点在1层。其他任一结点的层数是其父结点的层数加1。如图所示根结点A的层次为1,B、C、D的结点为2。
- 树的深度(Depth):树中所有结点中的最大层次是这棵树的深度。如图树的深度为L、M的层次,即为4。
如图为一个例子:
附上一位博主的文章,带图解释很清楚,敲棒!!!树的基本术语最全面最易懂解释
树的表示方法
以上图为例。
1.双亲表示法
设计思想:根据每个结点除根以外只有唯一双亲的性质;以一组连续空间存储树的结点,同时在每个结点中附设一个指示器指示其双亲结点在表中的位置。
根结点没有父结点,因此对应parent
在数组中下标设为-1,其他结点都为其父结点在数组中的下标。
#define MAXSIZE 100//定义最大结点数
typedef char TElemType;
typedef struct TreeNode
{
TElemType data;//树中数据结点类型
int parent;//结点父结点在数组中的位置下标
}Tree;
typedef struct
{
Tree node[MAXSIZE];//存放树中所有结点
int r, n;//根的位置下标和结点数
}PTree;
- 优点:方便查找双亲及其祖先结点。
- 缺点:查找孩子兄弟结点比较麻烦;未表示出结点之间的先后次序。
改进方法:可以在数组中按一定的顺序存放结点,如按层序遍历或先序遍历等。
2.孩子表示法
方案一:使用多重链表,即每个结点有多个指针域,分别指向一棵子树的根。
结点异构(变长结点):各结点的指针个数不等,分别为各自结点的度。
结点的结构:
缺点:1.存在很多空链域,较浪费空间。一棵有n个结点,度为k的树中,必有n(k-1)+1个空链域。
2.操作实现不方便。
方案二:使用孩子链表,即把每个结点的孩子排列在排列在一条单链表中;所有n个表头指针又组成一个线性表,为方便查找,可采用顺序存储结构。
如根结点的孩子表示如图:
代码如下:
#define MAXSIZE 100
typedef char TElemType;
typedef struct TNode//孩子结点结构定义
{
int child;//孩子结点下标
struct TNode* next;//指向下一个孩子结点的指针
}*childptr;
typedef struct//表头结点的结构定义
{
TElemType data;//树中结点的数据
int parent;//存放双亲的下标
childptr firstchild;//指向第一个孩子的指针
}TBox;
typedef struct//树结构的结构定义
{
TBox node[MAXSIZE];
int r;//根的下标位置
int t;//结点的数目
}Tree;
如果要找到双亲,也可以采用孩子双亲表示法,如下图的例子:
孩子兄弟表示法
- 用二叉链表作为树的存储结构;
- 链表中每个结点两个指针域分别指向其第一个孩子结点和下一个兄弟结点。
代码如下:
typedef char ElemType;
typedef struct TNode
{
ElemType data;
TNode* firstchild;
TNode* nextsibling;
}TNode,*Tree;
- 优点:(a)便于实现树的各种操作,如查找孩子、子孙或兄弟结点;
(b)能表示出结点之间的先后次序。 - 缺点:找结点的双亲及祖先比较费事。
- 改进:增设指示双亲的指针。
附上一位博主总结的表示方法,挺详细清楚的,传送门:数据结构中树的表示方法
二叉树
二叉树的定义
- 二叉树T:一个有穷的结点集合。这个集合可以为空;若不为空,则它是由根结点和称为其左子树TL,和右子树TR的两个不相交的二叉树组成。
- 二叉树的具体五种基本形态
- 二叉树的子树有左右顺序之分
这是两个不同的二叉树。 - 特殊二叉树
- 斜二叉树(Skewed Binary Tree)
- 完美二叉树(Perfect Binary Tree)或满二叉树(Full Binary Tree)
- 完全二叉树(Complete Binary Tree)
有n个结点的二叉树,对树中结点按从上至下、从左到右顺序进行编号,编号为i,(1≤i≤n)结点与满二叉树中编号为i结点在二叉树中位置相同。
特点:1.除最后一层外,各层均达到最大结点数
2.叶子结点只可能在层次最大的两层出现
3.对任一结点,若其右分支下的子孙的最大层次为h,则其左分支下的子孙的最大层次必为h或h+1
二叉树的性质
- 一个二叉树的第i层的最大结点数为:2(i-1),i≥1。
【证明思路:数学归纳法】第一层至多有1个结点;第k+1层的结点至多是第k层结点的2倍(k>0)。 - 深度为k的二叉树有最大结点总数为:2k-1,k≥1。
【证明思路:用求等比级数前k项和的公式】
20+ 21+…+2k-1=2k-1 - 对任何非空二叉树T,若n0表示叶结点的个数、n2是度为2的非叶结点个数,那么两者满足关系n0=n2+1。
【证明思路1:二叉树上结点的总数会比边的总数多1】
结点总数n=n0+n1+n2
边总数e=2n2+n1=n-1
可得n0=n2+1
【证明思路2:只有增加度为2的结点才会增加叶子结点的数量】
- 具有n个结点的完全二叉树的高度为𠃊log2n𠃎(向下取整,找不到向下取整符号)
【证明思路:利用第二点的性质和完全二叉树的定义反推】
2k-1-1<n≤2k-1→k-1≤log2n<k
二叉树的存储结构
1.顺序存储结构
1.完全二叉树:按从上至下、从左到右顺序存储。
n个结点的完全二叉树的结点父子关系:
- 非根结点(序号i>1)的父结点的序号是𠃊i/2𠃎;
- 结点(序号为i)的左孩子结点的序号是2i(若2i≤n,否则没有左孩子)
- 结点(序号为i)的右孩子结点序号是2i+1(若2i+1≤n,否则没有有孩子)
2.一般二叉树也可以采用这种结构,但会造成空间浪费。
#define MAXSIZE 100
typedef char TElemType;
typedef TElemType SqBiTree[MAXSIZE];
SqBiTree tree;
更详细的顺序存储结构及其实现康康这篇文章:二叉树的顺序存储结构
2.链式存储结构
- 二叉链表:结点至少包含3个域,数据域、左指针域和右指针域。
代码实现:
typedef char ElementType ;
typedef struct TreeNode* BinTree;
typedef BinTree Position;
struct TreeNode
{
ElementType Data;
BinTree Left;
BinTree Right;
};
【性质】含有n个结点的二叉链表中,有n+1个空指针域。
- 三叉链表:在二叉链表结点上再增加1个指向双亲结点的指针域。
代码实现:
typedef char ElementType ;
typedef struct TreeNode* BinTree;
typedef BinTree Position;
struct TreeNode
{
ElementType Data;
BinTree Left;
BinTree Right;
BinTree Parent;
};
二叉树的遍历
遍历:是按一定的规律巡访数据结构中的各个结点,使得每个结点均被访问一次,而且仅被访问一次。
- 访问:对结点做某种 处理,如输出结点信息、修改结点值等等。
- 应用场合:查找具有某种特征的结点;对全部结点逐一进行输出或修改操作等等。
二叉树的递归遍历
1.前序递归遍历
遍历过程:先访问根结点;先序遍历其右子树;先序遍历其右子树。
代码实现:
void PreOrderTraversal(BinTree BT)//前序递归遍历
{
if (BT)
{
printf("%d", BT->Data);
PreOrderTraversal(BT->Left);
PreOrderTraversal(BT->Right);
}
}
2.中序递归遍历
遍历过程为:中序遍历其左子树;访问根结点;中序遍历其右子树。
代码实现:
void InOrderTraversal(BinTree BT)//中序递归遍历
{
if (BT)
{
InOrderTraversal(BT->Left);
printf("%d", BT->Data);
InOrderTraversal(BT->Right);
}
}
3.后序递归遍历
遍历过程:后序遍历其左子树;后序遍历其右子树;访问其根结点。
代码实现:
void PostOrderTraversal(BinTree BT)//后序递归遍历
{
if (BT)
{
PostOrderTraversal(BT->Left);
PostOrderTraversal(BT->Right);
printf("%d", BT->Data);
}
}
递归遍历的小结:
先序、中序和后序遍历过程:遍历过程中经过结点的路线一样,只是访问各结点的时机不同(也就是输出结点信息的时机不同)。
二叉树的非递归遍历
非递归算法实现的基本思路:使用堆栈。
这位博主总结的很全面,很清楚,小白学习啦,感谢!传送门在这里:
C语言实现二叉树的非递归遍历
还有二叉树的遍历方法总结(谢谢大佬们的文章!),传送门:二叉树遍历算法总结
二叉树的层序遍历
- 二叉树遍历的核心问题:二维结构的线性化
- 从结点访问其左、右儿子结点;
- 访问左儿子后,右儿子结点怎么办?
因此,需要一个存储结构保存暂时不访问的结点,存储结构为堆栈和队列。 - 队列实现:遍历从根结点开始,首先将根结点入队,然后开始执行循环:结点出队、访问该结点、其左右儿子入队
- 层序基本过程:先将根结点入队,然后:
①从队列中取出一个元素;
②访问该元素所指结点;
③若该元素所指结点的左、右孩子结点非空,则将其左、右孩子的指针顺序入队。
代码实现:
void LevelOrderTraversal(BinTree BT)
{
Queue Q;
BinTree T;
if (!BT)return;//若是空树直接返回
Q = CreateQueue(MaxSize);//创建并初始化队列
AddQ(Q, BT);
while (!IsEmptyQ(Q))
{
T = DeleteQ(Q);
printf("%d\n", T->Data);//访问取出队列的结点
if (T->Left)AddQ(Q, T->Left);
if (T->Right)AddQ(Q, T->Right);
}
}
看看这篇博客,有图示的原理分析,还有测试代码。二叉树层次遍历算法——C/C++
二叉搜索树
二叉搜索树定义
二叉搜索树(BST,Binary Search Tree)也称二叉排序树或二叉查找树。
二叉搜索树:一棵二叉树,可以为空;如果不为空,满足以下性质:
- 非空左子树的所有键值小于其根结点的键值。
- 非空右子树的所有键值大于其根结点的键值。
- 左、右子树都是二叉搜索树。
二叉搜索树操作的特别函数
Position Find(ElementType X,BinTree BST):从二叉搜索树BST中查找元素X,返回其所在结点的位置;
Position FindMin(BinTree BST):从二叉搜索树BST中查找并返回最小元素所在结点的地址;
Position FindMax(BinTree BST):从二叉搜索树BST中查找并返回最大元素所在结点的地址。
BinTree Insert(ElementType X,BinTree BST)
BinTree Delete(ElementType X,BinTree BST)
二叉搜索树的查找操作:Find
- 查找从根结点开始,如果树为空,返回NULL
- 若搜索树非空,则根结点关键字和X进行比较,并进行不同处理:
①若X小于根结点键值,只需在左子树中继续搜索;
②如果X大于根结点的键值,在右子树中继续进行搜索;
③若两者比较结果是相等,搜索完成,返回指向此结点的指针。
代码实现:
Position Find(ElementType X, BinTree BST)
{
if (!BST)return NULL;//二叉搜索树为空
if (X->BST->Data)
return Find(X, BST->Right);//在右子树中继续查找
else if (X < BST->Data)
return Find(X, BST->Left);//在左子树中继续查找
else return BST;//查找成功,返回找到的结点的地址
}
//二叉搜索树的迭代查找
Position IterFind(ElementType X, BinTree BST)
{
while (BST)
{
if (X > BST->Data)//向右子树移动,继续查找
BST = BST->Right;
else if (X < BST->Data)//向左子树移动,继续查找
BST = BST->Left;
else
return BST;//查找成功
}
return NULL;//查找失败
}
查找最大和最小元素
- 最大元素一定是在树的最右分枝的端结点
- 最小元素一定是在树的最左分枝的端结点
代码实现:
/*查找最小元素的递归函数*/
Position FinMin(BinTree BST)
{
if (!BST)return NULL;//空的二叉搜索树,返回NULL
else if (!BST->Left)
return BST;//找到最左叶结点并返回
else return FinMin(BST->Left);//沿左分支继续查找
}
/*查找最大元素的迭代函数*/
Position FinMax(BinTree BST)
{
if (BST)
while (BST->Right)
BST = BST->Right;//沿右分支继续查找,直到最右结点
return BST;
}
二叉搜索树的插入
【分析】:关键是要找到元素应该插入的位置,可以采用与Find类似的方法。
代码实现:
BinTree Insert(ElementType X, BinTree BST)
{
if (!BST)//若原树为空,生成并返回一个结点的二叉搜索树
{
BST = malloc(sizeof(struct TreeNode));
BST->Data = X;
BST->Left = BST->Right = NULL;
}
else if (X < BST->Data)//递归插入左子树
BST->Left = Insert(X, BST->Left);
else if (X > BST->Data)//递归插入右子树
BST->Right = Insert(X, BST->Right);
return BST;
}
二叉搜索树的删除
考虑三种情况:
- 要删除的是叶结点:直接删除,并再修改其父结点指针—置为NULL;
- 要删除的结点只有一个孩子结点:将其父结点的指针指向要删除结点的孩子结点;
- 要删除的结点有左、右两棵子树:用另一结点替代被删除结点:右子树的最小元素或者左子树的最大元素。
代码实现:
BinTree Delete(ElementType X, BinTree BST)
{
Position Tmp;
if (!BST)printf("要删除的元素未找到");
else if (X < BST->Data)
BST->Left = Delete(X, BST->Left);//左子树递归删除
else if (X > BST->Data)
BST->Right = Delete(X, BST->Right);//右子树递归删除
else
if (BST->Left && BST->Right)//被删除的结点有左右两个子结点
{
Tmp = FindMin(BST->Right);//在右子树中找最小的元素填充删除结点
BST->Data = Tmp->Data;
BST->Right = Delete(BST->Data, BST->Right);//在删除结点的右子树中删除最小元素
}
else//被删除的结点有一个或无子结点
{
Tmp = BST;
if (!BST->Left)//有右孩子或无子结点
BST = BST->Right;
else if (!BST->Right)//有左孩子或无子结点
BST = BST->Left;
free(Tmp);
}
return BST;
}
平衡二叉树
平衡二叉树的概念
平衡因子(Balance Factor,简称BF):BF(T)=hL-hR,其中hL和hR分别为T的左、右子树的高度。
平衡二叉树(Balanced Binary Tree)(AVL树):空树,或者任一结点左、右子树高度差的绝对值不超过1,即|BF(T)|≤1
平衡二叉树的调整
一开始不怎么懂,看了一些博主的文章后,就明白了很多。
传送门:平衡二叉树详解
平衡二叉树之平衡调整图解
写在最后:整理了几天,终于把树的一些基础知识梳理了一遍,上完课感觉很乱,基础的代码都不会啊,用写博客的方法回顾一遍还是挺好滴。数据结构好难啊,还是要打好基础,慢慢来吧!一些图片和资源来自中国大学慕课和大佬们的博客,我写的地方如果有不正确的还请指出。