并查集:Union Find,也叫作不相交集合(Disjoint Set),具有两个核心功能
- 查找(Find):查找元素所在的集合(这里的集合不特指Set这种数据结构,而是广义的数据集合)
- 合并(Union):将两个元素所在的集合合并为一个集合
主要的应用:
求连接和求路径两个问题的侧重点不一样,求连接只需要合理设计数据结构,就不需要求出路径才知道是否联通
- 连接问题:网络中节点间的连接状态
- 数学中的集合类实现
并查集 Union Find:对于一组数据,主要支持两个动作
- union(p , q):连通两个节点
- same(p , q):判断两个节点是否连通
Quick Find
时间复杂度:
- 查找(Find):O(1)
- 合并(Union):O(n)
Quick Union
时间复杂度:
- 查找(Find):O(logn),可以优化到 O(a(n)),a(n)<5
- 合并(Union):O(logn),可以优化到 O(a(n)),a(n)<5
实现
1、如何存储数据:假设并查集处理的数据都是整形,那么可以用整形数组来存储数据
2、基本实现:
public interface UnionFind {/*** 找到根节点** @param v* @return*/int find(int v);/*** 连通v1和v2** @param v1* @param v2*/void union(int v1, int v2);/*** v1与v2是否属于同一集合** @param v1* @param v2* @return*/boolean same(int v1, int v2);}public abstract class AbstractUnionFind implements UnionFind {protected int[] parents;public AbstractUnionFind(int capacity) {if (capacity < 0) {throw new IllegalArgumentException("capacity must be >=1");}parents = new int[capacity];// 初始化for (int i = 0; i < capacity; i++) {parents[i] = i;}}@Overridepublic boolean same(int v1, int v2) {return find(v1) == find(v2);}public void rangeCheck(int v) {if (v < 0 || v >= parents.length) {throw new IllegalArgumentException("v is out of bounds");}}}
3、QuickFind实现:在 union 两个元素时,将左边子树的所有元素均指向右边节点的根节点
public class QuickFind extends AbstractUnionFind {public QuickFind(int capacity) {super(capacity);}/*** 查找v所属的集合** @param v* @return*/@Overridepublic int find(int v) {rangeCheck(v);return parents[v];}@Overridepublic void union(int v1, int v2) {int p1 = find(v1);int p2 = find(v2);if (p1 == p2) {return;}// 合并两个节点时,将 v1 节点的所有元素均指向 v2 的根节点for (int i = 0; i < parents.length; i++) {if (parents[i] == p1) {parents[i] = p2;}}}}
4、QuickUnion实现:在 union 时,使左边子树的根节点指向右边子树的根节点
public class QuickUnion extends AbstractUnionFind {public QuickUnion(int capacity) {super(capacity);}@Overridepublic int find(int v) {rangeCheck(v);while (v != parents[v]) {v = parents[v];}return v;}@Overridepublic void union(int v1, int v2) {int p1 = find(v1);int p2 = find(v2);if (p1 != p2) {parents[p1] = p2;}}}
5、左右子树元素个数平衡优化,基于QuickUnion,在数组中新增子树节点个数数组,在 union 时将元素个数少的子树的根节点指向元素个数更多子树的根节点
public class QuickUnionWithSize extends QuickUnion {private int[] sizes;public QuickUnionWithSize(int capacity) {super(capacity);// 创建size数组sizes = new int[capacity];// 初始化Arrays.fill(sizes, 1);}@Overridepublic 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] += p2;}}}
6、基于左右子树高度的优化,基于QuickUnion:在Union时,将高度低的子树的根节点指向高度更高的子树的根节点,高度不变;在高度一致时,被指向的根节点的高度 + 1
public class QuickUnionWithRank extends QuickUnion {private int[] ranks;public QuickUnionWithRank(int capacity) {super(capacity);ranks = new int[capacity];Arrays.fill(ranks, 1);}@Overridepublic void union(int v1, int v2) {int p1 = find(v1);int p2 = find(v2);if (p1 == p2) {return;}if (ranks[p1] > ranks[p2]) {parents[p2] = p1;} else if (ranks[p1] < ranks[p2]) {parents[p1] = p2;} else {parents[p1] = p2;ranks[p2]++;}}}
7、接下来是基于 QuickUnionWithRank 的优化,在这个基础上,可以在查找时对元素进行调整,达到优化的效果,具体有:
- 路径压缩(Path Compression)
- 两种更加优秀的做法:
- 路径分裂(Path Spliting)
- 路径减半(Path Halving)
路径压缩(Path Compression):使路径上所有的根节点都指向根节点
public class QuickUnionWithPathCompact extends QuickUnionWithRank {public QuickUnionWithPathCompact(int capacity) {super(capacity);}@Overridepublic int find(int v) {rangeCheck(v);// 将查找路径上的元素都指向根节点if (parents[v] != v) {parents[v] = find(parents[v]);}return parents[v];}}
路径分裂(Path Spliting):将 find 时路径上的节点均指向其祖父节点,从而将路径分成两条
public class QuickUnionWithPathSplit extends QuickUnionWithRank {@Overridepublic int find(int v) {rangeCheck(v);// 路径减半,是查找路径上的每个节点指向其祖父节点while (parents[v] != v) {int tmp = parents[v];parents[v] = parents[parents[v]];v = tmp;}return v;}public QuickUnionWithPathSplit(int capacity) {super(capacity);}}
路径减半(Path Halving):将 find 时路径上的奇数节点指向其祖父节点
public class QuickUnionWithPathHalf extends QuickUnionWithRank{public QuickUnionWithPathHalf(int capacity) {super(capacity);}@Overridepublic int find(int v) {rangeCheck(v);while (v != parents[v]) {parents[v] = parents[parents[v]];v = parents[v];}return v;}}
通用的并查集
基于QuickUnion 而且进行了高度优化和路径减半优化:
public class GenericQuickUnion<V> {private Map<V, Node<V>> nodes = new HashMap<>();public void makeSet(V v) {if (nodes.containsKey(v)) {return;}nodes.put(v, new Node<>(v));}/*** 找到v的根节点** @param v* @return*/private Node<V> findNode(V v) {Node<V> node = nodes.get(v);if (node == null) {return null;}// 判断两个对象是否相等while (!Objects.equals(node.value, node.parent.value)) {// 进行路径缩短node.parent = node.parent.parent;node = node.parent;}return node;}/*** 查找两个元素** @param v* @return*/public V find(V v) {Node<V> node = findNode(v);return node == null ? null : node.value;}public void union(V v1, V v2) {Node<V> node1 = findNode(v1);Node<V> node2 = findNode(v2);if ((node1 == null) || (node2 == null)) {return;}if (Objects.equals(node1, node2)) {return;}if (node1.rank < node2.rank) {node1.parent = node2;} else if (node1.rank > node2.rank) {node2.parent = node1;} else {node1.parent = node2;node2.rank++;}}public boolean same(V v1, V v2) {// 比较两个对象是否相等 (v1==v2)||(v1!=null&&v1.equals(v2))return Objects.equals(find(v1), find(v2));}private static class Node<V> {V value;Node<V> parent;int rank;public Node(V value) {this.value = value;parent = this;rank = 1;}}}
