并查集
1.概论
定义
并查集是一种树形的数据结构,用于处理一些不相交集合的合并以及查询的问题,他的本质是通过一个一维数组来维护一个森林,开始时森林中的每一个节点都是孤立的,各成一个树,进行若干次的合并操作,每次合并将两个树合并为一个更大的树。
主要解决问题:链接问题和路径问题。
并查集在一些有N个元素的集合应用问题中,我们通常是在开始时让每个元素构成一个单元素的集合,然后按一定顺序将属于同一组的元素所在的集合合并
,其间要反复查找
一个元素在哪个集合中。
操作
- 将两个集合合并
- 询问两个数是否在一个集合中
基本原理
每个集合用一棵树来表示。树根的编号就是整个集合的编号,每个节点存储他的父节点,也就是孩子指向父亲。
由简单到深入
公共接口: 设计两个接口 主要对其下标进行比较 isConnected
判断是否联合,unionElements
联合两个节点。
int getSize();
boolean isConnected(int p,int q);
void unionElements(int p,int q);
id 0 1 2 3 4 5 6 7 8 9
parent 0 1 0 1 0 1 0 1 0 1
①由上述格式可知道判断两个元素是否连接可以比较他们的下标值是否相同。完成
find()
函数。
private int find(int p){
if(p<0&&p>=id.length){
throw new IllegalArgumentException("p is out of bound!");
}
return id[p];
}
②接着实现
isConnected()
函数 主要判断两个元素是否属于同一个其中p,q是编号
public boolean isConnected(int p, int q) {
return find(p)==find(q);
}
③实现两个编号下元素的融合
unionElements()
pubilc void unionElements(int p,int q){
int pID=find(p);
int qID=find(q);
if(pID==qID){
return;
}
for(int i=0;i<id.length;i++){
if(id[i]==pID)
id[i]=qID;
}
}
这里我们那上个id 和parent 的例子当编号1和4融合后的结果为
id 0 1 2 3 4 5 6 7 8 9
parent 0 0 0 0 0 0 0 0 0 0
因为上面可以分为两波,当把他们不同的两个连接起来相当于整个元素全部连接了起来
首先定义一个公共的接口类 以下不同的提高均需要用到
public interface UF {
//设计两个接口 主要对其下标进行比较
int getSize();
boolean isConnected(int p,int q);
void unionElements(int p,int q);
}
程序源代码
public class UnionFind1 implements UF {
private int[] id;
public UnionFind1(int size) {
id = new int[size];
for (int i = 0; i < id.length; i++) {
id[i] = i;
}
}
private int find(int p) {
if (p < 0 && p >= id.length) {
throw new IllegalArgumentException("p is out of bound!");
}
return id[p];
}
@Override
public int getSize() {
return id.length;
}
@Override
public boolean isConnected(int p, int q) {
return find(p) == find(q);
}
@Override
public void unionElements(int p, int q) {
int pID = find(p);
int qID = find(q);
if (pID == qID) {
return;
}
for (int i = 0; i < id.length; i++) {
if (id[i] == pID)
id[i] = qID;
}
}
}
Quick Union
将每一个元素,看做是一个节点。 该过程适用于有序序列从0开始
id 0 1 2 3 4 5 6 7 8 9
parent 0 1 2 3 4 5 6 7 8 9
对于以上来说根据下图可以看出联合过程每次联合元素都找到他的祖先 ,让祖先进行连接。
图片解析:对于union(4,3)
这里规定将4连接到3;起初他们都是孤立的节点指向自己本身即 p=parent§;连接4,3时首先找到4,3的根节点都为其本身,其次让4的根节点指向3即 4的祖先指向3 先存储4,3的祖先为pRoot,qRoot然后连接 parent[pRoot]=qRoot;然后就实现了连接
所以我们可以对
find()
函数进行修改每次需要找到他的祖先,初始化的时候让id==parent
private int find(int p){
if(p<0&&p>=id.length){
throw new IllegalArgumentException("p is out of bound!");
}
//此处开始寻找其祖先
while(p!=parent[p]){
p=parent[p];
}
return p;
}
对其联合两组元素情况代码最终让patent指向根节点
public void unionElements(int p, int q) {
//得到p的根节点
int pRoot = find(p);
//得到q的根节点
int qRoot = find(q);
if (pRoot == qRoot) {
return;
}
//最终让pRoot指向qRoot实现了连接
parent[pRoot] = qRoot;
}
程序源代码
import java.util.Arrays;
public class UnionFind2 implements UF {
private int[] parent;
public UnionFind2(int size) {
parent = new int[size];
for (int i = 0; i < size; i++) {
parent[i] = i;
}
}
@Override
public int getSize() {
return parent.length;
}
private int find(int p) {
if (p < 0 && p >= parent.length)
throw new IllegalArgumentException("p is out of bound");
while (p != parent[p]) {
p = parent[p];
}
return p;
}
//判断p和q是否在同一个集合
@Override
public boolean isConnected(int p, int q) {
return find(p) == find(q);
}
@Override
public void unionElements(int p, int q) {
int pRoot = find(p);
int qRoot = find(q);
if (pRoot == qRoot) {
return;
}
parent[pRoot] = qRoot;
}
}
基于size的优化
在第二版本的并查集中find()
是一个不断索引的过程 不是顺次的访问,而是在不同的地址跳转所以访问较慢,其复杂度为O(h),isConnected()
复杂度高。
为了使形成的树不会因为不断连接而形成一条链表,所以我们让元素少的根节点指向元素多的根节点,如下图:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BXeLoixN-1662083200786)(https://cdn.jsdelivr.net/gh/7756JokerQAQ/picodemoo/img/asd123.png)]
按照正常情况连接成一条链表增加了树的高度提升了时间复杂度使效率变慢。但是考虑到size()情况后可以让元素少的指向元素多的,这样就避免了单链表的形成。
具体修改代码如下:
①首先添加私有成员变量private int []sz
便是以i为根的集合中元素的个数
②修改构造函数中对于初始化的数组每个节点添加sz[i]=1
;
③find()
函数和第版一致
④unionElements()
函数进行修改操作如下:
public void unionElements(int p,int q){
int pRoot=find(p);
int qRoot=fin(q);
if(pRoot==qRoot){
return;
}
//以下判断size的大小进行不同的连接方式可以实现树的深度降低;
if(sz[pRoot]<sz[qRoot]){
parent(pRoot)=qRoot;
sz[qRoot]+=sz(pRoot);
}else{
parent[qRoot]=pRoot;
sz[pRoot]+=sz[qRoot];
}
}
程序源代码
import java.util.Arrays;
//基于size的优化
public class UnionFind3 implements UF {
private int[] parent;
private int[] sz; //sz[i]表示以i为根的集合中元素的个数
public UnionFind3(int size) {
parent = new int[size];
sz = new int[size];
for (int i = 0; i < size; i++) {
parent[i] = i;
sz[i] = 1;
}
}
@Override
public int getSize() {
return parent.length;
}
private int find(int p) {
if (p < 0 && p >= parent.length)
throw new IllegalArgumentException("p is out of bound");
while (p != parent[p]) {
p = parent[p];
}
return p;
}
//判断p和q是否在同一个集合
@Override
public boolean isConnected(int p, int q) {
return find(p) == find(q);
}
@Override
public void unionElements(int p, int q) {
int pRoot = find(p);
int qRoot = find(q);
if (pRoot == qRoot) {
return;
}
if (sz[pRoot] < sz[qRoot]) {
parent[pRoot] = qRoot;
sz[qRoot] += sz[pRoot];
} else {
parent[qRoot] = pRoot;
sz[pRoot] += sz[qRoot];
}
}
}
基于rank的优化
目的:为了使每次两个不同的集合连接后数的高度尽量不增加此处我们进行rank优化代码与基于size优化代码类似 只不过将private int []sz
改为private int []rank
并且在构造函数中赋予初始高度为1;
下面主要修改unionElements()
函数
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BqivyPlt-1662083200786)(https://cdn.jsdelivr.net/gh/7756JokerQAQ/picodemoo/img/zzzz11.png)]
public void unionElements(int p,int q){
int pRoot=find(p);
int qRoot = find(q);
if (pRoot == qRoot) {
return;
}
//根据两个元素所在树的rank不同判断合并的方向
//将rank低的集合合并到rank高的集合上,代码实现逻辑如下
if(rank[pRoot]<rank[qRoot]){
parent[pRoot]=qRoot;
}else if(rank[qRoot]<rank[pRoot]){
parent[qRoot]=pRoot;
}else{
//当两个的rank相同时则合并后整体高度+1
parent[qRoot]=pRoot;
rank[pRoot]+=1;
}
}
基于rank的优化和路径压缩
目的:解决单链表的问题,这个解决方法发生在find过程中,在find过程中实现路径压缩在向上遍历的时候执行parent[p]=parent[parent[p]]
整体代码和基于rank的代码相同 原理图如下:
图片解析:首先当find(4)
的时候让其parent指向父亲的父亲也就是2。其次构成树Ⅱ然后走向节点2让其parent指向父亲的父亲构成树Ⅲ,当p!=parent[p]时终止条件此时的优化已经完成。
find()
代码如下:
private int find(int p) {
if (p < 0 && p >= parent.length)
throw new IllegalArgumentException("p is out of bound");
while (p != parent[p]) {
parent[p] = parent[parent[p]];
p = parent[p];
}
return p;
}
//也可以递归的实现find的路径压缩
//find的递归实现
private int find(int p) {
if (p < 0 && p >= parent.length)
throw new IllegalArgumentException("p is out of bound");
if (p != parent[p]) {
//路径压缩
parent[p] = find(parent[p]);
}
return parent[p];
}
程序源代码
import java.util.Arrays;
public class UnionFind6 implements UF {
private int[] parent;
private int[] rank; //sz[i]表示以i为根的集合中元素的个数
public UnionFind6(int size) {
parent = new int[size];
rank = new int[size];
for (int i = 0; i < size; i++) {
parent[i] = i;
rank[i] = 1;
}
}
@Override
public int getSize() {
return parent.length;
}
//find的递归实现
private int find(int p) {
if (p < 0 && p >= parent.length)
throw new IllegalArgumentException("p is out of bound");
if (p != parent[p]) {
//路径压缩
parent[p] = find(parent[p]);
}
return parent[p];
}
//判断p和q是否在同一个集合
@Override
public boolean isConnected(int p, int q) {
return find(p) == find(q);
}
@Override
public void unionElements(int p, int q) {
int pRoot = find(p);
int qRoot = find(q);
if (pRoot == qRoot) {
return;
}
//根据两个元素所在树的rank不同判断合并的方向
//将rank低的集合合并到rank高的集合上
if (rank[pRoot] < rank[qRoot]) {
parent[pRoot] = qRoot;
} else if (rank[qRoot] < rank[pRoot]) {
parent[qRoot] = pRoot;
} else {
parent[qRoot] = pRoot;
rank[pRoot] += 1;
}
}
}
) {
int pRoot = find§;
int qRoot = find(q);
if (pRoot == qRoot) {
return;
}
//根据两个元素所在树的rank不同判断合并的方向
//将rank低的集合合并到rank高的集合上
if (rank[pRoot] < rank[qRoot]) {
parent[pRoot] = qRoot;
} else if (rank[qRoot] < rank[pRoot]) {
parent[qRoot] = pRoot;
} else {
parent[qRoot] = pRoot;
rank[pRoot] += 1;
}
}
}