用数组实现树的存储遍历【复习笔记】
1. 树的基本概念
1.1 树的定义和术语
树是由 n(n≥0)个有限节点组成的一个具有层次关系的集合。当 n=0 时,称为空树。在一棵非空树中,有且仅有一个特定的节点称为根节点;其余节点可分为 m 个互不相交的有限集 T1、T2、…、Tm,其中每个集合本身又是一棵树,并且称为根结点的子树
因此,树是递归定义的
节点的度:一个节点拥有的子树个数称为该节点的度。度为 0 的节点称为叶子节点或终端节点;度不为 0 的节点称为分支节点。
树的度:树中节点的最大度数称为树的度。
路径:树中两个结点之间的路径是由这两个结点之间所经过的结点序列构成的,路径长度为序列中 边的个数
节点的层次:从根节点开始定义,根节点为第 1 层,根的子节点为第 2 层,以此类推,若某节点在第 i 层,则其孩子节点在第 i + 1 层。
树的高度或深度:树中节点的最大层次数称为树的高度或深度。
1.2 树的种类
有序树:结点的子树按照从左往右的顺序排列,不能更改
无序树:结点的子树没有顺序,可以更改
有根树:根节点已知
无根树:根节点未知,都可以是根节点
1.3 树的存储
树存储时,要保存值域,也要保存结点和结点的关系。存储方式有多种:双亲表示法、孩子表示法、孩子双亲表示法、孩子兄弟表示法......
1.3.1 孩子表示法
将每个结点的孩子信息存储下来(无根树中,父子关系不明,都存储)
假设树存储树时有n条信息,第一条为n个结点,接下来n-1行,每行两个树x,y表示x和y间有一条边
1. 使用 vector 数组
#include<iostream>
#include<vector>
using namespace std;
int n;
const int N = 1e5 + 10;
//创建N个vector数组,edges[i]中保存i号结点的孩子信息
vector<int> edges[N];
int main()
{
cin >> n;
for (int i = 1; i < n; i++)
{
int x, y; cin >> x >> y;
//x和y间有一条边
edges[x].push_back(y);
edges[y].push_back(x);
}
return 0;
}
2. 链式前向星(使用数组实现的链表)
链式前向星的本质是用数组来模拟链表
#include<iostream>
using namespace std;
int n;
const int N = 1e5 + 10;
//一条边的两个数据存储两遍,所有范围扩大2倍
int e[2 * N], ne[2 * N];
//h[N]表示每个父亲结点的集合
int h[N];
int id;
void put(int x, int y)
{
//先将y存储进数组
id++;
e[id] = y;
//将y进行头插进入父亲结点后
ne[id] = h[x];
//头标记移动
h[x] = id;
}
int main()
{
cin >> n;
for (int i = 1; i < n; i++)
{
int x, y; cin >> x >> y;
put(x, y); put(y, x);
}
return 0;
}
2. 树的遍历
树的遍历方式有两种,分别为深度优先遍历和宽度优先遍历
2.1 深度优先遍历(DFS)
从根结点出发,依次遍历每一颗子树;遍历子树时,重复第一步。
由此可以看出,深度优先遍历是一种递归形式
案例:
题目描述:给一棵树,共 n 个结点,编号 1~n
输入描述:第一行一个整数 n ,表示 n 个结点
接下来 n-1 行,每行两个整数 x,y,表示 x 和 y 间有一条边
1. 在 vector 存储下实现
#include<iostream>
using namespace std;
#include<vector>
const int N = 1e5 + 10;
int n;
//定义布尔数组进行标记
bool st[N];
vector<int> edges[N];
void dfs(int x)
{
//先打印该父结点
cout << x << " ";
//标记为打印过了
st[x] = true;
//依次遍历子结点
for (auto u : edges[x])
{
//如果没打印过,再次深度优先遍历
if (!st[x]) dfs(u);
}
}
int main()
{
cin >> n;
for (int i = 1; i < n; i++)
{
int x, y; cin >> x >> y;
edges[x].push_back(y);
edges[y].push_back(x);
}
//进行深度优先遍历
dfs(1);
return 0;
}
2. 在链式前向星存储下实现
#include<iostream>
using namespace std;
const int N = 1e5 + 10;
int e[2 * N], ne[2 * N];
int h[N];
bool st[N];
int n;
int id;
void put(int x, int y)
{
id++;
e[id] = y;
ne[id] = h[x];
h[x] = id;
}
void dfs(int x)
{
cout << x << " ";
st[x] = true;
//遍历每一个孩子
for (int i = h[x]; i; i = ne[i])
{
//递归
int v = e[i];
if (!st[v]) dfs(v);
}
}
int main()
{
cin >> n;
for (int i = 1; i < n; i++)
{
int x, y; cin >> x >> y;
put(x, y); put(y, x);
}
//DFS
dfs(1);
return 0;
}
dfs要将树的边扫描两边,边数量为 n-1 ,所以时间复杂度为O(N)
最坏情况,N个结点的树高也为N,此时深度为N,所以空间复杂度为O(N)
2.2 宽度优先遍历(BFS)
每次访问同一层的结点,同一层访问完再访问下一层
可以用队列实现:先初始化一个队列,根节点入队,同时标记;当队列不为空,依次拿出头元素访问,将队头元素的所有孩子入队,标记;重复该过程
案例:
题目描述:给一棵树,共 n 个结点,编号 1~n
输入描述:第一行一个整数 n ,表示 n 个结点
接下来 n-1 行,每行两个整数 x,y,表示 x 和 y 间有一条边
1. 用 vector 实现储存
#include<queue>
#include<iostream>
#include<vector>
using namespace std;
const int N = 1e5 + 10;
bool st[N];
vector<int> edges[N];
int n;
void bfs()
{
queue<int> q;
q.push(1);
st[1] = true;
while (q.size())
{
//父亲出队
auto u = q.front(); q.pop();
cout << u << " ";
//孩子入队
for (auto v : edges[u])
{
if (!st[v])
{
q.push(v);
st[v] = true;
}
}
}
}
int main()
{
cin >> n;
for (int i = 1; i < n; i++)
{
int x, y; cin >> x >> y;
edges[x].push_back(y);
edges[y].push_back(x);
}
bfs();
return 0;
}
2. 链式前向星储存
#include<iostream>
using namespace std;
#include<queue>
const int N = 1e5 + 10;
int e[2 * N], ne[2 * N];
int h[N];
int n;
int id;
bool st[N];
void put(int x, int y)
{
id++;
e[id] = y;
ne[id] = h[x];
h[x] = id;
}
void bfs()
{
queue<int> q;
q.push(1);
st[1] = true;
while (q.size())
{
auto u = q.front(); q.pop();
cout << u << " ";
//遍历孩子
for (int i = h[u]; i; i = ne[i])
{
int v = e[i];
if (!st[v])
{
q.push(v);
st[v] = true;
}
}
}
}
int main()
{
cin >> n;
for (int i = 1; i < n; i++)
{
int x, y; cin >> x >> y;
put(x, y); put(y, x);
}
bfs();
return 0;
}
所有结点入队一次,出队一次,时间复杂度O(N)
最坏情况下,所有非根结点同一层,队列最多 n - 1 个元素,空间复杂度O(N)