【洛谷】二叉树专题全解析:概念、存储、遍历与经典真题实战

文章目录


一、二叉树的概念

二叉树是一种特殊的树型结构,它的特点是:

  • 每个结点至多只有 2 棵子树(即二叉树中不存在度大于 2 的结点)
  • 并且二叉树的子树有左右之分,其次序不能任意颠倒,因此是一颗有序树。
  • 二叉树 = 根节点 + 左子树 + 右子树,其中左子树和右子树又是一颗二叉树,所以二叉树也是递归定义的。
    满二叉树:

(这里已知孩子结点求父节点不用减一除二而是直接除二,因为是从下标1开始存储的)

完全二叉树:

二、⼆叉树的存储

在上一节中,我们已经学过树的存储,⼆叉树也是树,也是可以⽤vector数组或者链式前向星来存储。仅需在存储的过程中标记谁是左孩⼦,谁是右孩⼦即可。

• ⽐如⽤ vector 数组存储时,可以先尾插左孩⼦,再尾插右孩⼦;

• ⽤链式前向星存储时,可以先头插左孩⼦,再头插右孩⼦。只不过这样存储下来,遍历孩⼦的时候先遇到的是右孩⼦,这点需要注意。

但是,由于⼆叉树结构的特殊性,我们除了⽤上述两种⽅式来存储,还可以⽤符合⼆叉树结构特性的⽅式:分别是顺序存储和链式存储。
顺序存储: 二叉树的顺序存储本质就是堆,小编这里就不过多讲解了。
链式存储: 链式存储类⽐链表的存储,都有静态实现和动态实现的⽅式,我们这⾥依旧只讲静态实现,也就是⽤数组模拟。 竞赛中给定的树结构⼀般都是有编号的,参考上⼀章的树结构。因此我们可以创建两个数组 l[N],r[N] ,其中 l[i] 表⽰结点号为

的结点的左孩⼦编号, r[i] 表⽰结点号为 的结点的右孩⼦编号。这样就可以把⼆叉树存储起来。

题目叙述如下:

代码实现:

cpp 复制代码
using namespace std;
#include <iostream>

const int N = 1e6 + 10;
int n, l[N], r[N];

int main()
{
	cin >> n;
	for(int i = 1; i <= n; i++)
	{
		cin >> l[i] >> r[i];
	}
	return 0;
}

dfs遍历二叉树

思路和我们在数据结构初阶学习一样,分为前、中、后序遍历,小编不过多解释了,直接看代码。这里小编要补充一点,递归实现时我们采用先判断左右子树是否为空,不为空再执行递归,而不是像传统方法一样无论子节点是否为空,都会执行递归调用。

补充:二叉树存储时没有像上一节介绍树一样存结点的父节点,每个结点只存储了它的左右孩子结点,所以不用bool数组标记。

cpp 复制代码
//传统方法
//void dfs1(int u)
//{
//	if (u == 0)
//		return;
//	cout << u << " ";
//	dfs1(l[u]);
//	dfs1(r[u]);
//}

//前序遍历
void dfs1(int u)
{
	cout << u << " ";
	if (l[u]) //左子树存在
		dfs1(l[u]);
	if (r[u]) //右子树存在
		dfs1(r[u]);
}
//中序遍历
void dfs2(int u)
{
	if (l[u]) 
		dfs2(l[u]);
	cout << u << " ";
	if (r[u]) 
		dfs2(r[u]);
}
//后序遍历
void dfs3(int u)
{
	if (l[u])
		dfs3(l[u]);
	if (r[u])
		dfs3(r[u]);
	cout << u << " ";
}

int main()
{
	//建树
	cin >> n;
	for (int i = 1; i <= n; i++)
	{
		cin >> l[i] >> r[i];
	}

	//遍历
	dfs1(1);
	cout << endl;

	dfs2(1);
	cout << endl;

	dfs3(1);
	cout << endl;

	return 0;
}

bfs遍历二叉树

cpp 复制代码
void bfs()
{
	queue <int> q;
	q.push(1);
	while (!q.empty())
	{
		int tmp = q.front();
		q.pop();
		cout << tmp << " ";
		if(l[tmp])
			q.push(l[tmp]);
		if (r[tmp])
		    q.push(r[tmp]);
	}
}

三、二叉树算法题

新二叉树

题目描述

题目解析

本题就是二叉树前序遍历,只不过之前数组存的是数字,结点本身存储的数据就是它的物理数组下标,比如l[1]和r[1]就是1号结点的左右孩子,所以可以拿到结点的数组下标就是拿到结点本身,就可以遍历它的左右孩子,而本题数组里存储的是字符,数组下标和结点本身解耦了,但是我们不能通过拿到字符反推出该字符代表的结点下标,所以我们需要把字符和数组下标强行耦合起来,思路就是用存储char类型数据的数组l[N], r[N],字符本质是ASCII码数值,所以可以把结点存储的字符转化成ASCII码数值作为l[N], r[N]的下标,然后把结点的左右孩子字符存储到l[N], r[N]对应位置中。

简化:利用字符的 ASCII 码作为数组下标,直接存储对应节点的左右孩子字符,实现节点与数组下标的关联

cpp 复制代码
	//若这里输入字符a
	cin >> a;  
	//若这里输入字符y,y字符会存储到数组的97下标对应位置中
	//因为这里编译器会自动将字符a转化为它的ASCLL码值
	cin >> arr[a]; 

代码

cpp 复制代码
using namespace std;
#include <iostream>

const int N = 300; //ASCII码范围
int n;
char e[N], l[N], r[N];

void bfs(char u)
{
	cout << u;
	if(l[u] != '*')
		bfs(l[u]);
	if(r[u] != '*')
	    bfs(r[u]);
}

int main()
{
	//建树
	cin >> n;
	//后面bfs要传root,所以不能在for循环里面输入root
	//在for循环外面特殊处理root
	char root;
	cin >> root;
	cin >> l[root] >> r[root];

	//根节点已经处理,所以i从2开始
	for (int i = 2; i <= n; i++)
	{
		char t;
		cin >> t;
		cin >> l[t] >> r[t];
	}

	bfs(root);

	return 0;
}

二叉树的遍历

题目描述

题目解析

本题和前面介绍的二叉树前、中、后序遍历几乎一样,小编就不过多解释了。

代码

cpp 复制代码
using namespace std;
#include<iostream>

const int N = 1e6 + 10;
int n, l[N], r[N];

void dfs1(int u)
{
	cout << u << " ";
	if(l[u]) dfs1(l[u]);
	if(r[u]) dfs1(r[u]);
}

void dfs2(int u)
{
	if (l[u]) dfs2(l[u]);
	cout << u << " ";
	if (r[u]) dfs2(r[u]);
}

void dfs3(int u)
{
	if (l[u]) dfs3(l[u]);
	if (r[u]) dfs3(r[u]);
	cout << u << " ";
}

int main()
{
	//建树
	cin >> n;
	for (int i = 1; i <= n; i++)
	{
		cin >> l[i] >> r[i];
	}

	dfs1(1);
	cout << endl;
	dfs2(1);
	cout << endl;
	dfs3(1);
	cout << endl;

	return 0;
}

二叉树深度

题目描述

题目解析

这道题我们也讲过了,这里小编也不细讲了:详情点这里
代码

cpp 复制代码
using namespace std;
#include <iostream>

const int N = 1e6 + 10;
int n, l[N], r[N];

int deep(int u)
{
	if (u == 0)
		return 0;

	int ldeep = deep(l[u]);
	int rdeep = deep(r[u]);
	return 1 + (ldeep > rdeep ? ldeep : rdeep);
}

int main()
{
	//建树
	cin >> n;
	for (int i = 1; i <= n; i++)
	{
		cin >> l[i] >> r[i];
	}

	cout << deep(1);
	return 0;
}

求先序排列

题目描述

题目解析

这种题是二叉树的经典题目,核心思路是递归,分两步走: 1、找到根节点 2、根据根节点划分左右子树

思路很简单,落实在题目中就是通过下标划分序列,这里要用到序列的一个特点,不论是前、中、后序遍历的序列,在序列根结点的左侧都是左子树的结点,在序列根结点的右侧都是右子树的结点,用下标把序列划分为左子树,根节点,右子树,然后再分别递归处理左子树,右子树,由于本题是输出前序遍历序列,所以递归过程中拿到后序序列后直接打印后序序列最后一个元素。


代码

cpp 复制代码
using namespace std;
#include <iostream>
#include <string>

string a, b; //代表输入的两个序列

void dfs(int l1, int r1, int l2, int r2)
{
	//递归出口
	if (l1 > r1)
		return;

	cout << b[r2];

	//1、找中序序列中根节点位置
	//根节点为后序遍历最后一个元素
	int p = l1;
	while (a[p] != b[r2])
		++p;
	//p为中序序列根节点下标
	//2、划分左右子树
	dfs(l1, p - 1, l2, l2 + (p - 1 - l1 + 1) - 1); //递归左子树
	dfs(p + 1, r1, l2 + (p - 1 - l1 + 1), r2 - 1); //递归右子树(b[r2]是根节点,所以需要r2 - 1)
}

int main()
{
	cin >> a >> b;
	dfs(0, a.size() - 1, 0, b.size() - 1);
	return 0;
}

美国血统

题目描述

题目解析

本题和前一题类似,上一题的知道中,后序序列求前序,本题的知道中、前序序列求后序序列,思路一样,无非是打印根节点时机的区别,上一题是先打印根节点再划分左右子树并递归,本题是划分左右子树并递归后再打印根节点。

代码

cpp 复制代码
using namespace std;
#include <iostream>
#include <string>

string a, b;

void dfs(int l1, int r1, int l2, int r2)
{
	//递归出口
	if (l1 > r1)
		return;

	//1、找根节点
	int p = l1;
	while (a[p] != b[l2])
		++p;

	//2、划分左右子树并递归
	dfs(l1, p - 1, l2 + 1, l2 + 1 + (p - 1 - l1 + 1) - 1);
	dfs(p + 1, r1, l2 + 1 + (p - 1 - l1 + 1), r2);

	cout << b[l2];
}

int main()
{
	cin >> a >> b;

	dfs(0, a.size() - 1, 0, b.size() - 1);

	return 0;
}

二叉树问题

题目描述

题目解析

1、本题是一道综合题目,首先需要理解题意,宽度和深度很好理解,结点间距离比较抽象,我们来举个例子,比如说结点6和8之间的距离:

我们要先画出两个结点的最短路径,如图所示,向根结点边数指的是从8到1三条边,向叶结点指的是从1到6两条边,根据题目描述距离就是8。
2、本题的输入只给了树上的边信息,并没有给左右结点信息,所以本题不能以二叉树的存储方式存树结构,而应该用上一节的存树的方式存储。本题还规定了u是v的父节点,所以输入的一对信息只用存u->v一条边,不用像以前存两条边。
3、解题思路:
第一步 首先创建二叉树,这里我们就用vector数组存储。 第二步是求二叉树的深度,逻辑是树高 = max(左子树树高,

右子树树高) + 1,而求左右子树树高又可以套用这个公式,所以树高可以通过递归求得。

这里补充一下max函数的用法,它是一个函数模板,可以用来比较两个元素大小:

第二步 求二叉树宽度,需要借助队列和bfs,思路是以bfs的思路借助队列按层遍历整棵二叉树,当队列不为空时一直执行结点入队列、把该结点的孩子结点带入队列、把队列头节点出队列,只不过现在不是一个一个结点的执行,而是按层执行,每次按层执行前需要统计当前队列中的元素个数,也就是当前层的结点个数size,如果当前层的结点个数比历史层结点个数ret更多,则更新ret,否则维持ret不变。
第三步求两个结点的距离,题目要求需要找到最短路径,由于我们站在两个结点视角无法知道在哪个结点拐弯是最短路径,所以需要两个结点都从原位置出发走到根结点,它们路径第一个相交的结点就是最短路径的拐弯点。

  • 要算距离就需要把结点x到相交点和结点y到相交点的距离记录下来,思路是创建一个dist数组,dist[i]即为结点i到结点x的距离,先让x结点出发向根节点走,走的过程中把从结点x到根节点途径的所有结点都用dist数组记录起来。
  • 但是这里会出现一个问题,要想让结点x走到根节点必须拿到对应结点的父节点,可是本题存储树时并没有存结点的父节点,所以我们需要创建一个fa数组,fa[i]表示结点i的父节点,当输入u、v时就用数组fa把对应结点父子关系记录起来。
  • 然后让结点y出发向根节点走,用一个变量len记录结点y走的距离,当走到根节点或者走到相交点就停止,判断依据是相交点一定被dist数组记录过,根据题意最后距离就是2 * dist[y] + len。

代码

cpp 复制代码
using namespace std;
#include <iostream>
#include <vector>
#include <queue>

const int N = 110;
vector<int> edges[N];
int n, u, v, x, y;

int fa[N];  //fa[i]表示i结点的父亲
int dist[N];//dist[i]表示i结点到X结点的最短距离

int deep(int u)
{
	int ret = 0;
	for (auto e : edges[u])
	{
		ret = max(ret, deep(e));
	}
	return 1 + ret;
}

int bfs()
{
	queue<int> q;
	int sz, ret = 0;
	q.push(1);
	while (!q.empty())
	{
		//循环更新每层宽度sz和历史最宽ret
		sz = q.size();
		ret = max(ret, sz);
		while (sz--)
		{
			//按层执行
			//把当前层结点全部出队列
			//并把下一层结点全部入队列
			int u = q.front(); 
			q.pop();
			for (auto e : edges[u])
			{
				q.push(e);
			}
		}
	}

	return ret;
}

int main()
{
	// 1、建树
	cin >> n;
	for(int i = 1; i < n; i++)
	{
		cin >> u >> v;
		edges[u].push_back(v);
		fa[v] = u; //u是v的父结点
	}

	// 2、求深度
	cout << deep(1) << endl;;

	// 3、求宽度
	cout << bfs() << endl;

	// 4、求距离
	cin >> x >> y;
	int len = 0;

	//把从结点x到根节点所有结点都用dist数组标记
	while (x != 1)
	{
		dist[fa[x]] = dist[x] + 1;
		x = fa[x];
	}

	//y也从原位置走到根节点
	// 当y走到根结点或者
	//当y和走到x走过的结点时停止
	//也就是当dist[y]不为0时停止
	while (y != 1 && !dist[y])
	{
		++len;
		y = fa[y];
	}
	cout << 2 * dist[y] + len << endl;

	return 0;
}

以上就是小编分享的全部内容了,如果觉得不错还请留下免费的关注和收藏
如果有建议欢迎通过评论区或私信留言,感谢您的大力支持。
一键三连好运连连哦~~

相关推荐
小谢在学习3 小时前
旋转图像
算法
CyHacker_10103 小时前
C++_day4
c++
hsjkdhs4 小时前
C++之友元函数与前向引用
开发语言·c++
北京地铁1号线4 小时前
机器学习笔试选择题:题组2
人工智能·算法·机器学习
heeheeai4 小时前
决策树,随机森林,boost森林算法
算法·决策树·随机森林·kotlin·boost
MOONICK5 小时前
数据结构——红黑树
数据结构
(●—●)橘子……5 小时前
记力扣2271.毯子覆盖的最多白色砖块数 练习理解
数据结构·笔记·python·学习·算法·leetcode
做运维的阿瑞5 小时前
Python 面向对象编程深度指南
开发语言·数据结构·后端·python
Tiny番茄6 小时前
排序算法汇总,堆排序,归并排序,冒泡排序,插入排序
算法·排序算法