并查集: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、基本实现:

  1. public interface UnionFind {
  2. /**
  3. * 找到根节点
  4. *
  5. * @param v
  6. * @return
  7. */
  8. int find(int v);
  9. /**
  10. * 连通v1和v2
  11. *
  12. * @param v1
  13. * @param v2
  14. */
  15. void union(int v1, int v2);
  16. /**
  17. * v1与v2是否属于同一集合
  18. *
  19. * @param v1
  20. * @param v2
  21. * @return
  22. */
  23. boolean same(int v1, int v2);
  24. }
  25. public abstract class AbstractUnionFind implements UnionFind {
  26. protected int[] parents;
  27. public AbstractUnionFind(int capacity) {
  28. if (capacity < 0) {
  29. throw new IllegalArgumentException("capacity must be >=1");
  30. }
  31. parents = new int[capacity];
  32. // 初始化
  33. for (int i = 0; i < capacity; i++) {
  34. parents[i] = i;
  35. }
  36. }
  37. @Override
  38. public boolean same(int v1, int v2) {
  39. return find(v1) == find(v2);
  40. }
  41. public void rangeCheck(int v) {
  42. if (v < 0 || v >= parents.length) {
  43. throw new IllegalArgumentException("v is out of bounds");
  44. }
  45. }
  46. }

3、QuickFind实现:在 union 两个元素时,将左边子树的所有元素均指向右边节点的根节点

  1. public class QuickFind extends AbstractUnionFind {
  2. public QuickFind(int capacity) {
  3. super(capacity);
  4. }
  5. /**
  6. * 查找v所属的集合
  7. *
  8. * @param v
  9. * @return
  10. */
  11. @Override
  12. public int find(int v) {
  13. rangeCheck(v);
  14. return parents[v];
  15. }
  16. @Override
  17. public void union(int v1, int v2) {
  18. int p1 = find(v1);
  19. int p2 = find(v2);
  20. if (p1 == p2) {
  21. return;
  22. }
  23. // 合并两个节点时,将 v1 节点的所有元素均指向 v2 的根节点
  24. for (int i = 0; i < parents.length; i++) {
  25. if (parents[i] == p1) {
  26. parents[i] = p2;
  27. }
  28. }
  29. }
  30. }

4、QuickUnion实现:在 union 时,使左边子树的根节点指向右边子树的根节点

  1. public class QuickUnion extends AbstractUnionFind {
  2. public QuickUnion(int capacity) {
  3. super(capacity);
  4. }
  5. @Override
  6. public int find(int v) {
  7. rangeCheck(v);
  8. while (v != parents[v]) {
  9. v = parents[v];
  10. }
  11. return v;
  12. }
  13. @Override
  14. public void union(int v1, int v2) {
  15. int p1 = find(v1);
  16. int p2 = find(v2);
  17. if (p1 != p2) {
  18. parents[p1] = p2;
  19. }
  20. }
  21. }

5、左右子树元素个数平衡优化,基于QuickUnion,在数组中新增子树节点个数数组,在 union 时将元素个数少的子树的根节点指向元素个数更多子树的根节点

  1. public class QuickUnionWithSize extends QuickUnion {
  2. private int[] sizes;
  3. public QuickUnionWithSize(int capacity) {
  4. super(capacity);
  5. // 创建size数组
  6. sizes = new int[capacity];
  7. // 初始化
  8. Arrays.fill(sizes, 1);
  9. }
  10. @Override
  11. public void union(int v1, int v2) {
  12. int p1 = find(v1);
  13. int p2 = find(v2);
  14. if (p1 == p2) {
  15. return;
  16. }
  17. if (sizes[p1] < sizes[p2]) {
  18. parents[p1] = p2;
  19. sizes[p2] += sizes[p1];
  20. } else {
  21. parents[p2] = p1;
  22. sizes[p1] += p2;
  23. }
  24. }
  25. }

6、基于左右子树高度的优化,基于QuickUnion:在Union时,将高度低的子树的根节点指向高度更高的子树的根节点,高度不变;在高度一致时,被指向的根节点的高度 + 1

  1. public class QuickUnionWithRank extends QuickUnion {
  2. private int[] ranks;
  3. public QuickUnionWithRank(int capacity) {
  4. super(capacity);
  5. ranks = new int[capacity];
  6. Arrays.fill(ranks, 1);
  7. }
  8. @Override
  9. public void union(int v1, int v2) {
  10. int p1 = find(v1);
  11. int p2 = find(v2);
  12. if (p1 == p2) {
  13. return;
  14. }
  15. if (ranks[p1] > ranks[p2]) {
  16. parents[p2] = p1;
  17. } else if (ranks[p1] < ranks[p2]) {
  18. parents[p1] = p2;
  19. } else {
  20. parents[p1] = p2;
  21. ranks[p2]++;
  22. }
  23. }
  24. }

7、接下来是基于 QuickUnionWithRank 的优化,在这个基础上,可以在查找时对元素进行调整,达到优化的效果,具体有:

  • 路径压缩(Path Compression)
  • 两种更加优秀的做法:
    • 路径分裂(Path Spliting)
    • 路径减半(Path Halving)

路径压缩(Path Compression):使路径上所有的根节点都指向根节点

  1. public class QuickUnionWithPathCompact extends QuickUnionWithRank {
  2. public QuickUnionWithPathCompact(int capacity) {
  3. super(capacity);
  4. }
  5. @Override
  6. public int find(int v) {
  7. rangeCheck(v);
  8. // 将查找路径上的元素都指向根节点
  9. if (parents[v] != v) {
  10. parents[v] = find(parents[v]);
  11. }
  12. return parents[v];
  13. }
  14. }

路径分裂(Path Spliting):将 find 时路径上的节点均指向其祖父节点,从而将路径分成两条

  1. public class QuickUnionWithPathSplit extends QuickUnionWithRank {
  2. @Override
  3. public int find(int v) {
  4. rangeCheck(v);
  5. // 路径减半,是查找路径上的每个节点指向其祖父节点
  6. while (parents[v] != v) {
  7. int tmp = parents[v];
  8. parents[v] = parents[parents[v]];
  9. v = tmp;
  10. }
  11. return v;
  12. }
  13. public QuickUnionWithPathSplit(int capacity) {
  14. super(capacity);
  15. }
  16. }

路径减半(Path Halving):将 find 时路径上的奇数节点指向其祖父节点

  1. public class QuickUnionWithPathHalf extends QuickUnionWithRank{
  2. public QuickUnionWithPathHalf(int capacity) {
  3. super(capacity);
  4. }
  5. @Override
  6. public int find(int v) {
  7. rangeCheck(v);
  8. while (v != parents[v]) {
  9. parents[v] = parents[parents[v]];
  10. v = parents[v];
  11. }
  12. return v;
  13. }
  14. }

通用的并查集

基于QuickUnion 而且进行了高度优化和路径减半优化:

  1. public class GenericQuickUnion<V> {
  2. private Map<V, Node<V>> nodes = new HashMap<>();
  3. public void makeSet(V v) {
  4. if (nodes.containsKey(v)) {
  5. return;
  6. }
  7. nodes.put(v, new Node<>(v));
  8. }
  9. /**
  10. * 找到v的根节点
  11. *
  12. * @param v
  13. * @return
  14. */
  15. private Node<V> findNode(V v) {
  16. Node<V> node = nodes.get(v);
  17. if (node == null) {
  18. return null;
  19. }
  20. // 判断两个对象是否相等
  21. while (!Objects.equals(node.value, node.parent.value)) {
  22. // 进行路径缩短
  23. node.parent = node.parent.parent;
  24. node = node.parent;
  25. }
  26. return node;
  27. }
  28. /**
  29. * 查找两个元素
  30. *
  31. * @param v
  32. * @return
  33. */
  34. public V find(V v) {
  35. Node<V> node = findNode(v);
  36. return node == null ? null : node.value;
  37. }
  38. public void union(V v1, V v2) {
  39. Node<V> node1 = findNode(v1);
  40. Node<V> node2 = findNode(v2);
  41. if ((node1 == null) || (node2 == null)) {
  42. return;
  43. }
  44. if (Objects.equals(node1, node2)) {
  45. return;
  46. }
  47. if (node1.rank < node2.rank) {
  48. node1.parent = node2;
  49. } else if (node1.rank > node2.rank) {
  50. node2.parent = node1;
  51. } else {
  52. node1.parent = node2;
  53. node2.rank++;
  54. }
  55. }
  56. public boolean same(V v1, V v2) {
  57. // 比较两个对象是否相等 (v1==v2)||(v1!=null&&v1.equals(v2))
  58. return Objects.equals(find(v1), find(v2));
  59. }
  60. private static class Node<V> {
  61. V value;
  62. Node<V> parent;
  63. int rank;
  64. public Node(V value) {
  65. this.value = value;
  66. parent = this;
  67. rank = 1;
  68. }
  69. }
  70. }