quick-union样本分析
在上一次的测试验证中,可以得出quick-union相对于quick-find算法有些优势,但是在连通数据集增大的情况下,quick-union的表现不是很好,仍需要分析下在不同的数据集中,quick-union算法的表现。
最优情况
最优的情况,就是规避掉了quick-union的劣势,寻找分量标识的时候也能通过O(1)时间就找到,举个例子,如果连通数据集如下:
| 序号 | p | q |
|---|---|---|
| 0 | 0 | 1 |
| 0 | 0 | 2 |
| 0 | 0 | 3 |
| 0 | 0 | 4 |
| 0 | 0 | 5 |
| 0 | 0 | 6 |
| 0 | 0 | 7 |
| 0 | 0 | 8 |
| 0 | 0 | 9 |
合并后结果:
-------------------------------
0 1 2 3 4 5 6 7 8 9
-------------------------------
0 0 0 0 0 0 0 0 0 0
化成图形
无疑,可以直观的感受到,在每次合并操作的时候,都是将新分量直接合并到分量0的标识下,分量1到9是平级机构,每个分量都可以经过一次查找找到自己对应的分量标识。这也就是在特殊的数据情况下,quick-union可以达到quick-find的效果,而且不用遍历!
最差情况
最差情况下,有多少个分量就有多少的深度,那么每次find操作,时间复杂度就是O(n),还是举个例子还说明一下。(为了便于说明问题,quick-union的代码有一处改动)
@Override
void union(int p, int q) {
// 找到p的标识
int pId = find(p);
// 找到q的标识
int qId = find(q);
// 如果两个标识相等,代表已经合并
if (pId == qId) return;
// 如果不相等,直接让分量q的标识指向p
id[pId] = qId;
// 每次合并操作,会降低一个不同分量组
count--;
}
| 序号 | p | q |
|---|---|---|
| 0 | 0 | 1 |
| 0 | 1 | 2 |
| 0 | 2 | 3 |
| 0 | 3 | 4 |
| 0 | 4 | 5 |
| 0 | 5 | 6 |
| 0 | 6 | 7 |
| 0 | 7 | 8 |
| 0 | 8 | 9 |
合并后结果:
-------------------------------
0 1 2 3 4 5 6 7 8 9
-------------------------------
1 2 3 4 5 6 7 8 9 9
-------------------------------
化成图形
也可以直观的看到,此时并查集严重不平衡,对算法的执行效率也会大打折扣。
针对这种情况,优化的目的也变得很清晰,我们要尽量维护这课树的平衡,尽量避免这种不平衡的局面出现,自然就可以提升算法的性能。
加权quick-union
从最差的样本中我们可以发现,之所以形成了这种极端不平衡的链表样式的树,是因为每次操作都是把大树合并到小树中,比如在执行最后一次p=8,q=9的连通合并操作时,我们的操作如下
我们在为加权的情况下,合并之后形成了
然而我们希望的结果应该是
所以,我们假设有一个变量来比较待合并的两个分量权重的大小,我们始终把权重小的分量合并到权重大的分量下,那么这种极端的倾斜树,应该可以得到好转。
经过上面的验证和分析,我们来分析加权quick-union的处理步骤
- 在quick的基础上,添加一个成员属性,标识分量的权重
- 在合并操作上,不再是简单的将一个分量归属到另一个分量下,而是进行一次权重的比较,将权重小的分量,合并到权重大的分量下。
直接看代码
package com.zt;
public class WeightedQuickUnionUnionFind extends UnionFind {
// 分量权重数组,数组里的值代表当前分量下的分量数量
private int[] weightArr ;
public WeightedQuickUnionUnionFind(int n) {
super(n);
weightArr = new int[n];
// 初始化权重数组,初始化是每个分量下的权重都为1
for (int i = 0; i < weightArr.length; i++) {
weightArr[i] = 1;
}
}
@Override
void union(int p, int q) {
// 找到p的标识
int pId = find(p);
// 找到q的标识
int qId = find(q);
// 如果两个标识相等,代表已经合并
if (pId == qId) return;
// 将权重小的树,合并到权重大的树下
if (weightArr[p] < weightArr[q]) {
id[p] = q;
weightArr[q] += weightArr[p];
} else {
id[q] = p;
weightArr[p] += weightArr[q];
}
// 每次合并操作,会降低一个不同分量组
count--;
}
@Override
int find(int p) {
// 沿着标识路径一路寻找,直到找到树的标识
while (p !=id[p]) p = id[p];
return p;
}
}
验证效果
再次验证合并后的最差数据集结果
-------------------------------
0 0 1
1 1 2
2 2 3
3 3 4
4 4 5
5 5 6
6 6 7
7 7 8
8 8 9
9 0 0
-------------------------------
0 1 2 3 4 5 6 7 8 9
-------------------------------
0 0 0 0 0 0 0 0 0 0
-------------------------------
可以看到经过加权操作后,quick-union变成了一颗扁平的树,打到了最优化的结果。
随机数据集
因为数据比较特殊,我们随机生成一组样本数据再次进行一下比较。
-------------------------------
0 5 4
1 9 5
2 4 7
3 4 5
4 5 1
5 1 0
6 5 1
7 6 7
8 1 2
9 6 1
-------------------------------
未加权结果
-------------------------------
0 1 2 3 4 5 6 7 8 9
-------------------------------
2 0 2 3 7 4 0 1 8 4
-------------------------------
加权结果
-------------------------------
0 1 2 3 4 5 6 7 8 9
-------------------------------
5 5 5 3 5 5 5 6 8 5
-------------------------------
总结
经过了加权操作的quick-union可以明显的优化quick-union的劣势,可以将树的深度控制在log(n)级别,甚至更优,经过加权后的并查集又经历了更进一步的演化。
展望
加权的quick-union其实还没有达到最优化的处理,有没有什么办法可以让quick-union只有一层呢?从O(log(n)) 的时间复杂度升级为O(1)? 下一次让我们继续探索一下。