并查集
1.基本定义
并查集(Union Find):一种用于管理分组的数据结构 (一般使用树形结构来表示)
(1)Find:查询 a 元素和 b 元素是否为同一组
(2)Union:合并元素 a 和 b 为同一组
我们将同一个组的元素例如用一颗树表示(比如 {'A','B','C'}
为同一组,{'D','E'}
为同一组)
(1)find()
判断为同一组:
只需要判断他们的 root 根节点是否为同一个即可
比如这里的 B 、C
节点的 root 根节点都为 A
,所以 B C
为同一组元素
B、E
的 root 根节点不是同个元素,所以 B、E
为不同一组的元素
重点是 求出root节点 (这个很简单,参考下面的代码)
(2)union()
例如下面合并主树 {'A','B','C'}
和丛树 {'D','E'}
union 合并只需要将丛树 {'D','E'}
的根节点 D
作为主树根节点的子节点即可(如下图所示)
2.代码
(1)定义数据机构
- items: 元素数组
- parents:父节点的 index 下标(比如
A
的父节点就是自己 0 , B 的父节点就是A
下标也是0)
char[] items = {'A','B','C','D','E'};
int [] parents = {0,0,0,3,3};
(2)find()根节点
如果父节点的下标根节点就是本身,那么这个节点就是根节点,使用递归求出根节点
public int find(int itemIndex){
//求出父节点的 index下标
int parentIndex = parents[itemIndex];
//如果父节点的下标等于本身,那么这个节点就是根节点
if (itemIndex == parentIndex){
return itemIndex;
}
//不是根节点,以父节点为基础,递归找到根节点
return find(parentIndex);
}
拓展:上面的代码可以使用三元表达式简化为一行
return itemIndex == parents[itemIndex]? itemIndex : find(parents[itemIndex]);
(2)union()合并
将从树根节点的父节点设置为主树根节点即可
public void union(int masterTree,int slaveTree){
//找到主树的根节点
int masterTreeRoot = find(masterTree);
//找到从树的根节点
int slaveTreeRoot = find(slaveTree);
//将从树的根节点的父节点设置为主树的根节点
parents[slaveTree] = masterTree;
}
2.优化——路径压缩
(1)问题
例如我们有4个点 {'A','B','C','D'}
char[] items = {'A','B','C','D'};
int [] parents = {0,1,2,3};
合并 A
、B
点,union(0,1)
,A 树为主树
合并 {'A','B'}
和 C
点, union(2,0)
,C 树为主树
合并 {'A','B','C'}
和 D
点, union(3,2)
,D 树为主树
在实际中有可能出现类似上面的很长的长链,就没有了树查找的优势,我们想要查找到根节点会越来越难。我们想要下图这种可以轻易得到root根节点的结构,解决方法之一就是 「路径压缩」。
(2)解决
修改 find()
方法如下
public int find(int itemIndex){
//求出父节点的 index下标
int parentIndex = parents[itemIndex];
//如果父节点的下标等于本身,那么这个节点就是根节点
if (itemIndex == parentIndex){
return itemIndex;
}
//递归将父节点设置为根节点
parents[itemIndex] = find(parents[itemIndex]);
return parents[itemIndex];
}
第9行代码parents[itemIndex] = find(parents[itemIndex]);
递归将这条线的所有节点的父节点设置为 root 根节点。
例如:find(2)
找出如下 B
节点的根节点。
(下面是树的结构)
char[] items = {'A','B','C','D'};
int[] parents = [2,0,3,3];
parents[itemIndex] = find(parents[itemIndex]);
递归将这条线的所有节点的父节点设置为 root 根节点。
递归之后的树结构
char[] items = {'A','B','C','D'};
int[] parents = [3,3,3,3];
(3)总结
路径压缩的本质:每次 find()
的时候将 find()
线路上的所有元素的父节点设置为 root 根节点
-
只有
find()
才会压缩 -
且只压缩一条线路(例如下图所示)
3.优化——减少合并层级
(1)问题
比如我们想要 union()
下面这 2 棵树
如果 union(4,3)
(4 -> E 为主树)
如果 union(3,4)
(3 -> D 为主树)
我们明显希望这一种(第二种)方式来 union()
合并,因为可以减少层级,降低 find()
的时间
(2)解决
需要记录树的层级,union()
的时候 层级多的为主树
这里使用 rank
来记录树的层级
/**
* 初始化所有的层级为1
*/
int[] rank = {1,1,1,1};
public void union2(int tree1,int tree2){
//找到根节点
int tree1Root = parents[tree1];
int tree2Root = parents[tree2];
//如果树 1 的层级更高,让树 2 的根节点的父节点的值为树 1 的根节点
if (rank[tree1Root] > rank[tree2Root]){
parents[tree2Root] = tree1Root;
}
//反之
else if (rank[tree1Root] < rank[tree2Root]){
parents[tree1Root] = tree2Root;
}
//如果相等的层级,那么谁当「主树」都一样,需要将树的层级增加一层。
else {
parents[tree2Root] = tree1Root;
rank[tree1Root] ++;
}
}
例如我们有4个点 {'A','B','C','D'}
private char[] items = {'A','B','C','D'};
private int [] parents = {0,1,2,3};
private int[] rank = {1,1,1,1};
合并 A
、B
点,union(0,1)
,A
、B
的层级 rank
都为 1 ,所以默认以 A
为主树,rank[0] = 2
合并 {'A','B'}
和 C
点, union(2,0)
,因为 A
的rank比较大,所以 A
为主树。
合并 {'A','B','C'}
和 D
点, union(3,0)
,因为 A
的rank比较大,所以 A
为主树。
这个方法从源头上解决了:形成类似长链表的问题。