并查集入门


tips:本人不小心发到blog去了,好像是要发贴的,不过blog也懒得删了,就这样吧。
在我们平常生活中,无论是人与人之间还是动物与动物之间,都有直接或间接的关系,比如亲戚关系等,那么如果我给你一堆人的亲戚关系,并且问你这一堆人中的某两个人是否是亲戚关系,这个该如何实现呢?
我们可以假设,没有亲戚关系的两个人属于不同的家族,而家族我们又可以假设为集合,即这两个人属于不同的集合。
相信大家都听过家族树这个东西,就是下图所示的东东:
图片说明
这颗树表示同一家族的人,不同的树表示不同的家族,所以我们可以用树来表示家族(即集合)。
这里就到了引入并查集的时候了。
并查集的步骤是怎样的呢?
首先:我们先假设每个人刚开始都是一个集合(即自己是一个家族)。
然后:每次给出具有亲戚关系的两人时,就把这两个家族合并(即把这两个集合合并),如果这两个人本来就属于同一家族则不需要合并。
最后:每次询问两个人是否具有亲戚关系,我们只需要看他们是否处在同一集合即可。
实现的代码和注释如下(最初代的):
//并查集的实现需要用到一个数组,我们定义一个全局数组来实现集合的功能。
int fa[10005];
(1)  初始化并查集
void init()//初始化并查集
{
//让每个元素自己成为一个集合,即元素只有自己的集合。
    for(int i=0;i<100005;i++)
        fa[i]=i;
}
(2)  合并结合
void Merge(int x,int y)//并查集合并结合
{
       int xx=Find(x),yy=Find(y);
      fa[xx]=yy;//让x所在的集合和y所在的集合合并。
}
(3)  查找节点所在集合
int Find(int x)//查找节点所在集合
{/*若一个元素的数值和所在集合对应数值相同,
说明这个集合的名字是这个元素的数值,即我们需要找到的集合*/
return fa[x]==x?x:Find(x);//找到根节点,即集合的名字
}
这时一个初代的并查集就完成了。

但是我们发现一个问题,这个初代的并查集效率特别低。
它的效率和树的深度有关,树的深度越大,查找所需要的时间就越长,所以如果要改善该算法,我们可以减小树的深度,那么我们该怎么改进呢?

  1. 路径压缩
    给出下面两幅图来说明:
    图片说明
    图片说明
    很明显,图一的树的深度为5,图二的树的深度为2,并查集询问操作时,图一最坏的情况要找五次才能找到所在集合,图二的最坏的情况只需要找两次即可找到所在集合,当数据量大的时候,在时间的效率上,明显图二对应的情况效率更高。
    那么我们如何达到图二这样的效果呢?
    就如图中的元素A,B,C,D,E,F,我们知道它们属于同一集合,并不需要知道他们的上一个元素是谁,而是只需要知道自己所在的集合,所以我们让所有的节点都指向根节点(即B,C,D,E,F指向A)。
    那么该操作如何实现呢?我们无论是在询问操作还是在合并操作中都需要知道元素所在的集合,所以我们只需要在查找元素所在的集合的函数稍作修改即可,即更新路径(路径压缩)。
    举个例子,过程如下:
    假设有一集合有A,B,C,3个元素,另一个集合有D,E,两个元素,现在需要将两个集合合并。
    图片说明
    合并后变成这样:
    图片说明
    是不是和我们最优的图不太一样。
    假设下一次调用该集合时,路过元素E,我们即可把元素节点E指向它的根节点。
    图片说明
    因为无论询问还是合并操作,都需要知道自己所在的集合,所以我们只需要对查找自己所在的集合的函数稍作修改即可。
    修改后的Find函数如下:
    int Find(int x)//查找节点所在集合
    {
        return fa[x]==x?x:(fa[x]=Find(fa[x]));
    //fa[x]=Find(fa[x])就是用递归将路径上经过的所有节点指向根节点
    }


  2. 按秩合并
    什么是按秩合并呢?假如你现在要合并一棵高度为3的树和高度为2的树,你是要让高度为3的树并入高度为2的树还是高度为2的树并入高度为3的树呢?
    下面给出两种并入结果
    高度为3的树并入高度为2的树:
    图片说明
    高度为2的树并入高度为3的树:
    图片说明
    很明显,两种合并的方法,合并后所得的树的深度不同,我们前面知道了并查集的效率和树的深度有关,所以明显将高度为2的树并入高度为3的树后面的效率更好,即将深度小的树并入深度大的树。
    那么我们如何实现这个过程呢?
    假设我们定义一个数组
    int Rank[10005];

    用来表示每个元素对应集合的树的深度,初始时每个元素自己就是一个集合,所以Rank的初始值为1。

    剩下的我们只需要修改合并函数即可,修改后的合并函数为:

    void Merge(int i,int j)//按秩合并
    {
        int x,y;
        x=findx(i),y=findx(j);
        if(Rank[x]<=Rank[y])//比较两棵树的深度
            fa[x]=y;
        else
            fa[y]=x;
    //如果两棵树的深度相同,那么被并入的那个集合的树的深度加一
        if(Rank[x]==Rank[y]&&x!=y)
                Rank[y]++;
    }
全部评论
感谢参与【创作者计划2期·技术干货场】!欢迎更多牛油来写干货,瓜分总计20000元奖励!!技术干货场活动链接:https://www.nowcoder.com/link/czz2jsghtlq(参与奖马克杯将于每周五结算,敬请期待~)
点赞 回复
分享
发布于 2021-02-25 20:15
帮顶
点赞 回复
分享
发布于 2021-02-26 10:48
滴滴
校招火热招聘中
官网直投

相关推荐

下午1:30面的,大概面了40分钟,面试官人还不错感觉问的比较基础,面试官也开了摄像头,捞捞1.&nbsp;拷打项目我说用了延时双删保持DB一致-----&amp;gt;说一说&nbsp;延时双删怎么做的?(先删redis,改完数据库,延时一会儿再删除redis,保持弱一致性)按什么依据设置延迟时间-------答:按照业务的需求,对并发量的需求。还能怎么做保持一致性?-------答:使用cannal监听binlog日志--异步删除。JWT令牌----->还能怎么办---用session ID---> 分布式有跨域问题--->怎么解决?(不会了)redis的基本类型--->&nbsp;正常答redis还能干什么----------->&nbsp;答:分布式锁&nbsp;&nbsp;setnx&nbsp;&nbsp;&nbsp;synchronized是jvm层面的不能用在分布式服务上为什么用拦截器,登陆也拦截了吗------------->&nbsp;&nbsp;答:登陆也拦截了,写了逻辑直接放行过滤器和拦截器的区别----------> 答:拦截器是spring里面的,过滤器是jdk的--->(瞎说的)spring下用拦截器好一点最难的部分觉得是什么?--->&nbsp;瞎说了一个公共字段填充&nbsp;&nbsp;&nbsp;&nbsp;要用AOP--&nbsp;反射&nbsp;&nbsp;&nbsp;救命!其他不记得了。。。再好好看看做项目2.&nbsp;八股索引的类型、B+树、&nbsp;聚簇索引与非聚簇索引SQL优化(走索引,别select*)是不是都要加索引还是慢怎么办----->(分表,分库,&nbsp;直接问的是不是没用,是。。。)常用的集合和底层实现:list&nbsp;set&nbsp;map&nbsp;&nbsp;&nbsp;hashmap说的多一点(数组+链表、数组+链表+红黑树、头插,尾插)&nbsp;&nbsp;线程池的原理?运行过程?synchronized是不是可重入锁?底层原理是什么?锁升级?sprin中有哪些设计模式?&nbsp;&nbsp;单例(bean)、工厂(beanFactory)、模板(jdbcTemplate)。。紧张&nbsp;代理&nbsp;都没说......其他不记得了&nbsp;感觉还是项目问的多一点。反问: 业务是什么----->&nbsp;酒店管理端的上线有什么建议--->&nbsp;把项目的亮点写出来、自己了解的东西多说一点。总体感觉比较基础,八股答得也还行----》&nbsp;给孝子个机会吧 #携程2025实习#&nbsp;&nbsp;
点赞 评论 收藏
转发
3 5 评论
分享
牛客网
牛客企业服务