版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/jt102605/article/details/84331088
一. 认识并查集
- 可以高效的解决连接问题(Connectivity Problem)
- 检查网络中节点间的连接状态(网络是个抽象概念:用户之间形成的网络)
- 数学中的集合类实现(合并问题)
- 连接问题和路径问题:连接问题只需回答是或否,而路径问题要回答出具体的路径;
- 对于一组数据,并查集主要支持三个动作:
- union(p, q) -----------------并操作,将元素p,q并入同一个组内
- find(p)------------------------查操作,返回元素所在组号,通常是一个private的辅助函数
- isConnected(p, q)---------查操作,查询元素p,q是否在一个组内(同一个组内的元素是相互连接的)
二.并查集的实现
1. Quick Find并查集:
基本数据表示:
- 并查集内部实际是存储了一个数组,数组的索引(id)表示元素的编号(这里的元素可以是各种类型),索引存储的值表示该元素所属的集合,而属于同一集合的元素即是连接起来的元素(下图,id为0,2,4,6,8的元素同属于集合0,是连接起来的)。
- Quick Find并查集,实现一个了一个find函数,用来支持快速查询操作,查询操作时间复杂度为O(1), 合并操作时间复杂度为O(n);
package UFs;
import UnionFind.UnionFind;
public class quickFindUF implements UnionFind {
//维护一个数组,数组的索引表示不同的元素(元素可以是各种类型),索引所对应的值表示该索引所属的集合
private int[] id;
public quickFindUF(int size){
id = new int[size];
//初始化每个元素都属于不同的集合
for(int i=0; i<id.length; i++){
id[i] = i;
}
}
@Override
public int size(){
return id.length;
}
//查看p元素和q元素是否属于同一个集合
@Override
public boolean isConnected(int p, int q){
return id[p] == id[q];
}
//合并p元素和q元素所属集合
@Override
public void unionElements(int p, int q){
int pUnion = id[p];
int qUnion = id[q];
if(pUnion == qUnion){
return;
}
for(int i=0; i<id.length; i++){
if(id[i] == pUnion){
id[i] = qUnion;
}
}
}
}
2. Quick Union并查集:
基本数据表示:
- 将每一个元素,看作是一个节点,是一种奇怪的树结构,是由孩子节点指向父亲节点
- 根节点的指针指向自己,合并两个元素,就是将其中一个元素的根节点指向另一个元素的根节点
- 底层仍然可以通过维护一个数组来实现,这里,数组的索引仍然表示元素的编号(元素可以是各种类型),但是,索引下存储的值表示的是该元素指向的父亲节点。注意:这里,下标表示元素,下标存储的值表示该下标元素的父亲元素
- 查询操作时间复杂度为O(h), 合并操作时间复杂度也为O(h) (h为当前树的深度);
- 初始化时:
package UFs;
import UnionFind.UnionFind;
public class quickUnionUF implements UnionFind {
//内部维护一个数组,数组索引表示元素编号,索引内存储的值表示该索引的父亲编号
//合并两个元素,就是将其中一个元素的根节点的父亲节点设为另一个元素的根节点
//这里,下标表示元素,下标存储的值表示该下标元素的父亲元素
private int[] parent;
public quickUnionUF(int size){
parent = new int[size];
//初始化并查集,将每个元素的父亲节点都设为自己
for(int i=0; i<size; i++){
parent[i] = i;
}
}
//查找过程,查找元素i所对应的集合编号,即元素i的根节点
//O(h)的复杂度,h为当前树的高度
private int find(int i){
//i不是根节点时,就上移i
while(i!=parent[i]){
i = parent[i];
}
return i;
}
@Override
public int size(){
return parent.length;
}
//查看元素p和元素q是否属于同一个集合,即根节点是否相同
//O(h)的复杂度,h为当前树的高度
@Override
public boolean isConnected(int p, int q){
int pRoot = find(p);
int qRoot = find(q);
return pRoot == qRoot;
}
//合并元素p和q,即将p的根节点指向q的根节点
//O(h)的复杂度,h为当前树的高度
@Override
public void unionElements(int p, int q){
int pRoot = find(p);
int qRoot = find(q);
if(pRoot == qRoot){
return;
}
//将pRoot的父亲节点设为qRoot,待优化
//优化思路,合并时,将深度小的树合并到深度大的树上,降低整体树的深度
parent[pRoot] = qRoot;
}
}
quickUnion并查集基于size的优化
- 基于size的优化,即维护一个sz数组,记录每个根节点所包含元素的个数,在合并元素时,将个数小的树合并到个数大的树上,避免合并后整棵树深度太深
- sz[i]表示以i为根的集合所表示的树的总节点个数
package UFs;
import UnionFind.UnionFind;
//基于size的优化,即维护一个sz数组,记录每个根节点所包含元素的个数,在合并元素时,将个数小的树合并到个数大的树上,避免合并后整棵树深度太深
public class quickUnionUFII implements UnionFind {
//内部维护一个数组,数组索引表示元素编号,索引内存储的值表示该索引的父亲编号
//合并两个元素,就是将其中一个元素的根节点的父亲节点设为另一个元素的根节点
//这里,下标表示元素,下标存储的值表示该下标元素的父亲元素
private int[] parent;
private int[] sz; //用于记录每个根节点所拥有的节点个数,sz[i]表示以i为根的集合所表示的树的节点的总个数
public quickUnionUFII(int size){
parent = new int[size];
sz = new int[size];
for(int i=0; i<size; i++){
parent[i] = i; //初始化并查集,将每个元素的父亲节点都设为自己
sz[i] = 1; //初始每个节点只包含自己1个节点
}
}
//查找过程,查找元素i所对应的集合编号,即元素i的根节点
//O(h)的复杂度,h为当前树的高度
private int find(int p){
while(parent[p]!=p){
p = parent[p];
}
return p;
}
@Override
public int size(){
return parent.length;
}
//查看元素p和元素q是否属于同一个集合,即根节点是否相同
//O(h)的复杂度,h为当前树的高度
@Override
public boolean isConnected(int p, int q){
return find(p) == find(q);
}
//合并元素p和q,即将p的根节点指向q的根节点
//O(h)的复杂度,h为当前树的高度
@Override
public void unionElements(int p, int q){
int pRoot = find(p);
int qRoot = find(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];
}
}
}
quickUnion并查集基于rank的优化
- 好基于rank的优化,即维护一个rank数组,记录每个根节点的层数,在合并元素时,将层数小的树合并到个层数大的树上,避免合并后整棵树深度太深
- rank[i]表示以i为根的集合所表示的树的层数
package UFs;
import UnionFind.UnionFind;
//基于rank的优化,即维护一个rank数组,记录每个根节点的层数,在合并元素时,将层数小的树合并到个层数大的树上,避免合并后整棵树深度太深
public class quickUnionUFIII implements UnionFind {
//内部维护一个数组,数组索引表示元素编号,索引内存储的值表示该索引的父亲编号
//合并两个元素,就是将其中一个元素的根节点的父亲节点设为另一个元素的根节点
//这里,下标表示元素,下标存储的值表示该下标元素的父亲元素
private int[] parent;
private int[] rank; //用于记录每个根节点的层数,rank[i]表示以i为根的集合所表示的树的层数
public quickUnionUFIII(int size){
parent = new int[size];
rank = new int[size];
for(int i=0; i<size; i++){
parent[i] = i; //初始化并查集,将每个元素的父亲节点都设为自己
rank[i] = 1; //初始每个节点自由一层
}
}
//查找过程,查找元素i所对应的集合编号,即元素i的根节点
//O(h)的复杂度,h为当前树的高度
private int find(int p){
while(parent[p]!=p){
p = parent[p];
}
return p;
}
@Override
public int size(){
return parent.length;
}
//查看元素p和元素q是否属于同一个集合,即根节点是否相同
//O(h)的复杂度,h为当前树的高度
@Override
public boolean isConnected(int p, int q){
return find(p) == find(q);
}
//合并元素p和q,即将p的根节点指向q的根节点
//O(h)的复杂度,h为当前树的高度
@Override
public void unionElements(int p, int q){
int pRoot = find(p);
int qRoot = find(q);
if(pRoot == qRoot){
return;
}
//合并过程,基于rank的优化,将层数小的树合并到层数大的树中
if(rank[pRoot]<rank[qRoot]){
parent[pRoot] = qRoot;
}else if(rank[pRoot]>rank[qRoot]){
parent[qRoot] = pRoot;
}else {
parent[qRoot] = pRoot;
rank[pRoot]++;
}
}
}
quickUnion并查集经典的“路径压缩”的优化
- 在find过程中实现
package UFs;
import UnionFind.UnionFind;
//基于rank的优化,即维护一个rank数组,记录每个根节点的层数,在合并元素时,将层数小的树合并到个层数大的树上,避免合并后整棵树深度太深
//同时做了一个经典的优化,在find过程中添加了路径压缩优化
public class quickUnionUFIV implements UnionFind {
//内部维护一个数组,数组索引表示元素编号,索引内存储的值表示该索引的父亲编号
//合并两个元素,就是将其中一个元素的根节点的父亲节点设为另一个元素的根节点
//这里,下标表示元素,下标存储的值表示该下标元素的父亲元素
private int[] parent;
private int[] rank; //用于记录每个根节点的层数,rank[i]表示以i为根的集合所表示的树的层数
public quickUnionUFIV(int size){
parent = new int[size];
rank = new int[size];
for(int i=0; i<size; i++){
parent[i] = i; //初始化并查集,将每个元素的父亲节点都设为自己
rank[i] = 1; //初始每个节点自由一层
}
}
//查找过程,查找元素i所对应的集合编号,即元素i的根节点
//O(h)的复杂度,h为当前树的高度
private int find(int p){
while(parent[p]!=p){
//路径压缩优化, 若i为根节点,则其parent是其本身,即parent[i] = i
parent[p] = parent[parent[p]];
p = parent[p];
}
return p;
}
@Override
public int size(){
return parent.length;
}
//查看元素p和元素q是否属于同一个集合,即根节点是否相同
//O(h)的复杂度,h为当前树的高度
@Override
public boolean isConnected(int p, int q){
return find(p) == find(q);
}
//合并元素p和q,即将p的根节点指向q的根节点
//O(h)的复杂度,h为当前树的高度
@Override
public void unionElements(int p, int q){
int pRoot = find(p);
int qRoot = find(q);
if(pRoot == qRoot){
return;
}
//合并过程,基于rank的优化,将层数小的树合并到层数大的树中
if(rank[pRoot]<rank[qRoot]){
parent[pRoot] = qRoot;
}else if(rank[pRoot]>rank[qRoot]){
parent[qRoot] = pRoot;
}else {
parent[qRoot] = pRoot;
rank[pRoot]++;
}
}
}
quickUnion并查集经典的“路径压缩”的优化,递归实现
- 可以在find(i)时,直接将i的parent设为根节点,时间性能上稍逊于非递归实现的“压缩路径”;
- 非递归的“压缩路径”虽然不能在一次调用find(i)后就达到将i的parent设为根节点的效果,但多次对i调用find(i)后是可以的,避免了递归过程中花费的开销
package UFs;
import UnionFind.UnionFind;
//基于rank的优化,即维护一个rank数组,记录每个根节点的层数,在合并元素时,将层数小的树合并到个层数大的树上,避免合并后整棵树深度太深
//"压缩路径"优化的递归实现,可以在find(i)时,直接将i的parent设为根节点,性能上稍微逊与非递归的“压缩路径”
//因为非递归的“压缩路径”在多次对i进行find(i)后,也可达到将i的parent设为根节点的效果,避免了递归的开销。
public class quickUnionUFV implements UnionFind {
//内部维护一个数组,数组索引表示元素编号,索引内存储的值表示该索引的父亲编号
//合并两个元素,就是将其中一个元素的根节点的父亲节点设为另一个元素的根节点
//这里,下标表示元素,下标存储的值表示该下标元素的父亲元素
private int[] parent;
private int[] rank; //用于记录每个根节点的层数,rank[i]表示以i为根的集合所表示的树的层数
public quickUnionUFV(int size){
parent = new int[size];
rank = new int[size];
for(int i=0; i<size; i++){
parent[i] = i; //初始化并查集,将每个元素的父亲节点都设为自己
rank[i] = 1; //初始每个节点自由一层
}
}
//查找过程,查找元素i所对应的集合编号,即元素i的根节点
//O(h)的复杂度,h为当前树的高度
private int find(int p){
if(parent[p]!=p){
//递归实现"路径压缩"优化, 若i为根节点,则其parent是其本身,即parent[i] = i
//直接将i的parent设为根节点
parent[p] = find(parent[p]);
}
return parent[p];
}
@Override
public int size(){
return parent.length;
}
//查看元素p和元素q是否属于同一个集合,即根节点是否相同
//O(h)的复杂度,h为当前树的高度
@Override
public boolean isConnected(int p, int q){
return find(p) == find(q);
}
//合并元素p和q,即将p的根节点指向q的根节点
//O(h)的复杂度,h为当前树的高度
@Override
public void unionElements(int p, int q){
int pRoot = find(p);
int qRoot = find(q);
if(pRoot == qRoot){
return;
}
//合并过程,基于rank的优化,将层数小的树合并到层数大的树中
if(rank[pRoot]<rank[qRoot]){
parent[pRoot] = qRoot;
}else if(rank[pRoot]>rank[qRoot]){
parent[qRoot] = pRoot;
}else {
parent[qRoot] = pRoot;
rank[pRoot]++;
}
}
}