本文我用Java实现不相交集类(DisjointSet)的数据结构与算法。在这里先解释一下不相交集类,你可以想象成为刚开始有N个独立的元素,N个元素两两都不等价(不属于一个同等价集合,刚开始有N个等价集合,每个等价集合都只有一个元素,且这个元素就是等价集合的根元素,可以理解为等价集合的代表)。然后可以通过合并操作,把两个元素变为等价,随着后面合并操作的量越来越大,两个元素的合并就等于这两个元素所代表的两个等价集合的合并,最后合并成一个最大的等价集合。除了合并操作,还有寻找操作,就是寻找一个元素所在的等价集合的根元素。如果两个元素寻找的根元素相等,则代表这两个元素在同一个等价集合,即二者等价。反之则证明这两个元素不在一个等价集合,不等价。
举个例子说明:刚开始的时候有很多的武林人士(感觉都喜欢用武林人士来举例子),他们都是游离的,如何知道自己和别人是否是同一个门派的呢?你遇到了同一个门派的,你们两个就合并,或者你认识了同一个门派的一个人,那个人已经聚团了,那你可以并到他们的集体。还有就是两个人是两个聚团的,然后发现是同一门派的,那么两个聚团就合并为一个团。聚团里面所有人都是等价的,都属于一个门派:
-
比如现在有一群人,他们刚开始都不认识,也就是两两都不等价,各自是一个等价集合,且等价集合的代表就是他自己:
[张无忌] [谢逊] [韦一笑] [殷天正] [空见大师] [空闻大师] [扫地神僧] [王重阳] [丘处机] [欧阳锋] -
然后张无忌和谢逊父子相识,韦一笑和殷天正老朋友遇到了,这些朋友两两合并为集合。结果如下:
[张无忌, 谢逊] [韦一笑, 殷天正] [空见大师] [空闻大师] [扫地神僧] [王重阳] [丘处机] [欧阳锋] -
后来少林寺开大会,空见空闻两位大师诵经念佛,共同钻研佛法,就遇到了,王重阳和丘处机去看热闹,也认识了,也是两两合并为集合:
[张无忌, 谢逊] [韦一笑, 殷天正] [空见大师, 空闻大师] [扫地神僧] [王重阳, 丘处机] [欧阳锋] -
再后来,张无忌又认了自己的外公殷天正,然后张无忌和殷天正所在的两个集合,合并为一个大集合:
[张无忌, 谢逊, 韦一笑, 殷天正] [空见大师, 空闻大师] [扫地神僧] [王重阳, 丘处机] [欧阳锋] -
然后空闻大师发现,我们少林还有一个扫地神僧得嘛,以前没发现,赶紧认识认识,然后空闻大师所在的集合与扫地神僧合并:
[张无忌, 谢逊, 韦一笑, 殷天正] [空见大师, 空闻大师, 扫地神僧] [王重阳, 丘处机] [欧阳锋] -
合并到现在,这里面的所有人都找到自己人了,这就是不相交集类的合并操作
我对合并的实现,则是通过一棵树通过某些方式合并到另一棵树,生成更大的树进行合并操作的。一个等价集合就是一棵树。我们可以通过查找两个元素的树的根节点,来判断是否位于同一棵树(在同一棵树就意味着二者是等价关系)。比如谢逊和韦一笑,都位于明教这棵树,根节点都找到张无忌了,那就是一个门派的。扫地神僧和欧阳锋分别找根节点,发现根节点是空闻大师和欧阳锋,不是同一个根,不在同一颗树上,也就不等价了。
实现不相交集类(DisjointSet),我这里有四种实现:
-
普通方法实现:合并操作先找到两个元素所在的树的根,然后直接把一棵树的根,作为另一棵树的根的一个子节点。查找则是朝着树的根节点方向递归查找,直到找到树的根节点。这种方式,根节点的数据为-1
-
查找优化实现:合并两棵树的方式与普通方法实现一样,只是说在查找的时候,记录下查找路径,然后路径上所有的等价元素,最后直接全部让他们指向根元素,也就是树的那一支全部打散,变为根的子节点。以后如果需要查这些节点,找一次就找到了,提升了效率。根据算法的实现,查得越多,查的效率就越高。这种方式,根节点的数据也为-1
-
大小依赖合并:合并两棵树,则是看两棵树谁大谁小。元素多的为大,元素少的为小。然后把小的树的根节点,作为大的树的根节点的子节点,这种优化可以让树的深度不会太深,提高查找的效率。这种方式,根节点的数据为树的大小的负数,相当于用负数表示根节点,绝对值记录树的大小
-
高度依赖合并:合并两棵树,则是看两棵树谁高谁矮。把矮的树的根节点作为高的树的根节点的子节点,这种优化可以让树的高度不会太高,也是提高了查找的效率。这种方式,根节点的数据为树的高度的负数,相当于用负数表示根节点,绝对值记录树的高度
接下来是我用Java实现不相交集类(DisjointSet)的代码,更详细的解释和算法精髓都在注释中:
- DisjointSet接口类,所有不相交集类的实例都要实现该接口里面的操作方法(find查找根元素,union合并元素,size返回元素个数):
/**
2. @author LiYang
3. @InterfaceName DisjointSet
4. @Description 不相交集类(并查集)的公用接口
5. @date 2019/11/14 10:42
*/
public interface DisjointSet {
//不相交集类(并查集)的元素个数
int size();
//查找元素在不相交集类(并查集)的根元素
//注意,这里都是操作数组下标,而每个下标
//可以映射到具体的事物
int find(int elementIndex);
//不相交集类(并查集)的两个元素的合并操作
//注意,这里也是下标的合并,也可映射具体事物
void union(int elementIndex1, int elementIndex2);
}
- DisjointSetUtil不相交集工具类,可以统计合并后的所有根节点,以及所在等价集合里面的所有等价元素:
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
/**
1. @author LiYang
2. @ClassName DisjointSetUtil
3. @Description 不相交集类(并查集)的工具类
4. @date 2019/11/14 10:40
*/
public class DisjointSetUtil {
/**
* 返回不相交集类(并查集)所有的根,以及对应的所有的等价元素
* @param disjointSet 不相交集类(并查集)
* @return Map<根,根的所有等价元素>
*/
public static Map<Integer, Set<Integer>> arrangeDisjointSet(DisjointSet disjointSet) {
//装结果的数据Map
Map<Integer, Set<Integer>> result = new HashMap<>();
//将不相交集类(并查集)所有元素遍历
for (int i = 0; i < disjointSet.size(); i++) {
//先找到当前元素的根元素
int root = disjointSet.find(i);
//如果数据Map中有根统计结果了
if (result.containsKey(root)) {
//拿到该根的等价Set,装入当前元素
result.get(root).add(i);
//如果数据Map中没有根统计结果
} else {
//创建当前根的等价Set
Set<Integer> equivalentSet = new HashSet<>();
//加入当前元素
equivalentSet.add(i);
//将当前根和等价Set放入数据Map
result.put(root, equivalentSet);
}
}
//统计完成,返回根和所有等价元素的统计结果Map
return result;
}
}
- 不相交集类(并查集)直接合并的实现类(普通方法实现):
import java.util.Map;
import java.util.Set;
/**
* @author LiYang
* @ClassName DisjointSetUnionDirectly
* @Description 不相交集类(并查集)直接合并的实现类
* @date 2019/11/14 10:29
*/
public class DisjointSetUnionDirectly implements DisjointSet {
//不相交集类(并查集)的元素数组
private int[] elements;
/**
* 不相交集类(并查集)的构造方法,入参元素个数
* @param elementSize 元素个数
*/
public DisjointSetUnionDirectly(int elementSize) {
if (elementSize <= 0) {
throw new IllegalArgumentException("不相交集合的元素个数要大于零");
}
//实例化化不相交集类(并查集)的元素数组
this.elements = new int[elementSize];
//初始化元素数组都为-1
for (int i = 0; i < elements.length; i++) {
elements[i] = -1;
}
}
/**
* 查询不相交集类(并查集)的元素个数
* @return 元素个数
*/
@Override
public int size() {
return elements.length;
}
/**
* 查询不相交集类(并查集)的某个元素的根元素
* 输入的是下标查,如果两个元素的根元素相同,
* 则这两个元素就是等价的。实际中还会有一个
* 与elements等长的数组,装的是真实的元素,
* elements只是相当于代号,记录等价关系,
* 二者通过下标,来映射真实元素
* @param elementIndex 待查询的元素下标
* @return 该元素的根元素
*/
@Override
public int find(int elementIndex) {
//如果记录小于0,那就是根
if (elements[elementIndex] < 0) {
//返回根元素
return elementIndex;
//如果记录不小于0,那还不是根,
//是等价森林中的上一个节点
} else {
//递归向上继续寻找根
return find(elements[elementIndex]);
}
}
/**
* 将不相交集类(并查集)的两个元素进行合并操作
* 注意,两个元素合并,代表这两个元素所在的两个
* 等价集合,全部变成一个大的等价集合。如果这
* 两个元素本来就等价,则不进行合并操作。
* 注意,这里同样是入参下标,下标映射真实元素
* @param elementIndex1 元素下标1
* @param elementIndex2 元素下标2
*/
@Override
public void union(int elementIndex1, int elementIndex2) {
//找到这两个元素的根
int root1 = find(elementIndex1);
int root2 = find(elementIndex2);
//如果两个元素的根相同,那这两个元素本来就等价
if (root1 == root2) {
//既然等价,那就不操作
return;
}
//如果不等价,则将第一个元素的树的根,合并到第二个
//元素的树的根上,后者的根的一个子树即为前者
elements[root1] = root2;
}
/**
* 相交集类(并查集)直接合并的实现类测试
* @param args
*/
public static void main(String[] args) {
//实例化相交集类(并查集)直接合并的实现类,并初始化元素个数为8个
DisjointSetUnionDirectly disjointSetUnionDirectly = new DisjointSetUnionDirectly(8);
//进行一系列的合并操作,其中包含已等价的合并
disjointSetUnionDirectly.union(2,3);
disjointSetUnionDirectly.union(5,6);
disjointSetUnionDirectly.union(3,5);
disjointSetUnionDirectly.union(0,7);
disjointSetUnionDirectly.union(2,6);
//获取该相交集类(并查集)实现类的等价记录Map
Map<Integer, Set<Integer>> equivalentMap = DisjointSetUtil.arrangeDisjointSet(disjointSetUnionDirectly);
System.out.println("不相交集类(并查集)直接合并的实现类测试结果:");
//输出等价记录Map的内容,进行测试验证
for (Map.Entry<Integer, Set<Integer>> item : equivalentMap.entrySet()) {
System.out.println(String.format("root:%d, elements:%s", item.getKey(), item.getValue().toString()));
}
}
}
运行main方法的测试用例,控制台输出如下,测试通过:
不相交集类(并查集)直接合并的实现类测试结果:
root:1, elements:[1]
root:4, elements:[4]
root:6, elements:[2, 3, 5, 6]
root:7, elements:[0, 7]
- 不相交集类(并查集)查找时调整优化树结构的实现类(查找优化实现):
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
/**
* @author LiYang
* @ClassName DisjointSetFindAdjust
* @Description 不相交集类(并查集)查找时调整优化树结构的实现类
* @date 2019/11/14 11:37
*/
public class DisjointSetFindAdjust implements DisjointSet {
//不相交集类(并查集)的元素数组
private int[] elements;
/**
* 不相交集类(并查集)的构造方法,入参元素个数
* @param elementSize 元素个数
*/
public DisjointSetFindAdjust(int elementSize) {
if (elementSize <= 0) {
throw new IllegalArgumentException("不相交集合的元素个数要大于零");
}
//实例化化不相交集类(并查集)的元素数组
this.elements = new int[elementSize];
//初始化元素数组都为-1
for (int i = 0; i < elements.length; i++) {
elements[i] = -1;
}
}
/**
* 查询不相交集类(并查集)的元素个数
* @return 元素个数
*/
@Override
public int size() {
return elements.length;
}
/**
* 查询不相交集类(并查集)的某个元素的根元素
* 输入的是下标查,如果两个元素的根元素相同,
* 则这两个元素就是等价的。实际中还会有一个
* 与elements等长的数组,装的是真实的元素,
* elements只是相当于代号,记录等价关系,
* 二者通过下标,来映射真实元素
* 注意,此实现类的find在往根节点方向寻找的路径
* 中的所有元素会被记录,最后全部直接指向根元素
* @param elementIndex 待查询的元素下标
* @return 该元素的根元素
*/
@Override
public int find(int elementIndex) {
//盛放find寻根路上所有的等价节点
Set<Integer> equivalentSet = new HashSet<>();
//如果还没找到根
while (elements[elementIndex] >= 0) {
//将当前寻路的元素放入等价Set中
equivalentSet.add(elementIndex);
//更新elementIndex
elementIndex = elements[elementIndex];
}
//while循环结束,也就是找到根了,当前elementIndex就是根
//将集合里面的所有等价元素,全部合并到根上
for (Integer equivalentElement : equivalentSet) {
//这下根就有很多子节点了
//随着find的不断调用,树的平均高度越来越接近2
//然后以后的find的效率就会越来越高了
elements[equivalentElement] = elementIndex;
}
//调整完毕后,返回找到的根节点
return elementIndex;
}
/**
* 将不相交集类(并查集)的两个元素进行合并操作
* 注意,两个元素合并,代表这两个元素所在的两个
* 等价集合,全部变成一个大的等价集合。如果这
* 两个元素本来就等价,则不进行合并操作。
* 注意,这里同样是入参下标,下标映射真实元素
* 此实现类沿用了直接合并的方式,指望find方法
* 能够在查找的过程中不断调整和优化查找效率
* @param elementIndex1 元素下标1
* @param elementIndex2 元素下标2
*/
@Override
public void union(int elementIndex1, int elementIndex2) {
//找到这两个元素的根
int root1 = find(elementIndex1);
int root2 = find(elementIndex2);
//如果两个元素的根相同,那这两个元素本来就等价
if (root1 == root2) {
//既然等价,那就不操作
return;
}
//如果不等价,则将第一个元素的树的根,合并到第二个
//元素的树的根上,后者的根的一个子树即为前者
elements[root1] = root2;
}
/**
* 不相交集类(并查集)查找时调整优化树结构的实现类测试
* @param args
*/
public static void main(String[] args) {
//实例化不相交集类(并查集)查找时调整优化树结构的实现类,并初始化元素个数为8个
DisjointSetFindAdjust disjointSetFindAdjust = new DisjointSetFindAdjust(8);
//进行一系列的合并操作,其中包含已等价的合并
disjointSetFindAdjust.union(2,3);
disjointSetFindAdjust.union(5,6);
disjointSetFindAdjust.union(3,5);
disjointSetFindAdjust.union(0,7);
disjointSetFindAdjust.union(2,6);
//获取该相交集类(并查集)实现类的等价记录Map
Map<Integer, Set<Integer>> equivalentMap = DisjointSetUtil.arrangeDisjointSet(disjointSetFindAdjust);
System.out.println("不相交集类(并查集)查找时调整优化树结构的实现测试结果:");
//输出等价记录Map的内容,进行测试验证
for (Map.Entry<Integer, Set<Integer>> item : equivalentMap.entrySet()) {
System.out.println(String.format("root:%d, elements:%s", item.getKey(), item.getValue().toString()));
}
}
}
运行main方法的测试用例,控制台输出如下,测试通过:
不相交集类(并查集)查找时调整优化树结构的实现测试结果:
root:1, elements:[1]
root:4, elements:[4]
root:6, elements:[2, 3, 5, 6]
root:7, elements:[0, 7]
- 不相交集类(并查集)按树的大小合并的实现类(大小依赖合并):
import java.util.Map;
import java.util.Set;
/**
* @author LiYang
* @ClassName DisjointSetUnionBySize
* @Description 不相交集类(并查集)按树的大小合并的实现类
* @date 2019/11/14 10:54
*/
public class DisjointSetUnionBySize implements DisjointSet {
//不相交集类(并查集)的元素数组
private int[] elements;
/**
* 不相交集类(并查集)的构造方法,入参元素个数
* @param elementSize 元素个数
*/
public DisjointSetUnionBySize(int elementSize) {
if (elementSize <= 0) {
throw new IllegalArgumentException("不相交集合的元素个数要大于零");
}
//实例化化不相交集类(并查集)的元素数组
this.elements = new int[elementSize];
//初始化元素的大小都为-1(如果是根,值就是负数,等价元素组成的树
//的大小是多少,则根元素就是负几)
for (int i = 0; i < elements.length; i++) {
elements[i] = -1;
}
}
/**
* 查询不相交集类(并查集)的元素个数
* @return 元素个数
*/
@Override
public int size() {
return elements.length;
}
/**
* 查询不相交集类(并查集)的某个元素的根元素
* 输入的是下标查,如果两个元素的根元素相同,
* 则这两个元素就是等价的。实际中还会有一个
* 与elements等长的数组,装的是真实的元素,
* elements只是相当于代号,记录等价关系,
* 二者通过下标,来映射真实元素
* @param elementIndex 待查询的元素下标
* @return 该元素的根元素
*/
@Override
public int find(int elementIndex) {
//如果记录小于0,那就是根
if (elements[elementIndex] < 0) {
//返回根元素
return elementIndex;
//如果记录不小于0,那还不是根,
//是等价森林中的上一个节点
} else {
//递归向上继续寻找根
return find(elements[elementIndex]);
}
}
/**
* 将不相交集类(并查集)的两个元素进行合并操作
* 注意,两个元素合并,代表这两个元素所在的两个
* 等价集合,全部变成一个大的等价集合。如果这
* 两个元素本来就等价,则不进行合并操作。
* 注意,这里同样是入参下标,下标映射真实元素
* 此实现类,根据树的大小来决定谁合并到谁上面,
* 小的树的根节点,会作为大的树的根节点的子节点
* @param elementIndex1 元素下标1
* @param elementIndex2 元素下标2
*/
@Override
public void union(int elementIndex1, int elementIndex2) {
int root1 = find(elementIndex1);
int root2 = find(elementIndex2);
//如果两个元素本属于一个集合
if (root1 == root2) {
//不作处理
return;
}
//比大小:如果root1比root2的树要大
if (elements[root1] < elements[root2]) {
//合并后,root1的高度为二者之和
elements[root1] = elements[root1] + elements[root2];
//将较小的root2合并到较大的root1上
elements[root2] = root1;
//比大小:如果root2的树大于等于root1的树
} else {
//合并后,root2的高度为二者之和
elements[root2] = elements[root2] + elements[root1];
//将较小或相等的root1合并到较大或相等的root2上
elements[root1] = root2;
}
}
/**
* 不相交集类(并查集)按树的大小合并的实现类测试
* @param args
*/
public static void main(String[] args) {
//实例化不相交集类(并查集)按树的大小合并的实现类,并初始化元素个数为8个
DisjointSetUnionBySize disjointSetUnionBySize = new DisjointSetUnionBySize(8);
//进行一系列的合并操作,其中包含已等价的合并
disjointSetUnionBySize.union(2,3);
disjointSetUnionBySize.union(5,6);
disjointSetUnionBySize.union(3,5);
disjointSetUnionBySize.union(0,7);
disjointSetUnionBySize.union(2,6);
//获取该相交集类(并查集)实现类的等价记录Map
Map<Integer, Set<Integer>> equivalentMap = DisjointSetUtil.arrangeDisjointSet(disjointSetUnionBySize);
System.out.println("不相交集类(并查集)按树的大小合并的实现测试结果:");
//输出等价记录Map的内容,进行测试验证
for (Map.Entry<Integer, Set<Integer>> item : equivalentMap.entrySet()) {
System.out.println(String.format("root:%d, elements:%s", item.getKey(), item.getValue().toString()));
}
}
}
运行main方法的测试用例,控制台输出如下,测试通过:
不相交集类(并查集)按树的大小合并的实现测试结果:
root:1, elements:[1]
root:4, elements:[4]
root:6, elements:[2, 3, 5, 6]
root:7, elements:[0, 7]
- 不相交集类(并查集)按树的高度合并的实现类(高度依赖合并):
import java.util.Map;
import java.util.Set;
/**
* @author LiYang
* @ClassName DisjointSetUnionByHeight
* @Description 不相交集类(并查集)按树的高度合并的实现类
* @date 2019/11/14 11:21
*/
public class DisjointSetUnionByHeight implements DisjointSet {
//不相交集类(并查集)的元素数组
private int[] elements;
/**
* 不相交集类(并查集)的构造方法,入参元素个数
* @param elementSize 元素个数
*/
public DisjointSetUnionByHeight(int elementSize) {
if (elementSize <= 0) {
throw new IllegalArgumentException("不相交集合的元素个数要大于零");
}
//实例化化不相交集类(并查集)的元素数组
this.elements = new int[elementSize];
//初始化元素的高度都为-1(如果是根,值就是负数,等价元素组成的树
//的高度是多少,则根元素就是负几)
for (int i = 0; i < elements.length; i++) {
elements[i] = -1;
}
}
/**
* 查询不相交集类(并查集)的元素个数
* @return 元素个数
*/
@Override
public int size() {
return elements.length;
}
/**
* 查询不相交集类(并查集)的某个元素的根元素
* 输入的是下标查,如果两个元素的根元素相同,
* 则这两个元素就是等价的。实际中还会有一个
* 与elements等长的数组,装的是真实的元素,
* elements只是相当于代号,记录等价关系,
* 二者通过下标,来映射真实元素
* @param elementIndex 待查询的元素下标
* @return 该元素的根元素
*/
@Override
public int find(int elementIndex) {
//如果记录小于0,那就是根
if (elements[elementIndex] < 0) {
//返回根元素
return elementIndex;
//如果记录不小于0,那还不是根,
//是等价森林中的上一个节点
} else {
//递归向上继续寻找根
return find(elements[elementIndex]);
}
}
/**
* 将不相交集类(并查集)的两个元素进行合并操作
* 注意,两个元素合并,代表这两个元素所在的两个
* 等价集合,全部变成一个大的等价集合。如果这
* 两个元素本来就等价,则不进行合并操作。
* 注意,这里同样是入参下标,下标映射真实元素
* 此实现类,根据树的高度来决定谁合并到谁上面,
* 矮的树的根节点,会作为大的树的根节点的子节点
* @param elementIndex1 元素下标1
* @param elementIndex2 元素下标2
*/
@Override
public void union(int elementIndex1, int elementIndex2) {
int root1 = find(elementIndex1);
int root2 = find(elementIndex2);
//如果两个元素本属于一个集合
if (root1 == root2) {
//不作处理
return;
}
//比高度:如果root1比root2的树要高
if (elements[root1] < elements[root2]) {
//将较矮的root2合并到较高的root1上
elements[root2] = root1;
//比高度:如果root2比root1的树要高
} else if (elements[root2] < elements[root1]) {
//将较矮的root1合并到较高的root2上
elements[root1] = root2;
//比高度:如果root1和root2一样高
} else {
//将root1合并到root2上
elements[root1] = root2;
//root2的高度增加1
root2 --;
}
}
/**
* 不相交集类(并查集)按树的高度合并的实现类测试
* @param args
*/
public static void main(String[] args) {
//实例化不相交集类(并查集)按树的高度合并的实现类,并初始化元素个数为8个
DisjointSetUnionByHeight disjointSetUnionByHeight = new DisjointSetUnionByHeight(8);
//进行一系列的合并操作,其中包含已等价的合并
disjointSetUnionByHeight.union(2,3);
disjointSetUnionByHeight.union(5,6);
disjointSetUnionByHeight.union(3,5);
disjointSetUnionByHeight.union(0,7);
disjointSetUnionByHeight.union(2,6);
//获取该相交集类(并查集)实现类的等价记录Map
Map<Integer, Set<Integer>> equivalentMap = DisjointSetUtil.arrangeDisjointSet(disjointSetUnionByHeight);
System.out.println("不相交集类(并查集)按树的高度合并的实现测试结果:");
//输出等价记录Map的内容,进行测试验证
for (Map.Entry<Integer, Set<Integer>> item : equivalentMap.entrySet()) {
System.out.println(String.format("root:%d, elements:%s", item.getKey(), item.getValue().toString()));
}
}
}
运行main方法的测试用例,控制台输出如下,测试通过:
不相交集类(并查集)按树的高度合并的实现测试结果:
root:1, elements:[1]
root:4, elements:[4]
root:6, elements:[2, 3, 5, 6]
root:7, elements:[0, 7]