算法基础——树
一、树的概念
1. 树的定义
树型结构是⼀类重要的⾮线性数据结构。
2. 树的基本术语
3. 有序树和⽆序树
4. 有根树和⽆根树
注意:
如果是⽆根树,⽗⼦关系不明确,此时我们需要把所有的情况都存下来。⽐如a和b之间有⼀条 边,我们不仅要存a有⼀个孩⼦b,也要存b有⼀个孩⼦a。
甚⾄有的时候,在有根树的题⽬⾥,也要这样存储。
二、树的存储
1. 孩⼦表⽰法
2. 实现⽅式⼀:⽤vector数组实现
#include <iostream>
#include <vector>
using namespace std;
const int N = 1e5 + 10;
int n;
vector<int> edges[N]; // 存储树
int main()
{
cin >> n;
for(int i = 1; i < n; i++)
{
int a, b; cin >> a >> b;
// a 和 b 之间有一条边
edges[a].push_back(b);
edges[b].push_back(a);
}
return 0;
}
3. 实现⽅式⼆:链式前向星
#include <iostream>
using namespace std;
const int N = 1e5 + 10;
// 链式前向星
int h[N], e[N * 2], ne[N * 2], id;
int n;
// 其实就是把 b 头插到 a 所在的链表后面
void add(int a, int b)
{
id++;
e[id] = b;
ne[id] = h[a];
h[a] = id;
}
int main()
{
cin >> n;
for(int i = 1; i < n; i++)
{
int a, b; cin >> a >> b;
// a 和 b 之间有一条边
add(a, b); add(b, a);
}
return 0;
}
关于vector数组以及链式前向星:
- 前者由于⽤到了容器vector,实际运⾏起来相⽐较于后者更耗时,因为vector是动态实现的。
- 但是在如今的算法竞赛中,⼀般不会⽆聊到卡这种常数时间。也就是vector虽然慢,但不会因此⽽超时。
三、树的遍历
1. 深度优先遍历-DFS
时间复杂度:
简单估计⼀下,在 dfs 的整个过程中,会把树中所有的边扫描量两遍。边的数量为n − 1 ,因此时间复杂度为O(N) 。
空间复杂度:
最差情况下,结点个数为 n 的树,⾼度也是 n ,也就是变成⼀个链表。此时递归的深度也是 n ,此时的空间复杂度为O(N) 。
2. 宽度优先遍历-BFS
时间复杂度:
所有结点只会⼊队⼀次,然后出队⼀次,因此时间复杂度为 O(N) 。
空间复杂度:
最坏情况下,所有的⾮根结点都在同⼀层,此时队列⾥⾯最多有 n − 1 个元素,空间复杂度为 O(N) 。
3. 练习
(1)题目描述
题⽬描述:
给定⼀棵树,该树⼀共有 n 个结点,编号分别是 1 ∼ n 。
输⼊描述:
第⼀⾏⼀个整数 n ,表⽰ n 个结点。
接下来 n − 1 ⾏,每⾏两个整数 u, v ,表⽰ u 和 v 之间有⼀条边。
测试⽤例:
11 1 3 7 3 3 10 1 5 4 5 2 1 11 2 6 11 11 8 11 9
(2)实现代码
深度优先遍历——DFS
// // 用 vector 数组存储树
// #include <iostream>
// #include <vector>
// using namespace std;
// const int N = 1e5 + 10;
// int n;
// vector<int> edges[N]; // 存储图
// bool st[N]; // 标记哪些点已经访问过了
void dfs(int u)
{
cout << u << " ";
st[u] = true; // 当前这个点已经访问过了
// 访问所有的孩子
for(auto v : edges[u])
{
if(!st[v])
{
dfs(v);
}
}
}
// int main()
// {
// // 建树
// cin >> n;
// for(int i = 1; i < n; i++)
// {
// int a, b; cin >> a >> b;
// edges[a].push_back(b);
// edges[b].push_back(a);
// }
// // 深度优先遍历
// dfs(1);
// return 0;
// }
// 用链式前向星存储
#include <iostream>
using namespace std;
const int N = 1e5 + 10;
// 存图
int h[N], e[N * 2], ne[N * 2], id;
int n;
bool st[N]; // 标记哪些点已经访问过了
void add(int a, int b)
{
id++;
e[id] = b;
ne[id] = h[a];
h[a] = id;
}
void dfs(int u)
{
cout << u << " ";
st[u] = true;
for(int i = h[u]; 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 a, b; cin >> a >> b;
add(a, b); add(b, a);
}
// 深度优先遍历
dfs(1);
return 0;
}
// 斐波那契数列
int fib(int n)
{
if(n == 0 || n == 1) return n;
return fib(n - 1) + fib(n - 2);
}
宽度优先遍历——BFS
// 用 vector 数组存树
// #include <iostream>
// #include <queue>
// #include <vector>
// using namespace std;
// const int N = 1e5 + 10;
// int n;
// vector<int> edges[N]; // 存树
// bool st[N]; // 标记哪些节点已经访问过了
// void bfs()
// {
// queue<int> q;
// q.push(1);
// st[1] = true;
// while(q.size())
// {
// int 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 a, b; cin >> a >> b;
// edges[a].push_back(b);
// edges[b].push_back(a);
// }
// bfs();
// return 0;
// }
// 用链式前向星存储树
#include <iostream>
#include <queue>
using namespace std;
const int N = 1e5 + 10;
int n;
int h[N], e[N * 2], ne[N * 2], id;
bool st[N];
void add(int a, int b)
{
id++;
e[id] = b;
ne[id] = h[a];
h[a] = id;
}
void bfs()
{
queue<int> q;
q.push(1);
st[1] = true;
while(q.size())
{
int 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 a, b; cin >> a >> b;
add(a, b); add(b, a);
}
bfs();
return 0;
}