在计算机网络中,对于任意两个节点,可以在其间构建一条连接建立通信,这样做没有问题,但是在整个网络中都这么做会导致需要建立的连接非常多,容易计算得到N
个节点的网络需要的连接数N(N-1)/2
,这增加了网络的负担,实现起来也不容易,有什么办法改进呢?六度空间理论大家都知道,说的是最多通过六个人,我们可以和世界上任何人建立联系,那么如何知道两个人之间是否可以建立联系呢?这两个问题其实都属于连通性问题,可以使用union-find
算法解决,步骤如下:
1. 数据准备
p
和q
表示两个抽象的节点,可以用整数表示,如果『p,q 是相连的』,则意味着:
- 自反性:
p
和p
是相连的。 - 对称性:如果
p
和q
相连,则q
和p
也是相连的。 - 传递性:如果
p
和q
相连,q
和r
相连,则p
和r
也相连。
我们可以用一个数组表示所有的节点,数组的每个位置表示一个节点,每个位置的值表示这个节点所在分量的标识符,初始化的时候每个节点的标识符都是其本身,如下所示:
1 | void UF(int N) { |
2. 实现
quick-find
在union-find
算法中,有两个目标需要实现:判断两个节点是否连通和连接两个节点的。一种简单的思路是这样的,规定同属一个连通分量的标识符相同,比如:节点0
,1
和2
是连通的,我们选择1
为标识符,那么a[0]=a[1]=a[2]=1
。这样做判断两个节点是否连通就可以在常数时间完成,基于此规则我们可以实现quick-find
算法,但是这样做union
的成本就会上升,每次union
,需要遍历所有的节点,并对合适节点的标识符进行改变,这是个平方级别的算法,如下所示。
1 | boolean isConnected(int p, int q) { |
quick-union
quick-find算法中,union的成本是平方级别的,其原因在于每次union需要遍历全部的节点,可以进行一些调整,得到改进,这就是quick-union算法。具体实现:在同一类别的中的节点,我们不再让所有节点保存相同的标识符,而是在节点中保持其父节点,类似一棵树,不过是向上走的,子节点指向父节点,根节点保存的是他自己,这样一来,进行union操作时,至于遍历找到根节点,然后让其中一个根节点指向另一个根节点即可,进行find操作时,只需遍历找到根节点即指向自身的节点。
1 | int find(int p) { |
加权 quick-union
这样做没什么问题,但是在最坏情况下,节点是依次相连的,串成一串,这会导致算法的性能下降,我们对其稍加改进,每次union时,都把节点数小的那一组连接到节点数大的那一组,这就是加权的quick-union算法。实现这个算法我们需要一个数组保存每个节点下节点的数目,对根节点而言,这个数就是它所在组的大小。
1 | UF(int N) { |
路径压缩
还有一种路径压缩算法可以改进quick-union,即每次查找时,都把途径的节点指向根节点,这样均摊下来的find成本比较小,可以证明,路径压缩的加权quick-union算法是实现union-find最快的算法。
1 | int find(int p) { |
回到开头的那两个问题,有了union-find算法,我们来尝试解决它们。对于网络中节点是否需要构建新的连接,抽象出网络的节点构建起UF
后,执行isConnected()
即可判断是否需要建立新的连接,对于两个人是否可以建立连接的问题,我们用同样的方法也可以解决,但要是想通过最少的中间人就认识一个人,该怎么做呢,这又是另外一个问题了。