【dawn·数据结构·笔记】二叉树的右视图(C++)

简要说明:
(1)题目来源:课程(上机考题)。
(2)由于作者水平限制和时间限制,代码本身可能仍有一些瑕疵,仍有改进的空间。也欢迎大家一起来讨论。
——一个大二刚接触《数据结构》课程的菜鸡留

题目简介

  • 给定一颗二叉树的前序遍历和中序遍历序列,先重建这棵树1,然后想象自己站在其右侧,按照从顶部到底部的顺序,返回右侧能看到的结点值。
  • 例如对于下图的树,它的右视图的序列便是1 3 6。
    图1:显然,最右边的三个结点自头至底依次为1 3 6,即所求右视图。

输入格式有如下要求:

  • 第一行是一个整数n,表示这棵树的结点总数。
  • 第二行有n个整数,用单个空格隔开,表示这棵树的先序遍历序列。
  • 第三行有n个整数,用单个空格隔开,表示这棵树的中序遍历序列。

参考样例:

输入样例:
6
1 2 4 3 5 6
4 2 1 5 3 6
输出样例:
1 3 6

思路分析

注:仅代表个人思路。

这道题一共分成两个部分:

  • 第一个部分是根据先序遍历和中序遍历建树。根据先序遍历和中序遍历是可以建成唯一一棵树的,这一点我们将在代码部分加以实现,并在讨论部分中会加深讨论。先序遍历的特点是先访问头结点,中序遍历的特点是先访问左孩子结点(或者更整体来说,是左孩子为根结点的一棵子树),因此可以通过这两个特点快速地区分左子树和右子树,从而可以使用递归的思路进行建树。
  • 第二个部分是右视图,这个问题实则和层次遍历的思路特别接近。不同的是,层次遍历关注的是每一层上的所有结点,并且根据子结点的情况决定是否再次入队;而本问题关注的是每一层的最后一个结点。

代码部分

  • 关于这个问题,给出两种代码实现。为了简化文章内容,第二种实现方法仅给出第二个部分的实现,其余部分(如输入输出、建树等)不再重复。
  • 代码1的思路是将这棵树在进行层次遍历时补成一棵“满二叉树”,队中允许存放空结点。由于满二叉树的第N(N=1, 2, …)层的结点数为2N个,因此只需要通过计数就能判断是否完成了对当前层的遍历。循环结束的条件,取决于是否探测到了全为空的一层,代表当前层事实上已经超出了原二叉树的层数,循环便可以终止。这种解法的优点在于比较清晰,且不用过多思考层次遍历中判断层结束的方法;但缺点是当树比较高时,计数器会有越界的风险。
//代码1
#include <iostream>
#include <vector>
#include <queue>
using namespace std;

//树结点的类定义
struct TreeNode {
	TreeNode(int d=0, TreeNode *l=NULL, TreeNode *r=NULL):data(d),lchild(l),rchild(r) {}
	int data;
	TreeNode *lchild, *rchild;
};

//树的类定义
class Tree {
public:
	Tree():head(NULL) {}
	TreeNode* create(vector<int>& v1, vector<int>& v2) {  head=p_create(v1,v2); return head;  }
	void side() {  side(head);  }  //调用私有函数side(head)实现

	TreeNode *head;

private:
	TreeNode* p_create(vector<int> v1, vector<int> v2);
	void side(TreeNode *root);
};

//部分1: 根据前序遍历和中序遍历生成树
//在这里, 假设给定的先序遍历和中序遍历序列是正确的, 即可以建成树
//则不考虑出错情况(体现在代码中可能会有vector越界)
TreeNode* Tree::p_create(vector<int> v1, vector<int> v2) {
	if (v1.size()==0||v2.size()==0)  return NULL;
	int head_node=v1[0];  //该值为头结点.
	int ptr,q;
	TreeNode *p=new TreeNode(head_node);
	vector<int> l_v1, l_v2, r_v1, r_v2;
	//创建左右子孩子的两个遍历数组.
	//l_v1和l_v2表示左子树的先序遍历和中序遍历.
	//r_v1和r_v2表示右子树的先序遍历和中序遍历.
	for (ptr=0;v2[ptr]!=head_node;ptr++)  l_v2.push_back(v2[ptr]);
	for (q=1;q<=ptr;q++)  l_v1.push_back(v1[q]);
	for (;q<v1.size();q++)  r_v1.push_back(v1[q]);
	for (ptr++;ptr<v2.size();ptr++)  r_v2.push_back(v2[ptr]);
	//递归.
	p->lchild=p_create(l_v1,l_v2);
	p->rchild=p_create(r_v1,r_v2);
	return p;
}

//右视图
void Tree::side(TreeNode *root) {
	int layer_count=1, right_number;
	queue<TreeNode*> q;
	bool flag=true;
	q.push(root);
	while (flag) {
		flag=false;
		for (int i=0;i<layer_count;i++) {
			if (q.front()!=NULL) {
				right_number=q.front()->data;
				q.push(q.front()->lchild);
				q.push(q.front()->rchild);
				flag=true;
			}
			else {
				q.push(NULL);
				q.push(NULL);
			}
			q.pop();
		}
		if (flag)  cout<<right_number<<' ';  //如果flag为false, 表明这一层全为NULL结点.
		layer_count*=2;
	}
}

int main() {
	int node_number,m;
	cin>>node_number;
	vector<int> vpre, vmid;
	for (int i=0;i<node_number;i++) {
		cin>>m;
		vpre.push_back(m);
	}
	for (int i=0;i<node_number;i++) {
		cin>>m;
		vmid.push_back(m);
	}
	Tree t;
	TreeNode *head=t.create(vpre, vmid);
	t.side();
}
  • 代码2仅关心上述代码中的Tree::side(TreeNode *root)。除了通过使用补成满二叉树并计数的方法判断层结束,我个人想到也可以通过加入“标记”来体现层的终点。这个标记可以是任一一个额外申请的新结点,也可以单单就是NULL。
//代码2
//只关心函数void Tree::side(TreeNode *root)
//使用空指针(NULL)作为一层结束的"标记"
void Tree::side(TreeNode *root) {
	if (root==NULL)  return;
	queue<TreeNode*> q;
	int right_number;
	q.push(root);
	q.push(NULL);  //第一层只会有一个头结点.
	while (!q.empty()) {
		if (q.front()==NULL) {  //代表到达层结束的标记. 
			cout<<right_number<<' ';
			q.pop();
			//如果队已空, 到达最底层, 不需要加入NULL, 否则会进入死循环.
			//队非空, 当前层的结束代表下一层也入队结束, 加入结束标记.
			//这里也可以调用q.size()来判断是否到达最底层.
			if (!q.empty())  q.push(NULL);
		}
		else {
			if (q.front()->lchild!=NULL)  q.push(q.front()->lchild);
			if (q.front()->rchild!=NULL)  q.push(q.front()->rchild);
			right_number=q.front()->data;
			q.pop();
		}
	}
}

讨论1:序列建树问题

  • 先就本题按照先序遍历和中序遍历序列建树讨论。在学习中,大部分情况下递归算法和非递归算法是成对出现的,那么本题是否可以改为非递归算法?就目前我个人的思考下来的话,感觉还是比较复杂的。栈中存放的数据可能不仅仅要求结点指针本身,可能需要保存左子树的序列,以及一个区分应建左子树还是右子树的一个“标记”,有点类似于后序遍历的非递归算法。
  • 接下去将分为更多情况讨论。一棵二叉树可以有4种遍历方式,分别是先序遍历、中序遍历、后序遍历和层次遍历。任意两两组合,能否实现建树?接下去逐一讨论(除本题中的先序遍历和中序遍历)。
  • 为方便起见,可针对本题的测试样例中的这棵树作为测试案例进行验证。分别写出它的四个遍历序列:
    先序遍历:1 2 4 3 5 6
    中序遍历:4 2 1 5 3 6
    后序遍历:4 2 5 6 3 1
    层次遍历:1 2 3 4 5 6
  • 在所有的讨论中,我们有两个前提。第一个前提如在代码部分中提到的,不考虑给定的两个序列在可以建树的情况下出现错误的情况(如两者的顶点无法一一对应等)。更重要的,也就是第二个前提,就是二叉树中没有重复结点。否则,给出这个反例:
    图2:缺失这一原则下,无法通过任意两个序列确定的两棵树的一个简单实例
    可以发现,这两棵树的先序遍历、中序遍历、后序遍历、层次遍历的序列都是1 0 1,两两任意组合都不能确定是这两棵树的哪一棵。因此,得出上述的前提是有必要的。在之后的情况讨论中,都基于这一前提。

情况1: 先序遍历和后序遍历

  • 在探讨这种情况是否成立之前,先要探讨先序遍历和中序遍历能唯一确定一棵树的理由。先序遍历的输出顺序是父结点、左孩子、右孩子,中序遍历是左孩子、父结点、右孩子。由于父结点永远在先序遍历的第一个位置,因此找到父结点后,再审视中序遍历时可以区分左孩子和右孩子。
  • 但先序遍历和后序遍历显然并不能。先序遍历和后序遍历都是左孩子后紧跟右孩子输出,唯一的区别在于父结点位置一个在头一个在尾。也就是说,当一棵树有多层时,无法划分左子树和右子树。
  • 最简单的例子是一棵2层、2个结点的二叉树,先序遍历是1 2,后序遍历是2 1。我们无法确定2应该是1的左孩子还是右孩子,这就不唯一了。
  • 可以得出结论:先序遍历和后序遍历不能唯一确定一棵树。

情况2: 先序遍历和层次遍历

  • 仍然搬出情况1的例子。在那样一棵二叉树下,先序遍历是1 2,层次遍历也是1 2,但无法确定2是1的左孩子还是右孩子。同样地,层次遍历并不能清楚地告诉我们如何区分左孩子和右孩子,尤其是当父结点只有一个子结点时。
  • 可以得出结论:先序遍历和层次遍历不能唯一确定一棵树。

情况3: 中序遍历和后序遍历

  • 这种情况是可行的,因为如我们之前讨论的那样,虽然后序遍历无法区分左子树和右子树,但中序遍历可以。通过后序遍历的特点,即父结点永远在最后一个位置,可以根据大小依次划分左子树和右子树,仍然是递归的思路。
  • 具体代码实现如下:(可比照先序遍历和中序遍历)
//类定义进行如下补充.
class Tree {
public:
	//...
	//情况3: 根据中序遍历和后序遍历建树
	void create_case3(vector<int>& v1, vector<int>& v2) {  p_create_case3(v1,v2);  }
private:
	//...
	TreeNode* p_create_case3(vector<int> v1, vector<int> v2);
};

//根据中序遍历和后序遍历生成树
//(v1是中序遍历序列, v2是后序遍历序列)
TreeNode* Tree::p_create_case3(vector<int> v1, vector<int> v2) {
	if (v1.size()==0||v2.size()==0)  return NULL;
	int head_node=v2[v2.size()-1];
	int ptr, q;
	TreeNode *p=new TreeNode(head_node);
	vector<int> l_v1, l_v2, r_v1, r_v2;
	for (ptr=0;v1[ptr]!=head_node;ptr++)  l_v1.push_back(v1[ptr]);
	for (q=0;q<ptr;q++)  l_v2.push_back(v2[q]);
	for (;q<v2.size()-1;q++)  r_v2.push_back(v2[q]);
	for (ptr++;ptr<v1.size();ptr++)  r_v1.push_back(v1[ptr]);
	p->lchild=p_create_case3(l_v1,l_v2);
	p->rchild=p_create_case3(r_v1,r_v2);
	return p;
}

情况4: 中序遍历和层次遍历

  • 沿着之前的想法来说,这两个遍历是可以唯一确定一棵二叉树的。而且思路应该也可以使用递归函数的思路,但如何从已有这一步推及下一步是一件比较麻烦的事。在具体代码实现之前,来整理一下我的思路。如果读者已有或欲跳过的话,可以直接前往之后的代码实现。
  • 为了方便起见,记中序遍历序列为Mid[N],层次遍历序列为Lay[N]。对于一棵树,层次遍历下的第一个结点Lay[0]一定会是根结点。
  • 如果这棵树的层次遍历长度为1,也就是说这代表一个叶子结点,那么只需要比较简单地将两个孩子设置为空即可。
  • 如果这棵树的层次遍历长度大于1,也就是说至少有2层。按照递推的思路,注意力应该被放在第二层上,也就是说Lay[1]以及可能的Lay[2]。
  • 如果根结点x只有一个孩子,那么事实上只有Lay[1]代表真正的第2层。它是左孩子结点还是右孩子结点,将取决于根结点x在Mid[N]中的位置。它只有可能在Mid[0],代表左孩子为空;或者在Mid[N-1],代表右孩子为空。
  • 如果根结点x有两个孩子,那么就说明Lay[1]和Lay[2]是两棵子树的根。确定根结点x有两个孩子的方法,就是排除前两种情况。
  • 接下去的问题是递归函数的参数传递。中序遍历序列的传递不成问题,只需要进行剪切即可。当根结点只有一个孩子时,层次遍历序列的传递也比较简单,只需要去除根结点在层次遍历中的出现即可。接下去把注意力放在当x有两个孩子时的情况。
  • 在传递和剪切层次遍历序列时,两个同属一棵子树的先后顺序是不会变的,出现在前面的结点的所在层数一定小于等于出现在后面的。因此在后面的代码实现中,给出一种最朴素的思路。记左(右)孩子的中序遍历序列为Mid_left(right)[],左(右)孩子的层次遍历序列为Lay_left(right)[]。
  • 自Lay[1]开始自前往后遍历Lay[N]。设遍历到Lay[n]。若Lay[n]∈Mid_left[],那么放在Lay_left[]队尾;否则,放在Lay_right[]队尾(必然会属于左或右孩子中序遍历序列其一之中)。这样既能分割,也能保留原先在Lay[]中的顺序。
  • 具体代码实现如下:
//类定义进行如下补充.
class Tree {
public:
	//...
	//情况4: 根据中序遍历和层次遍历建树
	void create_case4(vector<int>& v1, vector<int>& v2) {  p_create_case4(v1,v2);  }
private:
	//...
	TreeNode* p_create_case4(vector<int> v1, vector<int> v2);
};

//根据中序遍历和层次遍历生成树
//(v1是中序遍历序列, v2是层次遍历序列)
TreeNode* Tree::p_create_case4(vector<int> v1, vector<int> v2) {
	if (v1.size()==0||v2.size()==0)  return NULL;
	int head_node=v2[0];
	int ptr, q, r;
	TreeNode *p=new TreeNode(head_node);
	//叶子结点的情况
	if (v2.size()==1)  return p;  //在TreeNode构造函数定义中, 已初始化p->lchild和p->rchild为空.
	//根结点一个孩子的情况
	if (head_node==v1[0]||head_node==v1[v1.size()-1]) {
		if (head_node==v1[0]) {  //代表左孩子为空. 
			for (int i=0;i<v2.size()-1;i++) {
				v1[i]=v1[i+1];
				v2[i]=v2[i+1];
			}
			v1.pop_back();
			v2.pop_back();
			p->rchild=p_create_case4(v1,v2);
		}
		else {  //代表右孩子为空. 
			v1.pop_back();
			for (int i=0;i<v2.size()-1;i++)  v2[i]=v2[i+1];
			v2.pop_back();
			p->lchild=p_create_case4(v1,v2);
		}
	}
	//根结点两个孩子的情况 
	else {
		vector<int> l_v1, l_v2, r_v1, r_v2;
		for (ptr=0;v1[ptr]!=head_node;ptr++)  l_v1.push_back(v1[ptr]);  //找到根结点在中序遍历序列中的位置, 同时设置l_v1. 
		for (q=1;q<v2.size();q++) {
			for (r=0;r<ptr&&v1[r]!=v2[q];r++);
			if (r==ptr)  //代表在左子树序列中没有找到, 在右子树中.
				r_v2.push_back(v2[q]);
			else  //代表在左子树序列中.
				l_v2.push_back(v2[q]); 
		}
		for (ptr++;ptr<v1.size();ptr++)  r_v1.push_back(v1[ptr]);
		p->lchild=p_create_case4(l_v1,l_v2);
		p->rchild=p_create_case4(r_v1,r_v2);
	}
	return p;
} 

情况5: 后序遍历和层次遍历

  • 这个情况同我们之前讨论的一样,由于缺少对左右孩子的明确划分,是无法唯一确定一棵树的。仍然可以搬出之前的例子,即后序遍历为2 1、层次遍历为1 2的情况,无法确定值为2的结点是值为1的结点的左孩子还是右孩子。
  • 可以得出结论:后序遍历和层次遍历不能唯一确定一棵树。

讨论2:三视图

  • 虽然阔别高中数学已有一载有余,但做到这道题时仍然会联想起立体几何中的三视图。数学上的三视图应是主视图、侧(左)视图和俯视图,即从正面、左面和上面看图。沿用这么一个概念,进行简要地讨论。
  • 首先是主视图,这实则就是树的层次遍历,最多纠结的是是否带有格式,因此不表。倘若从背面角度来看,看到的每一层应该是倒序的。这个可以在具体的输出步骤中进行调整,可以在本文代码部分的代码2基础上进行解决。
  • 然后是俯视图,可以发现俯视图的实质就是二叉树的中序遍历序列。从理论上来说,仰视的角度看到的树和俯视图应是一样的。
  • 最后是左视图,实则上就是与本题解决的右视图的记录顺序做一个“颠倒”——右视图要求记录当前层的最后一个非空结点的值,而左视图要求记录当前层的第一个非空结点的值。解决这个问题难度应该不是特别大,沿用代码部分的代码2,解决如下:(由于代码类似,不给出具体的注释)
//类定义进行如下补充.
class Tree {
public:
	//...
	//左视图.
	void left() {  left(head);  }
private:
	//...
	void left(TreeNode *root);
};

void Tree::left(TreeNode *root) {
	if (root==NULL)  return;
	queue<TreeNode*> q;
	int left_number;
	q.push(root);
	q.push(NULL);
	while (!q.empty()) {
		if (q.front()==NULL) {
			cout<<left_number<<' ';
			q.pop();
			if (!q.empty())  q.push(NULL);
		}
		else {
			left_number=q.front()->data;
			while (q.front()!=NULL) {
				if (q.front()->lchild!=NULL)  q.push(q.front()->lchild);
				if (q.front()->rchild!=NULL)  q.push(q.front()->rchild);
				q.pop();
			}
		}
	}
}

补充部分

  • 文章中涉及了二叉树的两种顺序序列能否确定唯一一棵二叉树的内容,并且在可行的情况下给出了一种代码实现。由于题目本身是比较简单的(事实上在上机题中也是最简单的那题了),因此更多把这篇博客当作是一篇笔记,供日后查看,也希望能给一些读者一些小小的帮助。

  1. 题目中没有特别提及,但需要确定的一个前提是,二叉树中不能含有相同值的结点。理由可以参照讨论部分中的图2。 ↩︎

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
叉树是一种重要的数据结构,由于其特殊的结构和性质,需要具备一些基本的操作来对二叉树进行处理。 首先是创建二叉树。可以通过读取用户输入或者其他方式来构建一个二叉树。创建二叉树的过程可以使用递归的方式,通过不断地输入节点的值和连接关系来构造二叉树。 其次是遍历二叉树。常见的遍历方式有前序遍历、中序遍历和后序遍历。前序遍历先访问根节点,然后遍历左子树和子树;中序遍历按照左子树、根节点和子树的顺序遍历;后序遍历先遍历左子树和子树,最后访问根节点。通过递归的方式,可以实现这三种遍历方式。 另外一个常用的操作是查找二叉树中的节点。可以通过比较节点的值,逐层搜索二叉树,找到目标节点。如果目标节点不存在,可以返回一个特定的值来表示找不到。 还有一个重要的操作是插入节点。可以通过比较节点的值,找到插入的位置。如果待插入的节点小于当前节点,就插入到左子树中;如果待插入的节点大于当前节点,就插入到子树中。插入节点后,需要调整二叉树的结构,保持二叉树的性质。 最后,删除节点也是一个常见的操作。删除节点时,需要考虑节点的左子树。可以通过将节点的左子树的最大节点或者子树的最小节点上移来替代被删除的节点。删除节点后,同样需要调整二叉树的结构,保持二叉树的性质。 这些是二叉树的基本操作,它们在实际应用中有广泛的应用,比如在搜索、排序和图等领域。掌握这些操作,可以更好地理解和应用二叉树

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值