quick-union
quick-find是为了快速找到树的标识,quick-union顾名思义就是为了快速合并。
quick-find算法特点
- find时间复杂度O(1)
- union时间复杂度是O(n)
quick-union算法特点
- find最差情况下时间复杂度O(n),平均O(log(n))
- union时间复杂度是O(1)
两者算法时间复杂度比较
| quick-find | quick-union | |
|---|---|---|
| find | O(1) | 最差情况O(n),平均情况O(log(n)) |
| union | O(n) | O(1) |
之所以quick-union可已实现O(1)时间复杂度的合并操作,用两个分量a,b来解释,那是因为在连通过程中直接将b分量的标识指向了a,那么所有标识为b的分量都会随着b转移到a分量所在的树中。
具体实现步骤
- 定义QuickUnionUnionFind类继承UnionFind
- 实现find方法,找到数组下标与值相同的元素即为该树的标识元素
- 实现union方法,合并p,q时,将q的标识指向p的下标。
package com.zt;
public class QuickUnionUnionFind extends UnionFind {
public QuickUnionUnionFind(int n) {
super(n);
}
@Override
void union(int p, int q) {
// 找到p的标识
int pId = find(p);
// 找到q的标识
int qId = find(q);
// 如果两个标识相等,代表已经合并
if (pId == qId) return;
// 如果不相等,直接让分量q的标识指向p
id[qId] = pId;
// 每次合并操作,会降低一个不同分量组
count--;
}
@Override
int find(int p) {
// 沿着标识路径一路寻找,直到找到树的标识
while (p != id[p]) p = id[p];
return p;
}
}
quick-find与quick-union的比较
为了说明算法的优劣,需要用实际的数据来进行比较说明,首先准备了一个构造数据的方法,该方法可以为我们动态生成一个连通集。用二维数组来表示,每一行有两个分量,代表两个分量存在连通关系,不同行代表不同的连通关系。
public static int[][] setUpTestData10(int n, int digit) {
int[][] res = new int[n][2];
for (int i = 0; i < res.length; i++) {
Random random = new Random();
res[i] = new int[]{random.nextInt(digit), random.nextInt(digit)};
}
return res;
}
小数据量测试
首先准备测试用代码如下。
package com.zt;
import org.omg.CORBA.OBJ_ADAPTER;
import java.util.Random;
public class UnionFindHelper {
public static void main(String[] args) {
int num = 10;
int[][] res = setUpTestData(num, num);
QuickFindUnionFind qfuf = new QuickFindUnionFind(num);
QuickUnionUnionFind quuf = new QuickUnionUnionFind(num);
execute(res,qfuf);
System.out.println("****************************");
System.out.println("****************************");
System.out.println("****************************");
execute(res,quuf);
}
public static void execute(int[][] res, UnionFind uf) {
soutuf(uf);
soutLine("行数", "p", "q", "耗时");
long timeStart = System.currentTimeMillis();
for (int i = 0; i < res.length; i++) {
int p = res[i][0];
int q = res[i][1];
long timeStartOne = System.currentTimeMillis();
uf.union(p, q);
long timeEndOne = System.currentTimeMillis();
soutLine(i, p, q, timeEndOne - timeStartOne);
}
long timeEnd = System.currentTimeMillis();
System.out.println();
soutuf(uf);
System.out.println("总耗时时间" + (timeEnd - timeStart));
}
public static void soutuf(UnionFind uf) {
System.out.println("-------------------------------");
for (int i = 0; i < uf.id.length; i++) {
System.out.printf("%5d", i);
}
System.out.println();
System.out.println("-------------------------------");
for (int i = 0; i < uf.id.length; i++) {
System.out.printf("%5d", uf.id[i]);
}
System.out.println();
System.out.println("-------------------------------");
}
public static void soutLine(Object... args) {
String format = "";
for (Object arg : args) {
format = format + "%5s";
}
System.out.printf(format, args);
System.out.println();
}
public static int[][] setUpTestData(int n, int digit) {
int[][] res = new int[n][2];
for (int i = 0; i < res.length; i++) {
Random random = new Random();
res[i] = new int[]{random.nextInt(digit), random.nextInt(digit)};
}
return res;
}
}
构造一个十个元素的数据,初始时数组每个值都指向自己的下标
-------------------------------
0 1 2 3 4 5 6 7 8 9
-------------------------------
0 1 2 3 4 5 6 7 8 9
quick-find执行结果
-------------------------------
0 1 2 3 4 5 6 7 8 9
-------------------------------
4 4 4 3 4 3 6 4 4 4
-------------------------------
总耗时时间2
quick-union执行结果
-------------------------------
0 1 2 3 4 5 6 7 8 9
-------------------------------
4 4 7 3 4 3 6 0 1 0
-------------------------------
总耗时时间1
小数据量分析
从小数据量可以看出,quick-find的并查集构建结果,存在大量相同的标识,直观上可以通过不同的值就看出来并查集这个森林中有多少颗树,而在quick-union中,标识较为散乱,直观上看不出树的个数;全部数据处理的耗时上,两者只相差了1毫秒,看起来差别不大。接下来数据量进行扩大试验一次。
去掉日志打印代码后,数据统计如下
| 10 | 10^2 | 10^3 | 10^4 | 10^5 | 10^6 | |
|---|---|---|---|---|---|---|
| quick-find | 0 | 2 | 5 | 48 | 5379 | 超时 |
| quick-union | 0 | 0 | 1 | 6 | 1531 | 超时 |
数据增长方式是采用同步增长,指数增加一级,数据量增加一级,换个方式,在分量总数保持在10^4情况下,增加数据量,观测两种算法表现。
| 1倍 | 2倍 | 3倍 | 10^4 | 10^5 | 10^6 | |
|---|---|---|---|---|---|---|
| quick-find | 48 | 63 | 66 | 67 | 71 | 68 |
| quick-union | 6 | 59 | 90 | 138 | 172 | 220 |
可以观察到,在分量总数不变的情况下,随着连通数据集的增多,quick-find保持在一个稳定的时间内,而quick-union会随之不断增长,内在的原因是什么呢?
答案在这里
因为每次union操作都会进行两次查找,在quick-find算法中,该时间复杂度是O(1),在quick-union算法中,该复杂度最差情况下会达到O(n)
##总结
经过数据的验证与测试,我们可以总结道一下两点
- 随着分量总数增加,quick-union表现比quick-find要好,因为分量总量即id变多导致quick-find的union操作时间变高。
- 随着连通数据总量增加,quick-find相较quick-union表现要好,是因为连通数据量变大,导致quick-union的find操作耗时增长。
- 两者在处理10^6量级的数据时,都表现超时。
展望
总结过后,两种算法各有又是劣势,但是相对来说,在分量总数增多的情况下quick-union是线性增长,而quick-find是指数级增长,相比较而言,quick-union更升级了一点,那么有没有办法融合两种算法的优势呢?下一节继续探索并查集的升级路线。