cs61b数据结构与算法学习笔记 9. DisjointSets(并查集)

不相交集合(disjoint-set)又称为并查集(union-find),用于处理一系列没有重复元素的集合(不交集)的合并与查询问题,并查集支持如下操作:

  • 查询:查询某个元素属于哪个集合。返回集合内的一个“代表元素”。
  • 合并:两个集合并成一个。
  • 添加:添加一个新集合,其中有一个新元素。

9.2 快速查找

引子:实现高级算法的基础数据结构和工具很重要。不相交集合如果用集合列表实现的话会很慢,代码也很复杂。更适合的数据结构是只使用一个int列表,如下文所解释。


直观地说,我们可以首先考虑将 "互不相交集 "表示为一个集合列表,例如 List<Set<Integer>>。

例如,如果我们有 N=6 个元素,并且没有任何元素被连接,那么我们的集合列表看起来就像这样:[{0}, {1}, {2}, {3}, {4}, {5}, {6}].看起来不错。但是,考虑一下如何完成 connect(5, 6) 这样的操作。我们必须遍历多达 N 个集合才能找到 5,再遍历 N 个集合才能找到 6。我们的运行时间就变成了 O(N)。而且,如果要尝试实现这一点,代码会相当复杂。

QuickFind(Disjoint Set)如下图:

数组的索引代表集合的元素。
索引上的值就是它所属的集合编号。
例如,我们将{0, 1, 2, 4}、{3, 5}、{6}表示为

快速查找

数组索引(0...6)是元素。id[i]处的值是它所属的集合。只要同一集合中的所有元素都有相同的 id,那么具体的集合编号并不重要。
让我们看看连接操作是如何进行的。现在,id[2] = 4,id[3] = 5。调用 connect(2, 3) 后,id 为 4 和 5 的所有元素的 id 应该相同。让我们暂时把它们都赋值为 5:

isConnected(x, y)
要检查 isConnected(x,y),我们只需检查 id[x] 是否 == id[y]。请注意,这是一个恒定时间操作!

我们称这种实现为 "快速查找",因为查找元素是否相连需要恒定的时间。

QuickFind示例程序如下:

 // Java
public class QuickFindDS implements DisjointSets {

    private int[] id;

    /* Θ(N) */
    public QuickFindDS(int N){
        id = new int[N];
        for (int i = 0; i < N; i++){
            id[i] = i;
        }
    }

    /* need to iterate through the array => Θ(N) */
    public void connect(int p, int q){
        int pid = id[p];
        int qid = id[q];
        for (int i = 0; i < id.length; i++){
            if (id[i] == pid){
                id[i] = qid;
            }
        }
    }

    /* Θ(1) */
    public boolean isConnected(int p, int q){
        return (id[p] == id[q]);
    }
}
 // c++
class QuickFind {
private:
    int* id;
    int size;

public:
    QuickFind(int n) {
        size = n;
        id = new int[n];
        for (int i = 0; i < n; i++) {
            id[i] = i;
        }
    }

    bool connected(int p, int q) {
        return id[p] == id[q];
    }

    void unionSet(int p, int q) {
        int pid = id[p];
        int qid = id[q];
        for (int i = 0; i < size; i++) {
            if (id[i] == pid) {
                id[i] = qid;
            }
        }
    }
};

 

9.3 QuickUnion

QuickFind & QuickUnion

Quickfind的优点是IsConnect很快,缺点是Connect很慢。要修改connect情况。我们必须修改相当于一个集合里的项数那么多的数字。例如在这个图中,如果我们要把第一个集合和第二个集合连接起来,就要吧int[]里的所有3全改为2。为了加快Connect的速度,我们引入QuickUnion

为了使操作快速,我们仍然会用数组来表示我们的集合。我们将每个集合想象成一棵我们为每个项目分配其父节点的值,而不是其ID。如果一个项目没有父节点,那么他是一个根,根节点不妨赋值为-1。这样的结构实际上就是。例如:

如此,我们进行Connect的时候只需要修改一个值即可,很快啊。 最好是把短树的根节点连接成为大树的根节点的子节点。但是有一个问题是因为要连接到根部,就要找到根部,这时不得不从最下层的叶子节点一个一个遍历到根部,搜索到树的根部浪费时间,尤其是当树高度足够大时。

public class QuickUnion {
    private int[] id;

    public QuickUnion(int n) {
        id = new int[n];
        for (int i = 0; i < n; i++) {
            id[i] = i;
        }
    }

    private int root(int p) {
        while (p != id[p]) {
            p = id[p];
        }
        return p;
    }

    public boolean connected(int p, int q) {
        return root(p) == root(q);
    }

    public void unionSet(int p, int q) {
        int rootP = root(p);
        int rootQ = root(q);
        id[rootP] = rootQ;
    }
}

如果我们不把根部连接到根部,而是总是一个接一个连接节点,我们会得到一个高度为N的树。树的长度会快速增加,这很不好。

9.4 Weighted Quick Union(WQU)

在QuickUnion中,每次调用find时都必须遍历到树的根节点。因此,树的高度越小,计算越快

为了降低树的高度,我们对QuickUnion做如下修改:

1. 跟踪树的大小结点的数量,即"weight"),我们可以把这个信息(size of trees)存入树的根节点来代替-1。

2. 永远把较小的树的根连接到较大的树的根上,较小数的根作为较大树的根的子节点。如图:

这么做之后形成树的最大高度:log(N) 

Connect和IsConnected的最大运行时间为O(logN)。

既然目的是让高度(height)尽量小,为什么根据权重(weight)而不根据高度(height)来链接树。事实证明,Height Quick Union实现起来更复杂,而且得到的高度大小结果是同样的 Θ(log N) 高度限制。

具体实现(在QuickUnion基础上)

  • isConnect(int p, int q)函数无需更改
  • Connect(int p, int q)函数需要跟踪树的大小:
    • lab6中揭晓
    • 两个常用方法
      • 在树的根节点数组中用数字" - 数的大小"代替-1。
      • 或者直接创建一个单独的存放大小数据的数组。

在这里我们可以回到刚才提过的为什么不用Height Quick Union的问题,如果用高度标记根节点,高度大小的获取会相当之复杂,得不偿失。

public class WeightedQuickUnion {

    private int[] id;
    private int[] size;

    public WeightedQuickUnion(int n) {
        id = new int[n];
        size = new int[n];
        for (int i = 0; i < n; i++) {
            id[i] = i;
            size[i] = 1;
        }
    }

    private int root(int i) {
        while (i != id[i]) {
            id[i] = id[id[i]]; // 路径压缩
            i = id[i];
        }
        return i;
    }

    public boolean connected(int p, int q) {
        return root(p) == root(q);
    }

    public void union(int p, int q) {
        int rootP = root(p);
        int rootQ = root(q);
        if (rootP == rootQ) {
            return;
        }
        if (size[rootP] < size[rootQ]) {
            id[rootP] = rootQ;
            size[rootQ] += size[rootP];
        } else {
            id[rootQ] = rootP;
            size[rootP] += size[rootQ];
        }
    }

    public static void main(String[] args) {
        WeightedQuickUnion wqu = new WeightedQuickUnion(10);
        wqu.union(4, 3);
        wqu.union(3, 8);
        wqu.union(6, 5);
        wqu.union(9, 4);
        wqu.union(2, 1);
        System.out.println(wqu.connected(8, 9)); // true
        System.out.println(wqu.connected(5, 4)); // false
        wqu.union(5, 0);
        wqu.union(7, 2);
        wqu.union(6, 1);
        wqu.union(7, 3);
        for (int i = 0; i < 10; i++) {
            System.out.print(wqu.root(i) + " ");
        }
        // 输出为:1 8 1 8 3 0 1 8 8 8
    }

}

WQU Implementation

WQU的性能分析

最坏的结果中,树的高度为Θ(log N),如下图

所以可以认为WeightQuickUnion在尽量降低长度上并不比HeightQuickUnion差。

9.5 路径压缩(Path Compression)

我们不关心树的连通结构,只关心元素之间的连通性,以及树的高度——高度尽量小。

由此,我们可以将沿途的所有项目连接到根,将有助于使我们的树在每次调用find时都更短(不用爬树了)。相当于用空间换时间。

这样一层一层地往根节点上连,最终高度为2。

更具体地说,对于 N 个元素的 M 操作,具有路径压缩的WeightQuickUnion以 O(N + M (lg* N)) 为单位。lg* 是迭代对数,对于任何实际输入,它都小于 5。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值