数据结构之并查集Union Find

一、并查集简述

顾名思义,并查集即是针对集合的并和查找操作的一种数据结构。
并查集一般不考虑添加、删除操作,只对当前已有固定元素进行并和查找的操作.
并查集不关系具体的元素是谁,因此并查集可以用数组来实现,其中数组的下标用来标识特定的元素,数组中存储的元素代表下标所对应的元素所属的集合(即:数组中存储的是集合的编号)。

在这里插入图片描述

主要用途:
1. 连接问题;判断两个元素是否相连,需要注意的是:这里注重元素间是否连接,而不注重如何连接的,但路径问题更加侧重于如何连接的。
find(int p)操作:
功能:找出下标p所对应的元素所属的集合
并操作 unionElement(int p, int q) 简要介绍:
注意:

No1:这里p,q只是集合中某一元素的编号,可以通过find(int index)操作找出它们所属的集合。
No2:如果将两个元素并在一起,那么相当于这两个集合并成了一个集合

判断两个元素是否连接(是否属于同一个集合) isConnected(int p, int q)

这个功能的实现比较简单,只需要找出它们的集合是否相同即可。

二、代码实现部分

这里一共有六种实现方式,从一至六逐步进行优化

1. 定义UF接口

由于之后要以六种方式实现并查操作,定义接口是一个不错的选择。

public interface UF {
	//判断 id 为 p 和 id 为 q 的两个元素是否相连
	boolean isConnected(int p, int q);
	//将 p, q 所对应的元素以及它们所属的集合并起来
	void unionElements(int p, int q);
}

2. 版本一:QuickFind方式实现

/*
 * Quick Find实现法:
 * 1. 查找 和 判断是否连接的时间复杂度为O(1)
 * 2. 但并操作的时间复杂度为O(n)
 */
public class UnionFind1 implements UF{
	private int[] id;       //存储数据所处的集合的编号
	
	public UnionFind1(int size) {
		id = new int[size];
		
		//初始的时候可以令每一个元素都属于一个独立的集合
		for(int i = 0; i < size; ++i)
			id[i] = i;
	}
	
	@Override
	//判断 下标为 p 和 下标为 q 的两个元素是否相连
	public boolean isConnected(int p, int q) {
		return find(p) == find(q);
	}
	
	@Override 
	//将 p, q 所对应的元素以及它们所属的集合并起来
	public void unionElements(int p, int q) {
		//获取p, q所属的集合
		int pUnion = find(p);
		int qUnion = find(q);
		
		//p q 属于同一集合不做任何操作
		if(pUnion == qUnion)
			return;
		
		//连接操作:只要两个集合中任意的两个元素连接在一起,那么
		//两个集合中的所有元素都同属于一个集合,即任意两个元素
		//都是连接在一起的。
		for(int i = 0; i < id.length; i++) {
			if(id[i] == pUnion)
				id[i] = qUnion;
			//或者
			//if(id[i] == qUnion)
				//id[i] = pUnion;
		}
	}
	
	//查找下标p所对应的元素所属的集合
	private int find(int p) {
		//这里最好进行越界检查
		return id[p];
	}
}

3. 版本二:以树的方式实现(Quick Union)

首先明确一点:这里的树只是逻辑上的树,实际上操作时还是针对数组进行操作。

  1. 可以将每一个元素看做一个结点;
  2. 这里的树与平时的树有所不同,不是父亲结点指向孩子结点,而是孩子结点指向父亲结点;
  3. 当一个结点是根结点时,它指向它自己;
  4. 图示:
public class UnionFind2 implements UF{
	private int[] parent;  //parent[i]指的是第i个元素指向的结点
	
	public UnionFind2(int size) {
		parent = new int[size];
		
		//初始时每一个结点都指向它自己,各自形成一棵树
		for(int i = 0; i < size; ++i) {
			parent[i] = i;
		}
	}
	
	@Override
	public boolean isConnected(int p, int q) {
		return findRoot(p) == findRoot(q);
	}
	
	@Override
	public void unionElements(int p, int q) {
		int pRoot = findRoot(p);
		int qRoot = findRoot(q);
		
		if(pRoot == qRoot)
			return;
		
		parent[pRoot] = qRoot;
		//或者 parent[qRoot] = pRoot;
	}
	
	
	//查找index对应的元素的根结点
	//根结点特征:它指向它自己,即:parent[index] = index;
	//复杂度为O(h),h为树的高度
	private int findRoot(int index) {
		while(index != parent[index]) {
			index = parent[index];
		}
		return index;
	}
}

4. 版本三:基于size的优化

上面的两个版本在极端情况下都有可能形成一个链表,这会大大降低程序相关操作的执行效率,因此应尽量减小树的高度。
这里采用size优化:size指的是以当前结点为根结点的集合中元素的个数,
实现方式:将size较小的树的根结点的父结点指向size较大的根结点
如图:在这里插入图片描述

//基于size的优化
//UnionFind3的大部分代码实现与UnionFind2的相同
public class UnionFind3 implements UF{
	private int[] parent;  //parent[i]指的是第i个元素指向的结点
	private int[] size;    //size[i] 表示的是以i为根的集合中元素个数
	
	public UnionFind3(int size) {
		parent = new int[size];
		//初始时每一个结点都指向它自己,各自形成一棵树
		for(int i = 0; i < size; ++i) {
			parent[i] = i;
			this.size[i] = 1;
		}
	}
	@Override
	public boolean isConnected(int p, int q) {
		return findRoot(p) == findRoot(q);
	}
	
	@Override
	public void unionElements(int p, int q) {
		int pRoot = findRoot(p);
		int qRoot = findRoot(q);
		
		if(pRoot == qRoot)
			return;
		//将元素个数较小的根结点指向元素个数较多的根结点,
		//避免形成链表
		if(size[pRoot] < size[qRoot]) {
			parent[pRoot] = qRoot;
			size[qRoot] += size[pRoot];
		}
		else {
			parent[qRoot] = pRoot;
			size[pRoot] += size[qRoot];
		}
	}
	
	//查找index对应的元素的根结点
	//根结点特征:它指向它自己,即:parent[index] = index;
	//复杂度为O(h),h为树的高度
	private int findRoot(int index) {
		while(index != parent[index]) {
			index = parent[index];
		}
		return index;
	}
}

5. 版本四:基于rank的优化

rank指的是以当前结点为根结点的树的高度,将rank值较小的树的根结点指向rank值较大的树的根结点,这样,执行并操作之后将不会增加树的高度。
在这里插入图片描述
树1的rank值小于树2,如果将树2连接到树1上,形成的新的树的高度会变为4,而基于rank的操作只会使树的高度变为3

//基于rank的优化
public class UnionFind4 implements UF{
	private int[] parent;  //parent[i]指的是第i个元素指向的结点
	private int[] rank;    //rank[i] 表示的是以i为根的树的高度
	
	public UnionFind4(int size) {
		parent = new int[size];
		//初始时每一个结点都指向它自己,各自形成一棵树
		for(int i = 0; i < size; ++i) {
			parent[i] = i;
			rank[i] = 1;
		}
	}
	
	@Override
	public boolean isConnected(int p, int q) {
		return findRoot(p) == findRoot(q);
	}
	
	@Override
	public void unionElements(int p, int q) {
		int pRoot = findRoot(p);
		int qRoot = findRoot(q);
		
		if(pRoot == qRoot)
			return;
		//将深度较小的树的根结点指向深度较大的根结点,以减少树的高度
		if(rank[pRoot] < rank[qRoot]) {
			parent[pRoot] = qRoot;
		}
		else if(rank[pRoot] > rank[qRoot]) {
			rank[qRoot] = pRoot;
		}
		else {
			parent[qRoot] = pRoot;
			rank[pRoot] += 1;
		}
	}
	
	//查找index对应的元素的根结点
	//根结点特征:它指向它自己,即:parent[index] = index;
	//复杂度为O(h),h为树的高度
	private int findRoot(int index) {
		while(index != parent[index]) {
			index = parent[index];
		}
		return index;
	}
}

6. 版本五:路径压缩(Path Compression,经典优化方式)

这种实现方式基于版本四
将深度较高的树压缩成深度较低的树
压缩时间:在find()操作时。
压缩方式:将当前结点的父结点更改为当前结点的父结点的父结点
在这里插入图片描述
这里的代码实现与版本四的只有find实现不同,其它的相同

//路径压缩

	//查找index对应的元素的根结点,同时进行路径压缩
	//路径压缩方法:将当前结点指向当前结点的父结点的父结点
	private int findRoot(int index) {
		while(index != parent[index]) {
			parent[index] = parent[parent[index]];
			index = parent[index];
		}
		return index;
	}

注意点:这里并没有去维护rank数组,在这里,rank不反应树的深度,它只是进行并操作时的一种参考依据,rank值较低的依然位与下层,只是同层的rank值可能不同而已。

7. 版本六:递归路径压缩

思路大致和版本五相同
这种方式将会使树的高度最多为2
在这里插入图片描述

	//查找index对应的元素的根结点,同时进行路径压缩
	//路径压缩方法:将当前结点指向当前结点的父结点的父结点
	private int findRoot(int index) {
		while(index != parent[index]) {
			parent[index] = findRoot(parent[index]);
			index = parent[index];
		}
		return parent[index];
	}

-----------------------完结

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值