【算法导论-36】并查集(Disjoint Set)详解

WiKi

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。所以,并查集的三种典型操作的时间复杂度是线性的
#相关资料
并查集的维基百科
#并查集的java实现
这里根据《算法导论》的21.3节的伪代码,实现了一个泛型的并查集。输出时,打印节点及其集合的代表元素(即根元素,representative)。

import java.util.ArrayList;
import java.util.List;
import java.util.TreeSet;

/**
 * <p>并查集的实现<p/>
 * <p>参考:《算法导论》21.3节<p/>
 * <p>2016-08-31<p/>
 * 
 * */
public class DisjointSet<T> {
	private List<Node> forests;//所有节点
	public DisjointSet(){
		forests=new ArrayList<Node>();
	}
	/**
	 * 内部类,并查集的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<String> disjointSet=new DisjointSet<String>();
		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<String, DefaultEdge> 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<String> disjointSet=new DisjointSet<String>();
        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;

/**
 * <p>并查集的实现<p/>
 * <p>参考:《算法导论》21.3节<p/>
 * <p>2016-08-31<p/>
 * 
 * */
public class DisjointSet<T> {
	private List<Node> forests;//所有节点
	private int setCounter;//集合计数
	public DisjointSet(){
		forests=new ArrayList<Node>();
		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<String, DefaultEdge> 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<String> disjointSet=new DisjointSet<String>();
        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。

请我喝咖啡

如果觉得写得不错,可以扫描我的微信二维码请我喝咖啡哦~

在这里插入图片描述
或者点击 打赏地址 请我喝杯茶~

  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值