目录
树的定义
线性表是一对一的关系,而树结构是一种一对多的关系。
树:树(Tree)是n(n>=0)个结点的有限集。n=0时称为空树。
在任意一课非空树中:
(1)有且仅有一个特定的称为跟(Root)的结点;
(2)n>1时,其余结点可分为m(m>0)个互不相交的有限集T1,T2,....Tm,其中每一个集合本身有时一棵树,并且称为根的树(SubTree)
树的定义其实就是我们在讲解栈时提到的递归方法。也就是在树的定义之中还用到了树的概念,这是一种比较新的定义方法。
子树T1和子树T2就是根结点A的子树。当然,D、G、H、I组成的树又是以B为根结点的子树,E、J组成的树是以C为根结点的子树
对于树的定义还需强调两点:
(1)n>0时根结点是唯一的,不可能存在多个根结点,数据结构中的树只能有一个根结点
(2)m>0时,子树的个数没有限制,但它们一定不相交。
结点的分类:
结点拥有的子树数称为结点的度(Degree)。度为0的结点称为叶结点(Leaf)或终端结点;度不为0的结点称为非终端结点或分支结点。除根结点之外,分支结点也称为内部结点。树的度就是树内各结点的度的最大值
结点间的关系:
结点的子树的根称为该结点的孩子,相应地,该结点称为孩子的双亲。同一个双亲的孩子之间称为兄弟(Sibling),结点的祖先是从根到该结点所经分支上的所有结点,反之,以某结点为根的子树中的任一结点都称为该结点的子孙
树的其他相关概念:
结点的层次(Level)从根开始定义起,根为第一层,根的孩子为第二层。其双亲在同一层的结点互称为堂兄弟。树中结点的最大层次称为树的深度或高度
如果将数中结点的各子树看成从左至右是有次序的,不能互换,则称该树为有序树,否则称为无序树
森林(Forest)是m(m>=0)棵互不相交的树的集合。对于树中每个结点而言,其子树的集合即为森林
对比线性表和树的结构:
树的抽象数据类型
ADT (tree)
Data //树是由一个根节点和若干棵子树构成的。树中结点具有相同数据类型及层次关系
Operation
InitTree(*T)://构造空树
DestroyTree(*T)://销毁树T
CreateTree(*T,definition)://按照defintion中给出树的定义来构造树
ClearTree(*T)://若数存在,则将树T清空为空树
TreeEmpty(T)://若T为空树,返回true,否则返回false
TreeDepth(T)://返回T的深度
Root(T)://返回T的根结点
Value(T,cur_e)://cur_e是树T中一个结点,返回此结点的值
Assign(T,cur_e,value)://给树T的结点cur_e赋值给value
Parent(T,cur_e)://若cur_e是树T的非根结点,则返回它的双亲否则返回空
LeftChild(T,cur_e)://若cur_e是树T的非叶结点,则返回它的最左孩子,否则返回空
RightSibling(T,cur_e)://若cur_e有右兄弟,则返回它的右兄弟,否则返回空
InsertChild(*T,*p,i,c)://其中p指向树T的某个结点,i为所指结点p的度上加1,非空树C与T不相交,操作 //结果为插入C为树T中P所指结点的第i棵子树
DeleteChild(*T,*p,i)://其中p指向树T的某个结点,i为所指结点p的度,操作结果为删除T中p所指结点的第i棵子树
树的存储类型
通过充分利用线性存储和链式存储结构的特点,完全可以实现对树的存储结构的表示。
我们有三种不同的表示法:双亲表示法、孩子表示法、孩子兄弟表示法
双亲表示法:
在树结构中,除了根结点外,其余每个结点,它不一定有孩子,但是一定有且仅有一个双亲
我们假设以一组连续空间存储树的结点,同时在每个结点中,附设一个指示器指示其双亲结点在数组中的位置
其中,data是数据域,存储结点的数据心思;parent是指针域,存储该结点都得双亲在数组中的下标
结点结构定义代码:
//树的双亲表示法结点结构定义
#define MAX_TREE_SIZE 100
typedef int TElemType;
typedef struct PTNode{//结点结构
TElemType data;//结点数据
int parent;//双亲位置
}PTNode;
typedef struct//树结构
{
PTNode nodes[MAX_TREE_SIZE];//结点数组
int r,n;//根的位置和结点数
}PTree;
有了这样的存储定义,我们就可以来实现双亲表示法了。由于根结点是没有双亲的,所以我们约定根结点的位置域设置为-1,这也就意味着,我们所有的结点都存有它双亲的位置。
这样的存储结构,我们要找到孩子的双亲的时间复杂度为O(1),若我们要知道结点的孩子是谁。则需要遍历整个结构才行
当然,我们可以增加一个结点最左边孩子的域,称为长子域,这样就可以很容易得到结点的孩子,
如果没有孩子的结点,这个长子域设置为-1
我们又关注结点的双亲、又关注结点的孩子、还关注结点的兄弟,而且对时间便利要求还比较高,那么我们还可以把此结构拓展为有双亲域、长子域、再有兄弟域。
存储结构设计是一个非常灵活的过程。一个存储结构设计得是否合理,取决于基于该存储结构的运算是否合适、是否方便,时间复杂度好不好等。
孩子表示法:
每个结点有多个指针域,其中每个指针指向一棵子树的根结点,我们把这种方法叫做多重链表表示法
方案一:
一种是指针域的个数等于树的度,复习一下,树的度是树各个结点度的最大值
其中,data是数据域;指针域用来指向该结点的孩子结点
树的度是3,所以我们的指针域个数是3
这种方法对于树中各结点的度相差很大时,显然是很浪费空间的,因为有很多结点它的指针域是空的。不过树的各结点度相差很小时,就变成了优点
方案二:
我们使每个结点指针域的个数等于该结点的度,我们另外开辟一个空间来专门存放结点指针的数
这种方法克服了空间浪费的缺点,对空间利用率是很高了但是由于各个结点的链表是不相同的结构,加上要维护结点的度的数值,在运算上就会带来时间上的损耗
为了优化,我们提出孩子表示法:我们把每个结点的孩子结点排列起来,以单链表作存储结构,则n个结点有n个孩子链表,如果是叶子结点,则此单链表为空。然后n个头指针又组成一个线性表,然后采用顺序存储结构,存放进一个一维数组
为此,设计两种结点结构
(1)孩子链表的孩子结点
(2)表头数组的表头结点
//树的孩子表示法结构定义
#define MAX_TREE_SIZE 100
typedef int TElemType;
typedef struct CTNode//孩子结点
{
int child;
struct CTNode*next;
}*ChildPtr;
typedef struct//表头结点
{
TElemType data;
ChildPtr firstchild;
}CTBox;
typedef struct//树结构
{
CTBox nodes[MAX_TREE_SIZE};//结点数组
int r,n;//根的位置和结点数
}CTree;
这样的结构对于我们要查找某个结点的某个孩子,或者找某个结点的兄弟,只需要查找这个结点的孩子单链表即可
但是双亲则不行,可改进该代码
重点:
二叉树的定义
二叉树(Binary Tree)是n(n>=0)个结点的有限集合,该集合或者为空集(称为空二叉树),或者由一个结点和两棵互不相交、分别称为根结点的左子树和右子树的二叉树组成
二叉树的特点:
(1)每个结点最多有两棵子树
(2)左子树和右子树是有顺序的,次序不能任意颠倒
(3)即时树中某结点只有一棵子树,也要区分它是左子树还是右子树
二叉树具有以下五种基本形态:
(1)空二叉树
(2)只有一个根结点
(3)根结点只有左子树
(4)根结点只有右子树
(5)根结点既有左子树又有右子树
特殊二叉树:
斜树:
所有的结点都只有左子树的二叉树叫左斜树。所有结点都只有右子树的二叉树叫右斜树。两者统称为斜树(可以理解为线性表是树的一种极其特殊的表现形式)
满二叉树:
在一棵二叉树中,如果所有分支结点都存在左子树和右子树,并且所有叶子都在同一层,这样的二叉树称为满二叉树
满二叉树的特点:
(1)叶子只能出现在最下一层。出现在其他层便不可能达到平衡
(2)非叶子结点的度一定是2
(3)在同样深度的二叉树中,满二叉树的结点个数最多,叶子数最多
完全二叉树:
对一棵具有n个结点的二叉树按层序编号,如果编号为i(1<=i<=n)的结点与同样深度的满二叉树中编号为i的结点在二叉树中的位置完全相同,则这棵二叉树称为完全二叉树
理解一下:
满二叉树一定是完全二叉树,完全二叉树不一定是满二叉树
上图因为缺了10便不再是完全二叉树
完全二叉树的特点:
(1)叶子结点只能出现在最下两层
(2)最下层的叶子一定集中在左部连续位置
(3)倒数两层,若叶子结点,一定都在右部连续位置
(4)如果结点度为1,则该结点只有左孩子,即不存在只有右子树情况
(5)同样结点数的二叉树,完全二叉树的深度最小
二叉树的性质
二叉树有一些需要理解并记住的特性,以便于我们更好地使用它
二叉树的性质:
(1)在二叉树的第i层至多有2^i-1个结点
(2)在深度为k的二叉树至多有2^k —1个结点
(3)对任何一棵二叉树T,如果其终端结点数为n0,度为2的结点数为n2,则n0=n2+1
(4)具有n个结点的完全二叉树的深度为[log2n]+1([x]表示不大于x的最大整数)
(5)如果对一棵有n个结点的完全二叉树的结点按层序编号(从第1层到第[log2n]+1层,每层从左到右),对任一结点i有:
1.如果i=1时,则结点i是二叉树的根,无双亲;如果i>1,则双亲是结点[i/2]
2.如果2i>n,则结点i无左孩子(结点i为叶子结点);否则其左孩子是结点2i
3.如果2i+1>n,则结点i无右孩子;否则其右孩子是结点2i+1
二叉树的存储结构
二叉树的顺序存储结构:
在严格定义下,顺序存储结构一般只能用于完全二叉树
若一棵深度为k的右斜树,则它浪费了大量的空间
二叉链表:
既然顺序存储结构适用性不强,我们就考虑链式存储结构。二叉树每个结点最多有两个孩子,所以为它设计一个数据域和两个指针域。
我们称这样的链表叫做二叉链表
//二叉树的二叉链表结点结构定义
typedef struct BiTNode
{
TElemType data;//结点数据
struct BiTNode *lchild,*rchild;//左右孩子指针
}BiTNode,*BiTree;
遍历二叉树
二叉树的遍历原理:
二叉树的遍历来讲,次序非常重要(对效率非常重要)
二叉树的遍历(traversing binary tree)是指从根结点出发,按照某种次序依次访问二叉树的所有结点,使得每个结点北方问一次且仅被访一次
关键词:访问和次序
二叉树的遍历方法:
前序遍历:
规则是若二叉树为空,则空操作返回,否则先访问根结点,然后前序遍历左子树,再前序遍历右子树,遍历的顺序为ABDGHCEIF
中序遍历:
规则是若树为空,则空操作返回,否则从根结点开始(注意并不是先访问根结点),中序遍历根结点的左子树,然后是访问根结点,最后中序遍历右子树,遍历的顺序为GDHBAEICF
后序遍历:
规则是若树为空,则空操作返回,否则从左到右先叶子后结点的方式遍历访问左右子树,最后是访问根结点
层序遍历:
规则是若树为空,则空操作返回,否则从树的第一层,也就是根结点开始访问,从上而下逐层遍历,在同一层中,按从左到右的顺序对结点逐个访问
前序遍历算法:
//二叉树的前序遍历递归算法
//初始条件:二叉树T存在
//操作结果:前序递归遍历T
void PreOrderTraverse(BiTree T)
{
if(T==NULL)
return;
printf("%c",T->data)://显示结点数据
PreOrderTraverse(T->lchild);//先序遍历左子树
PreOrderTraverse(T->rchild)://先遍历右子树
}
中序遍历算法:
//二叉树中的中序遍历递归算法
//初始条件:二叉树T存在
//操作结果:中序递归遍历T
void InOrderTraverse(BiTree T)
{
if(T==NULL)
return;
InOrderTraverse(T->lchild);//中序遍历左子树
printf("%c",T->data);//显示结点数据
InOrderTraverse(T->rchild);//中序遍历右子树
}
后序遍历算法:
//二叉树的后序遍历递归算法
//初始条件:二叉树T存在
//操作结果:后序递归遍历T
void PostOrderTraverse(BiTree T)
{
if(T==NULL)
return;
PostOrderTraverse(T->lchild);//后序遍历左子树
PostOrderTraverse(T->rchild);//后序遍历右子树
printf("%c",T->data);//显示数据
}
二叉树的建立
我们要在内存中建立一棵树,为了能让每个结点确认是否有左右孩子,我们对它进行了扩展
有了这样的准备,我们就可以来看看如何生成一棵二叉树
//按前序输入二叉树中结点的值(一个字符)
//#表示空树,构造二叉链表表示二叉树T
void CreateBiTree(BiTree *T)
{
TElemType ch;
scanf("%c",&ch);
ch=str[index++];
if(ch=='#')
*T=NULL;
else
{
*T=(BiTree)malloc(sizeof(BiTNode));
if(!*T)
exit(OVERFLOW);
(*T)->data=ch;//生成根结点
CreateBiTree(&(*T)->lchild);//构造左子树
CreateBiTree(&(*T)->rchild);//构造右子树
}
}
线索二叉树
线索二叉树的原理:
我们把这种指向前驱和后继的指针称为线索,加上线索的二叉链表称为线索链表,相应地二叉树就称为线索二叉树(Thread Binary Tree)
通过中序遍历后得到上图所示(空心箭头实线为前驱,虚线黑头为后继)
我们对二叉树以某种次序遍历使其变为线索二叉树的过程称做线索化
问题仍没有解决:
因为我们不知道某一个结点的lchild是指向它的左孩子还是指向前驱
因此我们在每个节点再增加两个标志域Itag和rtag,这两个是布尔类型
线索二叉树结构的实现:
//二叉树的二叉线索存储结构定义
typedef char TElemType;
typedef enum{Link,Thread}PointerTag;//Link==0表示指向左右孩子指针,Thread==1表示指向前驱或者
//后继
typedef struct BiThrNode//二叉树线索存储结点结构
{
TElemType data;//结点数据
struct BiThrNode *lchild,*rchild;//左右孩子指针
PointerTag LTag;
PointerTag RTag;//左右标志
}BiThrNode,*BithrTree;
线索化的过程就是在遍历的的过程中修改空指针的过程
中序遍历线索化的递归函数代码如下:
BiThrTree pre;//全局变量,始终指向刚刚访问过的结点
//中序遍历进行中序线索化
void InThreading(BiThrTree p)
{
if(p){
InThreading(p->lchild);//递归左子树线索化
if(!p->lchild)//没有左孩子
{
p->LTag=Thread;//前驱线索
p->lchild=pre;//左孩子指针指向前驱
}
if(!pre->rchild)//前驱没有右孩子
{
pre->RTag=Thread;//后继线索
pre->rchild=p;//前驱右孩子指针指向后继(当前结点p)
}
pre=p;
InThreading(p->rchild);//递归右子树线索化
}
}
如果所用的二叉树需经常遍历或查找结点是需要某种遍历序列中的前驱和后继,那么采用线索二叉链表的存储结构就是非常不错的选择
树、森林与二叉树的转换
树转化为二叉树:
(1)加线。在所有兄弟节点之间加一条连线
(2)去线。对树中每个结点,只保留它与第一个孩子结点的连线,删除它与其他孩子结点之间的连线
(3)层次调整。以树的根结点为轴心,将整棵树顺时针旋转一定的角度,使之结构层次分明。注意第一个孩子是二叉树结点的左孩子,兄弟转换过来的的孩子是结点的右孩子
森林转换为二叉树:
森林是由若干棵树组成的,所以完全可以理解为,森林中的每一棵树都是兄弟,,可以按照兄弟的处理办法来操作
(1)把每个树转化为二叉树
(2)第一棵二叉树不动,从第二棵二叉树开始,依次把后一棵二叉树的根结点作为前一棵二叉树的根结点的右孩子,用线连接起来
二叉树转化为树:
二叉树转换为树是树转换为二叉树的逆过程,也就是反过来做而已
(1)加线。若某结点的左孩子结点存在,则将这个左孩子的n个右孩子与这个结点用线连接起来
(2)去线。删除原二叉树中所有结点点与其右孩子结点的连线
二叉树转换为森林:
判断一棵二叉树能够转化为一棵树还是森林,标准很简单,那就是只要看这棵二叉树的根结点有没有右孩子,有就是森林
(1)从根结点开始,若右孩子存在,则把右孩子结点的连线删除
(2)再查看分离后的二叉树,若右孩子存在,则连线删除......
(3)再把每棵分离后的二叉树转换为树即可
注意事项:
(1)前序、中序、后序、层序遍历需要熟练掌握
(2)学会用计算机的运行思维去模拟递归的实现
(3)二叉树遍历并非一定用递归
(4)线索二叉树给二叉树的结点查找和遍历带来了高效率