并查集

概述

并查集, 书面叫不相交集. 由于关于这个数据结构(集合)主要操作是Union(集合求并)和Find(查找一个元素属于哪个集合), 所以经常见到叫并查集.

问题引入

为了对这种数据结构有个感性的认识, 先给一个实际的问题:

有10台电脑, 任意两台电脑可能有网线连接, 此时说这两个电脑连通. 我们认为A和B连通, B和C连通时, 那么A和C也连通.
问题是: 现在给定一些电脑的连通情况, 求指定的两台电脑是否连通.

问题解决思路: 将10台电脑分成10个集合, 初始每个集合中只有一台电脑.
在给定两台电脑(如A和B)连通时, 将A, B所在集合合并, 依次这样操作.
最后问指定的两台电脑是否连通时, 只要看它们是不是在一个集合中就可以了.

上面的思路就是并查集的想法.

下面回到代码, 用代码如何表示上述结构?

代码演示

给定N个元素时, 我们将其编号0N-1, 开一个数组arr[N],
此时数组索引就对应一个元素, arr[i]表示元素i的父节点(一个集合对应着一棵树).
如果i为树根, 令arr[i]为负数以示区分.
此时Union操作, 就是改变一棵树的根的父节点, Find操作就是找父节点为负数的节点.

基本的数据结构定义

#define N 10 // 所有集合元素总数
typedef int DisjointSet[N]; // 所有集合元素
typedef int SetType;
typedef int ElementType;

初始化操作

初始化操作就是将每个元素单独做一个集合, 此时单个元素都在树根上.

void Initialize(DisjointSet s) {
    for (int i = 0; i < N; i++) {
        s[i] = -1; // 所有元素自成一个集合(树), 初始在树根的位置
    }
}

Find

查找操作就是给定一个元素, 找到它所属的集合(树根)
思路很简单, 当s[x]为负数时, 表示x就是树根, 否则令x=s[x]继续住上找.

// 查找元素x所在的集合(返回树根)
SetType Find(ElementType x, DisjointSet s) {
    if (s[x] < 0) { // s[x]负数, 说明x在树根的位置
        return x;
    }

    #if (FIND_TYPE == 1)
        return Find(s[x], s); // 直接递归
    #elif (FIND_TYPE == 2)
        return s[x] = Find(s[x], s); // 这叫"路径压缩", 其实就是尾递归
    #elif (FIND_TYPE == 3)
        while (s[x] >= 0) { // 自己写的迭代实现
            x = s[x];
        }
        return x;

    #endif
}

Union

并操作, 相当于两棵树根要连接上, 有很多方法. 比如将下边两棵树合并:
Union操作
其实Union要关心的是以谁的根为新根的问题. 为什么要关心这个呢? 观察上述的第1种合并,
明显新树高度变高了, 这样不利于查找(注意理解Find函数的工作过程).
基于此, 给出三种方法, 分别是:

  1. 一直以第一棵树根作为新树的根 (随机合并)
  2. 以元素个数多的树根作为新树的根 (按规模合并)
  3. 以高度(也叫秩, rank)高的根根作为新树的根 (按秩合并)

需要注意的是, 对于上述第2种和第3种合并方法, s[root], 即根的父节点存储的是规模或者高度的负值.

// 两个集合作并集, 合并后root1为新树树根
void Union(DisjointSet s, SetType root1, SetType root2) {
    int tmp;

    #if (UNION_TYPE == 1)
        s[root2] = root1; // root2所在树的根现在是root1
    #elif (UNION_TYPE == 2)
        // s[root]表示s[-N], N为集合元素的个数
        // 所以s[root2] > s[root1], 表示集合2比集合1"规模小"
        // 所以树2要贴到树1上
        if (s[root2] > s[root1]) {
            tmp = s[root2];
            s[root2] = root1;
            s[root1] += tmp; // 更新下集合1的规模
        } else {
            tmp = s[root1];
            s[root1] = root2;
            s[root2] += tmp;
        }
    #elif (UNION_TYPE == 3)
        if (s[root2] < s[root1]) { // 树2比树1高
            s[root1] = root2; // 树1贴到树2上
        } else {
            // 两树等高, 选择树2贴到树1上
            // 树1高度加1
            if (s[root1] == s[root2]) {
                s[root1]--;
            }
            s[root2] = root1;
        }
    #endif
}

Connected

当然还有一个不太标准的操作, 判断两个元素是否属于同一个集合(是否连通)

// 判断两个元素是否属于同一个集合(属于同一棵树)
bool Connected(ElementType x, ElementType y, DisjointSet s) {
    return Find(x, s) == Find(y, s);
}

利用上述的成果, 对开始的关于"电脑连通"的问题给出解答:
给电脑编号0到9, 测试数据testData表示已知的连通情况, 求解电脑0和9的连通情况.

int main(int argc, char *argv[])
{
    // {1, 2} 表示1, 2两个电脑连通
    int testData[][2] = {
        {0, 1},
        {1, 2},
        {2, 4},
        {4, 5},
        {5, 6},
        {6, 8},
        {8, 9}
    };

    DisjointSet s;
    Initialize(s);
    for (int i = 0; i < sizeof(testData) / sizeof(testData[0]); i++) {
        Union(s, Find(testData[i][0], s), Find(testData[i][1], s));
    }

    if (Connected(0, 9, s)) {
        printf("connected\n");
    } else {
        printf("not connected\n");
    }
    return 0;
}

完整代码参见这里

参考

数据结构与算法分析: C语言描述(第2版) 第8章 不相交集ADT

欢迎补充指正!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值