一、基本操作:
1、Find:当且仅当两个元素属于相同的集合时,返回相同的名字
2、Union:将两个不相交的集合合并为一个不想交的集合。
应用:在等价关系中,判断两个元素之间是否有关系或者添加等价关系。
二、基本数据结构
1、Quick-Find实现
采用一个数组保存每个等价类的名字,这种实现下Find的复杂度为O(1),而Union实现的复杂度为O(n)。
例如Union(i,j):Union操作需要遍历整个数组,将所有为j的元素改为i。
1 public class ADTQuickFind { 2 private int[] id; 3 private int size; 4 public ADTQuickFind(int size){ 5 this.size = size; 6 id = new int[size]; 7 //初始化默认所有节点不联通 8 for(int i = 0; i < size; i++) 9 id[i] = i; 10 } 11 12 public int find(int p){ 13 //p的值出现异常 14 if(p < 0 || p >= size) 15 return -1; 16 return id[p]; 17 } 18 19 public void Union(int p, int q){ 20 int pId = find(p); 21 int qId = find(q); 22 if(pId == qId) 23 return; 24 for(int i = 0; i < size; i++){ 25 if(id[i] == qId) 26 id[i] = pId; 27 28 } 29 } 30 31 public boolean isRelated(int p, int q){ 32 return find(p) == find(q); 33 } 34 }
如果最开始所有元素都不连通,那么最多执行N-1次Union运算,此时所有的元素都在一个等价类中,总的时间复杂度为O(n2)。
如果Find运算比较多的情况下,存在Ω(n2)次Find运算,那么平均每次Find或者Union的时间复杂度为O(1),但是如果Find的次数比较少,那么这种方法是不可以接受的。
2、Quick-Union实现
在数组中隐式地存储一个森林,每个数组对应的位置存储对应节点的父节点的编号,树根节点保存自己的编号。
Find:由于Find操作要求当且仅当两个元素在相同的集合时,作用在两个元素上的Find操作返回相同的名字。而返回元素的标记是什么并不重要,因此通过返回一个节点的根节点的编号代表节点所处等价类的编号。
Union:使一个节点的根指针指向另一棵树的根节点。
1 public class ADTQuickUnion { 2 int[] parent; 3 int size; 4 public ADTQuickUnion(int size){ 5 this.size = size; 6 parent = new int[size]; 7 for(int i = 0; i < size; i++) 8 parent[i] = i; 9 } 10 public int find(int p){ 11 if(p < 0 || p >= size) 12 return -1; //出现异常值 13 while(parent[p] != p) 14 p = parent[p]; 15 return p; 16 } 17 public void union(int p, int q){ 18 int pId = find(p); 19 int qId = find(q); 20 if(pId == qId) 21 return; 22 parent[qId] = pId; 23 } 24 public boolean isRelated(int p, int q){ 25 return find(p) == find(q); 26 } 27 }
算法中执行Find操作花费的时间与节点的深度成正比,最坏情况下,会建立一个深度为N-1的树,使用一次Find的最坏情形运行时间为O(n)。
3、QuickUnionBetter实现
在Union操作中,将元素个数较少的树插到元素个数较多的树的根节点上。
1 public class ADTQuickUnionBetter { 2 private int[] parent; 3 private int[] mSize; 4 private int size; 5 public ADTQuickUnionBetter(int size){ 6 this.size = size; 7 parent = new int[size]; 8 mSize = new int[size]; 9 for(int i = 0; i < size; i++){ 10 parent[i] = i; 11 mSize[i] = 1; 12 } 13 } 14 public int find(int p){ 15 if(p < 0 || p >= size) 16 return -1; //表明出现异常值 17 while(p != parent[p]) 18 p = parent[p]; 19 return p; 20 } 21 public void union(int p, int q){ 22 int pId = find(p); 23 int qId = find(q); 24 if(pId == qId) 25 return; 26 if(mSize[pId] < mSize[qId]){ 27 parent[pId] = qId; 28 mSize[qId] = mSize[pId] + mSize[qId]; 29 } 30 else{ 31 parent[qId] = pId; 32 mSize[pId] = mSize[qId] + mSize[pId]; 33 } 34 } 35 public boolean isRelated(int p, int q){ 36 return find(p) == find(q); 37 } 38 }
可以证明,任何节点的深度不会超过log(n),由于随着深度每次随着Union的结果而增大1时,该节点被置于节点数至少为原先两倍大的树上。
因此Find操作的时间复杂度为O(log(n)),连续M次操作的时间复杂度为O(MlogN),平均时间得到证明为O(M)时间,为线性的。
4、Find中优化路径压缩优化路径压缩
1 public int find(int p){ 2 if(p < 0 || p >= size) 3 return -1; //异常值 4 while(p != parent[p]){ 5 parent[p] = parent[parent[p]]; 6 p = parent[p]; 7 } 8 return p; 9 }