并查集
并查集也是一种树型结构,但这棵树跟我们之前讲的二叉树、红黑树、B树等都不一样,这种树的要求比较简单:
每个元素都唯一的对应一个结点;
每一组数据中的多个元素都在同一颗树中;
一个组中的数据对应的树和另外一个组中的数据对应的树之间没有任何联系;
元素在树中并没有子父级关系的硬性要求;
//并查集代码
public class UF {
private int[] eleAndGroup; //记录结点元素和所在分组的标识
private int count; //记录并查集中数据分组个数
//初始化并查集
public UF(int N){
//初始情况下,每个元素都在一个独立的分组中,所以,初始情况下,并查集的数据默认分组为N个组
this.count=N;
//初始化数组
eleAndGroup=new int[N];
//把eleAndGroup数组的索引看做是每个结点存储的元素,把eleAndGroup数组每个
//索引处的值看做是该节点所在的分组,那么初始情况下,i索引处存储的值就是i
for (int i = 0; i < N; i++) {
eleAndGroup[i]=i;
}
}
//获取当前并查集中的数据有多少个分组
public int count(){
return count;
}
//元素p所在分组的标识符
public int find(int p){
return eleAndGroup[p];
}
//判断并查集中元素p和元素q是否在同一个分组
public boolean connected(int p,int q){
return find(p)==find(q);
}
//把p元素所在分组和q元素所在分组合并
public void union(int p,int q){
//如果p和q已经在同一个分组中,则无需合并
if(connected(p, q)){
return;
}
//如果p和q不在同一个分组,则只需要将p元素所在组的所有元素的组标识符修改为q元素
//所在的组标识符即可
int pGroup=find(p);
int qGroup=find(q);
for (int i = 0; i < eleAndGroup.length; i++) {
if(eleAndGroup[i]==pGroup){
eleAndGroup[i]=qGroup;
}
}
//分组数量-1
count--;
}
}
测试代码
public class UFTest {
public static void main(String[] args) {
Scanner sc=new Scanner(System.in);
System.out.println("请录入并查集中元素的个数:");
int N=sc.nextInt();
UF uf=new UF(N);
while (true){
System.out.println("请录入你要合并的第一个点:");
int p=sc.nextInt();
System.out.println("请录入你要合并的第二个点:");
int q=sc.nextInt();
//判断p和q是否在同一个组
if(uf.connected(p,q)){
System.out.println("结点:"+p+"结点"+q+"已经在同一个组");
continue;
}
uf.union(p,q);
System.out.println("总共还有"+uf.count()+"个分组");
}
}
}
UF_Tree算法优化
为了提升union算法的性能,我们需要重新设计find方法和union方法的实现,此时我们先需要对我们的之前数据结构中的eleAndGourp数组的含义进行重新设定:
1.我们仍然让eleAndGroup数组的索引作为某个结点的元素;
2.eleAndGroup[i]的值不再是当前结点所在的分组标识,而是该结点的父结点;
public class UF_Tree {
private int[] eleAndGroup; //记录结点元素和该元素的父结点
private int count; //记录并查集中数据分组的个数
public UF_Tree(int N){
//初始情况下,每个元素都在一个独立的分组中,所以,初始情况下并查集数据默认分为N个组
this.count=N;
//初始化数组
eleAndGroup=new int[N];
//把eleAndGroup数组的索引看做是每个结点存储的元素,把eleAndGroup数组每个
//索引处的值看作是该节点的父结点,那么初始情况下。i索引处存储的值就是i
for (int i=0;i<N;i++){
eleAndGroup[i]=i;
}
}
//获取当前并查集中数据有多少个分组
public int count(){
return count;
}
//元素p所在分组的标识符
public int find(int p){
while (true){
//判断当前元素p的父结点eleAndGroup[p]是不是自己,如果是自己,则证明已经是根节点
if(p==eleAndGroup[p]){
return p;
}
//如果当前元素p的父结点不是自己,则让p=eleAndGroup[p],继续寻找
//父结点的父结点,直到找到根节点为止
p=eleAndGroup[p];
}
}
//判断并查集中元素p和元素q是否在同一个分组中
public boolean connected(int p,int q){
return find(p)==find(q);
}
//把p元素所在分组和q元素所在分组合并
public void union(int p,int q){
//找到p元素所在树的根节点
int pRoot=find(p);
//找到q元素所在树是根节点
int qRoot=find(q);
//如果p和q已经在同一颗树中,则无需合并
if(pRoot==qRoot){
return;
}
//如果p和q不在同一个分组,则只需要将p元素所在根节点的父结点设置为
//q元素的根节点
eleAndGroup[pRoot]=qRoot;
count--;
}
}
分析:
我们优化后的算法union,如果要把并查集中所有的数据连通,仍然至少要调用N-1次union方法,但是,我们发现union方法中已经没有了for循环,所以union算法的时间复杂度由O(N^2)变为了O(N)。
但是这个算法仍然有问题,因为我们之前不仅修改了union算法,还修改了find算法。我们修改前的find算法的时间复杂度在任何情况下都为O(1),但修改后的find算法在最坏情况下是O(N)
路径压缩
UF_Tree中最坏情况下union算法的时间复杂度为O(N^2),其最主要的问题在于最坏情况下,树的深度和数组的大小一样,如果我们能够通过一些算法让合并时,
生成的树的深度尽可能的小,就可以优化find方法。之前我们在union算法中,合并树的时候将任意的一棵树连接到了另外一棵树,这种合并方法是比较暴力的,如果我们把并查集中每一棵树的大小记录下来,然后在每次合并树的时候,把较小的树连接到较大的树上,就可以减小树的深度。
//路径压缩
public class UF_Tree_Weight {
private int[] eleAndGroup;//记录结点元素和该元素的父结点
private int[] sz; //存储每个根节点对应树中的元素个数
private int count; //记录并查集中数据的分组个数
public UF_Tree_Weight(int N){
//初始情况下,每个元素都在一个独立的分组中,所以,初始情况下,并查集中
//的数据默认分为N个组
this.count=N;
//初始化数组
eleAndGroup=new int[N];
sz=new int[N];
//把eleAndGroup数组的索引看做是每个结点存储的元素,把eleAndGroup
//数组每个索引处的值看作是该节点的父结点,那么初始化的情况下,i处的索引值
//就是i
for (int i = 0; i < N; i++) {
eleAndGroup[i]=i;
}
//把sz数组中所有元素初始化为1,默认情况下,每个结点都是一个独立的树
//每个树中只有一个元素
for (int i = 0; i < sz.length; i++) {
sz[i]=1;
}
}
//获取当前并查集中的数据有多少个分组
public int count(){
return count;
}
//元素p所在分组的标识符
public int find(int p){
while (true){
//判断当前元素p的父结点eleAndGroup[p]是不是自己,如果是,则证明
//自己是根节点了
if(p==eleAndGroup[p]){
return p;
}
//如果当前元素p的父结点不是自己,则让p=eleAndGroup[p],继续找
//父结点的父结点,直到找到根节点为止
p=eleAndGroup[p];
}
}
//判断并查集中元素p和元素q是否在同一个分组中
public boolean connected(int p,int q){
return find(p)==find(q);
}
//把p元素所在分组和q元素所在分组合并
public void union(int p,int q){
//找到p元素所在树的根节点
int pRoot=find(p);
//找到q元素所在树的根节点
int qRoot=find(q);
//如果p和q已经在同一个树中,则无需合并
if(pRoot==qRoot){
return;
}
//如果p和q不在同一个分组,比较p所在树的元素个数和q所在树的元素个数,
//则把较小的树合并到较大的树
if(sz[pRoot]<sz[qRoot]){
eleAndGroup[pRoot]=qRoot;
//重新调整较大树的元素个数
sz[qRoot]+=sz[pRoot];
}else {
eleAndGroup[q]=pRoot;
sz[pRoot]+=sz[qRoot];
}
//分组数量-1
count--;
}
}