注意,不同教材在介绍森林的遍历方法时会有不同的表述,有些教材会将后序遍历称为中序遍历,但实质上遍历方法是一样的。
森林的先序遍历的规则:
访问森林中第一棵树的根结点
先序遍历森林中第一棵树的子树森林
先序遍历森林中,除第一棵树外其余树构成的森林
森林的后序遍历的规则:
后序遍历森林中第一棵树的根结点的各子树所构成的森林
访问森林中第一棵树的根结点
后序遍历森林中除第一棵树外其余树构成的森林
二叉树是树的特例,而树也可以看作森林的特例。在接下来的课程中,会学习一种用森林来表示的高效数据结构——并查集,包括初始化、查询和合并操作,以及两种非常有效的优化。
在计算机科学中,并查集(Merge-Find Set),也被称为不相交集合(Disjoint Set),是用于解决若干的不相交集合的如下几种操作的统称:
MAKE-SET(x):即初始化操作,建立一个只包含元素 x 的集合。
UNION(x, y):即合并操作,将包含 x 和 y 的集合合并为一个新的集合。
FIND-SET(x):即查询操作,计算 x 所在的集合。
并查集通常同时指代不相交集合的数据结构及其对应的算法,其在有些教材中的英文名称也叫做 Disjoint Set Union,表示用于求不相交集合并集的相关算法。
通常我们会用有根树来表示集合,树中的每一个结点都对应集合的一个成员,每棵树表示一个集合。
每个成员都有一条指向父结点的边,整个有根树通过这些指向父结点的边来维护。每棵树的根就是这个集合的代表,并且这个代表的父结点是它自己。
通过这样的表示方法,我们将不相交的集合转化为一个森林,也叫不相交森林。接下来会介绍,如何通过不相交森林实现并查集的初始化、合并和查询操作。
通常并查集初始化操作是对每个元素都建立一个只包含该元素的集合。这意味着每个成员都是自身所在集合的代表,所以我们只需要将所有成员的父结点设为它自己就好了。
在不相交森林中,并查集的查询操作,指的是查找出指定元素所在有根树的根结点是谁。我们可以通过每个指向父结点的边回溯到结点所在有根树的根,也就是对应集合的代表元素。
并查集的合并操作需要用到查询操作的结果。合并两个元素所在的集合,需要首先求出两个元素所在集合的代表元素,也就是结点所在有根树的根结点。接下来将其中一个根结点的父亲设置为另一个根结点。这样我们就把两棵有根树合并成一棵了。
并查集的查询操作最坏情况下的时间复杂度为 O(n)O(n),其中 n 为总元素个数。最坏情况发生时,每次合并对应到森林上都是一个点连到一条链的一端。此时如果每次都查询链的最底端,也就是最远离根的位置的元素时,复杂度便是 O(n)O(n) 了。
为了改善时间效率,可以通过启发式合并方法,将包含较少结点的树接到包含较多结点的树根上,可以防止树退化成一条链。另外,我们也可以通过路径压缩的方法来进一步减少均摊复杂度。同时使用这两种优化方法,可以将每次操作的时间复杂度优化至接近常数级。
含秩优化
#include <iostream>
using namespace std;
class DisjointSet {
private:
int *father,*rank;//rank保存每个节点为根时,秩的大小
public:
DisjointSet(int size) {
father = new int[size];
rank=new int[size];
for (int i = 0; i < size; ++i) {
father[i] = i;
rank[i]=0;
}
}
~DisjointSet() {
delete[] father;
delete[] rank;
}
int find_set(int node) {
if (father[node] != node) {
return find_set(father[node]);
}
return node;
}
bool merge(int node1, int node2) {
int ancestor1 = find_set(node1);
int ancestor2 = find_set(node2);
if (ancestor1 != ancestor2) {
if(rank[ancestor1] > rank[ancestor2]){
swap(ancestor1 , ancestor2);
}
father[ancestor1] = ancestor2;
rank[ancestor2]=max(rank[ancestor1]+1,rank[ancestor2]);
return true;
}
return false;
}
};
int main() {
DisjointSet dsu(100);
int m, x, y;
cin >> m;
for (int i = 0; i < m; ++i) {
cin >> x >> y;
bool ans = dsu.merge(x, y);
if (ans) {
cout << "success" << endl;
} else {
cout << "failed" << endl;
}
}
return 0;
}
含路径优化
#include <iostream>
using namespace std;
class DisjointSet {
private:
int *father, *rank;
public:
DisjointSet(int size) {
father = new int[size];
rank = new int[size];
for (int i = 0; i < size; ++i) {
father[i] = i;
rank[i] = 0;
}
}
~DisjointSet() {
delete[] father;
delete[] rank;
}
int find_set(int node) {
//路径优化了
if (father[node] != node) {
//根不是father的情况
father[node]=find_set(father[node]);
//已经将node的根复制给node的father了
}
return father[node];
}
bool merge(int node1, int node2) {
int ancestor1 = find_set(node1);
int ancestor2 = find_set(node2);
if (ancestor1 != ancestor2) {
if (rank[ancestor1] > rank[ancestor2]) {
swap(ancestor1, ancestor2);
}
father[ancestor1] = ancestor2;
rank[ancestor2] = max(rank[ancestor1] + 1, rank[ancestor2]);
return true;
}
return false;
}
};
int main() {
DisjointSet dsu(100);
int m, x, y;
cin >> m;
for (int i = 0; i < m; ++i) {
cin >> x >> y;
bool ans = dsu.merge(x, y);
if (ans) {
cout << "success" << endl;
} else {
cout << "failed" << endl;
}
}
return 0;
}