并查集学习日志

Oneday

  • 并查集的概述:他是一种特殊的树形结构,是一颗倒着的树,每个节点都是孩子指向父亲
  • 并查集可以解决的问题:连接问题,用于看某两个节点是否有连接
  • 并查集可以实现的操作:isConnected()来看两个点是否在一个集合中、 union()将两个节点的集合合并
  • 并查集中数据的表示:
    元素:0 1 2 3 4 5 6
    id: 0 0 0 1 1 1 1 这里{0,1,2}属于0集合,{3,4,5,6}属于1集合

这是一个并查集的接口,后面所有的并查集类都实现了该接口

public interface UF {
     //实现看两个ID的元素是否在一个集合中
     boolean isConnected(int p,int q);
     //将两个ID的元素的集合合并
     void union(int p,int q);
     //求并查集的大小
     int getSize();
}
  • 并查集的实现:(后面的所有的优化都是基于第二版的并查集实现的)
  1. QUICK FIND(这种方式是可以快速的查看两个元素之间的关系,但是合并比较慢)
    isConnected() 时间复杂度O(1)
    union() 时间复杂度O(n)
//第一版并查集
/**
 * 
 * @author jhd
 *isConnected()    时间复杂度O(1)
 *union()          时间复杂度O(n)
 */
public class UnionFind1 implements UF{
     int []id;//设置每个元素对应的集合
     public UnionFind1(int size) {
         //由用户来提供大小,初始化id集合
         id=new int[size];
         for(int i=0;i<id.length;i++) {
             id[i]=i;
         }//初始化每个元素都属于自己的集合
     }
     @Override
     public int getSize() {
         return id.length;
     }//实现获得并查集个数的函数
     private int find(int p) {
         if(p<0||p>id.length) {
             throw new IllegalArgumentException("输入数据有误");
         }
         return id[p];
     }//获得p元素所在的集合
     @Override
     public boolean isConnected(int p,int q) {
         return find(p)==find(q);
     }//用于看两个元素是否在一个集合中
     @Override
     public void union(int p,int q) {
         int pID=find(p);
         int qID=find(q);
         if(pID==qID) {
             return;
         }//如果两个元素对应的id值相同,无需操作
         for(int i=0;i<id.length;i++) {
             if(id[i]==qID) {
                 id[i]=pID;
             }
         }//否则的话,将所有的qID变为pID,将两个集合合并
     }
}

  1. QUICK UNION
    isConnected() 时间复杂度O(h) (h为树的高度)
    union() 时间复杂度O(h)
//第二版并查集
/**
 * 
 * @author jhd
 *isConnected()  时间复杂度O(h)
 *union          时间复杂度O(h)
 */
public class UnionFind2 implements UF{
      int[]parent;//设置一个并查集,用于存储元素对应的父节点
      public UnionFind2(int size) {
          parent=new int[size];//根据用户提供的size初始化parent
          for(int i=0;i<parent.length;i++) {
              parent[i]=i;
          }//初始化每个元素的根都是自己
      }
      @Override
      public int getSize() {
          return parent.length;
      }
      private int findRoot(int p) {
          while(p!=parent[p]) {
              p=parent[p];
          }
          return p;
      }//这个函数是找到当前的这个节点的根节点
      @Override
      public boolean isConnected(int p,int q) {
          return findRoot(p)==findRoot(q);
      }//这时的判断两个元素是否在同一个集合是看它们的根节点是否相同
      @Override
      public void union(int p,int q) {
          int pRoot=findRoot(p);
          int qRoot=findRoot(q);
          if(pRoot==qRoot) {
              return;
          }
          parent[pRoot]=qRoot;
      }//合并两个集合的操作就是找到它们的根节点,让其中一个根节点指向另一个根节点
}

  1. 基于size的优化
    根据合并可以看出如果是这样的


    image.png
image.png

那么不断的union 0,x,就会形成一个很长很长,,,的链表,这样对于查找根节点以及合并操作的时间都产生了很大的影响,所以在此时要设置一个数组,用于存储每个节点下的节点的个数,然后在合并的时候比较两个根节点下节点的大小,用小的指向大的,这样就可以减少h的大小,优化时间

//第三版并查集
//对于合并时的策略进行了修改,使得树的高度变小,时间复杂度变小
public class UnionFind3 implements UF{
    int[]parent;//设置一个并查集,用于存储元素对应的父节点
    int[]sz;//用于看当前的根下有几个节点
    public UnionFind3(int size) {
      parent=new int[size];//根据用户提供的size初始化parent
      sz=new int[size];
      for(int i=0;i<parent.length;i++) {
          sz[i]=1;//初始化每一个根下的节点个数都为1
          parent[i]=i;
      }//初始化每个元素的根都是自己
    }
    @Override
    public int getSize() {
      return parent.length;
    }
    private int findRoot(int p) {
      while(p!=parent[p]) {
          p=parent[p];
      }
      return p;
    }//这个函数是找到当前的这个节点的根节点
    @Override
    public boolean isConnected(int p,int q) {
      return findRoot(p)==findRoot(q);
    }//这时的判断两个元素是否在同一个集合是看它们的根节点是否相同
    @Override
    public void union(int p,int q) {
      int pRoot=findRoot(p);
      int qRoot=findRoot(q);
      if(pRoot==qRoot) {
          return;
      }
      if(sz[pRoot]<sz[qRoot]) {
      parent[pRoot]=qRoot;
      sz[qRoot]+=sz[pRoot];//更新q节点下的节点个数
      }//如果p节点下的节点数小于q节点下的节点数,就让p节点指向q节点
      else {
        parent[qRoot]=pRoot;
        sz[pRoot]+=sz[qRoot];
      }//否则就让q节点指向p节点,并更新p节点下的节点数
    }//合并两个集合的操作就是找到它们的根节点,让其中一个根节点指向另一个根节点
}

Twoday

  • 基于rank的优化
    首先今天对昨天的针对size的优化有了疑问,因为昨天对于size的优化是让根节点下节点少的指向节点多的,并维护size的值,但是如果情况是下图所示:


    image.png

    这时如果按照size的合并方法如果union(2,8),就是8指向7,但是这最后使得高度变大,所以这里引入了rank,这个rank在这里指的是树的最大的高度,也就是如果遇到两棵树的合并的问题,这时判断两棵树的高度的大小,如果A树>B树,那就让B节点指向A节点,如果A树<B树,就让A节点指向B节点,如果相等,前面的两种的任意一种都行,这时要维护rank,也就是根节点的最大高度要增1,前面的大于和小于的情况都无需维护rank,因为如果是大于或者小于,两棵树的最大高度的差要大于等于1,所以两棵树合并后最大高度较小的那棵树的现在的高度是他原来的最大高度加上现在的根节点也就是加1,所以顶多相等,就无须维护rank的值

//第四版并查集
//同样也是对合并时的操作进行修改,只不过现在是看树的高度,而不是看每个根节点的节点的个数,而是要看这个根节点的树的高度
public class UnionFind4 implements UF{
    int[]parent;//设置一个并查集,用于存储元素对应的父节点
    int[]rank;//存储某个根节点的树的最大的高度
    public UnionFind4(int size) {
      parent=new int[size];//根据用户提供的size初始化parent
      rank=new int[size];
      for(int i=0;i<parent.length;i++) {
          rank[i]=1;//初始化每一个根节点的树的最大高度都为1
          parent[i]=i;
      }//初始化每个元素的根都是自己
    }
    @Override
    public int getSize() {
      return parent.length;
    }
    private int findRoot(int p) {
      while(p!=parent[p]) {
          p=parent[p];
      }
      return p;
    }//这个函数是找到当前的这个节点的根节点
    @Override
    public boolean isConnected(int p,int q) {
      return findRoot(p)==findRoot(q);
    }//这时的判断两个元素是否在同一个集合是看它们的根节点是否相同
    @Override
    public void union(int p,int q) {
      int pRoot=findRoot(p);
      int qRoot=findRoot(q);
      if(pRoot==qRoot) {
          return;
      }
      if(rank[pRoot]<rank[qRoot]) {
           parent[pRoot]=qRoot;
      }//如果以p为根节点的树的最大的高度小于以q为根节点的树的最大的高度,就让p节点指向q节点
      else if(rank[pRoot]>rank[qRoot]) {
           parent[qRoot]=pRoot;
      }//相反,就让q节点指向p节点
      //以上的两种情况都无需维护rank,是因为添加另一颗比自己最大高度小的树,最多也就是与自己的最大高度相等,不可能大于,所以无需维护
      else {
           parent[qRoot]=pRoot;
           rank[pRoot]+=1;
      }//如果以p为根节点的树的最大高度等于以q为根节点的树的最大高度,这时无所谓p指向q还是q指向p,但是这时树的最大高度会自增1,维护rank
    }//合并两个集合的操作就是找到它们的根节点,让其中一个根节点指向另一个根节点
}
  • 路径压缩☆
    这个是并查集中压缩高度比较重要的方法,其实操作只不过是在find函数中添加一步parent[x]=parent[parent[x]],就是让当前要找的这个节点指向他父节点的父节点,这种方法并不能使得每个节点都指向最终的父节点,但是压缩了树的高度h,减小了时间的消耗,如下图,就是find(4)执行后的压缩情况:
    image.png

对于这个路径压缩,里面会有一个问题值得思考,就是如果压缩路径,那么这棵树的最大的高度就会改变,这样不需要维护一下rank吗,其实是不用的,因为在合并的时候两者都要进行压缩,所以树的高度都会减少,所以rank在这里可以表示这棵树的高度的排名就好了,同上面一样,高度小的根节点指向高度大的根节点

//第五版并查集
//引入了路径压缩的概念
public class UnionFind5 implements UF{
    int[]parent;//设置一个并查集,用于存储元素对应的父节点
    int[]rank;//存储某个根节点的树的最大的高度
    /*
     * 对于此时的rank,肯定会有疑问,如果路径压缩那么这棵树的最大的高度肯定会改变,不需要维护rank吗
     * ,在这里,是不需要维护的,因为这里的rank指的是以这个节点为根节点的高度的排名,而路径压缩并不会
     * 改变它的排名,所以可以使用,无需维护
     */
    public UnionFind5(int size) {
      parent=new int[size];//根据用户提供的size初始化parent
      rank=new int[size];
      for(int i=0;i<parent.length;i++) {
          rank[i]=1;//初始化每一个根节点的树的最大高度都为1
          parent[i]=i;
      }//初始化每个元素的根都是自己
    }
    @Override
    public int getSize() {
      return parent.length;
    }
    private int findRoot(int p) {
      while(p!=parent[p]) {
          parent[p]=parent[parent[p]];//这里使用路径压缩,使得此时的节点向上移动
          p=parent[p];
      }
      return p;
    }//这个函数是找到当前的这个节点的根节点
    @Override
    public boolean isConnected(int p,int q) {
      return findRoot(p)==findRoot(q);
    }//这时的判断两个元素是否在同一个集合是看它们的根节点是否相同
    @Override
    public void union(int p,int q) {
      int pRoot=findRoot(p);
      int qRoot=findRoot(q);
      if(pRoot==qRoot) {
          return;
      }
      if(rank[pRoot]<rank[qRoot]) {
           parent[pRoot]=qRoot;
      }//如果以p为根节点的树的最大的高度小于以q为根节点的树的最大的高度,就让p节点指向q节点
      else if(rank[pRoot]>rank[qRoot]) {
           parent[qRoot]=pRoot;
      }//相反,就让q节点指向p节点
      //以上的两种情况都无需维护rank,是因为添加另一颗比自己最大高度小的树,最多也就是与自己的最大高度相等,不可能大于,所以无需维护
      else {
           parent[qRoot]=pRoot;
           rank[pRoot]+=1;
      }//如果以p为根节点的树的最大高度等于以q为根节点的树的最大高度,这时无所谓p指向q还是q指向p,但是这时树的最大高度会自增1,维护rank
    }//合并两个集合的操作就是找到它们的根节点,让其中一个根节点指向另一个根节点
}

  • 路径压缩的延伸
    这里还有另外的一种路径压缩的方式,是一种递归的方式,他可以实现将要查找的节点的所有的根节点除了最后的那一个都指向最后的根节点,如下图:
    image.png

    如果执行find(4)操作,就可以得到这样的结果
//第六版并查集
//引入了路径压缩的概念
public class UnionFind6 implements UF{
    int[]parent;//设置一个并查集,用于存储元素对应的父节点
    int[]rank;//存储某个根节点的树的最大的高度
    /*
     * 对于此时的rank,肯定会有疑问,如果路径压缩那么这棵树的最大的高度肯定会改变,不需要维护rank吗
     * ,在这里,是不需要维护的,因为这里的rank指的是以这个节点为根节点的高度的排名,而路径压缩并不会
     * 改变它的排名,所以可以使用,无需维护
     */
    public UnionFind6(int size) {
      parent=new int[size];//根据用户提供的size初始化parent
      rank=new int[size];
      for(int i=0;i<parent.length;i++) {
          rank[i]=1;//初始化每一个根节点的树的最大高度都为1
          parent[i]=i;
      }//初始化每个元素的根都是自己
    }
    @Override
    public int getSize() {
      return parent.length;
    }

    private int findRoot(int p) {
      if(p!=parent[p]) {
          parent[p]=findRoot(parent[p]);
      }//这里我们可以发现,find方法本来就是找当前的这个节点的根节点,故让当前的节点的根节点指向他的根节点的根节点
      return parent[p];//否则找到了最后的根节点返回就可以了
      //这个使用递归的方式来实现的路径压缩,可以使得根节点下的每一个节点都直接指向根节点
    }//这个函数是找到当前的这个节点的根节点
    @Override
    public boolean isConnected(int p,int q) {
      return findRoot(p)==findRoot(q);
    }//这时的判断两个元素是否在同一个集合是看它们的根节点是否相同
    @Override
    public void union(int p,int q) {
      int pRoot=findRoot(p);
      int qRoot=findRoot(q);
      if(pRoot==qRoot) {
          return;
      }
      if(rank[pRoot]<rank[qRoot]) {
           parent[pRoot]=qRoot;
      }//如果以p为根节点的树的最大的高度小于以q为根节点的树的最大的高度,就让p节点指向q节点
      else if(rank[pRoot]>rank[qRoot]) {
           parent[qRoot]=pRoot;
      }//相反,就让q节点指向p节点
      //以上的两种情况都无需维护rank,是因为添加另一颗比自己最大高度小的树,最多也就是与自己的最大高度相等,不可能大于,所以无需维护
      else {
           parent[qRoot]=pRoot;
           rank[pRoot]+=1;
      }//如果以p为根节点的树的最大高度等于以q为根节点的树的最大高度,这时无所谓p指向q还是q指向p,但是这时树的最大高度会自增1,维护rank
    }//合并两个集合的操作就是找到它们的根节点,让其中一个根节点指向另一个根节点
}

  • 并查集的时间复杂度的分析:
    在前面也有说明,并查集的时间复杂度为O(h),但h的变化不定,所以并不能保证时间复杂度是多少,其实它的严格意义上的时间复杂度为O(log*n),注意这个不是logn,这个函数分为两种情况,当n<=1时,log*0=0,当n>1时,log*n=1+log*(logn),上面的这些最终分析出来并查集的时间复杂度近乎等于O(1),是比logn要快的

这是我的学习笔记,如果有什么不足希望大家指出 ^_-

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,189评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,577评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,857评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,703评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,705评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,620评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,995评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,656评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,898评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,639评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,720评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,395评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,982评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,953评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,195评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,907评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,472评论 2 342

推荐阅读更多精彩内容