一、树的定义以及相关结构名词解释
树是n(n>=0)个结点的有限集。若n=0,则称其为空树。若n>0,则它满足如下两个条件:(1)有且仅有一个特定的成为根(Root)的结点;(2)其余结点可分为m(m>=0)个互不相关的有限集T1、T2、T3……Tm,其中每一个集合本身又是一颗树,并称为根的子树。
相关结构的名词可以看下图:
森林:是把m(m>=0)课互不相交的树的集合。吧啊根节点删除树就变成了森林。一棵树可以看成是一个特殊的森林。给森林的鸽子树加上一个双亲结点,森林就变成了树。树与森林的概念如下图所示:
二、二叉树的定义和相关性质
二叉树是n(n>=0)个结点的有限集。若n=0,则称其为空二叉树。若n>0,则它满足如下两个条件:(1)有且仅有一个特定的成为根(Root)的结点;(2)二叉树分为左右子树,有且只有两个子树,其子树也如此。
二叉树跟树的区别:二叉树不是树的特殊情况,这是两个概念。
二叉树结点的子树要区分左子树和右子树,即使只有一颗子树,也需要如此区分,说明它是左子树,还是右子树。
树当结点只有一个孩子是,就无须区分它是左还是右的次序。因此二者是不同的,这也是二叉树和树的最主要差别。
二叉树的性质:
性质1:在二叉树的第i层上至多有个结点
性质2:深度为k的二叉树至多有个结点
性质一二,可以通过如下图进行数数结点的方式理解。
性质3:对任何一颗二叉树T如果其叶子数为度为2的结点数为则
性质3,是通过对树的总边数B从下往上数,和从上往下数的边数是一样的和结点总数n的关系,以及度为2的结点和度为1的结点来推算出叶子结点的个数,用以下图来进行理解。
性质:具有n个结点的完全二叉树的深度为
性质4,首先认识下完全二叉树的概念,以及相关的特点。完全二叉树是深度为k的具有n个结点的二叉树,当且仅当其每一个结点都与深度为k的满二叉树(满二叉树就是二叉树每一层上的结点数都是最大节点数)中编号为1~n的结点一一对应时,称之为完全二叉树。下图为满二叉树和完全二叉树的图像。
性质5:如果对一颗有n个结点的完全二叉树(深度为)的结点按层序编号 (从1层到第层,每层从左到右),则对任意一结点i有如下:
如果 i=1,则结点是二叉树的根,无双亲;如果 i>1,则其双亲是结点。
如果2i>n,则结点为i叶子结点,无左孩子;否则,其左孩子为结点2i。
如果2i+1 > n,则i结点无有孩子;否则,其右孩子是结点 2i+1;
根据性质5的相关定理,可以根据如下图进行验证:
三、二叉树的存储与操作
-
二叉树的顺序存储
按照满二叉树的结点层次编号,一次存放如二叉树中的数据元素,而空的结点为0便可以进行线性表的存储。如下面两个图所示:
其代码实现可以如下表述:
#define MAXTSIZE 100
typedef int TElemType;
typedef TElemType SqBiTree[MAXTSIZE];
SqBiTree bt;
-
二叉树的链式存储
链式存储包括存储二叉树结点的左右孩子,以此来确定结点的下一个位置,方便遍历和其他操作。其结构如下图所示
其代码实现可以如下表述:
typedef struct BiTNode {
TElemType data;
struct BiTNode* lchid, * rchild; //左右孩子指针
}BiTNode,*BiTree;
链式存储的结果可以抽象成下图:
比较两种存储方式的优缺点:
顺序存储,比较容易进行查找操作,适合存放满二叉树。但因为树有许多的空分支,也得存进去所以造成极大的空间浪费,而且进行添加或删除等操作时,需要移动的元素过于多,造成的时间浪费比较严重。
链式存储,对空间的利用比较充足,进行插入删除等操作时更加方便,而且对树的遍历提供比较方便的便利方法,可以更好的存储树这种结构。但是结点的空指针域也是存在着浪费的现象,但却后面的线索二叉树却可以进行更好的利用。
-
二叉树的遍历
二叉树的遍历常见是规定先左后右的方式,从而有三种情况:
-
DLR —— 先序遍历(先遍历根节点在遍历左子树最后遍历右子树)
-
LDR —— 中序遍历(先遍历左子树在遍历根节点最后遍历右子树)
-
LRD —— 后序遍历(先遍历左子树在遍历右子树最后遍历根节点)
例如:对下图进行遍历
-
DLR —— 先序遍历 :abcdefgh
-
LDR —— 中序遍历 :cbedaghf
-
LRD —— 后序遍历:cedbhgfa
遍历都是个通过一个递归的思想进行的,所以在代码实现方面都是通过递推的干事进行的。如下进行代码实现:
int PreOrderTraverse(BiTree T) //先序遍历
{
if (T == NULL) return 1;
else {
visit(T);
PreOrderTraverse(T->lchild);
PreOrderTraverse(T->rchild);
}
}
int InOrderTraverse(BiTree T) //中序遍历
{
if (T == NULL) return 1;
else {
InOrderTraverse(T->lchild);
visit(T);
InOrderTraverse(T->rchild);
}
}
int PostOrderTraverse(BiTree T) //后序遍历
{
if (T == NULL) return 1;
else {
PostOrderTraverse(T->lchild);
PostOrderTraverse(T->rchild);
visit(T);
}
}
除了常见的三种遍历方法外,还有一个层次遍历的方法,顾名思义就是一层一层进行遍历。
上图的层次遍历结果为:abfcdgeh
层次遍历算法的思想是队列的思想,将根节点进入队列,出队时候将其左右孩子进入队列在循环此操作,以上图为例,
a进入队列,出队是 b f 进入队列,
b出队时, c d进入队列,此时队列中元素有 f c d
f出队时,g进入队列,此时队列中元素有 c d g
c出队时,没有进入队列,此时队列元素有 d g
d出队时,e 进入队列,此时队列元素有 g e
g出队时,h 进入队列,此时队列元素有 e h
后面没有进入了,全部出队。结果就为:a b f c d g e h
其代码实现如下:
void LevelOrder(BiTree T)
{
BiTNode* p;
SqQueue* qu;
InitQueue(qu);
enQueue(qu, T);
while (!QueueEmpty(qu)) {
deQueue(qu, p);
if (p->lchild != NULL)
enQueue(qu, p->lchild);
if (p->rchild != NULL)
enQueue(qu, p->rchild);
}
}
-
二叉树的建立
二叉树的建立,二叉树的建立就是将树的各个结点录入到链表中,而如果直接是以一种遍历顺序去建立的话,对于遍历结果一样而树的模样会出现不是唯一结果,因此将树的空节点也同样需要记录,从而可以保证生成的树是唯一的树。下面以先序遍历的结果生成树为例。
以#表示空的结点,这棵树的先序遍历的结果:ABC##DE#G##F###
其遍历的算法实现可以如下代码所示:
int CreateBiTree(BiTree T) {
char ch = 0;
scanf("%c", & ch);
if (ch == "#") T = NULL;
else {
if (!(T = (BiTree)malloc(sizeof(BiTNode)))) {
exit(0);
}
T->data = ch;
CreateBiTree(T->lchild);
CreateBiTree(T->rchild);
}
return 1;
}
5.二叉树的其他操作算法
计算二叉树的深度:判断空树,如果不是空,递归计算左子树的深度记为m,递归计算右子树的深度记为n,二叉树的深度则为m与n的较大者加1.
代码实现如下:
int Depth(BiTree T) {
int n = 0, m = 0;
if (T == NULL) return 0;
else {
m = Depth(T->lchild);
n = Depth(T->rchild);
if (m > n) return (m + 1);
else return (n + 1);
}
}
计算二叉树结点的总数:判断空树,不为空,节点个数为左子树的节点个数+右子树的节点个数。
代码实现如下:
int NodeCount(BiTree T) {
if (T == NULL) return 0;
else
return NodeCount(T->lchild) + NodeCount(T->rchild) + 1;
}
计算二叉树叶子的总数:判断空树,不为空,为左子树的叶子结点个数+右子树的叶子结点个数。
代码是实现如下:
int LeafCount(BiTree T) {
if (T == NULL) return 0;
if (T->lchild == NULL && T->rchild == NULL) return 1;
else return LeafCount(T->lchild) + LeafCount(T->rchild);
}
四、线索二叉树
线索二叉树,为了查找前驱后继的结点更加方便,也为了把空的左右孩子空间利用起来,所以产生了线索二叉树的概念。其构建是将左孩子空的指向其前驱结点,右孩子空的指向后继结点,从而可以更加方便得进行查找。而有些结点没有前驱或者后继,因此还需要一个标记位来实现,所以线索二叉树的结构就如下图所示:
其结构代码如下所示:
typedef struct BiThrNode {
TElemType data;
int ltag, rtag;
struct BiThrNode* lchild, * rchild; //左右孩子指针
}BiThrNode, * BiThrTree;
其构建的二叉树根据遍历的不同而构成的线索二叉树同样也不一样。如下图所示:
先序线索二叉树:其前驱后继是根据先序遍历排序后的结果来确定的。
中序线索二叉树:其前驱后继是根据中序遍历排序后的结果来确定的。
后序线索二叉树:其前驱后继是根据后序遍历排序后的结果来确定的。
如果增设了一个头结点,那么ltag = 0,lchild指向根节点,rtage = 1,rchild指向遍历序列的最后一个结点,那么遍历序列中的第一个结点的lc域和最后一个结点的rc域就都指向头结点了,就围成一个圈了。如下图所示: