参考视频:天勤率辉考研2020
目录
1.树的基础知识
树与线性结构有本质的区别,它是一种分支结构,有一对多的特性。
结点:树结构中体现数据信息以及逻辑关系的单元。
结点的度:结点所引出的分支的个数。
树的度:这棵树中所有结点的最大分支数。
(上图中的树的度为3)
叶子结点:度为0的结点。(即该结点没有分支)
(A3、A4、A5、A6)
孩子,双亲:与某个结点有直接关系的上一层结点是该结点的双亲,有直接关系的下一层结点是该结点的孩子。每个结点的孩子没有次序之分。
(A2、A3、A4的双亲是A1)
(A1的孩子是A2、A3、A4)
孩子存储结构:特点是存储了结点和其孩子的关系
typedef struct Branch{ int index;//存储该孩子结点的下标 struct Branch* next;//存储该孩子结点的双亲的其他孩子 //这里又相当于一个链式存储结构了 }Branch; typedef struct{ int data;//存储该结点的数据信息(这里暂用int代表) Branch* first;//表示该结点的孩子(按下标从小到大存) }TNode;
2.二叉树的逻辑结构
给树加上两个限制条件: 每个结点的孩子结点最多只有两个;给每个结点的孩子结点规定上次序,一个称之为左孩子(黄色),一个称之为右孩子(红色)。
蓝色是根节点。
二叉树的类型
满二叉树:除了最底层结点外,其余所有结点都有左孩子和右孩子。
完全二叉树: 对一棵满二叉树,从最底层开始,从右往左逐渐删除结点所得到的二叉树。
删完最底层结点之后就又变成了一棵满二叉树。
所以满二叉树可以看成一种特殊的完全二叉树。
满二叉树的高度(h)与结点个数的关系
对于一棵满二叉树,每一层的结点个数
第1层:1
第2层:2
第3层:4
第4层:8
第5层:16
……
第n层:2^(n-1)
求和得前n层共有结点(2^n-1)个
高为1:有2^1-1个结点
高为2:有2^2-1个结点
高为3:有2^3-1个结点
……
高为h:有2^h-1个结点
完全二叉树的深度(h)与结点个数(n)的关系
2^h-1<n<=2^(h+1)-1 (右侧取等是因为满二叉树是一种特殊的完全二叉树。)
去掉-1 强行+1
2^h<=n<2^(h+1) 2^h<n+1<=2^(h+1)
同时取对数(以2为底)
h<=(log2) n< h +1 h<(log2)(n+1)<=h+1
同时向下取整 同时向上取整
h<=[(log2)n]<h +1 h<[(log2)(n+1)]<=h+1
[(log2)n]=h h=[(log2)(n+1)]-1
h=[(log2)n]
二叉树的一些性质
总分支数=总结点数-1(因为除了根节点,每个结点上方都有一个分支连接到它的双亲)(此结论不局限于二叉树,适用于普通的树)
记叶子结点数为N0
单分支结点数为N1
双分支结点数为N2
总结点数=N0+N1+N2
总分支数=N1+2*N2
联立3个结论得到 N0=N2+1
叶子结点数=双分支结点数+1
求二叉树空分支个数
我们假设所有的空分支现在都有了一个结点,那么我们所加上的这些结点就成了叶子结点,而此时二叉树中的原有结点就都是双分支结点,根据结论叶子结点数=双分支结点数+1,公式中的叶子结点数就是我们要求的空分支个数,双分支结点数就是二叉树中原有的结点个数。
将二叉树的一些性质类推到树
解得N0=N2+2*N3+3*N4+……+(m-1)*Nm+1
3.二叉树的存储结构
二叉树的存储结构有两种,顺序存储结构和链式存储结构。
顺序存储结构
完全二叉树:
首先对每个结点从上至下,从左至右编号(从0开始),将其放在一个数组中,那么我们就存储好了二叉树的数据信息,还需要去存储他们之间的逻辑关系,可以发现对于一个下标为i的结点,它的左孩子是2*i+1,右孩子是2*i+2,超过了结点的最大下标说明没有该孩子。
若编号从1开始,对于一个下标为i的结点,它的左孩子是2*i,右孩子是2*i+1。
一般二叉树:
不满足上述结论
所以二叉树的顺序存储结构有局限性,只适用于存储完全二叉树。
链式存储结构
即二叉链表存储结构,可以存储所有二叉树。
结点有其数据信息和左孩子与右孩子。
typedef struct BTNode{ int data; struct BTNode* lchild; struct BTNode* rchild; }BTNode;
推广到树(通过转化)-------->树的孩子兄弟存储结构
每个结点只和它其中一个孩子有直接的联系。
每个结点往左找一个结点是它的孩子,孩子结点往右找是这个结点的其余孩子,也就是孩子结点的兄弟。
这样树的形状就是一棵二叉树了,但它表示的逻辑关系和二叉树不相同。
转化可以让本来为二叉树设计的算法也可以用于树了。
typedef struct BTnode{ int data; struct BTnode* child; struct BTnode* sibling; }BTnode;
4、树与二叉树的互相转换
树转换成二叉树
连接兄弟结点;指向孩子结点的各个分支中只保留最左侧的分支。
二叉树转换成树
对于某个结点A,先往左走一步找到其孩子结点,再往右一直找它的其他孩子,把A和它们都连接起来;删除孩子结点之间的关系。
森林转换成二叉树
森林:多个树放在一起。
森林转换成一棵二叉树:首先各个树自己先转换成一棵二叉树。
最后一棵树只是样式和二叉树一样,但和我们转化后的二叉树的含义不同,所以还是需要转化。
每棵树的根节点的右分支是空的,我们可以利用它将其连接起来就能得到一棵二叉树。
二叉树转换成森林
首先将根节点的右分支删掉,直至根节点的右分支为空。后同二叉树转换成树。
5、二叉树的遍历
层次遍历(广度优先遍历)
从上至下,从左至右
深度优先遍历(先/前序、中序、后序)
从根结点开始,沿着左边的分支一直走到底,然后回来,绕另一个分支一直走到底,然后又回来,一直重复之前的步骤。
我们可以发现每个结点都有三个指向它的箭头,也就是每个结点在遍历过程中会经过三次,选择不同的访问时机会有不同的遍历结果。
先/前序遍历:第一次来到某个结点进行访问(根左右)
中序遍历:第二次来到某个结点进行访问(左根右)
后序遍历:第三次来到某个结点进行访问(左右根)
6、树的遍历
层次遍历(广度优先遍历)
从上至下,从左至右。(和二叉树相同)
深度优先遍历
对于一棵树,指向每个结点的箭头数就不一定是三个了。(树是不分左右结点的!结点的空分支就是一个)(A1有4个箭头,叶子结点A6有2个箭头,最少就是2个箭头)
我们可以第一次来到某个结点进行访问,也可以最后一次来到某个结点进行访问。但对于中序遍历,A1我们是选择第二次还是第三次进行访问呢,A6我们若选择第一次或者第二次访问就会和先序或者后序遍历产生歧义。
所以对于一般的树,只有先序遍历和后序遍历,没有中序遍历。
先序遍历这时也可看为:先访问根节点,然后先序遍历其所有子树。
后序遍历这时也可看为:先后序遍历其所有子树,最后访问根节点。
先序:A1、A2、A5、A6、A3、A4
后序:A5、A6、A2、A3、A4、A1
如果一棵树被转换成了二叉树,那么对这颗树进行先序遍历等同于对二叉树进行先序遍历;对这棵树进行后序遍历等同于对二叉树进行中序遍历。
森林的先序遍历就是先序遍历森林中的每一棵树。(等同于转化后二叉树的先序遍历)
森林的后序遍历就是后序遍历森林中的每一棵树。(等同于转化后二叉树的中序遍历)
7、二叉树深度优先遍历的递归代码
区别就是在于访问结点p放在哪里
void xian(BTNode* p){
if(p!=NULL){
visit(p);
xian(p->lchild);
xian(p->rchild);
}
}
void zhong(BTNode* p){
if(p!=NULL){
zhong(p->lchild);
visit(p);
zhong(p->rchild);
}
}
void hou(BTNode* p){
if(p!=NULL){
hou(p->lchild);
hou(p->rchild);
visit(p);
}
}
8、二叉树深度优先遍历的非递归代码
递归其实本质上是有一个系统栈在进行操作,系统栈去完成保护现场和恢复现场的操作。
所以我们非递归代码就相当于要自己写一个栈去完成这些操作。
先序遍历非递归化 (根左右)
我们首先创建一个栈,先让根节点入栈,然后栈中出栈一个元素(也就是根节点),出栈也就是访问这个元素,然后让这个元素的左右结点入栈,右结点先进,左结点后进,因为栈后进先出。然后再出栈一个元素,以此类推。
(1入栈;1出栈,3、2入栈;2出栈,5、4入;4出栈;5出栈;3出栈,7、6入栈;6出栈,8入栈;8出栈;7出栈)
先序:1 2 4 5 3 6 8 7
代码:
void preorderNonrecursion(BTNode* bt){ if(bt!=NULL){ stack<BTNode*>s;//栈存储结点的地址,不需要把结点的所有信息都存储进去 s.push(bt); while(!s.empty()){ BTNode* p=s.top(); s.pop(); visit(p); if(p->rchild!=NULL){ s.push(p->rchild); } if(p->lchild!=NULL){ s.push(p->lchild); } } } }
后序遍历非递归化
先序遍历:根左右 1 2 4 5 3 6 8 7
后序遍历:左右根 4 5 2 8 6 7 3 1
逆后序遍历:根右左 1 3 7 6 8 2 5 4
所以先序遍历与逆后序遍历区别仅仅在于先遍历左子树还是先遍历右子树。我们把逆后序遍历的结果逆过来就是后序遍历的结果了,所以这时元素出栈后并不立即访问,而是把它压入另一个栈中,这样就相当于把结果逆过来了。
代码如下:
void postorderNonrecursion(BTNode* bt){ if(bt!=NULL){ stack<BTNode*>s1,s2; s1.push(bt); while(!s1.empty()){ BTNode* p=s1.top(); s1.pop(); s2.push(p); if(p->lchild!=NULL){ s1.push(p->lchild); } if(p->rchild!=NULL){ s1.push(p->rchild); } } while(!s2.empty()){ visit(s2.top()); s2.pop(); } } }
中序遍历非递归化
从根节点开始,一直往左走,让所有途径结点入栈,直至不能继续往左走为止;出栈一个结点并访问,看这个结点有没有右孩子,没有的话继续出栈元素,有的话让其右孩子入栈,然后从右孩子开始,一直往左走,途径结点均入栈,以此类推。
(1,2,4入栈,4出栈;2出栈;5入栈,5出栈;1出栈;3,6,8入栈,8出栈;6出栈;3出栈;7入栈,7出栈)
4 2 5 1 8 6 3 7
代码如下:
void inorderNonrecursion(BTNode* bt){ if(bt!=NULL){ stack<BTNode*>s; BTNode* p=bt; //加p!=NULL是为了防止if里面出栈一个元素导致栈空了,但此时p不为空,说明还有元素可以入栈 while(!s.empty()||p!=NULL){ while(p!=NULL){ s.push(p); p=p->lchild; } if(!s.empty()){ p=s.top(); s.pop(); visit(p); p=p->rchild; } } } }
9、二叉树层次遍历代码
这里用到的是队列。
首先创建一个队列,根节点先入队,然后队列中出队一个元素(根节点)访问,让其左右孩子入队(左孩子先入队) ;出队一个元素访问之,左右孩子入队,以此类推。
(1入队;1出队,2、3入队;2出队,4、5入队;3出队,6、7入队;4出队;5出队;6出队,8入队;7出队;8出队)
1 2 3 4 5 6 7 8
代码如下:
void level(BTNode* bt){ if(bt!=NULL){ deque<BTNode*>q; q.push_back(bt); while(!q.empty()){ BTNode* p=q.front(); q.pop_front(); visit(p); if(p->lchild!=NULL){ q.push_back(p->lchild); } if(p->rchild!=NULL){ q.push_back(p->rchild); } } } }
10、树的遍历代码
树的深度优先遍历代码
树的先序遍历代码
对于二叉树,遍历要访问它的左子树和右子树;以此类推,树也要访问它的所有子树。
通过树的孩子存储结构知道每个结点的孩子,即子树。数组存储所有的结点,每个结点后跟着的链表记录它的孩子。
代码如下:
void preorder(TNode* p,TNode tree[]){ if(p!=NULL){ visit(p); Branch* q=p->first; while(q!=NULL){ preorder(&tree[q->index],tree); q=q->next; } } }
树的后序遍历代码
void postorder(TNode* p,TNode tree[]){ if(p!=NULL){ Branch* q=p->first; while(q!=NULL){ postorder(&tree[q->index],tree); q=q->next; } visit(p); } }
树的广度优先(层次)遍历代码
二叉树是每次把左右结点入队,那么树就是把所有结点入队。
代码如下:
void level(TNode* tn,TNode tree[]){ if(tn!=NULL){ deque<TNode*>q; q.push_back(tn); while(!q.empty()){ TNode* p=q.front(); q.pop_front(); visit(p); Branch* b=p->first; while(b!=NULL){ q.push_back(&tree[b->index]); b=b->next; } } } }
11、线索二叉树
线索二叉树是对二叉树深度优先遍历的进一步改进,即对每个结点直接引出指向它的后继结点和前驱结点(指的是遍历序列中的前驱后继)的路径,这样就不需要再绕回它的父结点。
用叶子结点的空指针指向其前驱与后继的过程叫做把二叉树线索化。
上面说的空指针就是空分支。
如果一个结点有左空指针,则左空指针指向其前驱;有右空指针,则指向其后继。
因为要找到遍历序列中的前驱后继,所以一定先要对树进行遍历。
中序线索二叉树
A4是A2的前驱,A2是A4的后继,A4有空指针,所以A4的右空指针指向A2。
A2是A5的前驱,A5是A2的后继,A5有空指针,所以A5的左空指针指向A2。
A5是A1的前驱,A1是A5的后继,A5有空指针,所以A5的右空指针指向A1。
A1是A6的前驱,A6是A1的后继,A6有空指针,所以A6的左空指针指向A1。
A6是A3的前驱,A3是A6的后继,A6有空指针,所以A6的右空指针指向A3。
最终还有两个空指针剩余,一个是A4的左空指针,一个是A3的右空指针。也就是A4没有前驱,A3没有后继。
我们原来的二叉树结构除了数值,只有指向左右孩子的指针,如果还用这个结构的话,那么我们无法分清左右指针指向的到底是左右孩子还是前驱与后继。所以我们需要添加标识符去区分它指向的是左右孩子还是前驱与后继。
typedef struct TBTNode{ int data; bool ltag;//0表示左指针指向左孩子,1表示左指针指向前驱 bool rtag;//0表示右指针指向右孩子,1表示右指针指向后继 struct TBTNode* l; struct TBTNode* r; }TBTNode;
每当访问一个结点的时候,如果结点的左指针为空,就把左指针规定为线索,指向其前驱结点;如果前驱结点的右指针为空,就把右指针规定为线索,指向当前访问的结点。
void inThread(TBTNode* p,TBTNode* pre){ if(p!=NULL){ inThread(p->l,pre); //把原来的访问结点改成线索化二叉树 if(p->l==NULL){ p->l=pre; p->ltag=1; } if(pre!=NULL&&pre->r==NULL){ pre->r=p; pre->rtag=1; } pre=p; inThread(p->r,pre); } }
找遍历序列的第一个结点:从根节点开始往左走,一直走到不能走为止。
找遍历序列的最后一个结点:从根节点开始往右走,一直走到不能走为止。
如何找一个结点的后继结点:如果一个结点有后继结点,并且其右指针为线索,那么右指针所指的结点直接就是它的后继结点;如果右指针不是线索,那么从这个结点往右走一步,然后一直往左走,走到最左端的那个结点就是它的后继结点。(如A1后继是A6)
如何找一个结点的前驱结点:如果一个结点有前驱结点,并且其左指针为线索,那么左指针所指的结点直接就是它的前驱结点;如果左指针不是线索,那么从这个结点往左走一步,然后一直往右走,走到最右端的那个结点就是它的前驱结点。(如A1前驱是A5)
(比较好,在三种里面最简单找到前驱后继)
前序线索二叉树
void preThread(TBTNode* p,TBTNode* pre){ if(p!=nullptr){ if(p->l==nullptr){ p->l=pre; p->ltag=1; } if(pre!=nullptr&&pre->r==nullptr){ pre->r=p; pre->rtag=1; } pre=p; //递归前要先确保它不是线索,因为我们在前面进行了线索化,可能会导致指针指向指向其前驱后继, //假设此时p的左指针为线索,那么如果我们直接进行递归,p->l此时指向的是其前驱,也就是我们的p又去递归之前经历过的结点,会导致它死循环 if(p->ltag==0){ preThread(p->l,pre); } if(p->rtag==0){ preThread(p->r,pre); } } }
找遍历序列的第一个结点:根节点。(根左右)
找一个结点的后继结点:如果一个结点的左指针不空,并且左指针不是线索,那么左指针指向其后继(是线索就指向其前驱了);如果一个结点的左指针空,右指针不空,那么右指针指向的结点是其后继结点(不论是否是线索)。
线索二叉树前序遍历的代码:
void preOrder(TBTNode* tbt){ if(tbt!=nullptr){ TBTNode* p=tbt; while(p!=nullptr){ //p的左指针不是线索 while(p->ltag==0){ visit(p); p=p->l; } visit(p); p=p->r; } } }
后序线索二叉树
void postThread(TBTNode* p,TBTNode* pre){ if(p!=nullptr){ postThread(p->l,pre); postThread(p->r,pre); if(p->l==nullptr){ p->l=pre; p->ltag=1; } if(pre!=nullptr&&pre->r==nullptr){ pre->r=p; pre->rtag=1; } pre=p; } }
如何找一个结点的后继?(左右根)
1.若结点是二叉树的根,则其后继为空。
2.若结点是双亲的右孩子;或者结点是双亲的左孩子并且双亲没有右子树,那么结点的后继是其双亲结点。
3.若结点是双亲的左孩子并且双亲有右子树,那么其后继是双亲的右子树按后序遍历列出的第一个结点。(如A2的后继是A6)
12、哈夫曼树
编码问题
电脑中存储字符存的是它的编码,如下图我们就用三位数的编码来表示它们。给出一个字符串就一个一个字符对照写出它对应的编码二进制串。解码是就每三个编码对应解出一个字符。
但这样写出的编码串有点太长了,如何在编码串是正确的情况下让它变短一些?
A B C D E
5 2 3 1 2
我们发现每个字符出现的次数是不一样的,所以我们可以考虑让出现次数多的字符的编码短一些,出现次数少的字符的编码长一些。
具体实现:
对每一个字符建立一个结点,并在结点的下方标出该字符出现的次数,然后我们每次从中挑出两个出现次数最少的结点,把他们从集合中删除,用这两个结点构造一个新结点,新结点出现的次数是这两个结点出现次数之和,最后把这个新结点放入集合当中。以此类推。
最后就会构造出一棵二叉树,然后我们给每个左分支标记上0,每个右分支标记上1。
叶子结点就是字符,所以从根节点走到叶子结点所经过的分支上的编码组合起来就是叶子结点对应的字符的编码串。
这种做法一定满足出现次数多的字符的编码短一些,出现次数少的字符的编码长一些,因为出现次数多的字符对应的叶子结点离根节点近一些,走的路径短一些。
如何解码:此时每个字符对应的编码长度不一定相同,我们借助上面那棵树实现:从左往右扫描编码串,每扫描一个编码,就把它当做路径指示标。从根节点开始走,直到走到叶子结点,就解码出了一个字符,然后又重新从根节点开始。
按照上面的做法不用担心会出现下面的情况,因为这种情况的出现是因为一个字符的编码是另一个字符的编码的前缀,而我们上面构造的那棵树不可能会出现一条路径上出现两个叶子结点这种情况。
这棵树叫做哈夫曼树;字符对应的编码叫做前缀码;前缀码组成的字符串叫做哈夫曼编码。
哈夫曼树中的一些定义
路径:从树中一个结点到另一个结点的分支所构成的路线。
路径长度:路径上的分支数目。
树的路径长度:从根到每个结点的路径长度之和。
带权路径长度:结点具有权值,从该结点到根之间的路径长度乘以结点的权值。
(如E的带权路径长度=4*2=8)
树的带权路径长度(WPL):树中所有叶子结点的带权路径长度之和。
(5*1+3*2+2*3+2*4+1*4=29)
哈夫曼二叉树的特点
权值越大的结点,离根节点越近。
树中没有度为1的结点,这类树又叫做正则(严格)二叉树。
树的带权路径长度最短。
哈夫曼n叉树
把原来的选两个结点改成选n个结点。
如下是一棵哈夫曼三叉树:
对于哈夫曼二叉树,只要给出的结点个数大于等于2个,那么我们就可以构造出来;但是哈夫曼三叉树n叉树就不一定可以构造出来了。
如
此时只有两个结点了,所以我们给它添上一个权值为0的结点。
哈夫曼树的关键是树的带权路径长度最短,增加一个权值为0的结点不会影响哈夫曼树的WPL。
13、根据遍历序列确定二叉树
给我们一个遍历序列,显然不可以唯一确定一棵二叉树。
已知先序遍历序列和中序遍历序列
可以唯一确定一棵二叉树吗? ---->可以
先序:ABDECFGH 根左右
中序:DBEACGFH 左根右
先序遍历序列的第一个一定是二叉树的根节点。
确定了根节点之后判断左右子树分别有哪些结点(根据中序),然后就可以进一步找到左右子树的根节点又是谁(先序),以此类推。
首先A是根节点,那么BDE是左子树的结点,CFGH是右子树的结点。 A
先序:ABDECFGH 根左右
中序:DBEACGFH 左根右
然后B是左子树的根节点,C是右子树的根节点。 A
先序:ABDECFGH 根左右 B C
中序:DBEACGFH 左根右
对于B是根节点的左子树,D是左子树的结点,E是左子树的右节点。 B
对于C是根节点的右子树,GFH是右子树的结点。 D E
F是右子树的根。 C
F
先序:ABDECFGH 根左右
中序:DBEACGFH 左根右 F
G是左子树的结点,H是右子树的结点。 G H
代码
左子树
中序:左子树的序列的长度为i-1-L2+1=i-L2
前序:L1+i-L2
右子树
中序:右子树的序列的长度R2-i
前序:L1+i-L2+1
//返回根节点的指针来代表我们得到的这棵二叉树 //pre先序序列 int后序序列 我们需要不停划分这两个数组,当前先序序列操作的元素的下标l1到r1,后序序列操作的元素的下标l2到r2 //l1l2一开始值为数组第一个元素的下标,r1r2一开始值为数组最后一个元素的下标 BTNode* CreatBT(char pre[],char in[],int l1,int r1,int l2,int r2){ //递归出口 //表示当前子树为空,那么根节点就也为空 if(l1>r1){ return nullptr; } int i; //根节点 BTNode* p; p->data=pre[l1]; p->rchild=p->lchild=nullptr; //寻找根节点在中序序列的位置,划分左右子树结点 for(i=l2,i<=r2;i++){ if(pre[l1]==in[i]){ break; } } //注意是l1+1 p->lchild=CreatBT(pre,in,l1+1,l1+i-l2,l2,i-1); p->rchild= CreatBT(pre,in,l1+i-l2+1,r1,i+1,r2); return p; }
已知后序遍历序列和中序遍历序列
可以唯一确定一棵二叉树吗?----->可以
后序:DEBGHFCA 左右根
中序:DBEACGFH 左根右
后序遍历序列的最后一个一定是二叉树的根节点。
确定了根节点之后判断左右子树分别有哪些结点(根据中序),然后就可以进一步找到左右子树的根节点又是谁(后序),以此类推。
首先二叉树的根节点是A
左子树的结点有DBE,右子树的结点有CGFH A
后序:DEBGHFCA 左右根
中序:DBEACGFH 左根右
左子树的根结点是B,右子树的根结点是C B
后序:DEBGHFCA 左右根 D E
中序:DBEACGFH 左根右
B:左子树的结点有D,右子树的结点有E C
C:右子树的结点有GFH,左子树为空 F
右子树的根节点是F
后序:DEBGHFCA 左右根
中序:DBEACGFH 左根右 F
F的左子树的结点有G,右子树结点有H G H
左子树:
右子树
BTNode* CreatBT(char post[],char in[],int l1,int r1,int l2,int r2){ if(l1>r1){ return nullptr; } BTNode* p; p->data=post[r1]; p->rchild=p->lchild=nullptr; int i; for(i=l2;i<=r2;i++){ if(post[r1]==in[i]){ break; } } p->lchild=CreatBT(post,in,l1,l1+i-l2-1,l2,i-1); p->rchild=CreatBT(post,in,l1+i-l2,r1-1,i+1,r2); return p; }
已知层次遍历序列和中序遍历序列
可以唯一确定一棵二叉树吗?----->可以
层次:ABCDEFGH 从上至下,从左至右
中序:DBEACGFH 左根右
层次遍历序列划分出来的子序列是不连续的。
二叉树的根节点是A
A的左子树有DBE,右子树有CGFH
层次:ABCDEFGH 从上至下,从左至右
中序:DBEACGFH 左根右 A
A的左子树的根节点是B,右子树的根节点是C B C
层次:ABCDEFGH 从上至下,从左至右
中序:DBEACGFH 左根右 B
B的左子树D,右子树E D E
C右子树GFH,根节点F C
F左子树G,右子树H F
层次:ABCDEFGH 从上至下,从左至右 G H
中序:DBEACGFH 左根右
由于层次遍历划分出来的子序列是不连续的,所以我们分别挑出左右子树的结点,将其放入两个数组中,要保持左右子树的结点的相对顺序与层次序列中的相同,这样他们就连续了,然后我们再从中选出各自的根节点,接下来递归处理。
所以这时我们的函数的参数就不需要l1,r1了,因为l1、r1是为了控制子序列,现在子序列都不连续了,我们也没办法把它划出来。
//待查找数组,查找的目标,查找范围 int search(char arr[],char key,int l,int r){ int j; for(j=l;j<=r;j++){ if(arr[j]==key){ return j; } } return -1; } void getSubLevel(char subLevel[],char level[],char in[],int n,int l,int r){ int k=0,i; for(i=0;i<n;i++){ if(search(in,level[i],l,r)!=-1){ subLevel[k++]=level[i]; } } } BTNode* CreateBT(char level[],char in[],int n,int l,int r){ if(l>r){ return nullptr; } BTNode* p; p->data=level[0]; p->lchild=p->rchild=nullptr; //在中序序列中找根节点 int i=search(in,level[0],l,r); //把左右子树的孩子挑出来放在两个数组当中 int LN=i-l,RN=r-i; char LLevel[LN],RLevel[RN]; //i左侧是左子树的节点 //分离出连续的子序列 getSubLevel(LLevel,level,in,n,l,i-1); getSubLevel(RLevel,level,in,n,i+1,r); p->lchild= CreateBT(LLevel,in,LN,l,i-1); p->rchild= CreateBT(RLevel,in,RN,i+1,r); return p; }
已知先序遍历序列和后序遍历序列
不能确定
因为根据先序后序我们只能确定根节点,而不能确定出左右子树。
14、根据遍历序列估计二叉树 (不讨论空树)
前序遍历和后序遍历结果相同的二叉树为?
所以把左右子树的序列全删掉,只留下根节点。(T)
前序遍历和中序遍历结果相同的二叉树为?
只需要删掉所有左子树的结点即可。(TR )
结论
要注意第四条:TLR和LRT,删除L可以,删除R也可以,所以没有左子树或者右子树都行。
15、根据表达式建立二叉树
手工建树
3+4*5*(2+3)
我们既要存储操作数,又要存储运算符,还要存储他们的运算次序。
我们首先通过加括号去明确他们的运算次序。
(3+((4*5)*(2+3)))
然后用所有的操作数创造一组叶子结点。
接着以每一层括号里的运算数和操作符建立一棵子二叉树。
利用栈建树
这与利用栈求表达式的结果很相似,弹出两个操作数计算后重新压入栈中,区别在于我们这里并不计算,而是建立一棵二叉树,并把二叉树的根结点压入栈中。
我们需要建立两个栈,一个操作数栈,一个运算符栈。操作数直接入操作数栈,运算符的优先级如果大于运算符栈顶元素的优先级,或者是‘( ’,或者运算符栈为空,或者栈顶元素为左括号就直接入栈;如果运算符的优先级小于等于运算符栈顶元素的优先级,栈顶元素出栈作为根结点,操作数栈出栈两个元素分别为右结点和左结点,然后根节点入操作数栈中;遇到‘ )’应该把运算符栈中的从栈顶元素到左括号之间的所有运算符依次出栈。到达表达式尾部就把运算符栈中的运算符逐个出栈。
3入操作数栈。
运算符栈为空,+直接入运算符栈。
4入操作数栈。
*的优先级大于+,*直接入运算符栈。
5入操作数栈。
*的优先级与栈顶元素*的优先级相同,所以栈顶元素*出栈,操作数栈出栈两个操作数5和4,构建一棵子二叉树,5为右结点,4位左结点,*为根节点,根节点*入操作数栈中。此时*的优先级大于+,*入运算符栈中。
‘( ’直接入运算符栈中。
2入操作数栈。
+入运算符栈,因为栈顶为左括号。
3入操作数栈。
遇到‘ )’应该把运算符栈中的从栈顶元素到左括号之间的所有运算符依次出栈。
所以+以根节点出栈,3为右孩子出栈,2为左孩子出栈,+入操作数栈。
到达表达式尾部了。让*作为根节点出栈,+为右结点出栈,*为左节点出栈(不是单独建一棵二叉树,而是在原来+、*根节点的基础上建立),*入操作数栈。
最后+根节点出栈,3左结点,*右结点。
我们发现
所以可以利用这个方法去把中缀表达式转为前缀表达式和后缀表达式。
利用树求表达式的值的代码
void calSub(float opand1,char op,float opand2,float& result){ if(op=='+'){ result=opand1+opand2; } else if(op=='-'){ result=opand1-opand2; } else if(op=='*'){ result=opand1*opand2; } else{ result=opand1/opand2; } } float cal(BTNode* root){ //如果是叶子结点说明是操作数 if(root->lchild==nullptr&&root->rchild==nullptr){ return root->data-'0'; } //如果不是叶子结点,那么一定是存储了运算符的结点,递归去求其左子树的表达式的值和右子树的表达式的值 float opand1=cal(root->lchild); float opand2=cal(root->rchild); float result; //再以刚刚求得的左子树的表达式的值和右子树的表达式的值,和根节点的运算符求得结果 //左右根是后序遍历 calSub(opand1,root->data,opand2,result); return result; }