二叉树递归,非递归遍历及层序,前序创建树(可实现代码汇总)

提示:君子终日乾乾,夕惕若厉,无咎


前言

这篇文章主要侧重代码的实现 想了解概念的可以看这篇文章
遍历其实就是一种将非线性结构的树 转化成线性结构 怎么转化也就有了四种结构
这里先说方式,此时你的脑海中已经跳出来了前序,中序,后序,亦或者还能给我加上一个层序,
但是可能有部分人不知道的是其实这四种可以分为两大类 深度与广度后面学的的同学可能会把这两个联系起来
1.深度优先遍历(遇到叶子结点就会停止)
(1)前序遍历
(2)中序遍历
(3)后序遍历
2.广度优先遍历(一层一层遍历)
(1)层序遍历


一、遍历的一眼看穿

例如求下图中的中前中后
请添加图片描述

这里我们使用方式叫做树体描边(讲解起来可能较麻烦 但是等你理解了 直接就可以写了)我们将每一个结点分成三份 分别标记为一二三
请添加图片描述

请添加图片描述

若是前序则描边的时候经过1就写下来,若是中序描边经过2的时候就写下来 若是后序则经过三的时候写下来
请添加图片描述
三种遍历方式 其实也可以理解为第几次来到这个结点 若是前序第一次来到 我们就除了这个结点,中序是第二次来到 我们来处理 这个结点,后序就是第三次来我们来处理这个结点,上述描边标记123 就是看他是第几次访问这个结点,这样是可以写出遍历方式了,三种遍历的意义, 前序遍历说明此时我先处理此时子树的根结点,然后处理此时子树根结点的左子树,然后处理此时子树根结点的右子树, 中序表示先处理左子树,再子树根节点,再子树右子树, 后序亦是同样的道理,三种遍历方式隐含的意义对于为什么可以使用递归 以及三句话的顺序就很重要

二、结构体的定义

我们前面所学的顺序表或者链表统称为线性表 也就是这个结点与下一个结点是一个链接着一个的 但是生活中很多情况下并不是一个接着一个的 比如你家现在两个孩子 等两个孩子在结婚就是两个家庭,两个家庭每一家再有两个孩子 下一代就有四个家庭 为了反映他们之间的关系 ,我们每一个结点中就像普通的链表一样,能找到它的后继就行,但是因为它是二叉树,有两个后继 于是就需要两个指针
于是就有了下面的结构体定义

//二叉树的存储结构,一个数据域,2个指针域
 typedef struct BiTNode
{
     char data;
     struct BiTNode *lchild,*rchild;
  }BiTNode,*BiTree;

三、深度优先遍历(递归法)

关于递归你需要知道

递归,就是在运行的过程中调用自己,在此过程中问题不断缩小 ,直到来到了题目中给我们的初始条件,也就是函数的出口, 这个时候通过返回来解决更大的问题 直到 解决当前我们要处理的问题,
模板如下

public void Fun(参数0) {
    if (终止条件) {
        return;
    }
    Fun(参数1)
    Fun(参数2)
            ……
    Fun(参数n)
}

一个经典的问题就是斐波那契数列 其实本质是一样

int f(int n){
    if(n <= 2){
        return 1;
    }
    return f(n-1) + f(n - 2);
}

其实写到这我想很多人还是不懂 ,其实我的建议是大家自己来跟着程序走一步
假如我们来求 第F(5)的值
第一次进入
5>2
所以此时求得F(5)就变成了求F(4)+F(3) 你要知道程序是一步一步来走的 所以它并不是同时求F(4)和F(3) 而是先求F(4)再求F(3) 此时规模变小了

我们先来解决F(5)->F(4),第二次进入
4>2
所以此时F(4)就变成了求F(3)+F(2) ,但是此时问题又来了 是先求这个F(3)还是程序的上一步的F(3) 呢? 当然是这一步的F(3), 因为这一步的F(3)是上一步的F(4)转化的 就相对于我们杀人要灭口,斩草要除根,要不然春风吹又生,

解决F(4)->F(3) 第三次进入
3>2
此时又变成了求F(2)+F(1) ,

解决F(3)->F(2)
成功来到终止条件开始回溯 return 0

解决F(3)->F(1)
成功来到终止条件开始回溯 return 0

解决F(4)->F(2)
成功来到终止条件开始回溯 return 1

深入F(5)->F(3)
同样的道理解决F(3)相当于解决F(2)+F(1)

F(3)->F(2)
成功来到终止条件 返回 return 1;

F(3)->F(1)
同样来到了终止条件 返回 reuturn 0;

最后得到结果 F(5)=3
那么什么时候可以用递归?递归与分治是什么
分治与递归
1. 递归
两个核心
子问题必须与原始问题相同,且规模更小(递归式)
不能无限制地调用自身,必须有一个递归出口 (递归边界)

2.分治法
步骤
分:将问题分解为规模更小的问题(严格来说根据这一步的不同还可能分为减而治之和分而治之)
治:将这些规模更小的子问题逐个击破
合:将已解决的子问题合并,最终得出“母”问题的解
跟递归的关系
分治法既可以用递归求解也可以不用递归求解,但是一般来说都是用递归求解的,因为用递归代码写起来更加容易,因此我们将上面许多时候都是两种算法合到一起使用

为什么遍历树可以使用递归?

建议思考这个之前,先读加黄色边界字体三遍
子问题必须与原始问题相同,且规模更小(递归式)
不能无限制地调用自身,必须有一个递归出口 (递归边界)

  • 树如何能看成与子问题相同呢?
    把树看成是由一个根结点,左子树 右子树 构成的 ,同样的每一个左(右)子树又可以单独看成是由这三部分组成,
    正如前面我们模拟的时候一样(没有模拟的建议模拟一遍,或者认真看一遍我的模拟 脑子中要知道执行过程) ,递归会先把第一个递归处理好 才会处理第二个递归,这个特性就跟遍历的意义是一样的 前序遍历说明此时我先处理此时子树的根结点,然后处理此时子树根结点的左子树,然后处理此时子树根结点的右子树, 中序表示先处理左子树,再子树根节点,再子树右子树, 后序亦是同样的道理
  • 它的递归条件又是啥个子
    这个等价于——问题到什么时候可以化解到我们可以解决了?,树中通常递归大都是有两中结束方式 一种是到了叶子结点,另外一种就是到达null的时候,这里使用的是到null的时候,要处理完每一个结点之后我们才回溯,什么时候使用叶子结点,这个我也后面文章中应该会有体现 这里一时半会想不起来了什么题目了

1、前序遍历

基于上述模板我们进行填空

public void Fun(BiTNode* T) {//就跟传入链表一样 不过这里传入的是双指针链表
    if (T==null) {
        return;
    }
    visit(T->data)//记得我当年 我还在想这个visit是什么意思 
    //这里就是你要对这个结点进行的操作 你想干嘛干嘛 可以放肆一点
    Fun(T->Lchild)//规模缩小此时传入一个左子树
    Fun(T->Rchild)//规模缩小此时传入一个右子树
}

写完不知道你有没有我一样的感觉 他把一个整树 变成了一个一个只有根结点的树 这样来处理其中再分成更小两个树的时候 裸露出来的那个根结点 我们就来处理这个根结点

2、中序遍历

正如前面我们所说遍历意义
中序表示先处理左子树,再子树根节点,再子树右子树,其实结合递归的执行顺序,你就知道他们是有多契合了

public void Fun(BiTNode* T) {//就跟传入链表一样 不过这里传入的是双指针链表
    if (T==null) {
        return;
    }
    Fun(T->Lchild)//规模缩小此时只转入一个左子树
    visit(T->data)//j记得我当年 我还在想这个visit是什么意思 
    //这里就是你要对这个结点进行的操作 你想干嘛干嘛 可以放肆一点
    Fun(T->Rchild)//规模缩小此时传入一个右子树
}

3、后序遍历

与上面同样道理

public void Fun(BiTNode* T) {//就跟传入链表一样 不过这里传入的是双指针链表
    if (T==null) {
        return;
    }
    Fun(T->Lchild)//规模缩小此时只转入一个左子树
    visit(T->data)//j记得我当年 我还在想这个visit是什么意思 
    //这里就是你要对这个结点进行的操作 你想干嘛干嘛 可以放肆一点
    Fun(T->Rchild)//规模缩小此时传入一个右子树

四、深度优先遍历树(非递归法)

为什么可以使用栈来解决遍历树?

递归的实现就是:每一次递归调用都会把函数的局部变量、参数值和返回地址等压入调用栈中,然后递归返回的时候,从栈顶弹出上一次递归的各项参数,所以这就是递归为什么可以返回上一层位置的原因

怎么使用栈?

我们前面写过 栈是先进后出,我们这里若是要实现某种顺序,这里的输入也要是有讲究的,比如前序是先处理先当前结点,然后左子树,最后右子树 那么进栈的顺序就要是反过来的也就是 先右子树,然后左子树,最后当前结点 其实知道这些,程序就可以写出来,附上一个我写的思路

1、先序遍历(非递归版本1)

以下代码我第一次写 ,但是也可以写出来 聪明如你,肯定也没有问题 ,至于版本二 其实你看完下面的两个遍历方式之后 你就能写出来了 其实也是一个标记 有了标记之后 你只需要改变 三行代码的顺序即可。

void PreOrder(BiNtree* T){
	stack<BiNTree*> S;
	if(T==NULL) return ;  
	S.push(T); 
	 while(!S.empty()){//要再进入的while的时候不为空为空 ,则需要前面就加入结点
	 	T=S.top(); 
	 	visit(T->data);//因为是先序只要出来数据 我们就来拜访它 
	 	S.pop(); 
		if(T->Rchild!=NULL) S.push(T->Rchild);
		if(T->Lchild!=NULL) S.push(T->Lchild);	
	} 
} 

下面把我写非递归的时候的思路也附上

void PreOrder(BiNTree* T){//使用栈 注意顺序  我们心中要有一个处理的顺序  一定是先处理中间 再处理左子树 然后是右子树 
	stack<BiNTree*> S;//定义一个栈
	S.push(T->Rchild);
	S.push(T->Lchild);
	visit(T->data); //数组第一次访问就直接可以操作了 ,所以不需要放入栈中
	//我们此时栈中还有两颗树,我们要处理的是上面这个树  所以也就是要给他弹出 
	BiNTree* T=S.pop();//需要定义一个变量来接受弹出的树 
	//然后继续来处理
	S.push(T->Rchild);
	S.push(T->Lchild);
	visit(T->data); 
	。。。。//只有当上面结束了 才会来处理当最开始的右子树  上面我们也不知道什么有多是个结点  使用不可能一直这样写  于是想到了while
	while(!S.empty()){//若是栈不为空 ,则一直进行的操作就是弹出 处理一个结点 然后入栈 但是你别忘了 也有NULL的存在呀 
	// 所以我们想到进栈之前是不是需要判空一下 也应该想到树是不是也需要判空一下 不然T->Rchild  就会程序异常终结 
		if(T->Rchild!=NULL) S.push(T->Rchild);
		if(T->Lchild!=NULL) S.push(T->Lchild);	
		visit(T->data);
		T=S.pop();//T表示的是,每一个子树根结点是需要更新的	 
	} 
} 

1、先序遍历(迭代版本二)

void PreOrder(BiNTree* T){
	stack<BiNTree*> S;
	if(T==NULL) return;
	S.push(T);
	while(!S.empty()){//在这个判空之前肯定要先往里面放东西
		T=S.top();//此时的T是左子树   我们要左子树处理完才处理右子树 
		if(T==NULL){ 
			S.pop();//给T=NULL空的弹出 
			T=S.top();//弹出之后再取其中的结点
			visit(T->data);
			S.pop(); 
		} 
		else{
			S.pop();
			if(T->Rchild)S.push(T->Rchild);
			if(T->Lchild)S.push(T->Lchild);
			S.push(T);//这里只需改变这三句的顺序便可
			S.push(NULL);	
		}
	} 
} 

2、中序遍历(迭代版)

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);
		}
	} 
} 

以下是我写这个的思路共大家参考(此时未经过调试) 结果发现忽略了一个else 其他的好像就没有什么问题了,若是你有时间 你也可以尝试一下。

void InOrder(BiNTree* T){//这里是中序 正常的处理顺序是左子树 根 右子树  
//所以我们进栈的顺序是  右 根 左  然后我们是在第二次遇到根结点的时候  再进行操作根结点的 
	stack<BiNTree*> S;//定义一个栈
	S.push(T->Rchild);
	S.push(T); 
	S.push(T->Lchild);//他们三个放进去了  我们此时要对栈的最上面一个进行操作
	BideNTree* Node=S.top(); //此时取出右子树了 我们要把右子树也处理完才左子树 
	S.push(Node->Rchild);
	S.push(Node);
	S.push(Node->Lchild); 
	//但是写到这 我们发现我们不知道在何处添加visit, 我们每次分裂出两个子孩子都是直接加如栈中,
	//根结点也是直接加入进去了,并不知道它是第二次被访问了,此时的指针域什么的也没有改变
	//我们下一次判断的时候也不知道是不是第二次经过  也就不知道这个结点是遍历过一遍的还是是我们分裂出来的结点放入的 。 
	//所以我们是否可以来做一个标记 这标记应该是什么 栈中放的是指针所以标记 我认为取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();
			S.top(); 
			visit(T->data);
			S.pop(); 
		}
		S.pop();
		if(T->Rchild)S.push(T->Rchild);
		S.push(T);//这里不需要判空 因为是先弹出 在加入所以不会为空 
		S.push(NULL);//再结点前面添加还是后面添加呢 我想应该是后面 因为只有这这样访问的时候才能先于结点察觉到此时下一个结点第二次访问了 
		if(T->Lchild)S.push(T->Lchild);
	} 
} 

3、后序遍历(迭代版本)

这里同样附上一份我写的时候思路代码。

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);//4、若是不为空当然就可以加入了 
	while(!Q.empty()){//1、你要想肯定要使用while  while里面什么时候结束呢? 自然是需要一个队列判空操作 
		BiNTree* NewNode=Q.front();//5 此时就像栈中我们需要找到我们第一个可以操作的值,这里队列中找到要找的返回值
		visit(NewNode->data);//6.取出就可以访问了 但是要加入它的左右孩子 
		if(NewNode->Lchild!=NULL) Q.push(NewNode->Lchild);//7 ,加入队列之前必然要判空  所以要加if 
		if(NewNode->Rchild!=NULL) Q.push(NewNode->Rchild);
		//8,这样就操作完一个元素了, 但是我们发现我们似乎没有弹出操作 什么时候加入弹出操作呢 ?
		//这里我认为只要在操作下一个元素之前弹出都是没有问题的,所以我们直接在加入此元素的子元素之后 弹出此元素 
		Q.pop(); 
	}
} 


但是上述代码还不够好 层与层之间不够分明 所以我们优化一下代码如下 注意
这里需要使用一个size 将值单独保留下来 千万不能使用size() 因为是动态变化的,这样一个for遍历的时候才能控制一层

void LevelOrder(BiNTree* T){//这里我们是使用的是队列来完成这个操作
	queue<BiNTree*> Q; BiNTree* cur;
	if(T==NULL){//跟前面一样  若是为空则不需要进队  直接返回即可 
		return ; 
	}
	Q.push(T); 
	while(!Q.empty()){
		int size=Q.size();//需要一个值来保存 因为Q.size 是一直变得 
		for(int i=0;i<size;i++){
			cur=Q.front();
			cout<<cur->data<<" ";
			if(cur->Lchild) Q.push(cur->Lchild);
			if(cur->Rchild) Q.push(cur->Rchild); 
			Q.pop();
		}
		cout<<endl;
	}
} 

六、创建一棵树

输入需要按照上图加上-1之后的前序或者层序 也就是如下图一样
请添加图片描述

有人可能会有疑问为什么 这里是先写的遍历后写的创建树 ,其说创建一颗二叉树 倒不如说创建一个两个指针域的链表更容易理解 但是在链表中我们说链表分为带不带头节点的 这里分不分?(这里当然是不需要考虑这些的,若是硬要犟 都可以)

初始化

相当于创建一个空的双指针链表 与之前的单链表对应

void InitTree(BiNTree* &T){//链表的初始化
	T=NULL
}

前序创建一颗二叉树

其实你输入的也是前序遍历的顺序 但是其中要加入是空指针的标志 这里就是-1 这样计算机才好在知道哪一些是叶子结点 其实与前序遍历一起来看更好理解 接下来我们就对比递归的前序遍历来写,在递归遍历中这里是cur==NULL 表示此时是空结点了 这里前序输入中遇到了-1 , 说明若是前序遍历,此时也是叶子结点了,所以此时的指向我们就赋值为NULL,有点像两个时空结合了,你通过遍历就可以写出创建

void CreatTree(BiNTree* &T){ 
	ElemType val;
	//cin>>val;
	scanf("%d",&val);
	if(-1==val) {//在递归遍历中这里是cur==NULL 表示此时是空结点了  这里前序输入中遇到了
	//-1 , 说明若是前序遍历,此时也是叶子结点了,所以此时的指向我们就赋值为NULL,
	//有点像两个时空结合了,你通过遍历就可以写出创建
		T=NULL;
		return;//依然是按照上述递归的模板来写的
	}
	else{
		BiNTree* T=(BiNTree*)malloc(sizeof(BiNTree));  
		T->data=val;
		CreatTree(T->Lchild);
		CreatTree(T->Rchild);
	}	
}

其中这种递归简单一点理解就是 上级交给你的任务 你只能处理属于你的一部分这也就是处理当前结点,剩下的工作交给下面的人 下面的人依然会这样这样吩咐它的员工,什么时候不用往下继续吩咐了(也就是来到了终止条件) , 创建一颗二叉树也是一样,只需要处理你的当前结点就好 其他的交给下面去完成就好了,开始一层一层的往上交
但是这个递归方法只适用于前序 因为 中序,后序最左的结点是叶子结点 你输入的时候 直接-1 会判空也就是一个空树

层序创建一棵树

层序的思想是,首先需要判断此时第一个元素是否为空。
若是为空就不用需要进入树队中了 若是第一个元素不为空 则进入队列中
然后为其分配左右子树,若是值为-1 则表示此时的孩子为空 不需要入队
若是值不为-1 ,让其作为此时结点的孩子,然后此孩子入队 所以也就需要两个队列 一个用来放元素的值 一个用来放树的结点
直到元素值队列为空
后面写题目需要用到层序 这里就顺便写了下来 也放在这里吧

#include<bits/stdc++.h>
#define ElemType int
using namespace std;
typedef struct BiNTree{
	struct BiNTree* Lchild;
	ElemType data;
	struct BiNTree* Rchild;
}BiNTree;
/*层序的思想是,首先需要判断此时第一个元素是否为空 为此 
若是为空就不用需要进入树队中了  若是第一个元素不为空 则进入队列中
然后为其分配左右子树,若是值为-1 则表示此时的孩子为空  不需要入队
若是值不为-1 ,让其作为此时结点的孩子,然后此孩子入队 
直到元素值队列为空*/ 
void Creat(BiNTree* &T){
	int val;
	BiNTree* cur;//定义一个cur 
	queue<ElemType> Q;
	queue<BiNTree*> TreeQue;
	cout<<"请输入层序遍历的序列"<<endl;
	while(scanf("%d",&val)!=EOF) Q.push(val);
	if(Q.front()==-1){
		cout<<"树为空"<<endl; 
		T=NULL;
		return ;
	} 
	else{ 
		T=(BiNTree*)malloc(sizeof(BiNTree));
		T->data=Q.front(); 
		TreeQue.push(T);
	}
	Q.pop();
	while(!Q.empty()){//我们每一次处理 处理的都是左右孩子,所以也就需要两个if
		cur=TreeQue.front();//取出此时树队中的第一元素
		if(Q.front()==-1){//此时这个结点值为NULL  
			cout<<cur->data<<"左孩子为空"<<endl;
			cur->Lchild=NULL; 
		}
		else{//新生成一个树结点用来存放此时的结点信息 
			cout<<cur->data<<"左孩子为"<<Q.front()<<endl; 
			cur->Lchild=(BiNTree*)malloc(sizeof(BiNTree)) ;
			cur->Lchild->data=Q.front();
			TreeQue.push(cur->Lchild);
		}
		Q.pop();//处理完数据之后需要弹出 来取此结点右子树的值 
		//上面的if  else 是处理结点左孩子  下面我们来处理右孩子 
		if(Q.front()==-1){
			cout<<cur->data<<"右孩子为空"<<endl;;
			cur->Rchild=NULL;
		} 
		else{
			cout<<cur->data<<"右孩子为"<<Q.front()<<endl; 
			cur->Rchild=(BiNTree*)malloc(sizeof(BiNTree));
			cur->Rchild->data=Q.front();
			TreeQue.push(cur->Rchild); 
		}
		Q.pop();
		TreeQue.pop();
	}
} 
void Preorder(BiNTree* T){
	if(T==NULL){
		return;
	}
	cout<<T->data<<" ";
	Preorder(T->Lchild);
	Preorder(T->Rchild);
}
main(){
	BiNTree* T=NULL;
	Creat(T); 
	cout<<"此时树的前序遍历是"<<endl;
	Preorder(T);
}

对输入方式有要求 使用的树依然是第一个树
请添加图片描述

六、可执行代码汇总

这里遍历放的是非递归版本的 若是需要递归版本的 直接取上面递归版的直接替换即可 函数名都是一样的!

#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; BiNTree* cur;
	if(T==NULL){//跟前面一样  若是为空则不需要进队  直接返回即可 
		return ; 
	}
	Q.push(T); 
	while(!Q.empty()){
		int size=Q.size();//需要一个值来保存 因为Q.size 是一直变得 
		for(int i=0;i<size;i++){
			cur=Q.front();
			cout<<cur->data<<" ";
			if(cur->Lchild) Q.push(cur->Lchild);
			if(cur->Rchild) Q.push(cur->Rchild); 
			Q.pop();
		}
		cout<<endl;
	}
} 
void menu(){
	cout<<"请输入你的选择"<<endl;
	cout<<"1、前序遍历 2、中序遍历 3、后序遍历 4、层序遍历 5、退出"<<endl; 
}
void InitTree(BiNTree* &T){//链表的初始化  这里是不带头结点的  
	T=NULL; 
}
void CreatTree(BiNTree* &T){ 
	ElemType val;
	//cin>>val;
	scanf("%d",&val);
	if(-1==val) { 
		T=NULL;
		return;
	}
	else{
		T=(BiNTree*)malloc(sizeof(BiNTree)); //这里新生成的结点不能写成这样 BiNTree* T  写成这样不对 
		T->data=val;
		CreatTree(T->Lchild);
		CreatTree(T->Rchild);
	}	
}
int main(){
	int choice;int flag; BiNTree* T;//跟单链表一样定义一个头指针
	InitTree(T); 
	while(1){
		cout<<"是否重新创建一棵树?若是要重新创建请输入1"<<endl; 
		cin>>flag;
		if(1==flag){
			cout<<"请按照先序遍历的形式输入"<<endl; 
			cout<<"若是无左孩子或者右孩子其输入-1"<<endl;
			CreatTree(T);
		} 
		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 2 4 -1 -1 5 -1 -1 3 -1 -1;

运行截图

请添加图片描述

总结

若是文章对你的提升由哪怕一点帮助的话 请答应我 不要吝啬你的点赞评论 你的鼓励对作者是一种莫大的鼓励转载请告知哪一部分我检查一下

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值