并查集的实现有三个主要的方法:
- 初始化
- 查找该节点的根节点
- 合并
public interface DjsetsFunction {
public void init();
public int findRoot(int index); //return index of finding
public void union(String i, String j);
}
初始化
我采用的是链表的方式,其实数组等其他的结构都是可以的,考虑到数组需要在添加完所有数据之后才能初始化,所以我采用了链表,可以new对象一边添加数据。
初始化之前,我们需要有几个链表来存储一些相关的东西:
- int size:存储当前的节点的个数
- ArrayList parents:存储第i个节点的根节点;初始设置时,每个节点的根节点就是它自己
- ArrayList rank:存储第i个节点作为根节点时子树的深度;初始设置时,每个节点的rank是1层
public Djsets(String name) {
parents.add(size++);
names.add(name);
rank.add(1);
}
如果没有rank存储子树的深度的话,在合并的时候我们会将两个参数传入合并函数,此时要不就是左边节点变成右边的父节点,要不就是右边节点变成左边节点的父节点,这个过程的代码是写死的,不能发生变化。此时,如果出现了1-2(1节点与2节点合并),2-3,3-4…的情况,就会变成一个链表,而且递归寻找根节点的时候,需要全部遍历一次链表。这种情况下的时间复杂度是O(n)。为了降低时间复杂度,可以采用树的结构,就引入了rank这个深度的概念。在每次合并的时候,深度小的树合并到深度大的树里,这样树的深度会是两者的较大者,且发生变化。
查找根节点
第一种方式:
public int findRoot(int index) {
// TODO Auto-generated method stub
return parents.get(index);
}
这是最直接的通过链表里存放的数据返回当前节点的根节点的方式。但是这样不一定保证在别的地方调用一次findRoot函数就可以找到根节点,所以采用另一种实现的方式:
public int findRoot(int index) {
// TODO Auto-generated method stub
while(parents.get(index) != index) {
parents.set(index, parents.get(parents.get(index)));
index = parents.get(index);
}
return index;
}
通过把父节点改为祖父节点的方式,循环调用,在根节点的位置时,根节点没有父节点,只能指向自己,从而退出循环,返回真正的根节点。
合并
合并是我认为的并查集里最重要的部分。
首先当然是传入两个要合并的节点,然后找到这两个节点的根节点,要是属于同一个根节点,那就说明这两个节点已经合并过了,在“图”中有可能出现了回环的情况。要是两个节点的根节点不相同,则表示这两个节点所在的树不是同一棵,可以进行合并。
我们之前有讲到rank的概念,这里就要对每个节点的rank进行考虑。这个是我参考其他的讲解写的代码。
public void union(String i, String j) {
// TODO Auto-generated method stub
int iRoot = findRoot((names.indexOf(i)));
int jRoot = findRoot((names.indexOf(j)));
if(iRoot == jRoot) {
System.out.println(i + " " + j + " have combined");
}else {
if(rank.get(names.indexOf(i)) > rank.get(names.indexOf(j))) {
parents.set(names.indexOf(j), iRoot);
}else if(rank.get(names.indexOf(i)) < rank.get(names.indexOf(j))) {
parents.set(names.indexOf(i), jRoot);
}else {
parents.set(names.indexOf(i), jRoot);
rank.set(names.indexOf(j), rank.get(names.indexOf(j))+1);
}
}
}
以下是连接和合并的顺序:
String[][] NO1 = {
{"A","B"},
{"B","C"},
{"B","D"},
{"C","E"},
{"D","F"},
{"E","F"}
};
但是在运行的时候会存在一些问题:
我们绘制一幅这样的图:
A - B
/ |
C - D
| |
E - F
但是执行之后,情况变成了:
A-B D-F C-E
其实在运行 A B C D 的时候,运行的结果是这样的:
B
/|\
A C D
但是当C E合并的时候,因为此时 C 为根节点的子树的深度是 1, E 也同理,那么就会执行
parents.set(names.indexOf(i), jRoot);// i == "C", j == "E"
这时,C 的根节点就会变成了 E,但实际上我们希望的是在C的基础上继续添加E
D-F 同理
为了避免这种情况,我们需要找到在树中的 C 与 未被合并的 E 之前的区别,那就是他们的根节点不同,所以我们需要加上根节点的判断
public void union(String i, String j) {
// TODO Auto-generated method stub
int iRoot = findRoot((names.indexOf(i)));
int jRoot = findRoot((names.indexOf(j)));
if(iRoot == jRoot) {
System.out.println(i + " " + j + " have combined");
}else {
if(rank.get(names.indexOf(i)) > rank.get(names.indexOf(j))) {
parents.set(names.indexOf(j), iRoot);
}else if(rank.get(names.indexOf(i)) < rank.get(names.indexOf(j))) {
parents.set(names.indexOf(i), jRoot);
}else {
if(parents.get(names.indexOf(i)) != names.indexOf(i)
&& parents.get(names.indexOf(j)) == names.indexOf(j)) {
parents.set(names.indexOf(j), iRoot);
rank.set(names.indexOf(i), rank.get(names.indexOf(i))+1);
}else {
parents.set(names.indexOf(i), jRoot);
rank.set(names.indexOf(j), rank.get(names.indexOf(j))+1);
}
}
}
}
我们还可以在最后加一段代码判断建立好的结构里面,两点是不是联通的:
public boolean isConnected(String i, String j) {
int l = names.indexOf(i);
int r = names.indexOf(j);
if(this.findRoot(l) == this.findRoot(r)) {
return true;
}else {
return false;
}
}
最后加入测试代码:
public class DjsetsTest {
public static void main(String[] args) {
Djsets dj1 = new Djsets("A");
Djsets dj2 = new Djsets("B");
Djsets dj3 = new Djsets("C");
Djsets dj4 = new Djsets("D");
Djsets dj5 = new Djsets("E");
Djsets dj6 = new Djsets("F");
Djsets dj = new Djsets();
System.out.println("names: " + dj.names);
System.out.println("init root: " + dj.parents);
System.out.println("init rank: " + dj.rank);
System.out.println();
String[][] NO1 = {
{"A","B"},
{"B","C"},
{"B","D"},
{"C","E"},
{"D","F"},
{"E","F"}
};
for(int i=0; i<6; i++ ) {
String l = NO1[i][0];
String r = NO1[i][1];
dj.union(l, r);
}
System.out.println("A C are connected: " + dj.isConnected("A", "C"));
}
}
返回结果如下:
names: [A, B, C, D, E, F]
init root: [0, 1, 2, 3, 4, 5]
init rank: [1, 1, 1, 1, 1, 1]
E F have combined
A C are connected: true
(最后感谢b站up:正月点灯笼 & 中二病也要学算法 的投稿,对于我理解并查集提供了很多帮助)