算法1.5 union-find并查集

场景
当程序从输入中读取整数对(p,q)时,如果已知所有整数对都不能说明(p,q)是相连的,则输出整数对,否则忽略当前整数对,读取下一对.


1.5.1 union-find算法

API

返回类型 方法参数 说明
UnionFind(int N) 以整数标识初始化N个触点
void union(int p, int q) 在p和q之间添加一条连接
int find(int p) p所在分量的标识符
boolean connected(int p, int q) 如果p和q存在于同一个分量中返回true
int count() 已经连通的分量
  1. import edu.princeton.cs.algs4.StdDraw;
  2. import edu.princeton.cs.algs4.StdIn;
  3. import edu.princeton.cs.algs4.StdOut;
  4. public class UnionFind {
  5. private int[] id;//以触点为索引
  6. private int count;//分量个数
  7. public UnionFind(int N){
  8. count = N;
  9. id = new int[N];
  10. for(int i = 0; i<N; i++) id[i] = i;//开始每个分量只有一个触点
  11. }
  12. public int count(){ return count;}
  13. public boolean connected(int p, int q){
  14. return find(p) == find(q);
  15. }
  16. public int find(int p){
  17. return id[p];
  18. }
  19. //归并方法
  20. public void union(int p ,int q){
  21. int pID = find(p);
  22. int qID = find(q);
  23. if(pID == qID) return;
  24. for(int i = 0; i<id.length; i++){
  25. if(id[i] == pID) id[i] = qID;
  26. }
  27. }//union方法使得连通的触点对于的id[]值相同
  28. public static void main(String[] args){
  29. int N = StdIn.readInt();
  30. UnionFind uf = new UnionFind(N);
  31. while (!StdIn.isEmpty()){
  32. int p = StdIn.readInt();
  33. int q = StdIn.readInt();
  34. if(uf.connected(p,q)) continue;
  35. uf.union(p,q);
  36. StdOut.println(p + " " + q);
  37. }
  38. StdOut.println(uf.count + " components");
  39. }
  40. }

要点

  1. 主要实现在于维护一个整型数组id[],开始每个id[i]即代表一个连通的分量,元素值即索引值,共有N个,之后每union()两个触点p q,就让两个触点元素值都为q,最终一个分量中的元素id值都相等

特点

  1. 无法处理大型问题:每一对输入union()都需要扫面整个id[]数组,算法增长级别为平方级别.

1.5.2 quick-union算法

  1. import edu.princeton.cs.algs4.StdIn;
  2. import edu.princeton.cs.algs4.StdOut;
  3. public class QuickUnion {
  4. private int[] id;//以触点为索引
  5. private int count;//分量个数
  6. public QuickUnion(int N){
  7. count = N;
  8. id = new int[N];
  9. for(int i = 0; i<N; i++) id[i] = i;//开始每个分量只有一个触点
  10. }
  11. public int count(){ return count;}
  12. public boolean connected(int p, int q){
  13. return find(p) == find(q);
  14. }
  15. //回溯到根节点
  16. public int find(int p){
  17. while (id[p] != p) p = id[p];
  18. return p;
  19. }
  20. //归并方法
  21. public void union(int p ,int q){
  22. int pRoot = find(p);
  23. int qRoot = find(q);
  24. if(pRoot == qRoot) return;
  25. id[pRoot] = qRoot;
  26. count--;
  27. }//union方法使得连通的触点拥有相同的根节点
  28. }

对比1.5.1

  1. 基于同一组数据结构—以触点为索引的id[]数组
  2. find()中,从给定触点开始,不断回溯到上一节点,直至初值的根节点pRoot,union()直接将另一节点的根节点qRoot赋值给id[pRoot]使得两个分量归并为一棵树

要点

  1. union()的实现只用了一条语句就将一个根节点变为另一个根节点的父节点

特点

  1. quick-union算法的成本依赖于输入,如果最坏情况:不断输入0-i整数对,此时为平方级别,而最佳情况可能是现象级别,不能保证quick-union算法始终快于quick-find

1.5.3 加权quick-union算法

  1. import edu.princeton.cs.algs4.StdIn;
  2. import edu.princeton.cs.algs4.StdOut;
  3. public class WeightedQuickUnionUF {
  4. private int[] id;//父链接数组
  5. private int[] sz;//各个根节点对应分量大小
  6. private int count;//连通分量个数
  7. public WeightedQuickUnionUF(int N){
  8. count = N;
  9. id = new int[N];
  10. for(int i = 0; i < N; i++) id[i] = i;
  11. sz = new int[N];
  12. for(int i = 0; i < N; i++) sz[i] = 1;
  13. }
  14. public int count(){ return count;}
  15. public boolean connected(int p, int q){
  16. return find(p) == find(q);
  17. }
  18. public int find(int p){
  19. while (p != id[p]) p = id[p];
  20. return p;
  21. }
  22. public void union(int p, int q){
  23. int i = find(p);
  24. int j = find(q);
  25. if(i == j) return;
  26. if(sz[i] < sz[j]) { id[i] = j; sz[j] += sz[i];}
  27. else{ id[j] = i; sz[i] += sz[j];}
  28. count--;
  29. }
  30. }

对比1.5.2

  1. 多了int[] sz用来保存各个根节点对应分量大小(不同树的大小)
  2. union通过比较sz[find(p)]和sz[(q)]将小树连接到大树上

要点

  1. sz[i]开始都初始化为1,一个触点
  2. 将小树连接到大树上

特点

  1. 加权算法中远离根节点的节点较少,只有一个节点被归并到大树中的情况很常见
  2. 加权算法构造的森林中任意节点最多深度lgN,且最坏情况下find(),connected()和union()成本增长数量级为logN

1.5.4 路径压缩的加权quick-union算法(最优算法)

  1. //压缩路径算法区别只在find()
  2. public int find(int p){
  3. int root = p;
  4. while (root != id[root]) root = id[root];//找到根节点
  5. while (root != p) {
  6. int temp = id[p];//temp暂存父节点作为下次循环的索引
  7. id[p] = root;//将当前节点直接连接到根节点
  8. p = temp;//还原索引
  9. }
  10. return root;
  11. }

同未压缩路径方法的联系区别

  1. 只需要未find()增加一个循环,顺路将路径上遇到的节点都直接链接到根节点,最终可以得到几乎完全扁平化的树

特点

  1. 理性状态下,我们希望每个节点都直接链接到它的根节点上,但又不想像quick-find一样修改大量链接,于是在检查节点的同时将它们直接链接到根节点

四种算法比较

(最坏情况下存在N个触点时成本的增长数量级)

算法 构造函数 union() find()
quick-find算法 N N 1
quick-union算法 N 树的高度 树的高度
加权quick-union算法 N lgN lgN
路径压缩的加权quick-union算法 N 非常接近但没有达到一 同理
理想情况 N 1 1