顺序存储结构:双亲存储结构
用一维数组即可实现:用数组下标表示树中的结点,数组元素的内容表示该结点的双亲结点,这样有了结点(下标)以及结点之间的关系(内容),就可以表示一 棵树了。
链式存储结构:
孩子存储结构:即将每个结点的所有孩子结点都用单链表链接起来形成一个线性结构,此时n个结点就有n个孩子链表。
孩子兄弟存储结构:又称二叉树表示法,即以二叉链表作为树的存储结构。
每个结点都包含了三个内容:结点值、指向结点的第一个孩子结点的指针、指向结点的下一个兄弟结点的指针。
这种存储方法比较灵活,最大的优点是可以方便实现树转化成二叉树的操作,方便查找孩子,但查找双亲比较麻烦,对此,可以采用三叉链表,多加一个指向父结点的指针。
将一般的树加上如下两个限制条件就得到了二叉树:
总结点数=总分支数+1(根结点上面没有分支,其余每一个结点上面对应一个分支)
非空二叉树的叶子结点数=双分支结点数+1,即n0=n2+1(下标表示结点的分支个数)
证明:总结点数=n0+n1+n2 ,总分支数=n1+2n2 ,又因为树中除了根结点,其余每个结点都对应一个分支,即总结点数=总分支数+1,所以有n0+n1+n2=n1+2n2+1,整理得n0=n2+1。
二叉树中的空指针数=总结点数+1,空指针数也是线索二叉树的线索数
证明:假设所有的空指针都是叶子结点,那么树中的所有结点都变成了双分支结点(n个),根据性质1,那么空指针数=叶子结点数=双分支结点数+1=n+1;
在一个度为m的树中,度为1的结点数为n1,度为2的结点数为n2,……,度为m的结点为nm,则树中的叶子结点数n0=1+n2+2n3+…+(m-1)nm
证明:总结点数n=n0+n1+…+nm,总分支数=n1+2n2+3n3+…+mnm,总结点数=总分支数+1,则有n0+n1+n2…+nm=1+n1+2n2+3n3+…+mnm,整理得:n0=1+n2+2n3+…+(m-1)nm
二叉树的第i层上最多有2i-1个结点。(a0=1,q=2的等比数列第i项)
高度为k二叉树最多有2k-1结点(即高度为h的满二叉树)。(a0=1,q=2的等比数列前h项和)
高度为k二叉树最少有2k-1结点(=2k-1-1+1)。
有n个结点的完全二叉树,对各结点从上到下、从左到右依次编号(1~n),对于结点ai来说:
函数Catalan():给定n个结点,能构成h(n)种不同的二叉树: h ( n ) = C 2 2 n n + 1 h(n)=\frac{C_{2}^{2n}}{n+1} h(n)=n+1C22n
具有n(n>=1)个结点的完全二叉树的高度(或深度)为:
设Nh表示高度为h的平衡二叉树所含有的最少结点数,则有:N1=1,N2=2,N3=4,N5=7,……,Nh=Nh-2+Nh-1+1
顺序存储结构
即通过一个数组来存储一个二叉树。适用于完全二叉树,用于存储一般的二叉树会浪费大量的空间。
假如有n个结点的完全二叉树存储在数组a中,根结点的下标为1,对于结点a[i]
,它的:
2*i<=n
,则左孩子为a[2*i]
,否则没有左孩子2*i+1<=n
,则右孩子为a[2*i+1]
,否则没有右孩子a[j]
,j=取整{i/2}假如有n个结点的完全二叉树存储在数组a中,根结点的下标为0,对于结点a[i]
,它的:
2*i+1<=n
,则左孩子为a[2*i+1]
,否则没有左孩子2*i+2<=n
,则右孩子为a[2*i+2]
,否则没有右孩子a[j-1]
,j=取整{i/2}易错点:注意和树的顺序存储结构区分,在树的顺序存储结构中,数组下标代表结点编号,数组中所存的内容是各结点之间的关系。而在二叉树的顺序存储结构中,数组下标不仅是结点编号,还包含了各结点之间的关系。由于二叉树属于树的一种,所以树的顺序存储结构可以用来存储二叉树,但二叉树的顺序存储结构不能用来存储树。
链式存储结构
lchild | data | rchild |
---|
//二叉树链式存储结构
typedef struct BTNode {
char data; //默认char,可换
struct BTNode *lchild;
struct BTNode *rchild;
}BTNode;
二叉树的遍历主要分为先序遍历、中序遍历、后序遍历以及一个层次遍历。
这里“序“指的是根结点何时被访问。可以看出三种遍历方式只是访问结点的时机不一样。
层次遍历
按照从左到右(或从右到左),从上到下逐行遍历结点。
三种二叉树深度优先遍历算法的程序模板
//遍历模板
void trave(BTNode *p) {
if (p != NULL) {
//1.
trave(p->lchild);
//2.
trave(p->rchild);
//3.
}
}
对于树中的每一个结点,不管是采用先序遍历、中序遍历、后序遍历哪一种,每个结点都会被经过3次。
如果统一在第一次经过时访问结点,那就是先序遍历;此时把对结点的访问操作写在1处;
如果统一在第二次经过时访问结点,那就是中序遍历;此时把对结点的访问操作写在2处;
如果统一在第三次经过时访问结点,那就是后序遍历。此时把对结点的访问操作写在3处;
//先序遍历
void preOrder(BTNode *p) {
if (p!=NULL) {
visit(p); //对结点的访问操作
preOrder(p->lchild);
preOrder(p->rchild);
}
}
//中序遍历
void inOrder(BTNode *p) {
if (p != NULL) {
inOrder(p->lchild);
visit(p); //对结点的访问操作
inOrder(p->rchild);
}
}
//后序遍历
void postOrder(BTNode *p) {
if (p != NULL) {
postOrder(p->lchild);
postOrder(p->rchild);
visit(p); //对结点的访问操作
}
}
//visit()函数是自定义的,根据实际需要,可以用任何针对结点的操作来代替它
按照从左到右(或从右到左),从上到下逐行遍历结点。
要进行层次遍历,需要建立一 个队列。先将二叉树头结点入队列,然后出队列,访问该结点,如果它有左子树,则将左子树的根结点入队;如果它有右子树,则将右子树的根结点入队。然后出队列,对出队结点访问。如此反复,直到队列为空为止。
//层次遍历
void levelOorder(BTNode *p) {
BTNode *que[maxSize]; //定义一个循环队列
int front = 0, rear = 0; //初始化队列,队头与队尾归零
BTNode *q; //临时变量,用来临时存储出队元素
if (p != NULL) { //非空树
rear = (rear + 1) % maxSize;
que[rear] = p; //这两句是循环队列的入队操作,这里表示根结点入队
while (front!=rear) { //队列非空
front = (front + 1) % maxSize;
q = que[front]; //这两句是循环队列的出队操作,这里表示队头元素出队
visit(q); //访问结点
if (q->lchild != NULL) { //如果当前结点有左孩子,左孩子入队
rear = (rear + 1) % maxSize;
que[rear] = q->lchild;
}
if (q->rchild != NULL) { //如果当前结点有右孩子,右孩子入队
rear = (rear + 1) % maxSize;
que[rear] = q->rchild;
}
}
}
}
这里是借助了循环队列,乍一看好像很麻烦,其实只是循环队列的初始化、入队、出队看着比较麻烦,如果可以把这些操作写成函数放在外面,会看着简洁一些。但是,不管怎么变化,核心思想是不变的,根据借助的队列类型不同、针对结点的操作不同,可以根据此模板来记忆层次遍历:
//层次遍历模板
void levelOorder(BTNode *p) {
//1.初始化队列
BTNode *q; //临时变量,用来临时存储出队元素
if (p != NULL) { //树非空
//2.根结点入队
while (队列非空){
//3.出队 (出队元素赋值给q)
//4.visit(q); 针对结点q的操作
if (q->lchild != NULL) {
//5.如果q有左孩子,左孩子入队
}
if (q->rchild != NULL) {
//6.如果q有右孩子,右孩子入队
}
}
}
}
层次遍历的模板与下面的二叉树深度优先遍历算法的非递归实现中的先序遍历模板很相似,主要区别是:
注意不要记混。
先看最简单的情况:只有3个结点ABC
下面看更一般的情况:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vAUFRelu-1626800975200)(https://i.loli.net/2021/07/18/c8RfDynuWQqdGYh.png)]
先序遍历:ABDECFG
将其按照( A(BDE)(CFG))来划分,有没有观察到,BDE刚好是树A左下角以B为根结点的二叉树的先序遍历排列,CFG刚好是数A右下角以C为根结点的二叉树的先序遍历排列。往下还可以再划分,拿BDE来说,BDE又可以分成:(B(D)(E)),那么D自然就是树D的先序遍历了,E就是树E的先序遍历了。到这里就不能再划分了,因为D没有左子树了。有没有发现规律?
总结一下对A快速写出先序遍历结果的方法,即从大树化小树的方法:
中序遍历:DBEAFCG
方法是类似的,沿A最左边的分支一路向左下找,找到第一个没有左孩子的结点,这里就是树D,依次对树D、树B、树A完成后序遍历即可。即:(((D)B(E))A((F)C(G)))
后序遍历:DEBFGCA
方法是类似的,沿A最左边的分支一路向左下找,找到第一个没有左孩子的结点,这里就是树D,依次对树D、树B、树A完成后序遍历列即可。(((D)E(B))((F)G(C))A),即:DEBFGCA
本例中举的是最理想的情况,实际的树可能要比这复杂的多,越复杂的树利用这种方法就越方便。只要按照步骤1找到正确的开始结点,大树化小数的方法都是可以完成的(实际上我们写的遍历的递归程序就是这么做的)。熟悉这个过程这也为我们下面写非递归方法提供了思路。
已知前序遍历序列和中序遍历序列,可以唯一确定一棵二叉树
已知后序遍历序列和中序遍历序列,可以唯一确定一棵二叉树
但是已知前序遍历序列和后序遍历序列,是不能确定一棵二叉树
即:没有中序遍历序列的情况下是无法确定一颗二叉树的
why?拿上面的例子来说:
先序遍历:ABDECFG:(A(B(D)(E))(C(F)(G)))
中序遍历:DBEAFCG:(((D)B(E))A((F)C(G)))
后序遍历:DEBFGCA:(((D)E(B))((F)G(C))A)
几个规律:
所以,前序和后序在本质上可以将父子结点分离,但并没有指明左子树和右子树的能力,因此得到这两个序列只能明确父子关系,而不能确定一个二叉树。
按照上述的规律可以一步一步的还原二叉树,这里拿先序排列和中序排列举例:
Q1:为什么说二叉树的递归算法效率不高?如何解决?
递归函数所申请的系统栈,是一个所有递归函数都通用的栈。对于二叉树深度优先遍历算法,系统栈除了记录访问过的结点信息之外,还有其他信息需要记录,以实现函数的递归调用。
如果可以手动建立栈,仅保存遍历所需的结点信息,即对二叉树遍历算法进行针对性的设计,对于遍历算法来说,显然要比递归函数通用的系统栈更高效。
递归算法是把大问题逐渐化成一个越来越小的问题,再从小到大,从内到外逐个解决,这个核心思想在我们把递归转化成循环时是不变的。循环代替递归的关键,就是通过手动维护栈来实现递归,这个时候,结点入栈出栈的时机就显得非常重要。递归算法描述起来非常简洁而且想象,但运行过程并不容易搞透,若想把递归算法转化成非递归算法(循环),就要对他的运行过程非常清楚,手动尝试去模拟各种遍历算法的运行过程有利于理解这一部分内容。
先序遍历的非递归算法
//先序遍历的非递归算法
void preOrder2(BTNode *bt) {
//bt非空
if (bt != NULL) {
BTNode *stack[maxSize]; //定义栈
int top = -1; //初始化,栈顶指针top为-1时栈空
stack[++top] = bt; //根结点入栈
BTNode *q; //q是遍历指针,表示当前正在处理的元素
//开始遍历
while(top != -1) { //循环条件:栈非空
q = stack[top--]; //出栈,用q保存
visit(q); //访问q
if (q->rchild != NULL) { //如果q还有右孩子,右孩子入栈
stack[++top] = q->rchild;
}
if (q->lchild != NULL) { //如果q还有左孩子,左孩子入栈
stack[++top] = q->lchild;
}
}
}
}
先序遍历的非递归过程:从根结点开始,入栈。进入循环,出栈并访问根结点,先判断根结点是否有右孩子,如果有,右孩子入栈,然后判断根结点是否有左孩子,如果有,左孩子入栈。继续循环,根结点的左孩子出栈,如果它有右孩子,右孩子入栈,如果它有左孩子,左孩子入栈…栈空时退出循环。
关键之处:右孩子优先于左孩子入栈的顺序不能变。在先序遍历中,对左孩子的访问要优先于右孩子,又由于栈的先进后出特性,所以,每次访问完一个结点,它的右孩子要先于它的左孩子入栈,这样做才能保证左孩子先被访问到。
中序遍历的非递归算法
//中序遍历的递归算法
void inOrder2(BTNode *bt) {
if (bt != NULL) {
BTNode *stack[maxSize];
int top = -1;
BTNode *q = bt; //q是遍历指针,表示当前正在处理的元素,初始值为根结点bt
//开始遍历
while (top != -1 || q != NULL) { //注意这里的循环条件:栈非空或q非空
while (q != NULL) { //这个whiLe的作用是沿着q的左下方走到头,路过的结点依次入栈
stack[++top] = q;
q = q->lchild;
}
if (top != -1) { //这个if的作用是将出栈、访问栈顶元素之后,将遍历指针q指向出栈元素的右孩子
q = stack[top--];
visit(q);
q = q->rchild;
}
}
}
}
中序遍历的非递归过程:
从根结点出发,一路朝着树的左下走到头,找到第一个没有左孩子的结点a,路过的结点依次入栈。a就是中序遍历的第一个结点,它一定在左下角(但并不一定是叶子结点,a可能还有右孩子) 。
出栈并访问a,然后将遍历指针指向它的右孩子,去判断它的右孩子是否存在:
如果a的右结点b存在,就把b视为一个新树,回到步骤1,又一路朝着b的左下走…这里也就体现了递归的思想。
如果a的右结点b不存在(这意味着以a为根结点的数就只有它一个元素,那么树a此时已经遍历),那就去找a的父结点c,去完成对c的遍历(还是递归的思想)。c此时就在栈顶(如果栈非空),那么我们就出栈,完成对c的访问操作后,继续把遍历指针p指向c的右孩子…再接着判断c的右孩子是否存在…
重复这个过程,当栈空而且p也为空时循环结束。
关键之处:假设根结点是t,当t出栈并完成访问操作后,这就意味着这个数的左半部分(包括t)遍历完成,此时栈是空的,但树的右半部分还没有遍历,所以不能将栈空作为遍历循环的判断条件。此时遍历指针p指向的是t的右孩子,可以根据p的状态此来判断遍历是否继续,最后一个元素遍历完时p指向的是它的右孩子,此时p为空。这就是外层遍历循环的判断条件top != -1 || q != NULL
的原因。
后序遍历的非递归算法
后序遍历的非递归算法是最困难的,这里提供一种易于理解的版本。
先序遍历:ABDECFG (A(B(D)(E))(C(F)(G)))
中序遍历:DBEAFCG
后序遍历:DEBFGCA
逆后序遍历:ACGFBED
有一个规律是:逆后序遍历可以看成是把先序遍历过程中对左右子树遍历顺序交换所得的结果。
按照此规则实现后序遍历,要做两件事:
所以我们这里用到两个栈,一个栈是遍历本来就需要的,另一个则是来进行逆序的。
//非递归后序遍历二叉树
void postOrder2(BTNode *bt) {
if (bt != NULL) {
//栈1用来辅助进行交换了左右子树遍历顺序的先序遍历
//栈2用来实现上述遍历结果的逆序输出
BTNode *stack1[maxSize]; int top1 = -1;
BTNode *stack2[maxSize]; int top2 = -1;
BTNode *q; //遍历指针q
stack1[++top1] = bt;
//进行交换了左右子树遍历顺序的先序遍历
while(top1 != NULL) {
q = stack1[top1--];
stack2[++top2] = q; //每次从栈1出去的元素,就立即把它放入到栈2中
if (q->lchild != NULL) { //如果q还有左孩子,左孩子入栈,这里左右子树的入栈的先后顺序发生了变化
stack1[++top1] = q->lchild;
}
if (q->rchild != NULL) { //如果q还有右孩子,右孩子入栈
stack1[++top1] = q->rchild;
}
}
//先序遍历结束后,栈2元素逐个出栈即可实现后序遍历
while (top2 != NULL) {
q = stack2[top2--]; //栈2元素出栈并访问
visit(q);
}
}
}
对于先序遍历、中序遍历、后序遍历来说,存在一定的局限性:
对此,解决思路是,能不能通过某种方式把树中结点的前驱和后继的相关信息保存起来,这样后续查找时就非常高效。
n个结点的二叉树共计有n+1个空指针,利于这些空指针来保存前驱与后继信息。
lchild | ltag | data | rtag | rchild |
---|
在二叉树线索化的过程中会把树中的空指针(lchild与rchild)利用起来作为寻找当前结点前驱或后继的线索,这样就出线索和树中原有指向孩子结点的指针无法区分。为解决这个问题,增设两个标识域ltag和rtag,它们的具体意义如下:
//线索二叉树数结点结构
typedef struct BTBNode {
char data; //默认为char,可替换
int ltag, rtag;
struct BTBNode* lchild;
struct BTBNode* rchild;
}TBTNode;
先序遍历、中序遍历、后序遍历的线索化方式是不同的,对应的线索二叉树称为先序线索二叉树、中序线索二叉树、后续线索二叉树。
对一棵二叉树中所有结点的空指针域按照某种遍历方式加线索的过程叫作线索化,被线索化了的二叉树称为线索二叉树。
线索化从某种程度上讲,可以看成是对遍历算法的一种应用。
中序线索化的规则是:
按照上述规则可以写出两个结点线索化的过程,这就:
//线索化:p的左线索如果存在则让其指向pre
if (p->lchild == NULL) {
p->lchild = pre;
p->ltag = 1;
}
//如果pre非空且pre右线索存在则让其指向p
if (pre != NULL && p->rchild == NULL) {
pre->rchild = p;
pre->rtag = 1;
}
//p和pre线索化完成后,将pre指向p(p在之后将指向它的孩子)
pre = p;
二叉树进行中序线索化是在二叉树中序遍历算法的框架中进行的,先回顾下中序遍历递归算法:
//中序遍历
void inOrder(BTNode *p) {
if (p != NULL) {
inOrder(p->lchild);
visit(p); //线索化写在这里代替visit
inOrder(p->rchild);
}
}
把前面的visit()函数替换成线索化的代码块,即可得到中序线索化一个二叉树的代码:
//中序线索化
void inThread(TBTNode *p, TBTNode *&pre) {
if (p != NULL) {
inThread(p->lchild, pre); //递归,中序遍历并线索化左子树
//线索化:p的左线索如果存在则让其指向pre
if (p->lchild == NULL) {
p->lchild = pre;
p->ltag = 1;
}
//如果pre非空且pre右线索存在则让其指向p
if (pre != NULL && p->rchild == NULL) {
pre->rchild = p;
pre->rtag = 1;
}
//p和pre线索化完成后,将pre指向p(p在之后将指向它的孩子)
pre = p;
inThread(p->rchild, pre); //递归,中序遍历并线索化左子树
}
}
通过中序遍历建立中序线索二叉树的主程序为:
//通过中序遍历建立中序线索二叉树
void creatInThread(TBTNode *tbt) {
TBTNode *pre = NULL;
if (tbt != NULL) {
inThread(tbt, pre); //中序线索化,传入根结点tbt和它的前驱NULL
pre->rchild = NULL;
pre->rtag = 1;
}
}
/*inThread最后一次执行时:
p指向中序遍历的最后一个结点的右孩子(NULL),pre指向最后一个结点,不满足if(p!=NULL){...},函数结束。
此时还差最后一个结点的后继没有线索化,应该手动完成最后一个结点的右线索*/
经过上述操作后,可以理解为已经把二叉树变成了一个中序线索二叉树,可以将其视为一个链表。
中序线索二叉树中隐含了线索二叉树的前驱与后继信息。对其遍历时,只需要先找到序列中的第一个结点,然后依次找到结点的后继,直到后继为空即可。
查找中序线索二叉树的中序序列的第一个结点:
//求以p为根的中序线索二叉树中,中序序列下的第一个结点:
TBTNode *getFirst(TBTNode *p) {
while(p->ltag == 0) { //树中最左下的结点(不一定是叶结点)
p = p->lchild;
}
return p;
}
查找中序线索二叉树的中序序列的最后一个结点:
//求以p为根的中序线索二叉树中,中序序列下的最后一个结点:
TBTNode* getLast(TBTNode *p) {
while (p->rtag == 0) { //最右下的就是最后一个结点
p = p->rchild;
}
return p;
}
查找中序线索二叉树的中序序列的后继结点:
//结点p在中序线索二叉树的后继结点
TBTNode* getNext(TBTNode *p) {
if (p->rtag == 0)
return getFirst(p->rchild);
else
return p->rchild;
}
查找中序线索二叉树的中序序列的前驱结点:
//结点p在中序线索二叉树的前驱结点
TBTNode* getPrior(TBTNode *p) {
if (p->ltag == 0)
return getLast(p->lchild);
else
return p->lchild;
}
中序线索二叉树的中序遍历方法
//中序线索二叉树的中序遍历方法
void InOrder(TBTNode *t) {
for (TBTNode *p = getFirst(t);p != NULL;p = getNext(p)) {
visit(p);
}
}
二叉树线索化的代码块
//线索化:p的左线索如果存在则让其指向pre
if (p->lchild == NULL) {
p->lchild = pre;
p->ltag = 1;
}
//如果pre非空且pre右线索存在则让其指向p
if (pre != NULL && p->rchild == NULL) {
pre->rchild = p;
pre->rtag = 1;
}
//p和pre线索化完成后,将pre指向p(p在之后将指向它的孩子)
pre = p;
先序遍历的递归算法:
//先序遍历
void preOrder(BTNode *p) {
if (p!=NULL) {
visit(p); //线索化的代码块放这里代替visit
preOrder(p->lchild);
preOrder(p->rchild);
}
}
与中序线索二叉树一样,根据二叉树线索化的代码块以及先序遍历和后序遍历算法,只需要变动线索化代码块与递归的位置即可:
二叉树的先序线索化
//先序线索化
void preThread(TBTNode *p, TBTNode *&pre) {
if (p != NULL) {
//线索化:p的左线索如果存在则让其指向pre
if (p->lchild == NULL) {
p->lchild = pre;
p->ltag = 1;
}
//如果pre非空且pre右线索存在则让其指向p
if (pre != NULL && p->rchild == NULL) {
pre->rchild = p;
pre->rtag = 1;
}
//p和pre线索化完成后,将pre指向p(p在之后将指向它的孩子)
pre = p;
//注意,这里在递归入口处设有条件,左右指针不是线索才能继续递归
if(p->ltag==0)
inThread(p->lchild, pre);
if(p->rtag==0)
inThread(p->rchild, pre);
}
}
易错点:注意递归处的条件判断,这是先序线索化独有的,考虑一种特殊情况:加入p此时指向D,pre指向的是B,在对p完成线索化之后,p的lchild已经指向了B,此时按照程序,pre指向D,q指向D的lchild(注意,此时D已经完成线索化),即q又指向了B,又要去完成对B的线索化。可是, 在对D线索化前不是刚对B线索化过了吗?这就产生了死循环。所以,在递归入口前,我们要添加左右指针非线索的条件,就是为了避免这种情况。
查找先序线索二叉树的先序后继结点
先序遍历遵循的是“根左右”的原则,
也就是说,只需要判断p有没有左孩子,如果它有左孩子,左孩子就是它的后继,否则,它的rchild一定指向它的后继。
所以不难写出先序遍历一个先序线索二叉树的代码:
//先序遍历先序线索二叉树
void preOrder(TBTNode *root) {
if (root != NULL) {
for (TBTNode *p = root;p != NULL;) {
visit(p);
if (p->ltag == 0) //ltag == 0说明p的左链域没有被线索化,那么它一定有左孩子,左孩子就是p的后继
p = p->lchild;
else //只要p没有左孩子,那么不管右孩子是否被线索化,rchild一定指向p的后继
p = p->rchild;
}
}
}
天勤书上的版本,个人觉得没有上面这个好理解:
void preOreder(TBTNode *root) {
if (root != NULL) {
TBTNode *p = root;
while (p != NULL) {
while (p->ltag==0){
visit(p);
p = p->lchild;
}
visit(p);
p = p->rchild;
}
}
}
查找先序线索二叉树的先序前驱结点
改进:改二叉链表结构为三叉链表结构,每个结点再增设一个指向父结点的指针。
后序遍历的递归算法:
//后序遍历
void postOrder(BTNode *p) {
if (p != NULL) {
postOrder(p->lchild);
postOrder(p->rchild);
visit(p); //线索化的代码块放这里代替visit
}
}
二叉树的后序线索化
//后序线索化
void postThread(TBTNode *p, TBTNode *&pre) {
if (p != NULL) {
postThread(p->lchild, pre); //递归,后序遍历并线索化左子树
postThread(p->rchild, pre); //递归,后序遍历并线索化左子树
//线索化:p的左线索如果存在则让其指向pre
if (p->lchild == NULL) {
p->lchild = pre;
p->ltag = 1;
}
//如果pre非空且pre右线索存在则让其指向p
if (pre != NULL && p->rchild == NULL) {
pre->rchild = p;
pre->rtag = 1;
}
//p和pre线索化完成后,将pre指向p(p在之后将指向它的孩子)
pre = p;
}
}
查找后序线索二叉树的后序前驱结点
后序遍历遵循“左右根”的规则:
也就是说,只需要判断p到底有没有右孩子即可,有右孩子,p的前驱就是rchild,否则就是lchild。
查找后序线索二叉树的后序后继结点
改进:改二叉链表结构为三叉链表结构,每个结点再增设一个指向父结点的指针。
首先,上面已经分析了机器查找的过程,总结如下图:先序线索二叉树和后续线索二叉树都存在一定的局限性。为了解决这个问题,可以将二叉链表结构为三叉链表结构,多增设一个指向父结点的指针。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-n3SRU0Ni-1626800975205)(C:/Users/76583/Desktop/%E5%A4%A9%E5%8B%A4%E7%AC%94%E8%AE%B0/image-20210714205603680.png)]
如何人工的查找前驱与后继呢?这里重点看先序线索二叉树找前驱以及后序线索二叉树找后继。
查找中序线索二叉树的中序后继结点:
查找中序线索二叉树的中序前驱结点:
查找先序线索二叉树的先序后继结点
也就是说,只需要判断p有没有左孩子,如果它有左孩子,p的后继就是lchild,否则就是rchild。
查找先序线索二叉树的先序前驱结点
查找后序线索二叉树的后序前驱结点
也就是说,只需要判断p到底有没有右孩子即可,有右孩子,p的前驱就是rchild,否则就是lchild。
查找后序线索二叉树的后序后继结点
树的孩子兄弟存储结构与二叉树的存储结构本质上都是二叉链表,只是左右结点表达的含义不同:
规则:孩子兄弟表示法:
即“左孩子,右兄弟”,由于根结点没有兄弟,所以转换后的二叉树没有右子树。
树转换成二叉树的画法:
在各兄弟结点之间加一条连线
对每一个结点,只保留它与第一个孩子之间的连线,其余都抹去
以树根为圆心,顺时针旋转45度
把树转换为二叉树的过程逆过来即可:
树的遍历有两种方式:先序遍历和后序遍历。
对于如图所示的树:先序遍历的结果为ABEFCGDHIJ,后序遍历的结果为EFBGCHIJDA。
树转换为二叉树后,树的先序遍历对应二叉树的先序遍历,树的后序遍历对应二叉树的中序遍历(注意不是后序遍历)。
所以,可以将树转换为二叉树后,借助遍历二叉树的方法来遍历树。假如一 棵树已经转化为二叉树来存储,要得到其先序遍历序列,只需先序遍历这棵二叉树;要得到其后序遍历序列,只需中序遍历这棵二叉树。
森林的遍历方式有两种:先序遍历和后序遍历。
森林转换为二叉树,森林的先序遍历对应二叉树的先序遍历,森林的后序遍历对应二叉树的中序序列(注意不是后序遍历)。
赫夫曼树又叫作最优二叉树,它的特点是带权路径最短。几个相关概念:
构造赫夫曼二叉树
给定n个权值,用这n个权值来构造赫夫曼树的算法描述如下:
赫夫曼树的特点
在数据通信中,若对每个字符用相等长度的二进制位表示,称这种编码方式为固定长度编码。
若允许对不同字符用不等长的二进制位表示,则这种编码方式称为可变长度编码。
可变长度编码比固定长度编码要好得多,其特点是对频率高的字符赋以短编码,而对频率较低的字符则赋以较长一些的编码,从而可以使字符的平均编码长度减短,起到压缩数据的效果。
赫夫曼编码是一种被广泛应用而且非常有效的数据压缩编码。
任意一个字符的编码都不是另一个字符编码的前缀,则称这样的编码为前缀编码。前缀编码可以保证不会出现歧义。
由赫夫曼树得到哈赫夫曼编码
一个例子:假设有字符串s=“AAABBACCCDEEA”,按照上图的方法得到如图所示的赫夫曼树并对分支编号:
那么对ABCDE进行赫夫曼编码有:
可以看到,显然,所有字符结点都出现在叶结点中,且越靠近根结点的字符频度越高(权值越大),出现次数最多的字符编码长度就越短,而且赫夫曼编码属于前缀编码。根据赫夫曼树WPL最短的特性可知,赫夫曼编码产生的是最短前缀编码。
赫夫曼二叉树是赫夫曼n叉树的一 种特例。
对于结点数目>= 2 的待处理序列,都可以构造赫夫曼二叉树,但却不一 定能构造赫夫曼 n 叉树。
当发现无法构造时,需要补上权值为0的结点让整个序列凑成可以构造赫夫曼n叉树的序列。
例如:对于序列A(1)、B(3)、C(4)、D(6) (括号内为权值),就不能直接构造赫夫曼三叉树,需要补上一 个权值为0的结点H。
H结点的存在对WPL值没有影响,得到的仍然是最小WPL:(0*2)+(1*2)+(3*2)+(4*1)+(6*1)=18。
但要注意的是,二叉赫夫曼树和三叉赫夫曼树所得到的WPL是不同的,不要混淆最小的概念,这里的最小是说在含有n个带权叶结点的三叉树中,赫夫曼三叉树是WPL最小的。
BST(Binary Search Tree)定义
二叉排序树或者是空树,或者是满足以下条件的树:
根据定义可知,二叉排序数的中序遍历是非递减有序(或非递增有序)的,没有特殊说明,BST均采取左小右大的分布。
存储结构
二叉排序树和二叉树的存储结构没有差别,都是有一个值域和两个指针域组成:
typedef struct BSTNode {
int key;
struct BSTNode *lchild;
struct BSTNode *rchild;
}BSTNode;
查找关键字
//查找key
BSTNode* BSTSearch(BSTNode *bst,int key) {
if (bst == NULL) return NULL;
else {
if (key == bst->key) {
return bst;
}
else if (key < bst->key) {
return BSTSearch(bst->lchild, key);
}
else {
return BSTSearch(bst->rchild, key);
}
}
}
插入关键字
要插入关键字首先要找到插入位置,对于一个不存在于二叉排序树中的关键字,其查找不成功的位置就是该关键字的插入位置。
//插入
int BSTInsert(BSTNode *bst, int key) {
if (bst == NULL) {
bst = (BSTNode*)malloc(sizeof(BSTNode));
bst->lchild = bst->rchild = NULL;
bst->key = key;
return 1;
}
else {
if (key == bst->key) {
return 0;
}
else if (key < bst->key) {
return BSTInsert(bst->lchild, key);
}
else {
return BSTInsert(bst->rchild, key);
}
}
}
删除关键字
二叉排序树的删除操作是最麻烦的,因为必须保证删除操作之后继续维持树的“有序性”。假设将要被删除的结点为p,f是它的父结点,那么会出现三种情况:
p是叶子结点。直接删除即可。
p只有右子树而没有左子树,或者p只有左子树没有右子树。此时,只需要删除p,把它的子树接在f上取代p的位置即可。
p既有右子树也有左子树。按照以下操作方法可以将情况3转换为情况1,2:
找到中序遍历序列中p的直接前驱m,或者,找到中序遍历序列中p的直接后继n,将p的值改为m或者n,之后删除原m或者原n。
原先的m或者n的删除方式必然是情况1,2中的某一种,这样就完成了情况3向情况1,2的转换。
建立二叉排序树
void createBST(BSTNode *&bst,int key[],int n){
int i;
bst = NULL;
for(int i=0;i<n;i++){
BSTInsert(bst,key[i]);
}
}
定义
二叉平衡树或者是空树,或者是满足以下条件的树:
即:以树中所有结点为根的树的左右子树高度之差不超过1。
平衡因子
一个结点的平衡因子为其左子树的高度减去右子树高度的差。
对于平衡二叉树,树中的所有结点的平衡因子的取值只能是-1、0 、1 三个值。
平衡二叉树的查找
任意关键字的查找,比较次数不超过AVL树的高度。
设Nh表示高度为h的平衡二叉树所含有的最少结点数,则有:N0=0,N1=1,N2=2,N3=4,N5=7,……,Nh=Nh-2+Nh-1+1。
这个结论也可以反过来求给定结点数的AVL树的查找所需要的最多比较次数(或树的最大高度)。
平衡调整
建立平衡二叉树的过程和建立二叉排序树的过程基本一 样,都是将关键字逐个插入空树中的过程。不同的是,在建立平衡二叉树的过程中,每插入一 个新的关键字都要进行检查,看是否新关键字的插入会使得原平衡二叉树失去平衡,即树中出现平衡因子绝对值>1的结点。如果失去平衡则需要进行平衡调整。平衡二叉树的重点就是平衡调整。
平衡调整的方法
假定向平衡二叉树中插入一 个新结点后破坏了平衡二叉树的平衡性:
最小不平衡子树是指距离插入结点最近,且以平衡因子绝对值大于1的结点作为根的子树,又称为最小不平衡子树。
主要分为四种情况:LL右单旋转,RR左单旋转,LR先左后右双旋转,RL先右后左双旋转。
这里的L与R是对不平衡状态的描述:比如LL,就是指结点A的左孩子的左子树上插入了新结点导致A失去平衡。
LL调整:某时刻在a的左孩子b的左子树Y上插入 一 个结点,导致 a 的左子树高度为 h+2 , 右子树高度为h,发生不平衡。
此时应把b向右旋转代替a成为根结点,这一过程称为右单旋转。
具体操作为:将a下移一 个结点高度,b 上移一 个结点高度,也就是将 b 从 a 的左子树取下,然后将b的右子树挂在a的左子树上,最后将a挂在b的右子树上以达到平衡。
LR调整:某时刻在a的左孩子b的右子树Y上插入一 个结点(不管是插在Y的左孩子还是右孩子,做法一样)导致不平衡。
需要做两次旋转,先左旋c后右旋b。
具体操作为:将c作为a和b两棵子树的根,b为左子树,a为右子树,c原来的左子树U作为b的右子树,c原来的右子树V作为a的左子树以达到平衡。这就是LR调整,也叫先左后右双旋转调整,因为调整的过程可以看成是先左旋c后右旋b。
RL调整:如果b在a的右子树上,且插入的结点在b的左子树上,即与图9-8a对称的情况,则此时只需将上述过程做左右对称处理即可。这种调整叫RL调整,也叫先右后左双旋转调整。
一颗完全二叉树有1001个结点,其中叶子结点的个数为(501)个。
分析:完全二叉树一定是由一颗满二叉树从下到上,从右到左,挨个删除结点所得到的。
也就是说,一颗完全二叉树,度为1(分支为1)的结点一定是1或者0,如果有度为1的结点,它的孩子一定是二叉树最后一层最后一个结点。
假设度为1的结点数位1,即n1=1,那么n=n0+n1+n2=n0+1+n0-1=2n0=1001,n0=500.5,显然错误。
假设度为1的结点数为0,即n1=0,那么此时二叉树中只有度为0和度为2的结点,则n=n0+n2,根据性质2,又有n0=n2+1,则n=n0+n2=n0+n0-1=2n0-1=1001,得n0=501。
假设高度为h的二叉树中只有度为0和2的结点,那么此类二叉树中所包含的结点数最少为(2h-1)个。
要求度只有0和2,且结点最少,那么必然是类似二叉赫夫曼数的构造。
除了第一层只有1个根结点以外,其余每一层都只有2个结点,则结点数为2h-1。
如果题目要求结点最多,那么自然是满二叉树,此时结点数是2h-1。
设树的度为4,其中度为1、2、3、4的结点个数分别为4、2、1、1,则树中的叶子结点个数为(8)个。
根据性质4:n0=1+n2+2n3+…+(m-1)nm=1+2+2*1+3*1=8
有n个叶子结点的二叉赫夫曼树的结点总数是(2n-1)个。
n个结点的线索二叉树含有的线索数为(n+1)。
线索二叉树的线索数等于原二叉树中的空指针数=总结点数+1
一颗具有1025个结点的二叉树的高度h的范围为(11~1025)。
高度最高:每层只有一个结点,则高度为1025
高度最低:完全二叉树,根据性质10,h=[log2(1025+1)]向上取整,h=11
在度为m的赫夫曼树中,叶子结点的个数为n,则非叶子结点的个数为( ⌈ n − 1 m − 1 \frac{n-1}{m-1} m−1n−1⌉)个。
在构造度为m的赫夫曼树的过程中,每次把m个叶子结点合并为一个父结点 (第一 次合并可能少于 m 个子结点),每次合并减少 m -1个结点。加入第一次合并了m个结点,为了统一计算给n-1,把第一次合并看成m-1个结点,共需要 ⌈(n-1)/(m-1)⌉次合并,向上取整是因为最后不一定能整除,此时会人为的补上结点形成最后一次合并,每次合并增加一 个非叶子结点。下图展示了度为3,则叶子结点个数为8的赫夫曼三叉树的合并过程,橙色的为人为补上的结点。
已知一棵完全二叉树的第6层(设根为第1层)有8个叶子结点,则该完全二叉树的结点个数最多是(111)个。
需要注意的是,树不一定是6层,不要陷入思维惯性,看到第6层有8个叶子结点,就下定论树只有6层。实际上,结点个数最多的情况下树有7层,第6层的8个叶子结点在第7层均没有孩子,即7层的满二叉树从7层右边往左去掉8*2=16个结点形成的完全二叉树。前6层为满二叉树,共有26-1=63个结点,第7层有27-1-16=48个结点,共计111个结点。
已知一棵有2011个结点的树,它的叶子结点为116个,则该树转换成的二叉树中没有右孩子的结点个数为(1896)个。
考虑极端情况:
按照“左孩子,右兄弟”的规则构成二叉树,最后一个叶子结点加上上面1895个中间结点必然没有右孩子。
高度为6的平衡二叉树,所有非叶结点的平衡因子均为1,则该平衡二叉树的结点总数为(N6=20)
根据性质11可推得,高度为6的平衡二叉树的结点最少为20个,实际上,题目所有非叶结点的平衡因子均为1,暗含的信息也是指结点最少的极端情况,当所有非叶结点的平衡因子均为1时,此时增加一个结点,会使得某个结点的平衡因子变为0,而不影响平衡性。这也就是构成平衡二叉树结点最少的情况。
n和m为一颗二叉树的两个结点,在中序遍历时,n在m前的条件是n在m的左边。(对)
对于二叉树来说,不管是前序、中序、后序遍历,叶子结点在遍历序列中的先后顺序是相同的。(对)
一颗二叉树的先序序列和后续序列正好相反,则该二叉树的高度一定等于其结点数。(对)
采用中序、先序、后序任意一种方式遍历树来计算;
int nums = 0; //全局变量,一个计数器
int getNodeNums(BTNode *bt) {
if (bt != NULL) {
nums++; //每次经过一个结点,计数器+1
getNodeNums(bt->lchild);
getNodeNums(bt->rchild);
}
}
采用另外一种递归思路:如果树空,返回0;如果树非空,求其左子树的的结点数n1,右子树的结点数n2,返回n1+n2+1。
int getNodeNums(BTNode *bt) {
int n1, n2;
if (bt == NULL) {
return 0;
}else{
n1 = getNodeNums(bt->lchild);
n2 = getNodeNums(bt->rchild);
return n1 + n2 + 1;
}
}
给上一题加上限制条件:左右孩子非空即可。
方法一:
int nums = 0;
int getNodeNums(BTNode *bt) {
if (bt != NULL) {
if(bt->lchild == NULL && bt->rchild == NULL){
nums++;
}
getNodeNums(bt->lchild);
getNodeNums(bt->rchild);
}
}
方法二:
int getNodeNums(BTNode *bt) {
int n1, n2;
if (bt == NULL) { //空树返回0
return 0;
}else if(bt->lchild == NULL && bt->rchild == NULL){ //如果是叶子结点,返回1
return 1;
}else{
n1 = getNodeNums(bt->lchild); //既不是空树也不是叶子结点,求它的左子树的叶子结点数
n2 = getNodeNums(bt->rchild); //求它的右子树的叶子结点数
return n1 + n2 ;
}
}
具体要求就是修改叶子结点的rchild指针,指向它右边的叶子结点。用head和tail分别指向链表的表头和表尾。
分析:这里要用到一个非常重要的性质:不管是先序遍历、中序遍历还是后序遍历,在它们的遍历过程中叶子结点被访问的先后顺序都是不变的,都是从左往右。任选一种,修改visit函数即可,这里采用先序遍历的模板:
void linkNodes(BTNode *bt, BTNode *&head, BTNode *&tail) {
if (bt != NULL) {
//判断是否是叶子结点
if (bt->lchild == NULL && bt->rchild == NULL) {
//开始串接链表
//head为空,说明bt是表头,表尾指针和表头指针同时指向它
if (!head) {
head = bt;
tail = bt;
}
else {
//bt不是表头,就把它接在表尾,同时挪动表尾指针
tail->rchild = bt;
tail = bt;
}
}
linkNodes(bt->lchild, head, tail);
linkNodes(bt->rchild, head, tail);
}
}
修改数据结构
typedef struct BTNode_p {
char data;
struct BTNode_p *lchild;
struct BTNode_p *rchild;
struct BTNode_p *parent; //增加父亲指针
}BTNode_p;
遍历二叉树,给每个结点都设立父结点,我们需要两个参数,当前结点和它的父结点:
//遍历二叉树设立父结点
//传入根结点和它的父结点NULL即可
void setParentTree(BTNode_p *btp, BTNode_p *par) {
if (btp != NULL) {
btp->parent = par;
btp = par;
setParentTree(btp->lchild, btp);
setParentTree(btp->rchild, btp);
}
}
假设先序序列存储在数组pre[L1,…,R1]中,请把后序序列存储到post[L2,…,R2]数组中。
分析:根据满二叉树的特性可知,满二叉树具有一个重要的特性就是左右子树的结点数目是相等的,利用这一特性,我们可以很自然的根据先序序列唯一的满二叉树:序列的第一个元素就是根结点,然后将剩余的元素等分为两份,则分别为左子树序列和右子树序列。那如何把先序序列转换为后序序列呢?根据先序遍历和后序遍历的特性,它们的叶子结点的相对位置都是相同的,我们只需要递归地把先序序列中根结点的位置放到序列的末尾即可。但是,这种方法只适合于满二叉树,正是因为是满二叉树,我们才能每次正确的找到子树的根结点。
这里的主要难点就在于左右子树的下标问题,假设先序序列为pre[L1,…,R1],后序序列将存储在post[L2,…,R2]中,那么:
左子树的下标:
L1+1:在pre数组中除去第一个元素(根结点),剩下的第一个就是左子树下标的开始位置
(L1 + 1 + R1) / 2:在pre数组中L1+1到R2的中间位置就是左子树的末端下标
L2——0:在post数组中左子树的序列的开始位置始终在数组开始位置
R2——(L2 + R2 - 1) / 2:在post数组中L2到R2-1的中间位置就是左子树的末端下标
左子树的下标:
//把先序序列pre数组中下标L1到R1的元素转换为后序序列存到post数组中的L2到R2位置上
void preToPost(char pre[], int L1, int R1, char post[], int L2, int R2) {
//L1>R1为递归结束的条件
if (L1 <= R1) {
//将根结点放到后序序列的最后一位
post[R2] = pre[L1];
//递归转换左子树
preToPost(pre, L1 + 1, (L1 + 1 + R1) / 2, post, L2, (L2 + R2 - 1) / 2);
//递归转换右子树
preToPost(pre, (L1 + 1 + R1) / 2 + 1, R1, post, (L2 + R2 - 1) / 2 + 1, R2 - 1);
}
}
写一个算法求二叉树的深度,二叉树以二叉链表的形式存储。
分析:假设这棵树的左子树的深度为ld,右子树的深度为rd,那这棵二叉树的深度等于ld和rd中的较大者再加一(根结点本身),采用递归的思想,先求左子树的深度,再求右子树的深度,最后返回二者中的较大者+1,按照"左右中"的遍历顺序,这不就正好是后序遍历吗?
//求二叉树的深度
int getDepth(BTNode *bt) {
int ld, rd;
if (bt == NULL) { //作为递归结束标志,空树的深度自然为0
return 0;
}
else {
ld = getDepth(bt->lchild); //递归求得左子树的深度
rd = getDepth(bt->rchild); //递归求得右子树的深度
return (ld > rd ? ld : rd) + 1; //返回二者中的较大者+1
}
}
写一个算法,求出二叉树的宽度(结点数最多的那一层上的结点个数),二叉树以二叉链表的形式存储。
分析:直接在二叉树中求最大宽度显然不是一件容易的事情,如果我们能把树中的元素都存到线性表(队列)里,并记录下每一个结点所在的层数,直接遍历线性表就可以得到最大宽度了。那么我们要做的事情就变成了:
根结点的层数显然为1,根结点的孩子的层数就为1+1。这就说明,只要我们当前结点的层数,就能知道它的孩子所在的层数。那该采取哪种二叉树遍历方式呢?显然是层次遍历,我们当然希望把结点一层一层的从左往右逐个放到线性表里,那么在同一层的结点就是相邻的,这样只需要一次遍历就能求得最大宽度。
//根据我们的需求,定义队列中元素的结构体:结点+层数
typedef struct {
BTNode *node; //结点指针
int lno; //结点所在层数
}St;
//求二叉树的宽度
int getWidth(BTNode *bt){
//初始化队列
St que[maxSize]; //队列尽可能的大,能放下树中的所有结点
int front = 0, rear = 0;
//定义两个临时变量用于接收每次出队元素保存的信息
BTNode *q;
int Lno = 0;
if (bt != NULL) {
//根结点入队,它所在的层数是1
que[++rear].node = bt;
que[++rear].lno = 1;
//队列非空时循环
while (front != rear) {
//出队
q = que[++front].node;
Lno = que[++front].lno;
//左右孩子入队
if (q->lchild != NULL) { //如果出队结点q有左孩子,则左孩子入队,它所在的层数是q所在的层数+1
que[++rear].node = q->lchild;
que[++rear].lno = Lno + 1;
}
if (q->rchild != NULL) { //如果出队结点q有右孩子,则右孩子入队,它所在的层数是q所在的层数+1
que[++rear].node = q->rchild;
que[++rear].lno = Lno + 1;
}
}
}
/*
最后一个结点出队后,Lno就保存的是树中的最大层数;
上面所说的出队,并没有将元素从队列中删除,只是挪动了队头指针;
遍历队列,求得最大宽度:
*/
int maxWidth = 0; //宽度
int num; //计数器
int last=0; //last用来保存每次查找下一层结点时的开始位置
for (int i = 1; i <= Lno; i++) //分别查找第一层、第二层...第Lno层的结点个数
{
num = 0;
for (int j = last; j < rear; j++) //从last位置开始统计第i层结点的个数
{
if (que[j].lno == i) { //每发现一个第i层的结点,计数器+1
num++;
if (num > maxWidth) maxWidth = num; //刷新最大宽度值
}
else if (que[j+1].lno > i) { //下一个结点不是第i层(必然是i+1层),就记录下次开始的位置并跳出循环找下一层
last = j + 1;
break;
}
}
}
return maxWidth;
}
/*
由于树中结点时按照一层一层从左往右的顺序存放的,那么层数相同的结点在队列中必然是相互挨着的,所以我们可以设置一个结束标志last,当发现下一个元素的层数不是i时,就直接跳出循环,下一次,找i+1层的元素时就直接从last开始找。所以,只需要遍历一次队列就够了。
*/
方法一:利用层次遍历,把结点和它所在的层数信息保存在一个新的数据结构中(跟上一题求最大宽度是的做法一样),然后保存在一个队列中,遍历队列即可解决问题。
方法二:利用递归遍历,定义一个全局变量层数L,初始值为1,每次遍历左孩子的时候就L+1,每次遍历完右孩子的时候将要返回根结点时就给L-1:
int L = 1; //全局变量L表示层数
void leno(BTNode* p, char x) {
if (p != NULL) {
if (p->data == x) { //如果p->data==x,就输出层数L
cout << L << endl;
}
++L; //每次遍历左孩子前就给层数+1
leno(p->lchild,x);
leno(p->rchild,x);
--L; //每次遍历完右孩子放回根结点前,就给层数-1
}
}
二叉树的先序序列存储在一维数组pre[L1,…,R1]中,中序序列存储在一维数组in[L2,…,R2]中,(L1,L2,R1,R2均表示了数组中元素的下标范围,元素为char型),假设二叉树中各结点中数据值不相同,请给出由pre[L1,…,R1]和in[L2,…,R2]构造二叉树的算法。
分析:根据先序序列和中序序列构建二叉树:
根据先序序列的第一个结点找到根结点
在中序序列中找到根结点的位置i,i左边就是左子树,i右边就是右子树:
在in中,从L2到i-1就是左子树,i+1到R2就是右子树
与之对应的左子树在pre的位置是L1+1到L1+(i-L2),右子树的位置是L1+(i-L2)+1到R1
重复1,2两步,递归地构建左右子树,当L1-R1<0(表示待处理序列的长度<0)时递归结束
这个算法的关键是确定左右子树的序列在pre和in数组中的下标。给出实例结合代码分析。
先序遍历pre:ABDECFG:(A(B(D)(E))(C(F)(G)))
中序遍历in:DBEAFCG:(((D)B(E))A((F)C(G)))
// 由pre[L1,...,R1]和in[L2,...,R2]构造二叉树
BTNode *createBT(char pre[], char in[], int L1, int R1, int L2, int R2) {
//L1>R1说明处理的序列长度小于0,返回NULL,是递归结束的条件
if (L1 > R1) return NULL;
//构造根节点
BTNode *bt;
bt = (BTNode *)malloc(sizeof(BTNode));
bt->lchild = bt->rchild = NULL;
//查找pre[L1]在in数组中的位置,用i记录下来
int i;
for (i = L2;i <= R2;i++) {
if (in[i] == pre[L1])
break;
}
//给bt的各参数赋值
bt->data = in[i];
//递归构建bt的左右子树
//pre[L1 + 1,...,L1 + i - L2]是左子树先序序列,pre[L1 + i - L2 + 1,..., R1]是右子树的先序序列
//in[L2,..., i - 1]是左子树的中序序列,in[i + 1,..., R2]是右子树的中序序列
bt->lchild = createBT(pre, in, L1 + 1, L1 + i - L2, L2, i - 1);
bt->rchild = createBT(pre, in, L1 + i - L2 + 1, R1, i + 1, R2);
//构建完成后返回根节点
return bt;
}
1,每次遍历完右孩子的时候将要返回根结点时就给L-1:
int L = 1; //全局变量L表示层数
void leno(BTNode* p, char x) {
if (p != NULL) {
if (p->data == x) { //如果p->data==x,就输出层数L
cout << L << endl;
}
++L; //每次遍历左孩子前就给层数+1
leno(p->lchild,x);
leno(p->rchild,x);
--L; //每次遍历完右孩子放回根结点前,就给层数-1
}
}
二叉树的先序序列存储在一维数组pre[L1,…,R1]中,中序序列存储在一维数组in[L2,…,R2]中,(L1,L2,R1,R2均表示了数组中元素的下标范围,元素为char型),假设二叉树中各结点中数据值不相同,请给出由pre[L1,…,R1]和in[L2,…,R2]构造二叉树的算法。
分析:根据先序序列和中序序列构建二叉树:
根据先序序列的第一个结点找到根结点
在中序序列中找到根结点的位置i,i左边就是左子树,i右边就是右子树:
在in中,从L2到i-1就是左子树,i+1到R2就是右子树
与之对应的左子树在pre的位置是L1+1到L1+(i-L2),右子树的位置是L1+(i-L2)+1到R1
重复1,2两步,递归地构建左右子树,当L1-R1<0(表示待处理序列的长度<0)时递归结束
这个算法的关键是确定左右子树的序列在pre和in数组中的下标。给出实例结合代码分析。
先序遍历pre:ABDECFG:(A(B(D)(E))(C(F)(G)))
中序遍历in:DBEAFCG:(((D)B(E))A((F)C(G)))
// 由pre[L1,...,R1]和in[L2,...,R2]构造二叉树
BTNode *createBT(char pre[], char in[], int L1, int R1, int L2, int R2) {
//L1>R1说明处理的序列长度小于0,返回NULL,是递归结束的条件
if (L1 > R1) return NULL;
//构造根节点
BTNode *bt;
bt = (BTNode *)malloc(sizeof(BTNode));
bt->lchild = bt->rchild = NULL;
//查找pre[L1]在in数组中的位置,用i记录下来
int i;
for (i = L2;i <= R2;i++) {
if (in[i] == pre[L1])
break;
}
//给bt的各参数赋值
bt->data = in[i];
//递归构建bt的左右子树
//pre[L1 + 1,...,L1 + i - L2]是左子树先序序列,pre[L1 + i - L2 + 1,..., R1]是右子树的先序序列
//in[L2,..., i - 1]是左子树的中序序列,in[i + 1,..., R2]是右子树的中序序列
bt->lchild = createBT(pre, in, L1 + 1, L1 + i - L2, L2, i - 1);
bt->rchild = createBT(pre, in, L1 + i - L2 + 1, R1, i + 1, R2);
//构建完成后返回根节点
return bt;
}