树形结构是一对多的非线性关系。无论是顺序存储,还是链式存储,线性表均有其优缺点。顺序存储可以在O(1)时间内找到特定次序的元素,但是插入和删除元素需要移动大量元素,需要O(n)时间;而链式存储插入和删除元素需要O(1)时间,找到特定次序的元素需要从链表头部向后查找,需要O(n)时间。树形结构结合了两者的优点,可以在O(logn)的时间内完成查找、更新、插入、删除等操作。在实际应用中,很多算法可以借助于树形结构高效地实现。
树形结构就像一棵倒立的树,有唯一的树根,树根可以发出多个分支,每个分支也可以继续发出分支,树枝和树枝之间是不相交的
树的定义
树(tree)是n(n≥0)个节点的有限集合,当n=0时,为空树;n>0时,为非空树。任意一棵非空树,满足以下两个条件:
- 1)有且仅有一个称为根的节点;
- 2)除根节点以外,其余节点可分为m(m>0)个互不相交的有限集T1,T2, …,Tm,其中每一个集合本身又是一棵树,并且称为根的子树(subtree)。
例如,一棵树如图所示。该树除了树根之后,又分成了3个互不相交的集合T1、T2和T3,这3个集合本身又各是一棵树,称为根的子树。
该定义是从集合论的角度给出的树的递归定义,即把树的节点看作一个集合。除了树根以外,其余节点分为m个互不相交的集合,每一个集合又是一棵树。
例如,一棵树如图所示,该树的度为3,其内部节点和终端节点(叶子)均用虚线圈了起来。
● 节点的层次——从根到该节点的层数(根节点为第1层)。
● 树的深度(或高度)——指所有节点中最大的层数。例如,一棵树如下图所示,根为第1层,根的子节点为第2层……该树的最大层次为4,因此树的深度为4。
● 路径——树中两个节点之间所经过的节点序列。
● 路径长度——两节点之间路径上经过的边数。例如,一棵树如下图所示,D到A的路径为D—B—A, D到A的路径长度为2。由于树中没有环,因此树中任意两个节点之间的路径都是唯一的。
如果把树看作一个族谱,就成了一棵家族树,如下图所示。
● 双亲、孩子——节点的子树的根称为该节点的孩子,反之,该节点为其孩子的双亲。
● 兄弟——双亲相同的节点互称兄弟。
● 堂兄弟——双亲是兄弟的节点互称堂兄弟。
● 祖先——从该节点到树根经过的所有节点称为该节点的祖先。
● 子孙——节点的子树中的所有节点都称为该节点的子孙。
祖先和子孙的关系,如下图所示。D的祖先为B、A, A的子孙为B、C、D、E、F、G。
● 有序树——节点的各子树从左至右有序,不能互换位置,如下图所示。
● 无序树——节点各子树可互换位置。
● 森林——由m(m≥0)棵不相交的树组成的集合。例如,上图中的树在删除树根A后,余下的3个子树构成一个森林
树的存储结构
1.顺序存储
顺序存储采用一段连续的存储空间,因为树中节点的数据关系是一对多的逻辑关系,不仅要存储数据元素,还要存储它们之间的逻辑关系。顺序存储分为双亲表示法、孩子表示法和双亲孩子表示法。
以图6-10为例,分别讲述3种存储方法。
(1)双亲表示法
双亲表示法,除了存储数据元素之外,还存储其双亲节点的存储位置下标,其中“-1”表示不存在。每一个节点有两个域,即数据域data和双亲域parent,如上图(a)所示。
树根A没有双亲,双亲记为-1, B、C、D的双亲为A,而A的存储位置下标为0,因此,B、C、D的双亲记为0。同样,E、F的双亲为B,而B的存储位置下标为1,因此,E、F的双亲记为1。同理,其他节点也这样存储。
(2)孩子表示法
孩子表示法是指除了存储数据元素之外,还存储其所有孩子的存储位置下标,如上图(b)所示。
A有3个孩子B、C和D,而B、C和D的存储位置下标为1、2和3,因此将1、2和3存入A的孩子域。同样,B有2个孩子E和F,而E和F的存储位置下标为4和5,因此,将4和5存入B的孩子域。因为本题中每个节点都分配了3个孩子域(想一想,为什么?), B只有两个孩子,另一个孩子域记为-1,表示不存在。同理,其他节点也这样存储。
(3)双亲孩子表示法
双亲孩子表示法是指除了存储数据元素之外,还存储其双亲和所有孩子的存储位置下标,如上图(c)所示。此方法其实就是在孩子表示法的基础上增加了一个双亲域,其他的都和孩子表示法相同,是双亲表示法和孩子表示法的结合体。
以上3种表示法的优缺点如下:
双亲表示法只记录了每个节点的双亲,无法直接得到该节点的孩子;
孩子表示法可以得到该节点的孩子,但是无法直接得到该节点的双亲,而且由于不知道每个节点到底有多少个孩子,因此只能按照树的度(树中节点的最大度)分配孩子空间,这样做可能会浪费很多空间。
双亲孩子表示法是在孩子表示法的基础上,增加了一个双亲域,可以快速得到节点的双亲和孩子,其缺点和孩子表示法一样,可能浪费很多空间。
2.链式存储
由于树中每个节点的孩子数量无法确定,因此在使用链式存储时,孩子指针域不确定分配多少个合适。如果采用“异构型”数据结构,每个节点的指针域个数按照节点的孩子数分配,则数据结构描述困难;如果采用每个节点都分配固定个数(如树的度)的指针域,则浪费很多空间。
可以考虑两种方法存储:一种是采用邻接表的思路,将节点的所有孩子存储在一个单链表中,称为孩子链表表示法;另一种是采用二叉链表的思路,左指针存储第一个孩子,右指针存储右兄弟,称为孩子兄弟表示法。
(1)孩子链表表示法
孩子链表表示法类似于邻接表,表头包含数据元素并指向第一个孩子指针,将所有孩子放入一个单链表中。在表头中,data存储数据元素,first为指向第1个孩子的指针。单链表中的节点记录该节点的下标和下一个节点的地址。仍以图6-10为例,其孩子链表表示法如下图所示
A有3个孩子B、C和D,而B、C和D的存储位置下标为1、2和3,因此将1、2和3放入单链表中链接在A的first指针域。同样,B有2个孩子E和F,而E和F的存储位置下标为4和5,因此,将4和5放入单链表中链接在B的first指针域。同理,其他节点也这样存储。
孩子链表表示法中,如果在表头中再增加一个双亲域parent,则为双亲孩子链表表示法。
(2)孩子兄弟表示法
节点除了存储数据元素之外,还有两个指针域lchild和rchild,被称为二叉链表。lchild存储第一个孩子地址,rchild存储右兄弟地址。其节点的数据结构如下图所示
仍以图6-10为例
A有3个孩子B、C和D,其长子(第一个孩子)B作为A的左孩子,B的右指针存储其右兄弟C, C的右指针存储其右兄弟D。
B有2个孩子E和F,其长子E作为B的左孩子,E的右指针存储其右兄弟F。
C有1个孩子G,其长子G作为C的左孩子。
D有2个孩子H和I,其长子H作为D的左孩子,H的右指针存储其右兄弟I。
G有1个孩子J,其长子J作为G的左孩子。
孩子兄弟表示法的秘诀:长子当作左孩子,兄弟关系向右斜。
树、森林与二叉树的转换
根据树的孩子兄弟表示法,任何一棵树都可以根据秘诀转换为二叉链表来存储。二叉链表存储法中,每个节点都有两个指针域,也称为二叉树表示法。这样,任何的树和森林都可以转换为二叉树,其存储方式简单多了。这就完美地解决了树中孩子数量无法确定,难以分配空间的问题。
树转换为二叉树的秘诀:长子当作左孩子,兄弟关系向右斜。
1.树和二叉树的转换
根据树转换为二叉树的秘诀,可以把任何一棵树转换为二叉树
A有3个孩子B、C和D,其长子B作为A的左孩子,三兄弟B、C和D在右斜线上。
B有2个孩子E和F,其长子E作为B的左孩子,两兄弟E和F在右斜线上。
D有2个孩子G和H,其长子G作为D的左孩子,两兄弟G和H在右斜线上。
G有1个孩子I,其长子I作为G的左孩子。
那么二叉树怎么还原为树呢?仍然根据树转换二叉树的秘诀:长子当作左孩子,兄弟关系向右斜。反操作即可
B是A的左孩子,说明B是A的长子;B、C和D在右斜线上,说明B、C和D是兄弟,它们的父亲都是A。
E是B的左孩子,说明E是B的长子;E和F在右斜线上,说明E和F是兄弟,它们的父亲都是B。
G是D的左孩子,说明G是D的长子;G和H在右斜线上,说明G和H是兄弟,它们的父亲都是D。
I是G的左孩子,说明I是G的长子。
2.森林和二叉树的转换
森林是由m(m≥0)棵不相交的树组成的集合。
可以把森林中的每棵树的树根看作兄弟关系,因此3棵树的树根B、C和D是兄弟,兄弟关系在右斜线上,其他的转换和树转二叉树一样,长子当作左孩子,兄弟关系向右斜。或者把森林中的每一棵树转换成二叉树,然后把每棵树的根节点连接在右斜线上即可
同理,二叉树也可以还原为森林
首先看到B、C和D在右斜线上,说明它们是兄弟,将其断开,那么B和其子孙是第1棵二叉树,C是第2棵二叉树,那么D和其子孙是第3棵二叉树,再按照二叉树还原树的规则,将这3棵二叉树分别还原为树即可。
由于普通的树每个节点的子树个数不同,存储和运算都比较困难,因此在实际应用中,可以将树或森林转换为二叉树,然后进行存储和运算。二者存在唯一的对应关系,因此不影响其结果。
二叉树
二叉树(binary tree)是n(n≥0)个节点构成的集合,它或为空树(n=0),或满足以下两个条件:
- 1)有且仅有一个称为根的节点;
- 2)除根节点以外,其余节点分为两个互不相交的子集T1和T2,分别称为T的左子树和右子树,且T1和T2本身都是二叉树。
二叉树是一种特殊的树,它最多有两个子树,分别为左子树和右子树,二者是有序的,不可以互换。也就是说,二叉树中不存在度大于2的节点。
二叉树一共有5种形态
二叉树的性质
性质1:在二叉树的第i层上至多有2i-1个节点
例如,一棵二叉树如下图所示。由于二叉树每个节点最多有2个孩子,第一层树根为1个节点,第二层最多为2个节点,第三层最多有4个节点,因为上一层的每个节点最多有两个孩子,因此当前层最多是上一层节点数的2倍。
性质2:深度为k的二叉树至多有2k-1个节点
证明:如果深度为k的二叉树,每一层都达到最大节点数,如下图所示,把每层的节点数加起来就是整棵二叉树的最大节点数。
证明:二叉树中的节点度数不超过2,因此一共有3种节点,即度为0、度为1、度为2。设二叉树总的节点数为n,度为0的节点数为n0,度为1的节点数为n1,度为2的节点数为n2,总节点数等于3种节点数之和,即n=n0+n1+n2。
性质3:对于任何一棵二叉树,若叶子数为n0,度为2的节点数为n2,则n0=n2+1
而总节点数又等于“分支数b+1”,即n=b+1。为什么呢?如图所示,从下向上看,每一个节点对应一个分支,只有树根没有对应分支,因此总的节点数为“分支数b+1”。
而分支数b怎么计算呢?
从上向下看,每个度为2的节点产生2个分支,度为1的节点产生1个分支,度为0的节点没有分支,因此分支数b=n1+2n2,则n=b+1=n1+2n2+1。而前面已经得到n=n0+n1+n2,两式联合得:n0=n2+1
有两种比较特殊的二叉树:满二叉树和完全二叉树。
● 满二叉树:一棵深度为k且有2k-1个节点的二叉树。满二叉树每一层都“充满”了节点,达到最大节点数
● 完全二叉树:除了最后一层外,每一层都是满的(达到最大节点数),最后一层节点是从左向右出现的。深度为k的完全二叉树,当且仅当其每一个节点都与深度为k的满二叉树中编号1~n的节点一一对应。例如,完全二叉树如下图所示,它和上图的满二叉树编号一一对应。完全二叉树除了最后一层,前面每一层都是满的,最后一层必须从左向右排列。也就是说,如果2没有左孩子,就不可以有右孩子;如果2没有右孩子,3不可以有左孩子。
性质4:具有n个节点的完全二叉树的深度必为⌊log2n⌋ + 1
证明:假设完全二叉树的深度为k,那么除了最后一层外,前k-1层都是满的,最后一层最少有一个节点。最后一层最多也可以充满节点,即2k-1个节点
因此,2k-1≤n≤2k-1,右边放大后,2k-1≤n<2k,同时取对数,k-1≤log2n<k
性质5:对于完全二叉树,若从上至下、从左至右编号,则编号为i的节点,其左孩子编号必为2i,其右孩子编号必为2i +1,其双亲的编号必为i/2。
解题技巧
例题1:一棵完全二叉树有1001个节点,其中叶子节点的个数是多少?
解题思路:首先找到最后一个节点1001的双亲节点,其双亲节点编号为1001/2=500,该节点是最后一个拥有孩子的节点,其后面全是叶子,即1001-500=501个叶子
例题2:一棵完全二叉树第6层有8个叶子,则该完全二叉树最少有多少节点,最多有多少个节点?
解题思路:完全二叉树的叶子分布在最后一层或倒数第二层,因此该树有可能为6层或7层。
节点最少的情况(6层):8个叶子在最后一层,即第6层,前5层是满的。如下图所示,最少有25-1+8=39个节点。
节点最多的情况(7层):8个叶子在倒数第二层,即第6层,前6层是满的,第7层最少缺失了8×2个节点,因为第6层的8个叶子如果生成孩子的话,会有16个节点。如下图所示,最多有27-1-16=111个节点。
二叉树的存储结构
1.顺序存储
二叉树也可以采用顺序存储,按完全二叉树的节点层次编号,依次存放二叉树中的数据元素。完全二叉树很适合顺序存储方式,上图所示的完全二叉树的顺序存储结构如下图所示
而普通二叉树(如图6-34所示)在顺序存储时需要补充为完全二叉树,在对应完全二叉树没有孩子的位置补0,如图6-35所示。其顺序存储结构如图6-36所示。
显然,普通二叉树不适合顺序存储方式,因为有可能在补充为完全二叉树过程中,补充太多的0,而浪费大量空间,因此普通二叉树可以使用链式存储。
2.链式存储
二叉树最多有两个“叉”,即最多有两棵子树。
二叉树采用链式存储方式:每个节点包含一个数据域,存储节点信息;还包含两个指针域,指向左右两个孩子。这种存储方式称为二叉链表,其结构如图所示
二叉链表节点的结构体定义如图
于是,图6-37中的二叉树就可以存储为二叉链表的形式
一般情况下,二叉树采用二叉链表存储即可,但是在实际问题中,如果经常需要访问双亲节点,二叉链表存储则必须从根出发查找其双亲节点,这样做非常麻烦。例如,在上图中,如果想找F的双亲,就必须从根节点A出发,先访问C,再访问F,此时才能返回F的双亲为C。为了解决这一问题,可以增加一个指向双亲节点的指针域,这样每个节点就包含3个指针域,分别指向两个孩子节点和双亲节点,还包含一个数据域,用来存储节点信息。这种存储方式称为三叉链表,三叉链表结构如下:
三叉链表节点的结构体定义如图所示
于是,图6-37中的二叉树也可以存储为三叉链表的形式,如图所示
二叉树的创建
如果对二叉树进行操作,必须先创建一棵二叉树。如何创建一棵二叉树呢?从二叉树的定义就可以看出,它是递归定义的(除了根之外,左、右子树也是一棵二叉树),因此可以用递归来创建二叉树。递归创建二叉树有两种方法,分别是询问法和补空法。
1.询问法
每次输入节点信息后,询问是否创建该节点的左子树,如果是,则递归创建其左子树,否则其左子树为空;询问是否创建该节点的右子树,如果是,则递归创建其右子树,否则其右子树为空
算法步骤
1)输入节点信息,创建一个节点T。
2)询问是否创建T的左子树,如果是,则递归创建其左子树,否则其左子树为NULL。
3)询问是否创建T的右子树,如果是,则递归创建其右子树,否则其右子树为NULL。
算法图解
例如,一棵二叉树如图所示
该二叉树的创建过程如下
1)请输入节点信息:
A
输入后创建节点A
2)是否添加A的左孩子?(Y/N)
Y
3)请输入节点信息:
B
输入后创建节点B,作为A的左孩子,如图所示
4)是否添加B的左孩子?(Y/N)
Y
5)请输入节点信息:
D
输入后创建节点D,作为B的左孩子,如图所示
6)是否添加D的左孩子?(Y/N)
N
7)是否添加D的右孩子?(Y/N)
N
输入后D的左右孩子均为空,如图所示
8)是否添加B的右孩子?(Y/N)
Y
9)请输入节点信息:
E
输入后创建节点E,作为B的右孩子,如图所示
10)是否添加E的左孩子?(Y/N)
N
11)是否添加E的右孩子?(Y/N)
N
输入后E的左右孩子均为空,如图所示
12)是否添加A的右孩子?(Y/N)
Y
13)请输入节点信息:
C
输入后创建节点C,作为A的右孩子,如图所示
14)是否添加C的左孩子?(Y/N)
Y
15)请输入节点信息:
F
输入后创建节点F,作为C的左孩子,如图所示
16)是否添加F的左孩子?(Y/N)
N
即F的左孩子为空,如图所示
17)是否添加F的右孩子?(Y/N)
Y
18)请输入节点信息:
G
输入后创建节点G,作为F的右孩子,如图所示
19)是否添加G的左孩子?(Y/N)
N
20)是否添加G的右孩子?(Y/N)
N
输入后G的左右孩子均为空,如图所示
21)是否添加C的右孩子?(Y/N)
N
输入后C的右孩子为空,如图所示
22)二叉树创建完毕
代码实现
void createTree(Btree &T)
{
char check;
T = new Bnode;
cout<< "请输入结点信息:"<<endl;
cin >> T->data;
cout << "是否添加" << T->data << "的左孩子?(Y/N)" << endl;
cin >> check;
if(check == 'Y')
{
createTree(T->lchild);
}
else
{
T->lchild = NULL;
cout << "是否添加" << T->data << "的右孩子?(Y/N)" << endl;
}
cin >> check;
if(check == 'Y')
{
createTree(T->rchild);
}
else
{
T->rchild = NULL;
}
}
第二种类型的创建树算法
#include<stdio.h>
#include<stdlib.h>
typedef struct BiTNode{
char data;
struct BiTNode *lchild, *rchild;
}BiTNode, *Bitree;
void CreateBitree(Bitree *bt)
{
char ch;
scanf("%c", &ch);
if(ch == '#')
{
*bt = NULL;
}
else
{
*bt = (Bitree)malloc(sizeof(BiTNode));
(*bt)->data = ch;
CreateBitree(&(*bt)->lchild);
CreateBitree(&(*bt)->rchild);
}
}
void preorder(Bitree bt)
{
if(bt == NULL)
{
return;
}
printf("%c ", bt->data);
preorder(bt->lchild);
preorder(bt->rchild);
}
void inorder(Bitree bt)
{
if(bt == NULL)
{
return;
}
inorder(bt->lchild);
printf("%c ", bt->data);
inorder(bt->rchild);
}
void postorder(Bitree bt)
{
if(bt == NULL)
{
return;
}
postorder(bt->lchild);
postorder(bt->rchild);
printf("%c ", bt->data);
}
int main()
{
Bitree T;
printf("请输入二叉树结点序列:\n");
CreateBitree(&T);
printf("\n先序遍历:");
preorder(T);
printf("\n中序遍历:");
inorder(T);
printf("\n后序遍历:");
postorder(T);
printf("\n");
return 0;
}
为什么这里用二级指针呢(详解)
这个问题应该困扰了很多初学者,这里我想给大家好好梳理一下
首先我们看上图代码,在main函数里p指向a,a的值为1,进入f函数,p的值发生改变,变为2,也就是说p所指向的地址空间a的内容值就变为2了,接下来它又开辟新的空间给了p,注意,这时的p就不再是之前形参p了,变了,p指向新的空间,当函数调用完毕返回主函数时,新空间里的3这个值也不会随之带回,因为从底层角度看,当时在主函数执行调用f函数时,会保护地址,把这个调用地址以及相关调用函数的实参等都会得以保存,而如果你要让新空间的3生效带回去的话,C语言就能使用二级指针才能实现,不像C++可以用引用玩。
上面如果理解了,那么这里的树为何用二级指针你就能理解明白了吧,好好消化吧↖(▔▽▔)↗
树状输出二叉树
先序构建二叉树,我们输入ABD##E##CH###
#include<stdio.h>
#include<stdlib.h>
#include<math.h>
#define MAX_SIZE 30
#define NLAYER 4
typedef struct BiTNode{
char data;
struct BiTNode *lchild;
struct BiTNode *rchild;
}BiTNode, *BiTree;
void CreateBiTree(BiTree *bt)
{
char ch;
scanf("%c", &ch);
if(ch == '#')
{
*bt = NULL;
}
else
{
*bt = (BiTree)malloc(sizeof(BiTNode));
(*bt)->data = ch;
CreateBiTree(&((*bt)->lchild));
CreateBiTree(&((*bt)->rchild));
}
}
void TranslevelPrint(BiTree bt)
{
struct node{
BiTree vec[MAX_SIZE]; /* 存放树结点 */
int layer[MAX_SIZE]; /* 结点所在层 */
int locate[MAX_SIZE]; /* 打印结点位置 */
int front, rear;
}q;
int i, j = 1, k = 0, nLocate;
q.front = 0; /* 初始化队列 */
q.rear = 0;
printf(" ");
q.vec[q.rear] = bt;
q.layer[q.rear] = 1;
q.locate[q.rear] = 20;
q.rear += 1;
while(q.front < q.rear)
{
bt = q.vec[q.front];
i = q.layer[q.front];
nLocate = q.locate[q.front];
if(j < i)
{
printf("\n");
printf("\n");
j += 1;
k = 0;
while(k < nLocate)
{
printf(" ");
k++;
}
}
while(k < (nLocate - 1))
{
printf(" ");
k++;
}
printf("%c", bt->data);
q.front = q.front + 1;
if(bt->lchild != NULL)
{
q.vec[q.rear] = bt->lchild;
q.layer[q.rear] = i + 1;
q.locate[q.rear] = (int)(nLocate - pow(2, NLAYER - i - 1));
q.rear += 1;
}
if(bt->rchild != NULL)
{
q.vec[q.rear] = bt->rchild;
q.layer[q.rear] = i + 1;
q.locate[q.rear] = (int)(nLocate + pow(2, NLAYER - i - 1));
q.rear += 1;
}
}
}
int main()
{
BiTree T;
printf("请输入二叉树结点序列:\n");
CreateBiTree(&T);
TranslevelPrint(T);
return 0;
}
输出叶子结点、输出叶子结点数、输出二叉树的深度
#include<stdio.h>
#include<stdlib.h>
#include<math.h>
#define MAX_SIZE 30
#define NLAYER 4
typedef struct BiTNode{
char data;
struct BiTNode *lchild;
struct BiTNode *rchild;
}BiTNode, *BiTree;
void CreateBiTree(BiTree *bt)
{
char ch;
scanf("%c", &ch);
if(ch == '#')
{
*bt = NULL;
}
else
{
*bt = (BiTree)malloc(sizeof(BiTNode));
(*bt)->data = ch;
CreateBiTree(&((*bt)->lchild));
CreateBiTree(&((*bt)->rchild));
}
}
void TranslevelPrint(BiTree bt)
{
struct node{
BiTree vec[MAX_SIZE]; /* 存放树结点 */
int layer[MAX_SIZE]; /* 结点所在层 */
int locate[MAX_SIZE]; /* 打印结点位置 */
int front, rear;
}q;
int i, j = 1, k = 0, nLocate;
q.front = 0; /* 初始化队列 */
q.rear = 0;
printf(" ");
q.vec[q.rear] = bt;
q.layer[q.rear] = 1;
q.locate[q.rear] = 20;
q.rear += 1;
while(q.front < q.rear)
{
bt = q.vec[q.front];
i = q.layer[q.front];
nLocate = q.locate[q.front];
if(j < i)
{
printf("\n");
printf("\n");
j += 1;
k = 0;
while(k < nLocate)
{
printf(" ");
k++;
}
}
while(k < (nLocate - 1))
{
printf(" ");
k++;
}
printf("%c", bt->data);
q.front = q.front + 1;
if(bt->lchild != NULL)
{
q.vec[q.rear] = bt->lchild;
q.layer[q.rear] = i + 1;
q.locate[q.rear] = (int)(nLocate - pow(2, NLAYER - i - 1));
q.rear += 1;
}
if(bt->rchild != NULL)
{
q.vec[q.rear] = bt->rchild;
q.layer[q.rear] = i + 1;
q.locate[q.rear] = (int)(nLocate + pow(2, NLAYER - i - 1));
q.rear += 1;
}
}
}
/* 输出叶子结点 */
void PreOrderLeaf(BiTree root)
{
if(root != NULL)
{
if(root->lchild == NULL && root->rchild == NULL)
{
printf("%c ", root->data);
}
PreOrderLeaf(root->lchild);
PreOrderLeaf(root->rchild);
}
}
/* 输出叶子结点的个数 */
int LeafCount(BiTree root)
{
int LeafNum;
if(root == NULL)
{
LeafNum = 0;
}
else if((root->lchild == NULL) && (root->rchild == NULL))
{
LeafNum = 1;
}
else
{
LeafNum = LeafCount(root->lchild) + LeafCount(root->rchild);
}
return LeafNum;
}
/* 输出二叉树的深度 */
int PostTreeDepth(BiTree root)
{
int hl, hr, max;
if(root != NULL)
{
hl = PostTreeDepth(root->lchild);
hr = PostTreeDepth(root->rchild);
max = hl > hr ? hl : hr;
return(max + 1);
}
else
{
return 0;
}
}
int main()
{
BiTree T;
printf("请输入二叉树结点序列:\n");
CreateBiTree(&T);
TranslevelPrint(T);
printf("\n所有叶子结点:");
PreOrderLeaf(T);
printf("\n输出叶子结点个数:%d\n",LeafCount(T));
printf("树的深度为:%d\n",PostTreeDepth(T));
return 0;
}
最近公共祖先结点
#include<stdio.h>
char getValue(char tree[], int i, int j)
{
int k;
if(tree[i] != '#' && tree[j] != '#')
{
if(i > j)
{
k = i / 2;
getValue(tree, k, j);
}
else if(i < j)
{
k = j / 2;
getValue(tree, i, k);
}
else
{
return tree[i];
}
}
}
int main()
{
char tree[20] = {'0', 'A', 'B', 'C', 'D', 'E', 'H', '#'};
printf("最近公共祖先结点值为:%c\n", getValue(tree, 2, 3));
printf("最近公共祖先结点值为:%c\n", getValue(tree, 2, 6));
printf("最近公共祖先结点值为:%c\n", getValue(tree, 2, 5));
printf("最近公共祖先结点值为:%c\n", getValue(tree, 5, 3));
printf("最近公共祖先结点值为:%c\n", getValue(tree, 5, 2));
return 0;
}
2.补空法
补空法是指如果左子树或右子树为空时,则用特殊字符补空,如“#”,然后按照根、左子树、右子树的顺序,得到先序遍历序列,根据该序列递归创建二叉树。
算法步骤
1)输入补空后的二叉树先序遍历序列。
2)如果ch==’#’, T=NULL;否则创建一个新节点T,令T->data=ch;递归创建T的左子树;递归创建T的右子树。
算法图解
例如,一棵二叉树如图所示
首先将该二叉树补空,孩子为空时补上特殊符号“#”,如图所示。
二叉树补空后的先序遍历序列为:ABD##E##CF#G###
该二叉树的创建过程如下:
1)首先读取第1个字符A,创建一个新节点,如图所示
然后递归创建A的左子树
2)读取第2个字符B,创建一个新节点,作为A的左子树,如图所示,然后递归创建B的左子树。
3)读取第3个字符D,创建一个新节点,作为B的左子树,如图所示,然后递归创建D的左子树。
4)读取第4个字符#,说明D的左子树为空,如图所示,然后递归创建D的右子树。
5)读取第5个字符#,说明D的右子树为空,如图所示,然后递归创建B的右子树。
6)读取第6个字符E,创建一个新节点,作为B的右子树,如图所示,然后递归创建E的左子树。
7)读取第7个字符#,说明E的左子树为空,如图所示,然后递归创建E的右子树。
8)读取第8个字符#,说明E的右子树为空,如图所示,然后递归创建A的右子树。
9)读取第9个字符C,创建一个新节点,作为A的右子树,如图所示,然后递归创建C的左子树。
10)读取第10个字符F,创建一个新节点,作为C的左子树,如图所示,然后递归创建F的左子树
11)读取第11个字符#,说明F的左子树为空,如图所示,然后递归创建F的右子树。
12)读取第12个字符G,创建一个新节点,作为F的右子树,如图所示,然后递归创建G的左子树。
13)读取第13个字符#,说明G的左子树为空,如图所示,然后递归创建G的右子树。
14)读取第14个字符#,说明G的右子树为空,如图所示,然后递归创建C的右子树。
15)读取第15个字符#,说明C的右子树为空,如图所示,序列读取完毕,二叉树创建成功。
代码实现
void Createtree(Btree &T)
{
char ch;
cin >> ch;
if(ch == '#')
{
T = NULL;
}
else
{
T = new Bnode;
T->data = ch;
Createtree(T->lchild);
Createtree(T->rchild);
}
}
二叉树的遍历
按照根、左子树和右子树的访问先后顺序不同,二叉树的遍历可以有6种方案:DLR、LDR、LRD、DRL、RDL、RLD。如果限定先左后右(先左子树后右子树),则只有前3种遍历方案:DLR、LDR、LRD。按照根的访问顺序不同,根在前面称为先序遍历(DLR),根在中间称为中序遍历(LDR),根在最后称为后序遍历(LRD)。
先序遍历
先序遍历是指先访问根,然后先序遍历左子树,再先序遍历右子树,即DLR。
算法步骤
如果二叉树为空,则空操作,否则:
- 1)访问根节点;
- 2)先序遍历左子树;
- 3)先序遍历右子树。
先序遍历秘籍:访问根,先序遍历左子树,左子树为空或已遍历才可以遍历右子树。
代码实现
void preorder(Btree T)
{
if(T)
{
cout << T->data << " ";
preorder(T->lchild);
preorder(T->rchild);
}
}
中序遍历
中序遍历是指中序遍历左子树,然后访问根,再中序遍历右子树,即LDR。
算法步骤
如果二叉树为空,则空操作,否则:
- 1)中序遍历左子树;
- 2)访问根节点;
- 3)中序遍历右子树。
中序遍历秘籍:中序遍历左子树,左子树为空或已遍历才可以访问根,中序遍历右子树。
代码实现
void inorder(Btree T)
{
if(T)
{
inorder(T->lchild);
cout << T->data << " ";
inorder(T->rchild);
}
}
后序遍历
后序遍历是指后序遍历左子树,后序遍历右子树,然后访问根,即LRD。
算法步骤
如果二叉树为空,则空操作,否则:
- 1)后序遍历左子树;
- 2)后序遍历右子树;
- 3)访问根节点。
后序遍历秘籍:后序遍历左子树,后序遍历右子树,左子树、右子树为空或已遍历才可以访问根。
代码实现
void posorder(Btree T)
{
if(T)
{
posorder(T->lchild);
posorder(T->rchild);
cout << T->data << " ";
}
}
(1)中序遍历
中序遍历就像在无风的情况下,遍历顺序为左子树、根、右子树,太阳直射,将所有的节点投影到地上,如图6-109所示。图6-98中的二叉树的中序序列投影如图6-110所示。中序遍历序列为:DBEAFGC。
(2)先序遍历
先序遍历就像在左边大风的情况下,将二叉树树枝刮向右方,且顺序为根、左子树、右子树,太阳直射,将所有的节点投影到地上,如图6-111所示。图6-98中的二叉树的先序遍历投影序列如图6-112所示。先序遍历序列为:ABDECFG。
(3)后序遍历
后序遍历就像在右边大风的情况下,将二叉树树枝刮向左方,且顺序为左子树、右子树、根,太阳直射,将所有的节点投影到地上,如图6-113所示。图6-98中的二叉树的后序遍历投影序列如图6-114所示。后序遍历序列为:DEBGFCA。
层次遍历
二叉树的遍历除一般的先序遍历、中序遍历和后序遍历这3种遍历之外,还有另一种遍历方式——层次遍历,即按照层次的顺序从左向右进行遍历。
对上图所示的二叉树进行层次遍历:首先遍历第1层A,然后遍历第2层,从左向右为B、C,再遍历第3层,从左向右为D、E、F,再遍历第4层G,很简单吧,这就是层次遍历。层次遍历秘籍:首先遍历第1层,然后第2层……同一层按照从左向右的顺序访问,直到最后一层。
程序是怎么实现层次遍历的呢?通过观察可以发现,先被访问的节点,其孩子也先被访问,先来先服务,因此可以用队列实现。
算法图解
下面以图6-115中的二叉树为例,展示该二叉树层次遍历的过程。
1)首先创建一个队列Q,令树根入队。(注意:实际上是指向树根A的指针入队,这里为了图解方便,直接把数据入队了。)
2)队头元素出队,输出A,同时令A的孩子B、C入队(从左向右顺序,如果是普通树,则包含所有孩子),队列和二叉树状态如图6-117和图6-118所示。
3)队头元素出队,输出B,同时令B的孩子D、E入队,队列和二叉树状态如图6-119和图6-120所示。
4)队头元素出队,输出C,同时令C的孩子F入队,队列和二叉树状态如图6-121和图6-122所示。
5)队头元素出队,输出D,同时令D的孩子入队,D没有孩子,什么也不做,队列和二叉树状态如图6-123和图6-124所示。
6)队头元素出队,输出E,同时令E的孩子入队,E没有孩子,什么也不做,队列和二叉树状态如图6-125和图6-126所示。
7)队头元素出队,输出F,同时令F的孩子G入队,队列和二叉树状态如图6-127和图6-128所示。
8)队头元素出队,输出G,同时令G的孩子入队,G没有孩子,什么也不做,队列和二叉树状态如图6-129和图6-130所示。
9)队列为空,算法结束。
代码实现
bool Leveltraverse(Btree T)
{
Btree p;
if(!T)
{
return false;
}
queue<Btree>Q;
Q.push(T);
while(!Q.empty())
{
p = Q.front();
Q.pop();
cout << p->data << " ";
if(p->lchild)
{
Q.push(p->lchild);
}
if(p->rchild)
{
Q.push(p->rchild);
}
}
return true;
}
线索二叉树
二叉树是非线性数据结构,而遍历序列是线性序列,二叉树遍历实际上是将一个非线性结构进行线性化的操作。根据线性序列的特性,除了第一个元素外,每一个节点都有唯一的前驱,除了最后一个元素外,每一个节点都有唯一的后继。而根据遍历序列的不同,每个节点的前驱和后继也不同。采用二叉链表存储时,只记录了左、右孩子的信息,无法直接得到每个节点的前驱和后继。
线索二叉树存储结构
二叉树采用二叉链表存储时,每个节点有两个指针域。如果二叉链表有n个节点,则一共有2n个指针域,而只有n-1个是实指针,其余n+1个都是空指针,为什么呢?
因为二叉树有n-1个分支,每个分支对应一个实指针,如图6-131所示。从下向上看,每一个节点对应一个分支,只有树根没有对应分支,因此分支数等于节点数减1,即b=n-1。每个分支对应一个实指针,所以有n-1个实指针。总的指针数减去实指针数,即为空指针数,即2n- (n-1)=n+1。
n个节点的二叉链表中有n+1个空指针,可以充分利用空指针记录节点的前驱或后继信息,从而加快查找节点前驱和后继的速度。
每个节点还是两个指针域,如果节点有左孩子,则lchild指向左孩子,否则lchild指向其前驱;如果节点有右孩子,则rchild指向右孩子,否则rchild指向其后继。那么怎么区分到底存储的是左孩子和右孩子,还是前驱和后继信息呢?为了避免混淆,增加两个标志域ltag和rtag,节点的结构体如图所示
节点的结构体定义
这种带有标志域的二叉链表称为线索链表,指向前驱和后继的指针称为线索,带有线索的二叉树称为线索二叉树,以某种遍历方式将二叉树转化为线索二叉树的过程称为线索化。
构造线索二叉树
线索化的实质是利用二叉链表中的空指针记录节点的前驱或后继线索。而每种遍历顺序不同,节点的前驱和后继也不同,因此二叉树线索化必须指明是什么遍历顺序的线索化。线索二叉树分为前序线索二叉树、中序线索二叉树和后序线索二叉树。
二叉树线索化的过程,实际上是在遍历过程中修改空指针的过程。可以设置两个指针,一个指针pre指向刚刚访问的节点,另一个指针p指向当前节点。也就是说,pre指向的节点为p指向的节点的前驱,反之,p指向的节点为pre指向的节点的后继。在遍历的过程中,如果当前节点p的左孩子为空,则该节点的lchild指向其前驱,即p->lchild=pre;如果pre节点的右孩子为空,则该节点的rchild指向其后继,即pre->rchild=p。
算法步骤
1)指针p指向根节点,pre初始化为空,pre永远指向p的前驱。
2)若p非空,则重复下面操作。
- · 中序线索化p的左子树。
- · 若p的左子树为空,则给p加上左线索,即p->ltag=1, p的左子树指针指向pre(前驱),即p->lchild=pre;否则令p->ltag=0。
- · 若pre非空,则判断如果pre的右子树为空,给pre加上右线索,即pre->rtag=1, pre的右孩子指针指向p(后继),即pre->rchild=p,否则令pre->rtag=0。
- · p赋值给pre,转向p的右子树。
- · 中序线索化p的右子树。
3)处理最后一个节点,令其后继为空,即pre->rchild=NULL; pre->rtag=1。
算法图解
例如,一棵二叉树如图6-134所示,该二叉树中序线索化的过程如下
1)首先设置指向当前节点的指针变量p,指向当前节点前驱的指针变量pre,初始化pre=NULL。然后按照中序遍历的方式,遍历根的左子树,直到左子树为空时,即p指向D节点,则令p的前驱为pre,更新当前pre为p,如图6-135所示。
2)中序遍历p的右子树,右子树为空,返回到B节点,p指向B节点;此时pre的右子树为空,则令pre的后继为p,如图6-136所示。
3)更新当前pre为p,中序遍历p的右子树,p指向E节点;中序遍历E的左子树,其左子树为空,则令p的前驱为pre,如图6-137所示。
4)更新当前pre为p,中序遍历p的右子树,右子树为空,返回到A节点,p指向A节点;此时pre的右子树为空,则令pre的后继为p,如图6-138所示。
5)更新当前pre为p,中序遍历p的右子树,中序遍历C的左子树(p指向C),中序遍历F的左子树(p指向F),其左子树为空,则令p的前驱为pre,如图6-139所示。
6)更新当前pre为p,中序遍历p的右子树,中序遍历G的左子树(p指向G),其左子树为空,则令p的前驱为pre,如图6-140所示。
7)更新当前pre为p,中序遍历p的右子树,其右子树为空,返回到C节点,p指向C节点;此时pre的右子树为空,则令pre的后继为p,如图6-141所示。
8)更新当前pre为p,中序遍历p的右子树,其右子树为空,遍历结束。此时pre的右子树为空,则令pre的后继为NULL,如图6-142所示。