Disjoint是“不相交”的意思。Disjoint Set高效地支持集合的合并(Union)和集合内元素的查找(Find)两种操作,所以Disjoint Set中文翻译为并查集。
就《算法导论》21章来讲,主要设计这几个知识点:
用并查集计算图的连通区域;
判断两个顶点是否属于同一个连通区域;
链表实现并查集;
Rooted tree实现并查集;
Rooted tree实现并查集时采用rank方法和路径压缩算法。
《算法导论》21.4给出了一个结论:总计m个MAKE-SET、UNION、FIND-SET操作,其中MAKE-SET的个数为n,则采用rank和路径压缩算法实现的并查集最坏时间复杂度是O(m α(n) )。其中α是Ackerman函数的某个反函数,这个函数的值可以看成是不大于4。所以,并查集的三种典型操作的时间复杂度是线性的。
并查集的维基百科
这里根据《算法导论》的21.3节的伪代码,实现了一个泛型的并查集。输出时,打印节点及其集合的代表元素(即根元素,representative)。
import java.util.ArrayList;
import java.util.List;
import java.util.TreeSet;
/**
* 并查集的实现
* 参考:《算法导论》21.3节
* created by 曹艳丰
* 2016-08-31
*
* */
public class DisjointSet {
private List forests;//所有节点
public DisjointSet(){
forests=new ArrayList();
}
/**
* 内部类,并查集的rooted node
* */
private class Node{
Node parent;
int rank;
T t;
private Node(T t){
parent=this;
rank=0;
this.t=t;
}
}
//向森林中添加节点
public void makeSet(T t){
Node node=new Node(t);
forests.add(node);
}
//将包含x和包含y的两个集合进行合并
public void union(T x,T y){
Node xNode=isContain(x);
Node yNode=isContain(y);
if (xNode!=null&&yNode!=null) {
link(findSet(xNode), findSet(yNode));
}
}
//查找到节点node的根节点
public Node findSet(Node node){
if (node!=node.parent) {
//路径压缩,参考《算法导论》插图21.5
node.parent=findSet(node.parent);
}
return node.parent;
}
//查找到节点node的根节点
public Node findSet(T t){
Node node=isContain(t);
if (node==null) {
throw new IllegalArgumentException("不含该节点!");
}else {
return findSet(node);
}
}
//将两个根节点代表的集合进行连接
private void link(Node xNode,Node yNode){
if (xNode.rank>yNode.rank) {
yNode.parent=xNode;
}else {
xNode.parent=yNode;
if (xNode.rank==yNode.rank) {
yNode.rank+=1;
}
}
}
//森林是否包含这个节点
private Node isContain(T t){
for (Node node : forests) {
if (node.t.equals(t)) {
return node;
}
}
return null;
}
@Override
public String toString() {
// TODO Auto-generated method stub
if (forests.size()==0) {
return "并查集为空!";
}
StringBuilder builder=new StringBuilder();
for (Node node : forests) {
Node root=findSet(node);
builder.append(node.t).append("→").append(root.t);
builder.append("\n");
}
return builder.toString();
}
}
然后测试一下
public class Main{
public static void main(String[] args) {
// TODO Auto-generated method stub
DisjointSet disjointSet=new DisjointSet();
disjointSet.makeSet("cao");
disjointSet.makeSet("yan");
disjointSet.makeSet("feng");
disjointSet.union("cao", "yan");
disjointSet.union("cao", "feng");
System.out.println(disjointSet.toString());
}
}
输出格式,元素→代表元素
cao→yan
yan→yan
feng→yan
表明3个节点的代表元素一致,即处于一个集合中。
《算法导论》21.1节的伪代码,这里给出连通区域计算的例子。图的数据结构采用“【算法导论-35】图算法JGraphT开源库介绍 “中的无向图。
private static void connectedComponents(){
UndirectedGraph g =
new SimpleGraph<>(DefaultEdge.class);
String v1 = "v1";
String v2 = "v2";
String v3 = "v3";
String v4 = "v4";
// add the vertices
g.addVertex(v1);
g.addVertex(v2);
g.addVertex(v3);
g.addVertex(v4);
// add edges to create a circuit
g.addEdge(v1, v2);
g.addEdge(v2, v3);
//连通区域计算
//参考《算法导论》21.1节
DisjointSet disjointSet=new DisjointSet();
for ( String v : g.vertexSet()) {
disjointSet.makeSet(v);
}
// for ( DefaultEdge e : g.edgeSet()) {
// String source=e.getSource();//protected访问类型
// String target=e.getTarget();//protected访问类型
// if (disjointSet.findSet(source)!=disjointSet.findSet(target)) {
// disjointSet.union(source, target);
// }
// }
if (disjointSet.findSet(v1)!=disjointSet.findSet(v2)) {
disjointSet.union(v1, v2);
}
if (disjointSet.findSet(v2)!=disjointSet.findSet(v3)) {
disjointSet.union(v2, v3);
}
System.out.println(disjointSet.getSetCounter());
}
输出
v1→v2
v2→v2
v3→v2
v4→v4
v1、v2、v3的代表元素一致,表明三者在一个集合中,即三者连通。v4是另外一个集合。
举个例子,某人结婚时宴请宾客,A来宾认识B来宾,B来宾认识C来宾,则A、B、C安排在一桌。A来宾认识B来宾,且A、B的熟人及其熟人的熟人(熟人链)不包括C,则C与A、B不在一桌。问,需要多少桌子才能满足要求呢?
这个例子其实就是连通区域的具体到社交关系的1度、2度……n度关系。
稍微修改并查集的实例,添加集合的计数setCounter,每次makeset时递增,union时递减,这样就得到最后的集合个数。
import java.util.ArrayList;
import java.util.List;
import java.util.TreeSet;
/**
* 并查集的实现
* 参考:《算法导论》21.3节
* created by 曹艳丰
* 2016-08-31
*
* */
public class DisjointSet {
private List forests;//所有节点
private int setCounter;//集合计数
public DisjointSet(){
forests=new ArrayList();
setCounter=0;
}
public int getSetCounter() {
return setCounter;
}
/**
* 内部类,并查集的rooted node
* */
private class Node{
Node parent;
int rank;
T t;
private Node(T t){
parent=this;
rank=0;
this.t=t;
}
}
//向森林中添加节点
public void makeSet(T t){
Node node=new Node(t);
forests.add(node);
setCounter++;
}
//将包含x和包含y的两个集合进行合并
public void union(T x,T y){
if (x.equals(y)) {
throw new IllegalArgumentException("Union的两个元素不能相等!");
}
Node xNode=isContain(x);
Node yNode=isContain(y);
if (xNode!=null&&yNode!=null) {
link(findSet(xNode), findSet(yNode));
setCounter--;
}
}
//查找到节点node的根节点
public Node findSet(Node node){
if (node!=node.parent) {
//路径压缩,参考《算法导论》插图21.5
node.parent=findSet(node.parent);
}
return node.parent;
}
//查找到节点node的根节点
public Node findSet(T t){
Node node=isContain(t);
if (node==null) {
throw new IllegalArgumentException("不含该节点!");
}else {
return findSet(node);
}
}
//将两个根节点代表的集合进行连接
private void link(Node xNode,Node yNode){
if (xNode.rank>yNode.rank) {
yNode.parent=xNode;
}else {
xNode.parent=yNode;
if (xNode.rank==yNode.rank) {
yNode.rank+=1;
}
}
}
//森林是否包含这个节点
private Node isContain(T t){
for (Node node : forests) {
if (node.t.equals(t)) {
return node;
}
}
return null;
}
@Override
public String toString() {
// TODO Auto-generated method stub
if (forests.size()==0) {
return "并查集为空!";
}
StringBuilder builder=new StringBuilder();
for (Node node : forests) {
Node root=findSet(node);
builder.append(node.t).append("→").append(root.t);
builder.append("\n");
}
return builder.toString();
}
}
连通区域的计算,不过这里输出的是集合个数。
private static void connectedComponents(){
UndirectedGraph g =
new SimpleGraph<>(DefaultEdge.class);
String v1 = "v1";
String v2 = "v2";
String v3 = "v3";
String v4 = "v4";
// add the vertices
g.addVertex(v1);
g.addVertex(v2);
g.addVertex(v3);
g.addVertex(v4);
// add edges to create a circuit
g.addEdge(v1, v2);
g.addEdge(v2, v3);
//连通区域计算
//参考《算法导论》21.1节
DisjointSet disjointSet=new DisjointSet();
for ( String v : g.vertexSet()) {
disjointSet.makeSet(v);
}
// for ( DefaultEdge e : g.edgeSet()) {
// String source=e.getSource();//protected访问类型
// String target=e.getTarget();//protected访问类型
// if (disjointSet.findSet(source)!=disjointSet.findSet(target)) {
// disjointSet.union(source, target);
// }
// }
if (disjointSet.findSet(v1)!=disjointSet.findSet(v2)) {
disjointSet.union(v1, v2);
}
if (disjointSet.findSet(v2)!=disjointSet.findSet(v3)) {
disjointSet.union(v2, v3);
}
System.out.println(disjointSet.getSetCounter());
}
输出是2。