目录
需求分析
- 假设有 n 个村庄,有些村庄之间有连接的路,有些村庄之间并没有连接的路
- 设计一个数据结构,能够快速执行 2 个操作:
- 查询 2 个村庄之间是否有连接的路
- 连接 2 个村庄
- 使用数组、链表、平衡二叉树、集合(Set) 都可以完成需求,但是查询、连接的时间复杂度都是 O(n)。
- 并查集能做到查询、连接的均摊时间复杂度都是 O(α(n)),α(n) < 5。
- 非常适合解决这类“连接”相关的问题。
并查集概念
- 并查集也叫作不相交集合(Disjoint Set)
- 查找(Find):查找元素所在的集合(这里的集合并不是特指Set这种数据结构,是指广义的数据集合)
- 合并(Union):将两个元素所在的集合合并为一个集合
- 有 2 种常见的实现思路:
- Quick Find
- 查找(Find)的时间复杂度:O(1)
- 合并(Union)的时间复杂度:O(n)
- Quick Union
- 查找(Find)的时间复杂度:O(logn), 可以优化至 O(𝛼(𝑛)), α(𝑛) < 5
- 合并(Union)的时间复杂度:O(logn), 可以优化至 O(𝛼(𝑛)), α(𝑛) < 5
如何存储数据?
- 设并查集处理的数据都是整型,那么可以用整型数组来存储数据。
- 数组索引代表元素值
- 索引对应的值代表这个元素的根节点
- 因此,并查集是可以用数组实现的树形结构(二叉堆、优先级队列也是可以用数组实现的树形结构)
接口定义
元素初始化
- 初始化时,每个元素各自属于一个单元素集合
Quick Find实现原理
- Quick Find 的 union(v1, v2):让 v1 所在集合的所有元素都指向 v2 的根节点。
Quick Find - Union
- union 时间复杂度:O(n)
Quick Find - Find
- 直接获取数组中的值
Quick Union实现原理
Quick Union - Union
- 时间复杂度:O(logn)
Quick Union - Find
Quick Union – 优化
- 在 union 的过程中,可能会出现树不平衡的情况,甚至退化成链表。
- 有 2 种常见的优化方案:
- 基于 size 的优化:元素少的树 嫁接到 元素多的树
- 基于 rank 的优化:矮的树 嫁接到 高的树
【基于 size 的优化】
public class UnionFind_QU_S extends UnionFind_QU { private int[] sizes; public UnionFind_QU_S(int capacity) { super(capacity); sizes = new int[capacity]; for (int i = 0; i < sizes.length; i++) { sizes[i] = 1; } } /** * 将v1的根节点嫁接到v2的根节点上 */ public void union(int v1, int v2) { int p1 = find(v1); int p2 = find(v2); if(p1 == p2) return; if(sizes[p1] < sizes[p2]){ parents[p1] = p2; sizes[p2] += sizes[p1]; }else{ parents[p2] = p1; sizes[p1] += sizes[p2]; } } }
【基于 rank 的优化】
- 基于size的优化,也可能会存在树不平衡的问题。
- 基于rank的优化,让矮的树 嫁接到 高的树
- 代码实现
public class UnionFind_QU_R extends UnionFind_QU { private int[] ranks; public UnionFind_QU_R(int capacity) { super(capacity); ranks = new int[capacity]; for (int i = 0; i < parents.length; i++) { ranks[i] = 1; } } public void union(int v1, int v2){ int p1 = find(v1); int p2 = find(v2); if(ranks[p1] < ranks[p2]){ parents[p1] = p2; }else if(ranks[p1] > ranks[p2]){ parents[p2] = p1; }else{ // ranks[p1] == ranks[p2] parents[p1] = p2; ranks[p2] += 1; } } }
路径压缩(Path Compression)
- 虽然有了基于 rank 的优化,树会相对平衡一点,但是随着 union 次数的增多:树的高度依然会越来越高,导致 find 操作变慢,尤其是底层节点 (因为 find 是不断向上找到根节点) 。
- 什么是路径压缩?
- 在 find 时使路径上的所有节点都指向根节点,从而降低树的高度。
- 代码实现
/** * Quick Union - 基于rank的优化 - 路径压缩(Path Compression) * @author yusael */ public class UnionFind_QU_R_PC extends UnionFind_QU_R { public UnionFind_QU_R_PC(int capacity) { super(capacity); } /** * 在find时使路径上的所有节点都指向根节点,从而降低树的高度 */ public int find(int v){ rangeCheck(v); if(parents[v] != v){ parents[v] = find(parents[v]); } return parents[v]; } }
- 路径压缩使路径上的所有节点都指向根节点,所以实现成本稍高。
- 还有2种更优的做法,不但能降低树高,实现成本也比路径压缩低:
- 路径分裂(Path Spliting)
- 路径减半(Path Halving)
- 路径分裂、路径减半的效率差不多,但都比路径压缩要好。
路径分裂(Path Spliting)
- 路径分裂:使路径上的每个节点都指向其祖父节点(parent的parent)。
- 代码实现
/** * Quick Union - 基于rank的优化 - 路径分裂(Path Spliting) */ public class UnionFind_QU_R_PS extends UnionFind_QU_R { public UnionFind_QU_R_PS(int capacity) { super(capacity); } public int find(int v){ rangeCheck(v); while(v != parents[v]){ int p = parents[v]; parents[v] = parents[parents[v]]; v = p; } return parents[v]; } }
路径减半(Path Halving)
- 路径减半:使路径上每隔一个节点就指向其祖父节点(parent的parent)。
- 代码实现
/** * Quick Union - 基于rank的优化 - 路径减半(Path Halving) */ public class UnionFind_QU_R_PH extends UnionFind_QU_R { public UnionFind_QU_R_PH(int capacity) { super(capacity); } public int find(int v){ rangeCheck(v); while(v != parents[v]){ parents[v] = parents[parents[v]]; v = parents[v]; } return v; } }
-
并查集总结
- 使用路径压缩、分裂或减半 + 基于rank或者size的优化可以确保每个操作的均摊时间复杂度为 O(𝛼(𝑛)) ,α(𝑛) < 5。
- 建议搭配:
- Quick Union
- 基于 rank 的优化
- Path Halving 或 Path Spiliting