并查集
通常用来解决 “ 连接问题” 。 并查集可以非常快速的判断网络中节点的连接状态,而这里所说的网络是一个抽象的概念:是指用户之间形成的网络。
“连接问题和路径问题”
通常情况下,解决一个连接问题,我们也可以用解决路径问题来代替,如果二者之间存在路径,那他们一定是连接的,反之亦然。如果我们只需要了解二者之间的连接状态的话,我们没有必要借助他们之间的路径来求,这样我们会消耗很多额外的性能。
对于并查集而言,对于一种数据,主要支持两个动作:
- union(p,q) 将二者所在的集合合并
- isConnected(p,q) 查询二者是否属于同一个集合
接口的定义如下所示
1 | public interface UF { |
接下来我们实现第一版的并查集 —->QuickFind
1 | /** |
事实上,我们很少用到这样的并查集,因为其合并操作的时间复杂度达到了O(n),在数据量大的情况下,这种方式是不可取的。
接下来我们实现第二版的并查集 —->QuickUnion
1 | /** |
实际上,在QuickUnion中,我们使用数组模拟了一棵“奇怪的树” ,或者来说,在初始情况下,叫“森林” 。因为初始情况下,有n棵树,其中每棵树都指向自己。与平常的树不同的是,他总是由孩子节点指向父亲节点。
上述实现中也存在一定的问题,如果我们只是一味的合并,不去考虑树的高度,那么将可能会退化成链表。
解决方案:考虑当前的树整体有多少个节点。让节点个数少的那棵树,去指向节点个数多的那棵树。这样我们可以有效的减少树的高度,相对来说树的高度比较低。
1 | public class QuickUnion_size implements UF { |
上述的优化还是会存在一些问题,我们更应该考虑一个并查集的高度值,而不是其节点的个数。
这种思想,在并查集中,更为普遍的说法叫“基于Rank的优化”
1 | public class QuickUnion_Rank implements UF { |
接下来我们来进行使用并查集时, 最经典的一个优化操作 “路径压缩”**
在我们查找一个元素的根节点时,我们是一层一层往上查询,在寻找的过程中,我们顺便让深度降低,及达到路径压缩的目的。我们在向上搜索时,执行 parent[p]=parent[parent[p]]。
实际上,要完成上述步骤,非常容易,我们只用在find操作中添加一行代码
1 | private int find(int p){ |
这样的话,将会大大改善并查集的性能!