提示:寒塘渡鹤影,冷月葬花魂
文章目录
前言
本文是对前面所写树的相关文章进行梳理 ,并且是基于我自己写的文章进行的梳理 所以知识点可能与有些书上并不相同,毕竟已经四五个月了,不感兴趣的建议直接跳过
树的遍历
先序非递归版本一
非递归版本 需要借助栈这种结构,而所以也就需要先放右子树再放左子树,并且在进去while之前最好将跟结点放入进去 既然要放入栈中 那么这第一个版本就要求进栈之前需要进行一波非空判断接下来进入while中 进入while也就意味着循环开始 所以也就是为什么在进入while之前需要先将根结点放入进去 在while中获取第一个栈顶元素S.top() 访问这个栈顶元素 然后弹出S.pop(),然后将此栈顶元素的右孩子放入栈中 同样的你也需要判空操作
中序非递归
这里前面while之前都是一样的 先将根结点放入栈中 然后进入while 首先考虑一下入栈顺序 自然是先处理左子树 然后处理根结点 最后处理右子树 我们如何避免根结点的左右子树是第一次进栈还是第二次进栈呢?为了结局这个问题 我们就需要一个标记,若是已经将此结点的左右孩子放入栈中过了 那么我们就再在其后放入一个NULL 当我们遇到NULL的时候 就知道下一个结点是可以直接访问的了 所以这里if 分成两种情况判断当前取取出的是否是NULL 若是直接弹出 然后再区栈顶 然后直接访问 访问之后继续弹出 若是取出的不为空,则先取出此结点 然后将它的右孩子放入 然后放入它 然后放入NULL 然后再放入左孩子 进入下一次while 当然这里依然是每一次放入之前都是需要判空操作的 因为为了避免和标记搞浑
后序非递归
同样的道理因为我们处理的方式是先左子树 然后右结点 然后根结点,所以进栈也就是先根结点 然后右子树 然后左子树 同样的道理 为了使得它的两个子树进入也就需要处理它 但是处理它却又不知道是不是第一次处理 也就需要标记NULL 来区分是第一次还是第二次处理 进入while之后先取出一个结点 判断此结点是否是NULL 若是NULL 则弹出 然后取栈顶访问 然后弹出 若不是NULL 则添加NULL 然后将它的右子树放入 然后将它的左子树放入 同样的放入之前需要判空
前序非递归版本二
这样为了与前两个统一 同样的也是放一个标记 一样的思路 处理方式是根左右 所以放入的应该就是右左根 进入while之前都是一样的 先对根结点判空 然后将根结点放入 可以开始while了 取出此时栈顶 若不为空 将右子树放入 然后将左子树放入 再将根结点放入 然后再放入NULL 若为空 则先将空弹出 然后取栈顶 访问 然后再弹出
层序遍历
这里层序遍历需要使用队列,首先也是将非空根放入队列中 然后将此时队列中所有的结点的非空的左右孩子放入队列中 也就实现了层序遍历 但是这样的未免有些太粗糙了 我们使用一个size来记录每一层中的元素的个数 然后每一次都是遍历size个元素在这一层遍历之后使用Q.size()来计算此时队列的大小 然后再遍历size个数值,同样的道理放入之前需要进行判空操作 队列使用的是Q.front() 添加同样的是Q.push(***) 删除同样的是Q.pop();
创建树
初始化
自然就是一个空节点了 T=NULL;
前序创树
首先我们考虑一下我们输入的方式 我们输入值并且没有确定树中结点的个数 所以我们需要动态的申请结点 若是我们一开始并没有思路 那就想一下使用递归的方式 因为递归可以将处理的规模变小 我们需要从获取数据 但是这个数据可能是-1 这也就是标志的到达了空 此时就要返回了,若是不为-1 则创建一个结点 作为此时的根结点然后递归的创建此时的左子树右子树 但是同样的 你要考虑是否需要判空 这里认为不需要判空 深入之后若是为-1 自然返回给上一层的就是NULL若是不为-1 此时申请的位置自然也是给T的 并且传入的时候我们都是使用的引用类型 也就是中间修改 全部可见 你可以认为他传入的是T 同样的你也可以认为他传入的就是上一层的T->child
层序创建树
主要是使用两个队列 一个用于存放数值 一个用于存放结点 存放结点的就类似于层序遍历的情况 若是第一个就是-1 则直接返回 说明此时是一个空树 若是不是-1 则就像之前层序遍历一样将此时树中唯一一个结点放入栈中 然后就可以启动while了 首先弹出结点栈中的第一个结点 再弹出一个数值 若是此数值是-1 则弹出的结点的左孩子就是null 若是数值不是-1 则此时结点的左孩子就不是空 就新申请一个结点 并将其作为T的左孩子 并且将此非空左孩子也放入队列中 但是别忘了处理完一个值的时候需要将值弹出 一个结点处理完的时候 别忘了弹出一个结点栈
构建二叉树(已知前序中序 或者中序后序)
主要思想就是根据前序或者后序 到中序中去寻找根结点,将中序分为左子树 和右子树 再将分开过的带回到原来的 原来的前序或者后序就也被分成两个子树的前序或者后序再像上一步一样找这两个子树的根便可
代码部分
前中
通过上述,想必你也清楚了 其实就是找分 找再分的过程,首先需要两个容器将字母的值放入 假设这里前序中序来进行构建二叉树 首先我们取得前序的第一个字母 然后到中序中去遍历,并且需要知道他是中序中的第几个字母,以这个字母为分界线将中序序列分成两个部分,分别放入两个容器中 确定这两个容器的大小 然后再到前序中去除第一个之后根据两个容器的大小将前序分成两个部分 这个时候再递归的调用 分别构建左右子树 此时也就需要考虑一下递归的结束条件 若是此时容器中的数值的个数是零个的时候 自如也就是空树 也就返回NULL 这里同样的道理,这里使用使用的引用类型的
中后
同样的道理 这里取根结点是后序中的最后一个,根据这最后一个将中序分成两个部分,再根据数量 将后序序列分成两个部分 但是这里同样的需要把后序的最后一个刨除掉
二叉搜索树(二叉排序树)
创建BST
创建一个BST ,这里我们也考虑一下使用递归的方法,跟当前根结点进行比较 若是比根结点大则深入右子树 若是比根结点小深入左子树 但是深入之前需要判断此时的T是否是空 若是NULL 此时也就是函数递归的出口 此时创建一个结点 并将此结点的值赋值给T 便可
创建BST的方式二
其实根上述方法差不多 甚至我觉得甚至不如上一个简单,但是这个思想倒是有值得学习的地方,之前我们是没有返回值的 但是若是给他一个返回值 这个返回值最后的去向应该来到上一层 所以上一层也就是也就需要一个来接收它 并且则一层结束后同样需要将这一层的T 传递给上一层
非递归版(带头节点)
非递归版 也就是使用一个指针 比较这个指针所指向的值与当前val 的关系 并且要求此时指针最后指向的是根结点 若是比此时p所指向的结点值大就深入右子树中 若是比结点值小 就深入到左子树中 主要是这里需要注意一个点malloc 申请之后将它的子节点也赋值NULL 尽量面面俱到 注意处理空树的时候第一个结点便可
思考题
插入返回父节点,之前我们有多种方式插入 而且每一次插入都是需要找到父节点的 树中题目首相想一下递归的方法,之前我们写的递归插入的时候 返回的是这一层的因为那个是到达的空结点 返回的是这一层本身的 这里稍作修改也可以,并且我感觉也不需要双指针,看我操作,这里申请变量的时候需要赋值
#include<bits/stdc++.h>
typedef struct BST{
int data;
BST* Lnext;
BST* Rnext;
}BST;
using namespace std;
BST* insert(BST* &T,int val){
if(T==NULL){
T=(BST*)malloc(sizeof(BST));
T->data=val;
T->Lnext=NULL;
T->Rnext=NULL;
cout<<"此时就是根结点 并没有父结点"<<endl;;
return NULL;
}
if(T->data<val&&T->Lnext==NULL){
T->Lnext=(BST*)malloc(sizeof(BST));
T->Lnext->data=val;
T->Lnext->Lnext=NULL;
T->Lnext->Rnext=NULL;
cout<<"此时返回的父节点是"<<T->data<<endl;
return T;
}
else if(T->data>val&&T->Rnext==NULL){
T->Rnext=(BST*)malloc(sizeof(BST));
T->Rnext->data=val;
T->Rnext->Lnext=NULL;
T->Rnext->Rnext=NULL;
cout<<"此时返回的父节点是"<<T->data<<endl;
return T;
}
if(T->data<val){
return insert(T->Lnext,val);
}
else{
return insert(T->Rnext,val);
}
}
int main(){
cout<<"请输入你要创建的值"<<endl;
int input;vector<int> V;BST* T=NULL;
while(scanf("%d",&input)!=EOF) V.push_back(input);
for(int i=0;i<V.size();i++){
insert(T,V[i]);
}
return 0;
}
这里也需要大家有时间看一下其中任何一个双指针的方法 这里双指针再加上一个头节点 就像链表中操作是差不多的
查找
查找与插入是几乎一样的 这里就不写了
删除
待删除结点无非三种情况,没有孩子 有一个孩子 有两个孩子,其中没有孩子的直接删除便可 有一个孩子的直接用这个孩子来取代待删除的结点 若是有两个孩子 为了保持从上往下投影的有序性 这里需要找左子树的最右孩子 或者右子树的最左孩子 来进行值的替换 然后再删除替换之后的值 此时就将删除两个结点的转化成删除一个结点甚至没有结点的,这里同样使用递归的方式来写 但是要注意分情况的时候前面两种是不需要找pre的 但是最后一个删除两个结点需要找到替换结点的cur 并且你需要知道PreCur 与Cur的关系 是左子树还在右子树
线索二叉树
实不相瞒 这个代码是有一定难度的
今天回过头来看之前写的这一篇文章 真的是写的狗都不看,居然是我写出来的 今天进行一波完善 首先之前代码有几个地方写的不好 甚至可以说根本就没有写清楚,比如其中递归深入的时候 为什么T向左深入了 但是Pre依然是没有像深入 这里递归里面如何确定它们就一定是前驱后继的关系最后为什么又要加上一句Pre等于T? 首先先来考虑一下如何确定一个结点某种遍历方式下的前驱或者后继,目前以我不充裕的知识来看 只有通过遍历来实现 遍历的过程中 T是一直按照它的遍历方式有序的移动的,所以这里我们将其设置为全局遍历 那么在初始化的时候赋值为NULL 之后T开始按照中序移动 我的Pre自然也能按照中序来移动 并且可以保证一定一直是它的前驱,王道上那样写也没有错 不过是全局变量的另外一种表达方式
注意转圈问题
在先序线索化二叉树时,要注意条件的判断,由于先序遍历的顺序为根->左->右,因此对于最先进行的是根结点线索化,让其左指针指向前驱,这时继续访问左孩子将会出现"转圈"现象,处理方法是判断当前处理结点是否被线索化,若已被线索化,则跳过。同样的,如果左子树没有问题,右子树是最后被访问的,在访问完最后一层的左子树后该递归访问每一层的右子树,但后一层结点本来为空的右孩子指针在前面的访问中已经指向了其后继结点,也会出现"转圈"现象,处理方法同左子树。
关于先序前驱
上述代码只实现了找先序后继的操作,那为什么没实现找先序前驱的操作呢?不妨我们先来探讨为什么中序遍历中既可以找到中序前驱,又可以找到中序后继呢?
在线索二叉树中,每个结点有指向其左孩子和右孩子的指针。根据中序遍历的顺序左->根->右,对于指定结点来说,它的左子树一定是在它之前访问的,因此可以找到其前驱。同样的,它的右子树一定是在它之后访问的,也可以找到其后继。
对于先序线索二叉树来说,根据先序遍历的顺序根->左->右,对每个结点来说,他自己都是最先被访问的,然后再访问其左右孩子,即其左右孩子永远都只可能是他的后继,所以普通方法是找不到先序的前驱的。
可行的解决办法有两种:一种是从头遍历找到指定结点的前驱,效率较为低下;另一种可以将二叉链表改为三叉链表,给各个结点设置一个指向其父节点的指针,然后再根据不同的情况讨论其前驱,这里不再展开。
题目复习
层序遍历从下往上
之前我们学过层序遍历从上往下 并且是借助队列来实现的 并且实现了按照行来进行放入 这里其实可以只用一个二维结果集的形式,每一层遍历的同时都放入到一维结果集中 等一层遍历结束了 将其放入栈中(但是关键是栈和容器能不能嵌套使用?) 还有第二种方法就是依然是层序遍历 不过使用一个二维容器每一层放入一维容器中 然后反转最外层的那个容器
法一(这里需要其实就是层序遍历中加上一个数据添加到一维数组的语句,在一层遍历结束之后,将此时获取的结果放入二维数组中, 最后反转是需要reverse(result.begin(),result.end()))输出的时候需要两层for循环 一层用于遍历二维数组 一层用于遍历二维数组中的一维数组
法二使用容器与栈的结合
依然是一个一个层序遍历,不过这里是将层序遍历的结果放入栈中 若是放入栈,那么取出的方法就是每一层从中弹出一个 然后进行循环,取出此一维向量的
树的右视图
同样的道理 这里也是使用层序遍历将每一层的最后一个放入结果集中,怎么确定是一层的的最后一个呢?这里自然是加一个判断if(i==size-1) 自然就是这一层的最后一个
找出树行中最大值
依然是层序遍历,每一次层序遍历中 将最大值初始化为这一层的第一个max=Q.front()然后在for中使用Q.front来动态获取这一行中给各个元素
填充每个结点的下一个右侧结点指针
同样的是层序遍历,每一层的第一个元素我们是容易获取的 这一层for 中的第一个,这里每一层的最后一个是不需要在进行next赋值了
for(i=0;i<size;i++){
cur=Q.front()
Q.pop();
if(i<size-1){cur->next=Q.front}
if(cur->Lchild) Q.push();
if(cut->Rchild) Q.push();
}
这里输出也是有技巧的,因为是完全二叉树,我们只需要找到这一行的第一个结点便可 然后再其中嵌套一个while 遍历这一行的元素
填充一个普通的树
这里使用的方法可以和上一个一样 但是就是遍历的时候不一样了,这里就在遍历的过程中将第一个元素保存下来,然后再借用这个便可
树的最小深度
依然是层序遍历 不过在层序遍历的过程中判断此结点是否是叶子结点 若是叶子结点便是最小高度
二叉树的层平均值
依然是层序遍历 使用一个值来记录这一层的和便可sum+=cur->data;
翻转二叉树
这里很明显 层序是不行的,既然是二叉树 这里首先考虑递归的方式
fun(Tree* T){
if(T==NULL) return;
fun(T->lchild)//翻转左子树
Tree* cur=T->lchlid;
T->lchild=T->rchild;
T->rchild=cur;
fun(T->rchild)//翻转右子树
}
这让写是不对的 ,你深入左子树是在T交换之前 你深入右子树 是在T交换之后,这时就是导致你两次反转的都是左子树 所以这里你需要两次反转同样的一个T->rchild 就可以了
对称二叉树
这里使用两个指针同时遍历两个树 不过深入的时候注意 一个向左深入另外一个是向右深入的,那么问题变成了如何判断两颗树是不是同一个树,先使用一个函数将树分成两个部分,判断这两个子树是否是对称的,这里我使用的相当于是前序遍历,先分别判断两个的根,然后判断这个树的左子树 与另外一个树的右子树是否一样,这里不要忘了深入是需要注意是否是为空的
solution(Tree* T){
return fun(T->lchild,T->rchild);
}
fun(Tree* T1,T2){
if(T1->rchild!=T2->Lchid||T1->rchild!=T2->rchild){
cout<<“此时并不成立”;
return 0;
}
return fun(T1->rchild,T2->lchild)&&fun(T1->lchid,T2->rchld);
}
非迭代
使用一个栈,同时放两个树,放入的方式是左子树的左孩子,右子树的右孩子,左子树的右孩子,右子树的左孩子,然后每一次都是弹出两个,比较这两个的关系
判断是否是平衡二叉树
这里依然是首先考虑迭代版本,先来考虑一下处理部分的书写,首先想到的自然是计算左子树的高 右子树的高 计算两个高度的差,借此来判断是否是平衡二叉树,这里最好单独写一个计算高度的操作函数,并且这里需要 后序遍历 因为只有处理好了左右才能知道当前的高度
fun(Tree* T){
if(T==NULL){return 0};
H1=fun(T->lchild);
H2=fun(T->lichild);
if(H1-H2<=1){
return H1>H2? H1:H2;//返回两个中较大的
}
else{
flag= =1//flag作为一个全局变量 来判断是否平衡
}
}
二叉树的所有路径
这里我们也考虑递归
fun(Tree* T,path,result){
if(T->lchild= =NULL&&T->rchild= =NULL){
//此时是叶子结点就将这一条路径放入结果集中
path+to_string(T->data);
result.push_back(path);
}
path+to_string(T->dara);这里是先序先添加当前结点之后再深入
if(T->lchild)fun(T->lchild,path,result);
path-to_string(T->data)回溯
if(T->rchilld)fun(T->rchild,path,result);
}
因为修改了path 自然也就需要回溯
若是不修改path 自然不需要回溯
这里参数不是引用类型的,并且中间不修改便可 不需要回溯
左叶子之和
既然要求是左叶子也就需要知道他和它父结点的关系 这里可以使用双指针
fun(T ,Pre){
if(T->lchild= =NULL&&T->rchild= =NULL){
if(T==pre->child){
sum+=T->data;
}
return ;
}
if(T->lchild)fun(T->lchild,T);
if(T->rchild)fun(T->rchild,T);
}
不使用双指针
上题中使用双指针是为了判断与父亲结点之间的关系,若是我们能能通过返回值来确定与父结点的关系 倒也可以不用双指针
fun(T){
if(T->lchild= =NULL&&T->rchild==NULL)
ruturn 0;//返回的零作为一种到达叶子结点的标志
if(T->lchild&&!fnu(T->lchild)){
sum+=T->data;
};
if(T->rchild)fun(T->rchild)
}
非递归
使用层序遍历 通过判断每一个结点是否右左孩子 并且左孩子且是叶子结点 若是同时满足累加便可
哈夫曼树
首先需要知道怎么计算手动写出哈夫曼树的
给你一组数据 每一次从中选取两个然后构成一个树,将这个新形成的树再放入序列中,再从中选取一个,而且默认小的坐在左边 ,现在作为一个读者再来看之前写的 不得不说 有些地方的思路是真的挺好的 但是许多地方也非常菜,就像这里 我当时居然是一遍写出的,怎么可能!!! 既然是一个填表的过程 那么这个结点中结构体的定义自然也就是这几个元素,然后需要申请一个数组 既然是n个叶子结点 并且是二叉树那么这里应该申请A[n*2-1]个空间,就像我们手动计算的一样 每次从中选取两个最小的填入表中,并且需要将选取的两个最小的的父亲标上序号,孩子标记为-1 将合成的父亲的左右孩子也标上序号,所以这也就导致我们每一次选取最小值的时候 都是需要判断选择出来的结点是否有父亲 若是有父亲则不能选!这样看来每一次选择最小值反而成为一件较麻烦的事情!
哈夫曼编码
哈夫曼编码就是从根结点到叶子结点,向左是零向右是1 这里二维数组中 前n个结点就是叶子结点,我们从叶子结点向上找 直到找到根结点才结束 最后反转一下序列便是此结点对应的字符串
哈夫曼解码
解码简单来说就是给定一个01字符串,然后到树中 若是0 就向左深入若是1 就向右深入 直到到达叶子结点 这会出现一个字母 这个时候再从根结点开始继续重新匹配,根结点其实就是最后一个结点 其实也不需要找
另(重要)
若是需要搜索整棵树,那么递归函数就不要返回值,若是只要其中一个符合条件的路径,递归函数就需要返回值 因为遇到符合条件的路径就要及时返回