您的位置:首页 > 编程语言 > Java开发

算法入门---java语言实现的并查集(Union-Find)小结

2017-04-08 12:22 531 查看
图片来自慕课网,仅仅为了记录学习。
基本概念
/**

*

*并查集,用来解决连通问题的,两个节点之间是否是连通的。

*此处的节点是抽象的概念:比如用户和用户之间,港口和港口之间。

*用来看他们是否是连通的。典型的就是看你社交中任何人之间的关系是否认识.

*并查集问题和路径问题的区别:并查集比路径能做的操作少,它只能回答两个节点是否连通

*路径还可以找到类似最短的连通点等等.但正因为并查集专注于连接问题,所以判断是否连接

*修改连接状态时比较高效.

*

*Union-Find 如其名字我们主要提供

*union(p,q); //合并p、q两点使他们两个连通.

*find(p); //找到节点q的连通性,(处在什么状态合谁联通)

*通过find的api,我们可以找到两个节点是否会连通的,即api

*isConnected(p,q);

*@author zhaoyuan.

*

*/


1、第一种实现:quick-find



union的时候每一个相等的都要改,如:1所以和2索引连通,那么1、3、5、7、9索引 对应的id必须都变为0.

//第一种基本的实现quick-find.

public class UnionFind {

//此处用一个id数组来表示每个节点的连通性。

//当节点连接到一起的时候那么它们有相同的id号

private int[] mIds;

//表示描述的节点的规模,总共有多少个

private int mCount;


//构造中实例化用来保存连通状态的数组,并初始化连通状态

//传入的并查集要表示多少个元素

public UnionFind(int capacity){

 mCount = capacity;

 mIds = new int[mCount];

 //初始为每个点都不连通,此处i不同就表示不连通,想要连通是就把i设置为同一个即可

 //同时也隐含着mCount各节点元素,每个节点元素的对应索引0...n,连通性,此处默认赋值都不连通

 for (int i = 0; i < mCount; i++) {

  mIds[i] = i+5;//注意id代表的含义不要和索引混了

  }

}

//寻找p索引对应的连通性的状态,可以看到查找某个元素的连通状态码

//是非常的快的,直接在数组中索引即可时间复杂度O(1)

public int find(int p){

 if( p<0 || p>=mCount){

  //...做一些异常处理

 }

 //直接返回当前索引所对应的元素的连通性,

 //此处设计的是每个连通性默认是索引号.

 return mIds[p];

}

//此处设计是用的数组存储元素,传入的是数组内元素的索引,注意这个数组不是指mIds.

public boolean isConnected(int p,int q){

 //返回p和q在ids数组中对应的连通状态码是否一致。

 return find(p) == find(q);

}

//联合的整体思路:

//要么把p索引在mIds中的状态变成q的,

//要么把q索引在mIds中的状态变成p的

//mIds中的状态代表了连通性,id号相等就代表连通。

//此时就遍历mIds数组,然后把p/q索引对应的id进行相关赋值

public void union(int p,int q){

 

 //先拿到p和q的id

 int pId = find(p);

 int qId = find(q);

 

 //如果已经相等那么直接返回

 

 if( pId == qId ){

  return;

 }

 //注意如下为什么不直接mIds[p] = qId,不要被初始状态迷惑

 //此处的设计思想quick-find查找快,但想要改变连通性的时候

 //需要把所有的节点中的和pId相等的状态码,全部变成qId的状态码

 //只有这样才能算是完全的连通了,你不能只改一个啊!!

 //这种设计模式下的union的时间复杂度是O(n).

 for(int i=0;i<mCount;i++){

  if(mIds[i] == pId){

mIds[i] = qId;

  }

 }

}

}


这种实现最终整体的时间复杂度还是O(n)的级别,并且每次p、q连通的时,要改变所有连通id等于p的元素,把它们全都赋值成q对应的ids。

第二种实现:quick-union



初始的时候每个人的parent中的数指向自己的索引:

  


 然后执行union (4 3):就是把索引4对应的元素的parent指向索引3.



 再比如:union(4,9)



找到4所在的属性集合的根节点,然后连入到9所在的树形结构的根节点,但此时9正好是根节点。



 

/**

*快速合并的并查集实现的思路:(还是用数组)

*每个元素层都看成是一个节点,该节点有一个引用指向它的父节点。

*如果一个元素指向父节点那么它就和父节点连通,当这个元素本身就是根的时候,

*那么父节点就指向本身。

*由于当前只需一个用来存储父节点的空间,所以依然可以用数组来实现,此处用一个

*int[] parent数组,里面存放的是元素要连通的父节点的索引.初始状态都连接自己

*注意和quick-find区别,它表示连通的时可以 4--->3--->8 表示3 4 8都连通

*单用quick-find的时候,就需要 元素索引4、3 、8对应的id都为一样的id号。

*

*@author zhaoyuan

*

*/

public class QuickUnion {

 private int[] mParents;

 private int mCount;

 

 public QuickUnion(int capacity){

  mCount = capacity;

  mParents = new int[mCount];

  //初始化时每个索引对应的mParents都为自己的索引+5,表示谁也不连接

  for (int i = 0; i < mCount; i++) {

mParents[i] = i;//初始状态为每个节点自己的索引

  }

 }

 

 //查找索引p在parent中对应的连通状态码,当它是在一个树的结构中时,

 //需要找到它一直往上直到根节点的对应码,因为我们联合的时候都是按照

 //根节点进行联合的

 public int find(int p){

  if( p<0 || p>=mCount){

//...做一些异常处理

  }

  //最根部的肯定是等于当前索引的.

  while(p!= mParents[p]){

//依次往上,把指向的父索引值赋值给当前的p循环查找.

p = mParents[p];

  }

  return p;

 }

 //是否连通

 public boolean isConnected(int p,int q){

  return find(p)==find(q);

 }

 //联合p所以和q索引对应的状态.此处的设计:

 //  1)、把p所在的树的根节点指向q所在的树的根节点

 //  2)、把q所在的树的根节点指向p所在的树的根节点

 //但从此角度考虑的话两种实现其实是一样的

 public void union(int p,int q){

  //还是先找到pId和qId。

  int pRoot= find(p);

  int qRoot = find(q);

  //如果相等的时候,证明已经联合

  if(pRoot == qRoot){

return;

  }

  //第一版我们什么也不考虑直接把p所在的树的根节点pRoot指向q所在的树的根。

  //所以注意不是mParents[p] = qRoot,应该是p索引找到的根

  //这个根肯定这会也是指向自己的元素的索引,直接mParents[pRoot]

//把mParents中pRoot索引对应的值变成qRoot,也就是指向qRoot. 

  mParents[pRoot] = qRoot;

 }

}


这种实现测试发现联合、判断是否连接公共花费的时间不比quick-find少,因此我们进一步优化,让树的层次更
为平缓,我们在前面联合的时候直接把pRoot的根指向了qRoot的根,这样假如p这个树的节点比较多的时候,把
它指向q所在的树的时候,这个树的深度会增加!因此我们可以以此为优化,把深度小的树的根节点指向深度
大的树的根节点,这样让整体更为扁平化。

就比如前面的union(4,9)



 我们可以把元素比较少的如:9的节点连接到元素比较多的4节点所在的根8上,这样树形图的整体深度就不会增加。

第三种实现

public class QuickUnionBetter {


 private int[] mParents;

 

 //新加一个数组用来记录每一个节点,以它为根的元素的个数。

 //mSize[i]表示以i为根的树结构中的元素个数。

 private int[] mSize;

 private int mCount;

 public QuickUnionBetter(int capacity){

  mCount = capacity;

  mParents = new int[mCount];

  mSize = new int[mCount];

  for (int i = 0; i < mCount; i++) {

mParents[i] = i;

//默认每个都是1:独立的时候含有一个元素.

mSize[i] = 1;

  }

 }

 //以下find和isConnected都用不到mSize.

 public int find(int p){

  if( p<0 || p>=mCount){

//...做一些异常处理

  }

  while(p!=mParents[p]){

p = mParents[p];

  }

  return p;

 }

 public boolean isConnected(int p,int q){

  return find(p)==find(q);

 }

 //联合的时候就需要用到mSize了.看看那个节点为根的树形集合中元素多,

 //然后把少的那个节点对应的根,指向多的那个节点对应的根。

 public void union(int p,int q){

  //前两步不变

  int pRoot= find(p);

  int qRoot = find(q);

  if(pRoot == qRoot){

return;

  }

  int pSize = mSize[pRoot];//初始事都是根,为1

  int qSize = mSize[qRoot];

  //如果pRoot为根的树形集合含有的元素比qRoot的多

  if(pSize > qSize){

//注意是少的索引的父节点指向多的

mParents[qRoot] = pRoot;

//注意此时mSize的改变,由于qRoot归并到了pRoot当中那么

//需要加上相应数量的size,注意qRoot对应的size并没有改变

mSize[pRoot] = pSize+qSize;

  }/*else if(pSize < qSize){//同理

mParents[pRoot] = qRoot;

mSize[qRoot] = pSize+qSize;

  }else{//如果两个相等那么就无所谓了,谁先合并到谁都可以.

mParents[qRoot] = pRoot;

mSize[pRoot] = pSize+qSize;

  }*/

  //然后就可以把等于的合入到大于或者小于的里面.

  else{//此处把小于和等于合到一块

mParents[pRoot] = qRoot;

mSize[qRoot] = pSize+qSize;

  }

 }

}


但是还有可能出问题,因为以某个节点为根的树的集合元素多并不一定代表深度就大,我们还可以按照深度来进行优化。把深度小的合并到深度大的节点中,这种优化叫做基于rank的。

特殊情况如下:



按照size的优化方式进行合并的时候,最终树的深度又增加一层
 


按照我们新的rank的优化方式进行优化把层数比较少的连入层数比较多的!

 


 

第四种实现:

public class QuickUnionBest {

 private int[] mParents;

 //mRank[i]表示以i为根节点的集合所表示的树的层数

 private int[] mRank;

 private int mCount;

 public QuickUnionBest(int capacity){

  mCount = capacity;

  mParents = new int[mCount];

  mRank = new int[mCount];

  for (int i = 0; i < mCount; i++) {

mParents[i] = i;

//默认每个都是1:表示深度为1层

mRank[i] = 1;

  }

 }

 //以下find和isConnected都用不到mRank.

 public int find(int p){

  if( p<0 || p>=mCount){

//...做一些异常处理

  }

  while(p!=mParents[p]){

p = mParents[p];

  }

  return p;

 }

 public boolean isConnected(int p,int q){

  return find(p)==find(q);

 }

 //找到p、q节点所在的树形集合的根节点,它的深度。然后把深度小的根节点合入到深度大的根节点当中

 public void union(int p,int q){

  //前两步不变

  int pRoot= find(p);

  int qRoot = find(q);

  if(pRoot == qRoot){

return;

  }

  int pRank = mRank[pRoot];//初始事都是深度为1

  int qRank= mRank[qRoot];

  //如果p的深度比q的深度大.

  if(pRank > qRank){

//注意是小的指向大的,也就是为小的重新读之

mParents[qRoot] = pRoot;

//此时把并不需要维护pRank,因为qRank是比pRank小的

//也就是q更浅,它不会增加p的深度,只会增加去p的宽度

  }else if(pRank < qRank){

mParents[pRoot] = qRoot;

//同样的道理不需要维护qRank,p只会增加它的宽度

  }else{

//当两个深度相同的时候,谁指向谁都可以,但是注意此时的深度维护

//被指向的那个的深度需要加1.

//此时让qRoot指向pRoot吧.

mParents[qRoot] = pRoot;

mRank[pRoot]++;

  }

 }

}


以上这种实现其实和第三种差不多,有时候甚至比第三种还要费一点点时间,但整体来说理论上可以防止出现
意外的概率。

最后一种优化路径压缩

特殊情况时:



 

第一种压缩方法:


 
 

第二种压缩方法:

 


只是修改了find方法而已.

 //为防止极端的情况,可以再在find的时候经行路径压缩有两种压缩方法:

 //1)、一层层的跳着压缩(隔一层走一下):

 //就是当前节点的父节点parent[i]指向它父节点的parent,此时当前节点的父节点的

 //parent不用担心不存在,因为不存在时parent会指向自己!这也是我们退出循环的条件

 //2)、压缩到深度只有两层

 //利用递归实现,让最终只有树形集合中除了根意外其它节点都在第二层.

 // 理论上看第二种广度更大,应该时间更少,实测很多时候甚至比第一种多一点点。个人感觉可能是

 // 由于一是用了递归,而这种方式在quick-union这种情况下优化费的步骤过于多.

 public int find(int p){

  if(p < 0||p > mParents.length){

//异常处理

  }

  while(p!=mParents[p]){

 //首先是拿到父节点的父节点指向,然后赋值给当前节点的父节点。

 //也就是parent[p]:当前节点的父节点;

 //mParents[mParents[p]]:当前节点的父节点的父节点指向。

 mParents[p] = mParents[mParents[p]];

 //然后是当前节点跳一下,直接指向新得到的mParents[p].

 p = mParents[p];

 //继续循环

  }

  /*//第二种:通过如下,递归调用本函数find,

  //mParents[p]为当前节点的父节点的索引,循环传入(递归)直到根节点

  //p == mParents[p] 返回当前节点的索引,然后就层层返回。

  if(p!=mParents[p]){

//其实也不该考虑一层层的就,考虑这一层,当前节点的索引和父节点不同,

//也就是说当前不是根节点,那么传入父节点的索引,递归调用把上个节点的父节点

//传给当前节点的父节点。

mParents[p] = find( mParents[p] );

  }

  //因为最终走到这的是根节点,根节点的parent是自己.

  return mParents[p];*/

  return p;

 }


附上java测试代码:

package com.zy.tt;


public class Helper {

 public void testUF1( int n ){

  UnionFoundBest uf = new UnionFoundBest(n);

  long start = getTime();

  for( int i = 0 ; i < n ; i ++ ){

int a = (int) (Math.random()*n);

int b = (int) (Math.random()*n);

uf.union(a,b);

  }

  for(int i = 0 ; i < n ; i ++ ){

int a = (int) (Math.random()*n);

int b = (int) (Math.random()*n);

uf.isConnected(a,b);

  }

  long end = getTime();

  showTimeDiff("quick-union no opt", start, end);

 }

 public void testUF2( int n ){

  UniodFoundFinal uf = new UniodFoundFinal(n);

  long start = getTime();

  for( int i = 0 ; i < n ; i ++ ){

int a = (int) (Math.random()*n);

int b = (int) (Math.random()*n);

uf.union(a,b);

  }

  for(int i = 0 ; i < n ; i ++ ){

int a = (int) (Math.random()*n);

int b = (int) (Math.random()*n);

uf.isConnected(a,b);

  }

  long end = getTime();

  showTimeDiff("quick-union optmization", start, end);

  //uf.show();

 }

 

 

 /**

*以s为单位获取当前时间

*@return 当前时间的秒数。

*/

 public long getTime(){

  long time = System.currentTimeMillis();

  return time;

 }

 

 /**

*显示时间差

*@param name 排序算法的名字

*@param start 开始的时间

*@param end 结束的时间

*/

 public void showTimeDiff(String name ,long start,long end){

  

  long diff = end - start;

  System.out.println("name: "+name+" 花费了 = "+diff+"ms");

 }

}
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息