数据结构学习笔记之并查集

  1. 定义

  并查集是一种树型的数据结构,用于处理一些不相交集合(Disjoint Sets)的合并及查询问题。常常在使用中以森林来表示。

  并查集是一种特殊的树结构,在其他的树结构中都是由父节点指向孩子节点,而在并查集中是由孩子节点指向父亲节点,并查集主要是用来解决连接问题的。
  对于一组数据,并查集支持两个基本的动作:isConnected(p, q) p 和 q 是否属于同一个集合;union(p, q) 将 p 和 q 所在的集合合并起来。由于可以使用不同的方式实现并查集,所以我们可以定义以下接口。

public interface UnionFindSet{
    //将 p 位置对应数据的所在集合和 q 位置对应的数据的所在集合合并起来
    public void union(int p, int q);

    // p 位置的数据和 q 位置的数据是否属于同一个集合
    public boolean isConnected(int p, int q);

    // 数据 q 对应的集合编号
    public int find(int q);

    int getSize();
}

  并查集的基本数据表示可以使用数组实现,我们可以对并查集中的数据进行编号,比如使用数组的索引表示对应的数据编号,数组中的值表示该数据对应的集合编号。如在下列中,分别使用索引 0 - 9 表示并查集中的数据,而 0、2、4、6、8 在 0 这个集合中,而其他数据在另一个集合 1 中。

索引: 0 1 2 3 4 5 6 7 8 9
数据: 0 1 0 1 0 1 0 1 0 1
  1. 基本使用
      在并查集的初始化时,应该指定每个数据都在不同的集合中,如下,此时数组的索引和对应的值相等,表示每个数据都在不同的集合中。
    private int[] data;
    //把每个数据所在集合初始化为其自身, 也就是每个数据都在不同的集合中
    public UnionFind(int size){
        data = new int[size];
        for (int i = 0; i < size; i++) {
            data[i] = i;
        }
    }

  (1)Quick Find
  我们首先来看并查集的第一个实现,其主要思想是:当连接两个数据q, p时,通过遍历数组将q的集合编号修改为 p 的集合编号,具体实现如下:

     /*
     *  0 1 2 3 4 5  ==> 0 1 2 3 4 5
     *  0 1 0 1 0 1      0 0 0 0 0 0
     *  通过遍历数组,将 数组中 p 的集合编号全部修改为 q 的集合编号的值 ,时间复杂度为O(n)
     * */
    @Override
    public void union(int p, int q) {
        int qListNo = find(p);
        int pListNo = find(p);
        if(pListNo == qListNo) return;
        for (int i = 0; i < data.length; i++) {
            if(qListNo == data[i]) data[i] = pListNo;
        }
    }

    // 在这种情况下,判断两个数据是否在同一个集合,只需要判断对一个的值是否相等即可,O(1)级别,所以叫 Quick Find
    @Override
    public boolean isConnected(int p, int q) {
        return find(q) == find(p);
    }
    @Override
    public int find(int q) {
        if(q < 0 || q >= data.length) throw new IllegalArgumentException("参数无效");
        return data[q];
    }

  (2)Quick Union
  在标准情况下,并查集的实现思路是第二种,也就是Quick Union,其主要思想为:将每个元素看做一个节点(数组索引),节点的值(数组中的值)就表示当前节点指向的节点(索引),当前的节点和所指的节点所处通过一个集合中。当节点指向自己时,表示当前节点为根节点,连接两个数据时,只需要将其中一个数据的根节点指向另个数据(节点)的根节点即可;判断两个数据是否处于同一个集合中,只需要判断两个数据的根节点是否相等即可。

连接
    // 将 p 的根节点 指向 q 的根节点
    @Override
    public void union(int p, int q) {
        int qRootIndex = find(q);
        int pRootIndex = find(p);
        if(qRootIndex == pRootIndex) return;
        data[qRootIndex] = pRootIndex;
    }

    // 如果 p, q的根节点相同,表示处于同一个集合中
    @Override
    public boolean isConnected(int p, int q) {
        return find(q) == find(p);
    }

    // 寻找 q 对应的根节点 对应的位置
    @Override
    public int find(int q) {
        while(data[q] != q) q = data[q];
        return q;
    }

  (3)基于 size 的优化
  在第二个实现的连接方法中,我们并没有对根节点之间的连接做判断,这样可能会导致形成的树的高度过高(甚至变为链表),导致合并或者查询性能变低,解决方案就是给每个树维持一个节点个数size,该值表示当前节点为根的树所包含的节点个数。在合并时,让size小的根节点指向大的根节点。具体实现如下:


    private int[] data;
    private int[] sz; //sz[i] 表示以 i节点 为根的树所包含的元素个数

    //把每个数据所在集合初始化为其自身, 也就是每个数据都在不同的集合中
    public UnionFindThree(int size){
        data = new int[size];
        sz = new int[size];
        for (int i = 0; i < size; i++) {
            data[i] = i;
            sz[i] = 1;
        }
    }
    // 将 p 的根节点 指向 q 的根节点
    @Override
    public void union(int p, int q) {
        // 分别获得q, p 的根节点的索引
        int qRootIndex = find(q);
        int pRootIndex = find(p);
        if(qRootIndex == pRootIndex) return;

        //如果q 高, 让 p 指向 q
        if(sz[qRootIndex] >= sz[pRootIndex]) {
            data[pRootIndex] = qRootIndex;
            sz[qRootIndex] += sz[pRootIndex];
        }else{
            data[pRootIndex] = pRootIndex;
            sz[pRootIndex] += sz[qRootIndex];
        }
    }

  (4)基于 Rank 的优化
  在基于 size的优化时,我们假设size 较小的树高度可能会更小,但实际上不是这样的,比如下列这种情况,当连接【1,9】时,如果使用第三种优化,那么树的整体高度会变高,性能会降低。所以基于 Rank 的优化和第三种方式类似,只不过不同的是它是给每一个节点维持一个高度,让高度低根节点指向高度高的根节点。

特例
private int[] height; //height[i] 表示以 i节点 为根的树的高度

    //把每个数据所在集合初始化为其自身, 也就是每个数据都在不同的集合中
    public UnionFindFour(int size){
        data = new int[size];
        height = new int[size];
        for (int i = 0; i < size; i++) {
            data[i] = i;
            height[i] = 1;
        }
    }

    // 将 p 的根节点 指向 q 的根节点
    // 将 p 的根节点 指向 q 的根节点
    @Override
    public void union(int p, int q) {
        // 分别获得q, p 的根节点的索引
        int qRootIndex = find(q);
        int pRootIndex = find(p);
        if(qRootIndex == pRootIndex) return;

        //如果q 高, 让 p 指向 q
        if(height[qRootIndex] > height[pRootIndex]) {
            data[pRootIndex] = qRootIndex;
         //如果 p 高,让 q 指向 p
        }else if(height[qRootIndex] < height[pRootIndex]){
            data[qRootIndex] = pRootIndex;
        }else{
            data[qRootIndex] = pRootIndex;
            height[pRootIndex] ++;
        }
    }
}

  (5)基于路径压缩的优化
  如果并查集中的树的高度越低,那么寻找根节点的速度就越快,效率就越高。路径压缩是一种将已将形成的树动态的降低树的高度的过程。无论是并查集的合并还是查询都必须寻找当前节点对应集合的根节点,那么如何在寻找过程中降低树的高度呢?首先看一下实现代码:

    // 寻找 q 对应的根节点 对应的位置
    @Override
    public int find(int q) {
        /*
         让所有节点都直接指向根节点,通过递归实现
        if(data[q] != q){
            data[q] = find(data[q]);
        }
        */
        while(data[q] != q) {
            data[q] = data[data[q]];  //************
            q = data[q];
        }
        return q;
    }

  以上代码的data[q] = data[data[q]]是重点,其动态压缩的过程如下:

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