数据结构之构建二叉树

提示:宽而栗,柔而立,愿而恭,乱而敬,扰而毅,直而温,简而廉,刚而塞,强而义
人要有九德:宽厚而庄重,温和而有主见,讲原则而谦逊有礼,聪明能干而敬业,善于变通而有毅力,正直而友善,直率而有节制,刚强而务实,勇敢而符合道义


做树相关的题 遍历不是主要 了解遍历中的递归也是同样重要 ,否则你会发现你做不做的对完全看运气 建议看完这一篇遍历 我在其中写了部分的递归再来看这个构建树

前言

依据上一篇文章 我们知道若是给你一个遍历序列 你要构建一个二叉树的话 你至少要做到两点 ,一是知道遍历顺序 二是输入的时候我们要补充的是-1 这样程序才知道我这个结点有没有孩子, 但是有时并不会给你-1 这种值 只是单纯的给你遍历序列 此时确定的树往往是不唯一的 比如上一节篇文章中 我们的前序是 1 2 4 5 3 由下面两个图的前序是一样的但是很明显 树不一样!!!
请添加图片描述
请添加图片描述
这篇文章计划 写三个部分 一个的分析 一个是手动写 一个是代码块但是你也知道我往往是武断而又独裁的 所以也不一定

一、什么序列组合可以确定二叉树?

这里先放出 前序与中序,中序于后序,可以确定一棵树,至于为什么 这就是接下来要写的
前面所学的值我们知道
前序遍历的结果是 【中】【左子树】【右子树】
中序遍历的结果是【左子树】【中】【右子树】
后序遍历的结构是【左子树】【右子树】【中】
你现在可能只知道遍历的写法 ,但是我们先就前序来考虑一下

各种序列的特点

1、给我们一个前序序列我们可以知道什么
因为根结点是第一访问的 所以我们可以知道 根节点一定是第一个
2、给我们一个后序我们可以知道什么
根结点一定是最后一个 因为后根 而树的根又是所有子树的根 所有必然是最后一个访问
3、给我们中序我们可以知道什么 ,实不相瞒,若是单纯给我们一个中序,我们什么也看不出来 但是它却依然是有特点的 这个要结合树来看 就比如我们的图 从上而下来投影就是它的中序遍历顺序 不要怀疑是不是我的树 所有的树都是这样的 从上而下的投影就是中序遍历的序列
请添加图片描述

确定树的方式说明

-前序,后序只能知道根 中序可以知道整课树的投影 若是我们将

  • 前序和中序结合,
    前序告诉我们根 通过这个根的值来到中序序列中来找 是不是就可以把左右子树分裂开(因为是通过值来分裂 所有不能有重复的值) 就可以找到两个树 我们再到前序中来找这两个子树各自的根 这样就可以确定一棵树
  • 中序与后序结合
    与前序 中序同样的道理,通过后序来确定最大这课树的根 然后再回到中序中分左右,然后再到后序中找根 但是与前序不同的是根在后序序列后面 求求你不要问我前序是不是在前序的前面 要不然什么叫做’‘但是不同’‘?
  • 前序与后序
    前序与后序不能!前序与后序不能!前序与后序不能 因为只能找到根 无法分割序列 也就是无法找左右子树

二、手动写

(这里就不献丑了 用word来画)

2.1、通过前中序列来画出树

请添加图片描述

2.2、通过中序后序来画树

请添加图片描述

两个的区别就是找根的时候不一样 前序是此时分出最前面一个是根
后序是此时分出的最后面一个是根 分左右子树是没有区别的
其实我建议你画一遍,画完之后你就知道了 其实其实我们画图的时候就是这样一直分左右子树的过程

三、代码解析

详解写在了代码中 有时间建议自己动手打一遍,眼说 你会了 手说你不会 我可以打出来,聪明如你肯定没有问题

3.1、由前序中序来构建二叉树

第一个for中到i-1 第二个for中从i+1开始 的
这样在将中序划分为两个的时候就成功的去掉了 中间的根
第三个for从1 开始因为前序需要舍弃根 也就是前序遍历的第一个元素就是根

	void CreatPreTree(BiNTree* &T,vector<int> pre,vector<int> in){//先写前序与中序构造树 
		int i=0;
		if(pre.empty()){//传入进来的两个容器中的指的个数一定是一样的 
			T=NULL;
			return; 
		} 
		int root=pre[0];
		T=(BiNTree*)malloc(sizeof(BiNTree));
		T->data=root;
		while(root!=in[i]){
			++i;//找到i根在中序队列中的位置
		}
		vector<int> in1;vector<int> in2;
		//给中序序列分成两个部分 构建左子树与右子树   但是怎么样分比较清晰呢? 这里我认为把i 理解为有几个元素其实可能更好
		for(int j=0;j<i;j++) in1.push_back(in[j]); 
		for(int j=i+1;j<in.size();j++) in2.push_back(in[j]);
		//i第一个for中到i-1 第二个中从i+1开始 的
		//这样在将中序划分为两个的时候就成功的去掉了  中间的根 
		//有同样的将前序序列也分成两个部分  同样也是为了构建左子树与右子树 
		vector<int> pre1;vector<int> pre2;
		for(int j=1;j<=i;j++) pre1.push_back(pre[j]);
		for(int j=i+1;j<pre.size();j++) pre2.push_back(pre[j]); 
		//for从1 开始因为前序需要舍弃根 也就是前序遍历的第一个元素
		CreatPreTree(T->Lchild,pre1,in1); //填入它同样需要此时分裂出来的左子树的前序和中序 
		CreatPreTree(T->Rchild,pre2,in2);
		//这种题目的关键也即是区间的分割 
}
}

效果截图

请添加图片描述

3.2由中序后序构建树

与上面同样的道理 主要难度是数组的切分 注意从后面取根就可以了
注意两个for中我们 j的取值第一个到i-1 第二个for是从i+1开始 也就是舍弃了一个i
第四个for 中post容器中的最后一个元素 也就是根元素

void CreatPostTree(BiNTree* &T,vector<int> post,vector<int> in){//通过中序后序来构建一个树 
		int i=0;
		if(post.empty()){//传入进来的两个容器中的指的个数一定是一样的 
			T=NULL;
			return; 
		} 
		int root=post.back(); 
		T=(BiNTree*)malloc(sizeof(BiNTree));
		T->data=root; 
		while(root!=in[i]) ++i;//也是寻找根在中序中的坐标 
		//开始分中序为左右子树  这个跟上面一样 
		vector<int> in1; vector<int> in2;
		for(int j=0;j<i;j++) in1.push_back(in[j]);
		for(int j=i+1;j<post.size();j++) in2.push_back(in[j]); 
		//将之前的后序序列 分成两个后序序列
		//注意两个for中我们 j的取值第一个到i-1  第二个for是从i+1开始  也就是舍弃了一个i
		vector<int> post1;vector<int> post2;
		for(int j=0;j<i;j++) post1.push_back(post[j]);
		for(int j=i;j<post.size()-1;j++) post2.push_back(post[j]);
		//这里看的是j 的取值舍弃了post容器中的最后一个元素 也就是根元素
		CreatPostTree(T->Lchild,post1,in1);
		CreatPostTree(T->Rchild,post2,in2);
}

可执行代码汇总

#include<bits/stdc++.h>
#define ElemType int
using namespace std;
typedef struct BiNTree{
	ElemType data;
	struct BiNTree* Lchild;
	struct BiNTree* Rchild;
}BiNTree;
void visit(ElemType val){
	cout<<val<<" ";
}
void PreOrder(BiNTree* T){//首先跟我们前面的思路一样 首先我们正常的遍历后序遍历的顺序是左子树 右子树 中间结点 
 //所以我们放入栈中的顺序是中右左  要第三次访问的时候才操作此此时的元素  难道我们要加两个NULL ? 
	stack<BiNTree*> S;
	if(T==NULL) return;
	S.push(T);
	while(!S.empty()){//在这个判空之前肯定要先往里面放东西
		T=S.top();//此时的T是左子树   我们要左子树处理完才处理右子树 
		if(T==NULL){//若是等于NULL 说明下一个结点是已经访问过了 而且它的左右子树都已经访问过了 此时我们再访问也就是后序遍历了 
			S.pop();//给T=NULL空的弹出 
			T=S.top();//弹出之后再取其中的结点 此结点的左右子树已经被访问过了  再访问也就是此结点的后序遍历 
			visit(T->data);
			S.pop(); 
		} 
		else{
			S.pop();//虽然有的人可能后想 这两步不是一样的吗  但是 其实是两个意思 一个是为了弹出树来分裂它的左右子树 还有下面一个则是添加根结点 
			 //我觉得依然需要一个标记  但是是放一个NULL 还是放两个NULL呢?再一次来到这个结点的时候 它的左右子树都被处理完了  
			//所以 添加一个标记便可 此时直接取出其中的元素便可  
			if(T->Rchild)S.push(T->Rchild);
			if(T->Lchild)S.push(T->Lchild);
			S.push(T);
			S.push(NULL);	
		}
	} 
} 


void InOrder(BiNTree* T){
	stack<BiNTree*> S;
	if(T==NULL) return;
	S.push(T);
	while(!S.empty()){//在这个判空之前肯定要先玩里面放东西 
		T=S.top();
		if(T==NULL){//说明下一个结点是第二次访问了  此时再一次弹出 然后取此时的元素 
			S.pop();
			T=S.top(); 
			visit(T->data);
			S.pop(); 
		}
		else{//本来没有加else 现在来看确实是不应该, 若是不加 则此时 T指向的结点已经放进去了 
			S.pop();//弹出没有返回值  不要问我怎么知道 因为我也错了 
			if(T->Rchild!=NULL)S.push(T->Rchild);
			S.push(T);//当前树的根结点不需要判断是否为空  因为不可能为空 
			S.push(NULL);//再结点前面添加还是后面添加呢 我想应该是后面 因为只有这这样访问的时候才能先于结点察觉到此时下一个结点第二次访问了 
			if(T->Lchild!=NULL)S.push(T->Lchild);
		}
	} 
} 
void PostOrder(BiNTree* T){//首先跟我们前面的思路一样 首先我们正常的遍历后序遍历的顺序是左子树 右子树 中间结点 
 //所以我们放入栈中的顺序是中右左  要第三次访问的时候才操作此此时的元素  难道我们要加两个NULL ? 
	stack<BiNTree*> S;
	if(T==NULL) return;
	S.push(T);
	while(!S.empty()){//在这个判空之前肯定要先往里面放东西
		T=S.top();//此时的T是左子树   我们要左子树处理完才处理右子树 
		if(T==NULL){//若是等于NULL 说明下一个结点是已经访问过了 而且它的左右子树都已经访问过了 此时我们再访问也就是后序遍历了 
			S.pop();//给T=NULL空的弹出 
			T=S.top();//弹出之后再取其中的结点 此结点的左右子树已经被访问过了  再访问也就是此结点的后序遍历 
			visit(T->data);
			S.pop(); 
		} 
		else{
			S.pop();//弹出此时的树 
			S.push(T); //我觉得依然需要一个标记  但是是放一个NULL 还是放两个NULL呢?再一次来到这个结点的时候 它的左右子树都被处理完了  
			//所以 添加一个标记便可 此时直接取出其中的元素便可   这个添加的是根结点   与上一个要区别开来一个是根结点 一个是树 
			S.push(NULL); 
			if(T->Rchild)S.push(T->Rchild);
			if(T->Lchild)S.push(T->Lchild);	
		}
	} 
} 
void LevelOrder(BiNTree* T){//这里我们是使用的是队列来完成这个操作
	queue<BiNTree*> Q;
	if(T==NULL){//3、当然也不能平白无辜的加 此时必然需要判空  空的加入 取出的话还需要判断空
		return; 
	}
	Q.push(T);//2、既然都有判空了 自然也就需要进入之前就需要往队列中加入元素  加入什么呢? 自然是根结点 
	while(!Q.empty()){//1、你要想肯定要使用while  while里面什么时候结束呢? 自然是需要一个队列判空操作 
		BiNTree* NewNode=Q.front();//4 此时就像栈中我们需要找到我们第一个可以操作的值,这里队列中找到要找的返回值
		visit(NewNode->data);//5.取出就可以访问了 但是要加入它的左右孩子 
		if(NewNode->Lchild!=NULL) Q.push(NewNode->Lchild);//6 ,加入队列之前必然要判空  所以要加if 
		if(NewNode->Rchild!=NULL) Q.push(NewNode->Rchild);
		//7,这样就操作完一个元素了, 但是我们发现我们似乎没有弹出操作 什么时候加入弹出操作呢 ?
		//这里我认为只要在操作下一个元素之前弹出都是没有问题的,所以我们直接在加入此元素的子元素之后 弹出此元素 
		Q.pop(); //8 弹出 
	}
} 
void menu(){
	cout<<"请输入你的选择"<<endl;
	cout<<"1、前序遍历 2、中序遍历 3、后序遍历 4、层序遍历 5、退出"<<endl; 
}
void InitTree(BiNTree* &T){//链表的初始化  这里是不带头结点的  
	T=NULL; 
}
void CreatPreTree(BiNTree* &T,vector<int> pre,vector<int> in){//先写前序与中序构造树 
		int i=0;
		if(pre.empty()){//传入进来的两个容器中的指的个数一定是一样的 
			T=NULL;
			return; 
		} 
		int root=pre[0];
		T=(BiNTree*)malloc(sizeof(BiNTree));
		T->data=root;
		while(root!=in[i]){
			++i;//找到i根在中序队列中的位置
		}
		vector<int> in1;vector<int> in2;
		//给中序序列分成两个部分 构建左子树与右子树   但是怎么样分比较清晰呢? 这里我认为把i 理解为有几个元素其实可能更好
		for(int j=0;j<i;j++) in1.push_back(in[j]); 
		for(int j=i+1;j<in.size();j++) in2.push_back(in[j]);
		//这样在将中序划分为两个的时候就成功的去掉了  中间的根 
		//有同样的将前序序列也分成两个部分  同样也是为了构建左子树与右子树 
		vector<int> pre1;vector<int> pre2;
		for(int j=1;j<=i;j++) pre1.push_back(pre[j]);
		for(int j=i+1;j<pre.size();j++) pre2.push_back(pre[j]); 
		CreatPreTree(T->Lchild,pre1,in1); //填入它同样需要此时分裂出来的左子树的前序和中序 
		CreatPreTree(T->Rchild,pre2,in2);
		//这种题目的关键也即是区间的分割 
}
void CreatPostTree(BiNTree* &T,vector<int> post,vector<int> in){//通过中序后序来构建一个树 
		int i=0;
		if(post.empty()){//传入进来的两个容器中的指的个数一定是一样的 
			T=NULL;
			return; 
		} 
		int root=post.back(); 
		T=(BiNTree*)malloc(sizeof(BiNTree));
		T->data=root; 
		while(root!=in[i]) ++i;//也是寻找根在中序中的坐标 
		//开始分中序为左右子树  这个跟上面一样 
		vector<int> in1; vector<int> in2;
		for(int j=0;j<i;j++) in1.push_back(in[j]);
		for(int j=i+1;j<post.size();j++) in2.push_back(in[j]); 
		//将之前的后序序列 分成两个后序序列
		vector<int> post1;vector<int> post2;
		for(int j=0;j<i;j++) post1.push_back(post[j]);
		for(int j=i;j<post.size()-1;j++) post2.push_back(post[j]);
		CreatPostTree(T->Lchild,post1,in1);
		CreatPostTree(T->Rchild,post2,in2);
}

int main(){
	vector<int> pre; vector<int> in,post; int val;
	int choice;int flag; BiNTree* T;int prepostchoice; //跟单链表一样定义一个头指针 
	InitTree(T); 
	while(1){
		cout<<"是否重新创建一棵树?若是要重新创建请输入1"<<endl; 
		cin>>flag;
		if(1==flag){
			cout<<"前序中序请输入 1"<<endl;
			cout<<"中序后序请输入 2"<<endl; 
			cin>>prepostchoice;
			switch(prepostchoice){
				case 1:{
					cout<<"请输入前序遍历"<<endl;
					while(scanf("%d",&val)!=EOF) pre.push_back(val);
					cout<<"请输入中序遍历"<<endl;
					while(scanf("%d",&val)!=EOF) in.push_back(val); 
					CreatPreTree(T,pre,in);	
					break;
				}
				case 2:{
					cout<<"请输入后序遍历"<<endl;
					while(scanf("%d",&val)!=EOF) post.push_back(val);
					cout<<"请输入中序遍历"<<endl;
					while(scanf("%d",&val)!=EOF) in.push_back(val); 
					CreatPostTree(T,post,in);	
					break;
				}
				default:{
					cout<<"小老弟你输入的有问题"<<endl; 
					break;
				}
			}
		} 
		menu();
		cin>>choice;
		if(choice==5) break; 
		switch(choice){
			case 1:{
				PreOrder(T);
				break;
			}
			case 2:{
				InOrder(T);
				break;
			}
			case 3:{
				PostOrder(T);
				break;
			} 
			case 4:{
				LevelOrder(T);
				break;
			}
			default: break;
		}
	}
	return 0;
}

总结

若是文章对你的提升有哪怕一点帮助的话 请答应我 不要吝啬你的点赞评论

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值