「数据结构详解·二」二叉树的初步

1. 二叉树的定义、构成和术语

二叉树(Binary tree)是一种特殊的树,每一个节点最多有两个儿子
如下图,这就是一棵普通的二叉树。
二叉树
二叉树也分成了一些特殊的种类。

  • 满二叉树:除叶子节点外,每个节点都有两个儿子(左儿子右儿子),且左右子树结构相同。
  • 完全二叉树:设一共有 k k k 层,前 k − 1 k-1 k1 层是一课满二叉树,剩下的一层节点全部居于左侧。

下面就展示了一棵满二叉树和一棵完全二叉树。
满二叉树
完全二叉树

2. 二叉树的性质

  • 所有树的性质。
  • 对于一棵深度为 k k k(根节点深度为 1 1 1)的满二叉树,其结点数为 2 k − 1 2^k-1 2k1
  • 对于一棵结点数为 k k k 的满二叉树,其层数为 ⌈ log ⁡ 2 k ⌉ \lceil\log_2k\rceil log2k
  • 对于一棵满二叉树,其叶子结点数 = = = 非叶子结点数 + 1 +1 +1
  • 结点数为 k k k 的二叉树个数为 Catalan \text{Catalan} Catalan 数第 k k k 项。

大家可以尝试证明一下上述性质。

3. 二叉树的存储

3-1. 顺序存储

观察上面满二叉树的编号和父子关系,假如补成一棵完全二叉树,重新编号,你发现了什么?
没错,设父节点为 k k k,则左儿子编号为 2 k 2k 2k,右儿子编号为 2 k + 1 2k+1 2k+1
因此定义数组为 int tree[1<<N-1];
假如将最上面的那棵普通的二叉树存进来,那存下来就是这样:

123456789101112131415
tree i \text{tree}_i treei111101100001100

访问 k k k 的左儿子就是 k*2k<<1,访问右儿子就是 k*2+1k<<1|1
但是有时这种存储空间复杂度为 O ( 2 n ) O(2^n) O(2n),浪费空间,根节点不一定为一,而且要存储其他东西。因此我们还有另外一种存储方法。

3-2. 链式存储

定义一个结构体,里面有 l l l(左儿子编号)、 r r r(右儿子编号)、 d d d(所存储的值,有时会用到)。

struct node{
	int l,r,d;
}tree[N];

和上面的例子一样,如果输入顺序正确,存下来后是这样:

12345678
tree.l245-17-1-1-1
tree.r3-16-18-1-1-1

空间复杂度降到了 O ( n ) O(n) O(n)
在解决简单二叉树一类的题目时,通常使用链式存储。

4. 二叉树的遍历

4-1. 先/前序(根)遍历

遍历顺序为根→左子树→右子树
示例代码(输出二叉树的先序遍历):

void pre(int p)//p 为当前节点编号
{
	if(p==-1) return;
	cout<<p<<' ';
	pre(tree[p].l);
	pre(tree[p].r);
}

输出为:1 2 4 3 5 7 8 6

4-2. 中序(根)遍历

遍历顺序为左子树→根→右子树
示例代码(输出二叉树的中序遍历):

void in(int p)//p 为当前节点编号
{
	if(p==-1) return;
	pre(tree[p].l);
	cout<<p<<' ';
	pre(tree[p].r);
}

输出为:4 2 1 7 5 8 3 6

4-3. 后序(根)遍历

遍历顺序为左子树→右子树→根
示例代码(输出二叉树的后序遍历):

void post(int p)//p 为当前节点编号
{
	if(p==-1) return;
	pre(tree[p].l);
	pre(tree[p].r);
	cout<<p<<' ';
}

输出为:4 2 7 8 5 6 3 1

4-4. 层次遍历

和 bfs 一样。
示例代码(输出二叉树的层次遍历):

queue<int>q;
void bfs()
{
	q.push(root);//root 为根节点编号
	while(!q.empty())
	{
		int x=q.front();
		cout<<x<<' ';
		q.pop();
		if(tree[x].l!=-1) q.push(tree[x].l);
		if(tree[x].r!=-1) q.push(tree[x].r);
	}
}

5. 例题详解

5-1. 洛谷 P4913 【深基16.例3】二叉树深度

求其深度,我们可以使用 dfs 或 bfs 的方法。
对于 dfs,我们多加一个参数 d e p dep dep,其表示当前节点的深度。

#include<bits/stdc++.h>
using namespace std;

struct node{
	int l,r;
}tree[1000005];
int n,ans;

void dfs(int p,int dep)//dep 表示编号为 p 的节点的深度
{
	if(!p) return;
	ans=max(ans,dep);//不断地存答案
	dfs(tree[p].l,dep+1);
	dfs(tree[p].r,dep+1);
}

int main()
{
	cin>>n;
	for(int i=1;i<=n;i++)
	{
		cin>>tree[i].l>>tree[i].r;
	}
	dfs(1,1);
	cout<<ans;
 	return 0;
}

对于 bfs,我们在队列里多加一个数,表示当前节点的深度。

#include<bits/stdc++.h>
using namespace std;

struct node{
	int l,r;
}tree[1000005];
queue<pair<int,int>>q;//pair 的第一项为编号,第二项为深度
int n,ans;

void bfs()
{
	q.push({1,1});
	while(!q.empty())
	{
		int x=q.front().first;
		ans=max(ans,q.front().second);//记答案
		q.pop();
		if(tree[x].l) q.push(tree[x].l);
		if(tree[x].r) q.push(tree[x].r);
	}
}

int main()
{
	cin>>n;
	for(int i=1;i<=n;i++)
	{
		cin>>tree[i].l>>tree[i].r;
	}
	bfs(1,1);
	cout<<ans;
 	return 0;
}

5-2. 洛谷 P4913 加强版

这一次,我们不知道根节点是谁,需要你自己去求根节点。
那我们要如何求呢?
我们可以利用树的性质:一棵树有且仅有一个根节点
也就是说,输入中左右儿子中没有提到的编号,就是根节点。

bool f[1919810];//f[i] 表示是否不是根节点
for(int i=1;i<=n;i++)
{
	cin>>tree[i].l>>tree[i].r;
	f[tree[i].l]=f[tree[i].r]=1;//标记
}
int root;
for(int i=1;i<=n;i++)
{
	if(!f[i])
	{
		root=i;//存下根节点
		break;
	}
}

5-3. 洛谷 P1030 [NOIP2001 普及组] 求先序排列

我们知道,中序遍历顺序为左子树→根→右子树,后序遍历顺序为左子树→右子树→根。这意味着,我们可以通过后序遍历找到根,通过根由中序遍历分割左右子树,然后递归下去。
以样例 BADC BDCA 为例。

  • 由后序得知根节点为 A,输出。
  • 在中序中找到 A,分割出左右子树。
    • 递归左子树(中序 B,后序 B),由后序得知根节点为 B,输出。
    • 在中序中找到 B,分割出左右子树。
      • 递归左子树,字符串为空,回溯。
      • 递归右子树,字符串为空,回溯。
    • 递归右子树(中序 DC,后序 DC),由后序得知根节点为 C,输出。
    • 在中序中找到 C,分割出左右子树。
      • 递归左子树(中序 D,后序 D),由后序得知根节点为 D,输出。
      • 在中序中找到 D,分割出左右子树。
        • 递归左子树,字符串为空,回溯。
        • 递归右子树,字符串为空,回溯。
      • 递归右子树,字符串为空,回溯。

代码实现:

#include<bits/stdc++.h>
using namespace std;

void f(string a,string b)//a 为中序,b 为后序
{
	if(a=="") return;
	cout<<b[b.size()-1];//根节点
	int t=a.find(b[b.size()-1]);//中序中找根节点
	f(a.substr(0,t),b.substr(0,t));
	f(a.substr(t+1),b.substr(t,a.size()-t-1));
}

int main()
{
	string a,b;
	cin>>a>>b;
	f(a,b);
	return 0;
}

6. 练习

加*的表示选做。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值