问题:问题的输入是一列整数对,其中每一个整数都表示一个某种类型的对象,一对整数p q能够被理解为“p和q是相连的”。java
当程序从输入中读取了整数对p q时,若是已知的全部整数对都不能说明p和q是相连的,那么则将这一对整数写入到输出中;若是已知数据能够说明p和q是相连的,那么程序应该忽略p q这对整数并继续处理输入中的下一对整数。咱们将这个问题通俗地叫作动态连通性问题。算法
应用以下:输入的整数表示的多是一个大型计算机网络中的计算机,而整数对则表示网络中的链接。这个程序可以判断咱们是够须要在p和q之间架设一条新的链接才能进行通讯,或是咱们能够经过已有的链接在二者之间创建通讯线路。数组
union-find算法的API网络
public class UF | |
UF (int N) | 以整数标志(0到N-1)初始化N个触点 |
void union (int p,int q) | 在p和q之间创建一条链接 |
int find (int p) | p(0到N-1)所在份量的标识符 |
boolean connected (int p,int q) | 若是p和q存在于同一个份量中则返回true |
int count() | 连通份量的数量 |
咱们将讨论三种不一样的实现,他们均根据以触点为索引的id[]数组来肯定两个触点是否存在于相同的连通份量中。数据结构
1. quick-find算法ui
保证当且仅当id[p]等于id[q]时p和q是连通的,换句话说,在同一个连通份量中的全部触点在id[]中的值必须所有相同。算法以下:spa
public int find(int p){ return id[p]; } public void union(int p,int q) { //将p和q归并到相同份量中 int pID=find(p); int qID=find(q); //若是p和q已经在相同的份量之中则不须要采起任何行动 if(pID==qID) return; //将p和份量重命名为q的名称 for(int i=0;i<id.length;i++) if(id[i]==pID)id[i]=qID; count--; }
分析:find()操做的速度显然是很快的,觉得他只须要访问id[]数组一次。。可是quick-find算法通常没法处理大型问题,由于每一对输入union()都须要扫描整个id[]数组。计算机网络
每次find()调用都只须要访问一次数组,而归并两个份量的union()操做访问数组的次数在(N+3)到(2N+1)之间。code
假设咱们使用quick-find算法解决动态连通性问题,而且最后只获得了一个连通份量,那么至少须要调用N-1次union(),即至少(N+3)*(N-1)~N²次数访问,于是能够得出结论,quick-find算法的运行时间对于最终只能获得少数连通份量的通常应用是平方级别的。对象
2. quick-union算法
此算法重点是提升union()方法的速度,它和quick-find算法是互补的。
定义数据结构时,咱们须要每一个触点所对应的id[]元素都是同一个份量中的另外一个触点的名称(也多是它本身)——咱们将这种联系称为连接。
在实现find()方法时,咱们从给定的触点开始,由他的连接获得另外一个触点,再由这个触点连接到第三个触点,如此继续跟随连接直到到达一个根触点。不一样触点,通过一系列的连接,若是能够到达同一个根触点,则说明这两个触点存在于同一个连通份量中。
使用森林的概念,根触点做为根节点,quick-union算法更容易让人理解。算法以下:
public int find(int p){ //找出份量的名称,即根触点的名称 while(p!=id[p])p=id[p];//存储的连接不等于自己,则继续追溯下一个触点 return p; } public void union(int p,int q) { //将p和q归并到相同份量中,即将p和q的根触点统一 int pRoot=find(p); int qRoot=find(q); //若是p和q已经在相同的份量之中则不须要采起任何行动 if(pRoot==qRoot) return; //将p的根触点,指向q的根触点 id[pRoot]=qRoot; count--; }
分析:quick-union算法看起来比quick-find算法更快,可是分析它的算法成本难度很大,由于这依赖于输入的特色。在最好状况下,find()只须要访问一次数组就能获得一个触点所在的份量标识符;而在最坏状况下,这须要2N+1次数组访问。不可贵出,quick-union算法在构造有一个最佳状况输入使得解决动态连通性的问题的用例的运行时间是线性级别(1*N)的;而最坏状况下,他的运行时间是平方级别(N*(2N+1))的。
3.加权quick-union算法
quick-union算法能够看作quick-find算法的一种改良,由于它解决了quick-find算法中最主要的问题(union()操做老是线性级别的,由于每次都须要遍历整个数组来改掉某个连通份量内的值)。
可是quick-union算法仍存在问题,咱们不能保证任何状况下它都能比quick-find算法快的多。由于以前提到quick-union算法利用到森林,树的概念,每次find()都须要层层遍历到根节点,所以运行时间和节点在书中的深度息息相关。所以咱们须要一个改进的方法减少节点的深度。
加权quick-union算法就能实现这样的改进,由于他老是能让较小的树链接到较大的树上。固然,这须要咱们对数据结构进行相应的改进,即添加实例变量来记录每一棵树的大小,即份量大小。算法以下:
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++)id[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--; } }
对于动态连通性问题,加权quick-union算法是三种算法中惟一可以用于解决大型实际问题的算法。
最优算法
先说结论,路径压缩的加权quick-union算法是最优算法。
路径压缩即在检查每一个节点的同时将他们直接连接到根节点,也就是实现了树的几乎彻底的扁平化,这和quick-find算法理想状况下所获得的树很是接近。