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

图片来自慕课网,仅仅为了记录学习。
基本概念
  
  
  1. /**
  2. *
  3. * 并查集,用来解决连通问题的,两个节点之间是否是连通的。
  4. * 此处的节点是抽象的概念:比如用户和用户之间,港口和港口之间。
  5. * 用来看他们是否是连通的。典型的就是看你社交中任何人之间的关系是否认识.
  6. * 并查集问题和路径问题的区别:并查集比路径能做的操作少,它只能回答两个节点是否连通
  7. * 路径还可以找到类似最短的连通点等等.但正因为并查集专注于连接问题,所以判断是否连接
  8. * 修改连接状态时比较高效.
  9. *
  10. * Union-Find 如其名字我们主要提供
  11. * union(p,q); //合并p、q两点使他们两个连通.
  12. * find(p); //找到节点q的连通性,(处在什么状态合谁联通)
  13. * 通过find的api,我们可以找到两个节点是否会连通的,即api
  14. * isConnected(p,q);
  15. * @author zhaoyuan.
  16. *
  17. */
1、第一种实现:quick-find

union的时候每一个相等的都要改,如:1所以和2索引连通,那么1、3、5、7、9索引 对应的id必须都变为0.
   
   
  1. //第一种基本的实现quick-find.
  2. public class UnionFind {
  3. //此处用一个id数组来表示每个节点的连通性。
  4. //当节点连接到一起的时候那么它们有相同的id号
  5. private int[] mIds;
  6. //表示描述的节点的规模,总共有多少个
  7. private int mCount;
  8. //构造中实例化用来保存连通状态的数组,并初始化连通状态
  9. //传入的并查集要表示多少个元素
  10. public UnionFind(int capacity){
  11. mCount = capacity;
  12. mIds = new int[mCount];
  13. //初始为每个点都不连通,此处i不同就表示不连通,想要连通是就把i设置为同一个即可
  14. //同时也隐含着mCount各节点元素,每个节点元素的对应索引0...n,连通性,此处默认赋值都不连通
  15. for (int i = 0; i < mCount; i++) {
  16. mIds[i] = i+5;//注意id代表的含义不要和索引混了
  17. }
  18. }
  19. //寻找p索引对应的连通性的状态,可以看到查找某个元素的连通状态码
  20. //是非常的快的,直接在数组中索引即可时间复杂度O(1)
  21. public int find(int p){
  22. if( p<0 || p>=mCount){
  23. //...做一些异常处理
  24. }
  25. //直接返回当前索引所对应的元素的连通性,
  26. //此处设计的是每个连通性默认是索引号.
  27. return mIds[p];
  28. }
  29. //此处设计是用的数组存储元素,传入的是数组内元素的索引,注意这个数组不是指mIds.
  30. public boolean isConnected(int p,int q){
  31. //返回p和q在ids数组中对应的连通状态码是否一致。
  32. return find(p) == find(q);
  33. }
  34. //联合的整体思路:
  35. // 要么把p索引在mIds中的状态变成q的,
  36. // 要么把q索引在mIds中的状态变成p的
  37. //mIds中的状态代表了连通性,id号相等就代表连通。
  38. //此时就遍历mIds数组,然后把p/q索引对应的id进行相关赋值
  39. public void union(int p,int q){
  40. //先拿到p和q的id
  41. int pId = find(p);
  42. int qId = find(q);
  43. //如果已经相等那么直接返回
  44. if( pId == qId ){
  45. return;
  46. }
  47. //注意如下为什么不直接mIds[p] = qId,不要被初始状态迷惑
  48. //此处的设计思想quick-find查找快,但想要改变连通性的时候
  49. //需要把所有的节点中的和pId相等的状态码,全部变成qId的状态码
  50. //只有这样才能算是完全的连通了,你不能只改一个啊!!
  51. //这种设计模式下的union的时间复杂度是O(n).
  52. for(int i=0;i<mCount;i++){
  53. if(mIds[i] == pId){
  54. mIds[i] = qId;
  55. }
  56. }
  57. }
  58. }
这种实现最终整体的时间复杂度还是O(n)的级别,并且每次p、q连通的时,要改变所有连通id等于p的元素,把它们全都赋值成q对应的ids。
第二种实现:quick-union

初始的时候每个人的parent中的数指向自己的索引:
  
 然后执行union (4 3):就是把索引4对应的元素的parent指向索引3.

 再比如:union(4,9)

找到4所在的属性集合的根节点,然后连入到9所在的树形结构的根节点,但此时9正好是根节点。
 
   
   
  1. /**
  2. * 快速合并的并查集实现的思路:(还是用数组)
  3. * 每个元素层都看成是一个节点,该节点有一个引用指向它的父节点。
  4. * 如果一个元素指向父节点那么它就和父节点连通,当这个元素本身就是根的时候,
  5. * 那么父节点就指向本身。
  6. * 由于当前只需一个用来存储父节点的空间,所以依然可以用数组来实现,此处用一个
  7. * int[] parent数组,里面存放的是元素要连通的父节点的索引.初始状态都连接自己
  8. * 注意和quick-find区别,它表示连通的时可以 4--->3--->8 表示3 4 8都连通
  9. * 单用quick-find的时候,就需要 元素索引4、3 、8对应的id都为一样的id号。
  10. *
  11. * @author zhaoyuan
  12. *
  13. */
  14. public class QuickUnion {
  15. private int[] mParents;
  16. private int mCount;
  17. public QuickUnion(int capacity){
  18. mCount = capacity;
  19. mParents = new int[mCount];
  20. //初始化时每个索引对应的mParents都为自己的索引+5,表示谁也不连接
  21. for (int i = 0; i < mCount; i++) {
  22. mParents[i] = i;//初始状态为每个节点自己的索引
  23. }
  24. }
  25. //查找索引p在parent中对应的连通状态码,当它是在一个树的结构中时,
  26. //需要找到它一直往上直到根节点的对应码,因为我们联合的时候都是按照
  27. //根节点进行联合的
  28. public int find(int p){
  29. if( p<0 || p>=mCount){
  30. //...做一些异常处理
  31. }
  32. //最根部的肯定是等于当前索引的.
  33. while(p!= mParents[p]){
  34. //依次往上,把指向的父索引值赋值给当前的p循环查找.
  35. p = mParents[p];
  36. }
  37. return p;
  38. }
  39. //是否连通
  40. public boolean isConnected(int p,int q){
  41. return find(p)==find(q);
  42. }
  43. //联合p所以和q索引对应的状态.此处的设计:
  44. // 1)、把p所在的树的根节点指向q所在的树的根节点
  45. // 2)、把q所在的树的根节点指向p所在的树的根节点
  46. //但从此角度考虑的话两种实现其实是一样的
  47. public void union(int p,int q){
  48. //还是先找到pId和qId。
  49. int pRoot= find(p);
  50. int qRoot = find(q);
  51. //如果相等的时候,证明已经联合
  52. if(pRoot == qRoot){
  53. return;
  54. }
  55. //第一版我们什么也不考虑直接把p所在的树的根节点pRoot指向q所在的树的根。
  56. //所以注意不是mParents[p] = qRoot,应该是p索引找到的根
  57. //这个根肯定这会也是指向自己的元素的索引,直接mParents[pRoot]
  58. //把mParents中pRoot索引对应的值变成qRoot,也就是指向qRoot.
  59. mParents[pRoot] = qRoot;
  60. }
  61. }
这种实现测试发现联合、判断是否连接公共花费的时间不比quick-find少,因此我们进一步优化,让树的层次更
为平缓,我们在前面联合的时候直接把pRoot的根指向了qRoot的根,这样假如p这个树的节点比较多的时候,把
它指 向q所在的树的时候,这个树的深度会增加!因此我们可以以此为优化,把深度小的树的根节点指向深度
大的树的根节点,这样让整体更为扁平化。
就比如前面的union(4,9)

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

第三种实现
   
   
  1. public class QuickUnionBetter {
  2. private int[] mParents;
  3. //新加一个数组用来记录每一个节点,以它为根的元素的个数。
  4. //mSize[i]表示以i为根的树结构中的元素个数。
  5. private int[] mSize;
  6. private int mCount;
  7. public QuickUnionBetter(int capacity){
  8. mCount = capacity;
  9. mParents = new int[mCount];
  10. mSize = new int[mCount];
  11. for (int i = 0; i < mCount; i++) {
  12. mParents[i] = i;
  13. //默认每个都是1:独立的时候含有一个元素.
  14. mSize[i] = 1;
  15. }
  16. }
  17. //以下find和isConnected都用不到mSize.
  18. public int find(int p){
  19. if( p<0 || p>=mCount){
  20. //...做一些异常处理
  21. }
  22. while(p!=mParents[p]){
  23. p = mParents[p];
  24. }
  25. return p;
  26. }
  27. public boolean isConnected(int p,int q){
  28. return find(p)==find(q);
  29. }
  30. //联合的时候就需要用到mSize了.看看那个节点为根的树形集合中元素多,
  31. //然后把少的那个节点对应的根,指向多的那个节点对应的根。
  32. public void union(int p,int q){
  33. //前两步不变
  34. int pRoot= find(p);
  35. int qRoot = find(q);
  36. if(pRoot == qRoot){
  37. return;
  38. }
  39. int pSize = mSize[pRoot];//初始事都是根,为1
  40. int qSize = mSize[qRoot];
  41. //如果pRoot为根的树形集合含有的元素比qRoot的多
  42. if(pSize > qSize){
  43. //注意是少的索引的父节点指向多的
  44. mParents[qRoot] = pRoot;
  45. //注意此时mSize的改变,由于qRoot归并到了pRoot当中那么
  46. //需要加上相应数量的size,注意qRoot对应的size并没有改变
  47. mSize[pRoot] = pSize+qSize;
  48. }/*else if(pSize < qSize){//同理
  49. mParents[pRoot] = qRoot;
  50. mSize[qRoot] = pSize+qSize;
  51. }else{//如果两个相等那么就无所谓了,谁先合并到谁都可以.
  52. mParents[qRoot] = pRoot;
  53. mSize[pRoot] = pSize+qSize;
  54. }*/
  55. //然后就可以把等于的合入到大于或者小于的里面.
  56. else{//此处把小于和等于合到一块
  57. mParents[pRoot] = qRoot;
  58. mSize[qRoot] = pSize+qSize;
  59. }
  60. }
  61. }
但是还有可能出问题,因为以某个节点为根的树的集合元素多并不一定代表深度就大,我们还可以按照深度来进行优化。把深度小的合并到深度大的节点中,这种优化叫做基于rank的。
特殊情况如下:

按照size的优化方式进行合并的时候,最终树的深度又增加一层
 
按照我们新的rank的优化方式进行优化把层数比较少的连入层数比较多的!
 
 
第四种实现:
   
   
  1. public class QuickUnionBest {
  2. private int[] mParents;
  3. //mRank[i]表示以i为根节点的集合所表示的树的层数
  4. private int[] mRank;
  5. private int mCount;
  6. public QuickUnionBest(int capacity){
  7. mCount = capacity;
  8. mParents = new int[mCount];
  9. mRank = new int[mCount];
  10. for (int i = 0; i < mCount; i++) {
  11. mParents[i] = i;
  12. //默认每个都是1:表示深度为1层
  13. mRank[i] = 1;
  14. }
  15. }
  16. //以下find和isConnected都用不到mRank.
  17. public int find(int p){
  18. if( p<0 || p>=mCount){
  19. //...做一些异常处理
  20. }
  21. while(p!=mParents[p]){
  22. p = mParents[p];
  23. }
  24. return p;
  25. }
  26. public boolean isConnected(int p,int q){
  27. return find(p)==find(q);
  28. }
  29. //找到p、q节点所在的树形集合的根节点,它的深度。然后把深度小的根节点合入到深度大的根节点当中
  30. public void union(int p,int q){
  31. //前两步不变
  32. int pRoot= find(p);
  33. int qRoot = find(q);
  34. if(pRoot == qRoot){
  35. return;
  36. }
  37. int pRank = mRank[pRoot];//初始事都是深度为1
  38. int qRank= mRank[qRoot];
  39. //如果p的深度比q的深度大.
  40. if(pRank > qRank){
  41. //注意是小的指向大的,也就是为小的重新读之
  42. mParents[qRoot] = pRoot;
  43. //此时把并不需要维护pRank,因为qRank是比pRank小的
  44. //也就是q更浅,它不会增加p的深度,只会增加去p的宽度
  45. }else if(pRank < qRank){
  46. mParents[pRoot] = qRoot;
  47. //同样的道理不需要维护qRank,p只会增加它的宽度
  48. }else{
  49. //当两个深度相同的时候,谁指向谁都可以,但是注意此时的深度维护
  50. //被指向的那个的深度需要加1.
  51. //此时让qRoot指向pRoot吧.
  52. mParents[qRoot] = pRoot;
  53. mRank[pRoot]++;
  54. }
  55. }
  56. }
以上这种实现其实和第三种差不多,有时候甚至比第三种还要费一点点时间,但整体来说理论上可以防止出现
意外的概率。

最后一种优化路径压缩
特殊情况时:
 
第一种压缩方法:
 
 
第二种压缩方法:
 
只是修改了find方法而已.
   
   
  1. //为防止极端的情况,可以再在find的时候经行路径压缩有两种压缩方法:
  2. // 1)、一层层的跳着压缩(隔一层走一下):
  3. // 就是当前节点的父节点parent[i]指向它父节点的parent,此时当前节点的父节点的
  4. // parent不用担心不存在,因为不存在时parent会指向自己!这也是我们退出循环的条件
  5. // 2)、压缩到深度只有两层
  6. // 利用递归实现,让最终只有树形集合中除了根意外其它节点都在第二层.
  7. // 理论上看第二种广度更大,应该时间更少,实测很多时候甚至比第一种多一点点。个人感觉可能是
  8. // 由于一是用了递归,而这种方式在quick-union这种情况下优化费的步骤过于多.
  9. public int find(int p){
  10. if(p < 0||p > mParents.length){
  11. //异常处理
  12. }
  13. while(p!=mParents[p]){
  14. //首先是拿到父节点的父节点指向,然后赋值给当前节点的父节点。
  15. //也就是parent[p]:当前节点的父节点;
  16. //mParents[mParents[p]]:当前节点的父节点的父节点指向。
  17. mParents[p] = mParents[mParents[p]];
  18. //然后是当前节点跳一下,直接指向新得到的mParents[p].
  19. p = mParents[p];
  20. //继续循环
  21. }
  22. /*//第二种:通过如下,递归调用本函数find,
  23. //mParents[p]为当前节点的父节点的索引,循环传入(递归)直到根节点
  24. //p == mParents[p] 返回当前节点的索引,然后就层层返回。
  25. if(p!=mParents[p]){
  26. //其实也不该考虑一层层的就,考虑这一层,当前节点的索引和父节点不同,
  27. //也就是说当前不是根节点,那么传入父节点的索引,递归调用把上个节点的父节点
  28. //传给当前节点的父节点。
  29. mParents[p] = find( mParents[p] );
  30. }
  31. //因为最终走到这的是根节点,根节点的parent是自己.
  32. return mParents[p];*/
  33. return p;
  34. }

附上java测试代码:
   
   
  1. package com.zy.tt;
  2. public class Helper {
  3. public void testUF1( int n ){
  4. UnionFoundBest uf = new UnionFoundBest(n);
  5. long start = getTime();
  6. for( int i = 0 ; i < n ; i ++ ){
  7. int a = (int) (Math.random()*n);
  8. int b = (int) (Math.random()*n);
  9. uf.union(a,b);
  10. }
  11. for(int i = 0 ; i < n ; i ++ ){
  12. int a = (int) (Math.random()*n);
  13. int b = (int) (Math.random()*n);
  14. uf.isConnected(a,b);
  15. }
  16. long end = getTime();
  17. showTimeDiff("quick-union no opt", start, end);
  18. }
  19. public void testUF2( int n ){
  20. UniodFoundFinal uf = new UniodFoundFinal(n);
  21. long start = getTime();
  22. for( int i = 0 ; i < n ; i ++ ){
  23. int a = (int) (Math.random()*n);
  24. int b = (int) (Math.random()*n);
  25. uf.union(a,b);
  26. }
  27. for(int i = 0 ; i < n ; i ++ ){
  28. int a = (int) (Math.random()*n);
  29. int b = (int) (Math.random()*n);
  30. uf.isConnected(a,b);
  31. }
  32. long end = getTime();
  33. showTimeDiff("quick-union optmization", start, end);
  34. //uf.show();
  35. }
  36. /**
  37. * 以s为单位获取当前时间
  38. * @return 当前时间的秒数。
  39. */
  40. public long getTime(){
  41. long time = System.currentTimeMillis();
  42. return time;
  43. }
  44. /**
  45. * 显示时间差
  46. * @param name 排序算法的名字
  47. * @param start 开始的时间
  48. * @param end 结束的时间
  49. */
  50. public void showTimeDiff(String name ,long start,long end){
  51. long diff = end - start;
  52. System.out.println("name: "+name+" 花费了 = "+diff+"ms");
  53. }
  54. }


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值