图解并查集
作者:互联网
前言
并查集是一种非常有用且高效的数据结构,千万不要被这个极具专业性的名字吓到了,它的算法思想和代码实现都非常简单,不需要花太大力气就可以轻松掌握。下面就通过画图等方式为大家介绍一下这种神奇的数据结构。
一、 图解并查集
并查集有两个英文名:1、Disjoint Set,2、Union Find。它的作用就是把一个数据集分成若干个子集,每个子集内部数据可以互联互通,而子集之间则不具有连通性。并查集的底层结构类似于堆(不熟悉堆的同学赶紧去复习一下堆排序,面试频率很高哦),也是用数组描述一种树结构,但不同的是,堆是一棵独立的二叉树,并查集的树是多叉树,而且可能不止一棵树(也就是森林)。在并查集中,我们把相互独立的数据集称为连通分量,连通分量在逻辑上可以形象地描述为一棵树,每棵树都有一个根节点,其他的元素都会直接或间接指向根节点。
比如下图这个并查集,我们维护一个parent数组,每个元素初始化为对应的数组下标,代表自己是独立的一棵树,且是树根。以第一棵树为例,在后续数据处理过程中,我们把与所有与"2"同属一个连通分量的元素都连到"2"上,并把数组对应下标的元素赋值为2,其中"5"先连接到了"1"上,"1"又连接到了"2"上。最后,数组每个元素都代表其指向的父节点。
并查集底层结构
知道了并查集的底层结构,那我们该如何去构建这个结构并进行查找操作呢?并查集有两个最重要的方法:union 和 find,这里先给出UnionFind 最完善的 API 框架伪代码:
public class UnionFind {
public UnionFind(int N) {} // 构造方法
public int find(int x) {} //查找某个元素的根节点
public void union(int x, int y) {} // 为x和y建立联系
public boolean connected(int x, int y) {} //判断x和y是否相连(在同一棵树也就是连通分量中)
public int count() {} // 返回连通分量的个数,也就是多少棵树
}
1. find 方法实现
find方法的目的是寻找某个元素所在树的根节点,代码如下:
public int find(int x) {
while (x != parent[x]) {
x = parent[x];
}
return x;
}
解释一下这短短几行代码做了什么,前面的介绍我们已经知道,根节点元素的数组值就是自身的下标,也就是parent[x] = x; 那么当数组元素值不等于其下标时,说明它不是根节点,就一直循环找下去,直到找到根节点。 如下图是寻找5的根节点的过程:
寻找根节点的过程
2. union 方法实现
union 方法顾名思义就是把两个元素联系起来,具体的做法先找到各自的根节点,再把其中一个元素的根节点连接到另一个元素的根节点上,代码如下:
public void union(int x, int y) {
int rootX = find(x);
int rootY = find(y);
// 根节点相同则无需操作
if (rootX == rootY) {
return;
}
parent[rootX] = rootY;
}
下图是对元素 4 和 5 进行 union 的操作示意图:
union 操作
3. 并查集优化:路径压缩和按秩合并
从上图中我们可以看出一个问题,如果 union 操作直接将两棵树合并,那么多次 union 之后,树的高度可能会很高,那么寻找一个节点的根节点的路径就会很长,导致查找效率降低,那该如何对其进行优化呢?主要有两种优化方式,那就是路径压缩和按秩合并,路径压缩是在 find 方法里进行,而按秩合并则是在 union 方法中进行,二者选其一即可,前者代码更简洁。
3.1 路径压缩
路径压缩又有两种方式:隔代压缩和彻底压缩
- 「隔代压缩」性能比较高,虽然压缩不完全,不过多次执行「隔代压缩」也能达到比较好的效果,只需要在原 find 方法中加上一句parent[x] = parent[parent[x]];这句代码的意思是把路径上的每个节点的父节点指向其祖父节点。
- 「彻底压缩」需要借助系统栈,使用递归的写法 。或者使用迭代写法,先找到当前结点的根结点,然后把沿途上所有的节点都指向根节点,需要遍历两次。彻底压缩需要消耗更多的性能,但是压缩的更彻底,可以提高查询效率。
隔代压缩与彻底压缩 此图引用了leetcode题解
3.2 按秩合并
这个“秩”一般是指树的高度,也有地方解释为树节点个数,我们这里取前者。具体实现就是新增一个ranks数组记录以各个元素为根节点的树的高度,在做合并操作时,把高度较小的树的根节点连接到高度较大的树的根节点上。如下图,在未优化前,合并后的树高度增加为4,而按秩合并后的树高仍然为3。这里要注意的是,如果两棵树高度相同,那么两者均可作为根节点,则合并后的新树高度需要加一。
按秩合并
// 如果代码片段看不懂,可以结合后面的完整版代码去理解
public void union(int x, int y) {
int rootX = find(x);
int rootY = find(y);
// 根节点相同则不需要操作
if (rootX == rootY) {
return;
}
// 比较树高,高度小的树合并到另一颗树上,相等的话两者均可作为根节点,并把高度加一
if (ranks[rootX] > ranks[rootY]) {
parent[rootY] = rootX;
} else if (ranks[rootX] < ranks[rootY]) {
parent[rootX] = rootY;
} else {
parent[rootY] = rootX;
ranks[rootX]++;
}
count--;
}
4. 完整代码
4.1 路径压缩版本
class UnionFind {
private int[] parent;
// count用来记录连通分量的个数
private int count;
public UnionFind(int n) {
// count初始化为n,也就是最开始有n个连通分量(n棵树)
count = n;
// parent数组各个元素初始化为其自身下标,代表自己是一棵树
parent = new int[n];
for (int i = 0; i < n; i++) {
parent[i] = i;
}
}
/**查找根节点*/
public int find(int x) {
while (x != parent[x]) {
// 路径压缩(隔代压缩)
parent[x] = parent[parent[x]];
x = parent[x];
}
return x;
}
/**合并操作*/
public void union(int x, int y) {
int rootX = find(x);
int rootY = find(y);
// 根节点相同则不需要操作
if (rootX == rootY) {
return;
}
parent[rootX] = rootY;
// 合并之后连通分量(树)个数减一
count--;
}
/**判断x和y是否在同一个连通分量(同一棵树)*/
public boolean isConnected(int x, int y) {
return find(x) == find(y);
}
/**返回连通分量个数*/
public int count() {
return count;
}
}
4.2 按秩合并版本
class UnionFind {
private int[] parent;
//新加一个ranks数组,记录树的高度
private int[] ranks;
// count记录连通分量的个数
private int count;
public UnionFind(int n) {
count = n;
parent = new int[n];
for (int i = 0; i < n; i++) {
parent[i] = i;
}
// 高度都初始化为1
ranks = new int[n];
for (int i = 0; i < n; i++) {
ranks[i] = 1;
}
}
/**按秩合并版本的 find 方法不需要做优化*/
public int find(int x) {
while (x != parent[x]) {
x = parent[x];
}
return x;
}
/**按秩合并*/
public void union(int x, int y) {
int rootX = find(x);
int rootY = find(y);
if (rootX == rootY) {
return;
}
if (ranks[rootX] > ranks[rootY]) {
parent[rootY] = rootX;
} else if (ranks[rootX] < ranks[rootY]) {
parent[rootX] = rootY;
} else {
parent[rootY] = rootX;
ranks[rootX]++;
}
count--;
}
public boolean isConnected(int x, int y) {
return find(x) == find(y);
}
public int count() {
return count;
}
}
标签:parent,int,查集,find,rootY,rootX,图解,节点 来源: https://blog.csdn.net/qq_25933249/article/details/112597361