并查集
并查集首先它是一个集合,并、查是它的操作,其中,“查”是它最终的目的,“并”则是生成可查集合的过程,即:合并以查找。并查集维护的是一系列的集合,而不是单独的一个集合。可以更高效率地判断集合元素是否属于同一集合的问题。
引入
有10 台电脑,分别为{1,2,3,4,5,6,7,8,9,10},现在已知下列电脑之间实现了网络连接:1-2,2-4,3-5,4-7,5-8,6-9,6-10,求解:2-7、5-9之间是否是相联通的。
如果我们将示意图画出后,其实很明显可以发现,2-7之间通过4是联通的;而5-9则不是联通的。
基于图的数据结构其实可以通过深度优先搜索、广度优先搜索等方法判断节点是否联通,但是现在可以给出一种新的思路:并查集。
思路
- 将所有的电脑单独地看作一个集合:{1},{2},{3},{4} ……;
- 对于已知的网络连接,我们将两个集合合并;
- 查询X、Y是否连接的,就等价于查询X、Y是否同属于一个集合。
集合的表示
集合可以用树结构表示集合,每一个树节点即一个集合内元素,集合的指针指向树根,引例中,构建出如下的数据结构:
其中的S1、S2、S3是三个集合,如果我们要判断节点2属于哪一个集合,只需要查找节点2的树根,直到节点1,即可知道节点2是属于集合S1的。
那么如何表示一棵树呢?树的传统表示方法有以下几种:
兄弟孩子表示法
这种表示法采用两个引用域,其一指向孩子,另一个指向兄弟。不适合并查集的逆方向查找。
孩子表示法
这种表示法适合从树根向树只查找,也不适合逆向查找。
class Node<T>{
T value;
List<Node<T>> children;
public Node(T value) {
this.value = value;
this.children = new LinkedList<>();
}
}
双亲表示法
双亲表示法中,树的孩子节点直接指向双亲,并且采用数组即可存储,对于以上的例子,我们可以列出如下的左边的数组:Data即为所有的电脑的编号,而Parent则是其父亲节点的下标,例如电脑2,则它的父亲是电脑1,对应的Parent下标是0,如果是树根则Parent设置为1即可。根节点Parent则置-1。
这种表示法中,父亲节点没有记录下其子节点的信息,而是通过子节点来记录父节点的信息,非常适合逆方向的查找。
实际执行
问题:有10 台电脑,分别为{1,2,3,4,5,6,7,8,9,10},现在已知下列电脑之间实现了网络连接:1-2,2-4,3-5,4-7,5-8,6-9,6-10,求解:2-7、5-9之间是否是相联通的。
思路:我们只需要进行集合合并,再判断2、7以及5、9是否有公共的祖先即可;
根据上面的问题,我们先作集合合并,首先拿到第一个网络连接:1-2,1作为根节点,2作为1的孩子,更新数组,如下:
下标 | Data | Parent |
---|---|---|
0 | 1 | -1 |
1 | 2 | 0 |
2 | 3 | - |
3 | 4 | - |
4 | 5 | - |
5 | 6 | - |
6 | 7 | - |
7 | 8 | - |
8 | 9 | - |
9 | 10 | - |
拿到2-4后,我们先检查2,4,其中4是单独的集合元素不做处理;然后2已经和1合并过了,其1是集合S1的根,我们要将1-2、4合并,那么就要将4也挂在树根上,更新数组: 网络连接:1-2,2-4,3-5,4-7,5-8,6-9,6-10,求解:2-7、5-9之间是否是相联通的。
根据上面的问题,我们先作集合合并,首先拿到第一个网络连接:1-2,1作为根节点,2作为1的孩子,更新数组,如下:
下标 | Data | Parent |
---|---|---|
0 | 1 | -1 |
1 | 2 | 0 |
2 | 3 | - |
3 | 4 | 0 |
4 | 5 | - |
5 | 6 | - |
6 | 7 | - |
7 | 8 | - |
8 | 9 | - |
9 | 10 | - |
以此类推,构建数组,直到构建完成。
以此类推,构建数组,直到构建完成,之后按照Data列访问,找到各自的祖先,如果相等则说明二者是联通的,可达的(例如2-7);如果祖先不相等则说明二者是不连通的(5-9)。
优化思路
我们一开始就是简单粗暴的把p所在的树接到q所在的树的根节点下面,那么这里就可能出现「头重脚轻」的不平衡状况,比如下面这种局面:
所以,我们希望,将小树添加到大树底下,而不是将大树贴在小树下,以减小树高。对于集合S1、S2、S3来说,我们新建一个数组来记录各自的尺寸。合并时通过比较尺寸来选择谁作为树根。