算法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() | 已经连通的分量 | 
import edu.princeton.cs.algs4.StdDraw;import edu.princeton.cs.algs4.StdIn;import edu.princeton.cs.algs4.StdOut;public class UnionFind {private int[] id;//以触点为索引private int count;//分量个数public UnionFind(int N){count = N;id = new int[N];for(int i = 0; i<N; i++) id[i] = i;//开始每个分量只有一个触点}public int count(){ return count;}public boolean connected(int p, int q){return find(p) == find(q);}public int find(int p){return id[p];}//归并方法public void union(int p ,int q){int pID = find(p);int qID = find(q);if(pID == qID) return;for(int i = 0; i<id.length; i++){if(id[i] == pID) id[i] = qID;}}//union方法使得连通的触点对于的id[]值相同public static void main(String[] args){int N = StdIn.readInt();UnionFind uf = new UnionFind(N);while (!StdIn.isEmpty()){int p = StdIn.readInt();int q = StdIn.readInt();if(uf.connected(p,q)) continue;uf.union(p,q);StdOut.println(p + " " + q);}StdOut.println(uf.count + " components");}}
要点
- 主要实现在于维护一个整型数组id[],开始每个id[i]即代表一个连通的分量,元素值即索引值,共有N个,之后每union()两个触点p q,就让两个触点元素值都为q,最终一个分量中的元素id值都相等
 
特点
- 无法处理大型问题:每一对输入union()都需要扫面整个id[]数组,算法增长级别为平方级别.
 
1.5.2 quick-union算法
import edu.princeton.cs.algs4.StdIn;import edu.princeton.cs.algs4.StdOut;public class QuickUnion {private int[] id;//以触点为索引private int count;//分量个数public QuickUnion(int N){count = N;id = new int[N];for(int i = 0; i<N; i++) id[i] = i;//开始每个分量只有一个触点}public int count(){ return count;}public boolean connected(int p, int q){return find(p) == find(q);}//回溯到根节点public int find(int p){while (id[p] != p) p = id[p];return p;}//归并方法public void union(int p ,int q){int pRoot = find(p);int qRoot = find(q);if(pRoot == qRoot) return;id[pRoot] = qRoot;count--;}//union方法使得连通的触点拥有相同的根节点}
对比1.5.1
- 基于同一组数据结构—以触点为索引的id[]数组
 - find()中,从给定触点开始,不断回溯到上一节点,直至初值的根节点pRoot,union()直接将另一节点的根节点qRoot赋值给id[pRoot]使得两个分量归并为一棵树
 
要点
- union()的实现只用了一条语句就将一个根节点变为另一个根节点的父节点
 
特点
- quick-union算法的成本依赖于输入,如果最坏情况:不断输入0-i整数对,此时为平方级别,而最佳情况可能是现象级别,不能保证quick-union算法始终快于quick-find
 
1.5.3 加权quick-union算法
import edu.princeton.cs.algs4.StdIn;import edu.princeton.cs.algs4.StdOut;public class WeightedQuickUnionUF {private int[] id;//父链接数组private int[] sz;//各个根节点对应分量大小private int count;//连通分量个数public WeightedQuickUnionUF(int N){count = N;id = new int[N];for(int i = 0; i < N; i++) id[i] = i;sz = new int[N];for(int i = 0; i < N; i++) sz[i] = 1;}public int count(){ return count;}public boolean connected(int p, int q){return find(p) == find(q);}public int find(int p){while (p != id[p]) p = id[p];return p;}public void union(int p, int q){int i = find(p);int j = find(q);if(i == j) return;if(sz[i] < sz[j]) { id[i] = j; sz[j] += sz[i];}else{ id[j] = i; sz[i] += sz[j];}count--;}}
对比1.5.2
- 多了int[] sz用来保存各个根节点对应分量大小(不同树的大小)
 - union通过比较sz[find(p)]和sz[(q)]将小树连接到大树上
 
要点
- sz[i]开始都初始化为1,一个触点
 - 将小树连接到大树上
 
特点
- 加权算法中远离根节点的节点较少,只有一个节点被归并到大树中的情况很常见
 - 加权算法构造的森林中任意节点最多深度lgN,且最坏情况下find(),connected()和union()成本增长数量级为logN
 
1.5.4 路径压缩的加权quick-union算法(最优算法)
//压缩路径算法区别只在find()public int find(int p){int root = p;while (root != id[root]) root = id[root];//找到根节点while (root != p) {int temp = id[p];//temp暂存父节点作为下次循环的索引id[p] = root;//将当前节点直接连接到根节点p = temp;//还原索引}return root;}
同未压缩路径方法的联系区别
- 只需要未find()增加一个循环,顺路将路径上遇到的节点都直接链接到根节点,最终可以得到几乎完全扁平化的树
 
特点
- 理性状态下,我们希望每个节点都直接链接到它的根节点上,但又不想像quick-find一样修改大量链接,于是在检查节点的同时将它们直接链接到根节点
 
四种算法比较
(最坏情况下存在N个触点时成本的增长数量级)
| 算法 | 构造函数 | union() | find() | 
|---|---|---|---|
| quick-find算法 | N | N | 1 | 
| quick-union算法 | N | 树的高度 | 树的高度 | 
| 加权quick-union算法 | N | lgN | lgN | 
| 路径压缩的加权quick-union算法 | N | 非常接近但没有达到一 | 同理 | 
| 理想情况 | N | 1 | 1 | 
