题目描述
给定一个没有重复值的整形数组arr,初始时认为arr中每一个数各自都是一个单独的集合。请设计一种叫UnionFind的结构,并提供以下两个操作。
- boolean isSameSet(int a, int b): 查询a和b这两个数是否属于一个集合
- void union(int a, int b): 把a所在的集合与b所在的集合合并在一起,原本两个集合各自的元素以后都算作同一个集合
[要求]
如果调用isSameSet和union的总次数逼近或超过O(N),请做到单次调用isSameSet或union方法的平均时间复杂度为O(1)
初始化
按照树形结构给每个数设置自己的父亲 可以用map或者数组来表示
比如{1, 2}, {3}, {4}, {5}, {6} => [1, 1, 3, 4, 5, 6],数组的下标表示数 值表示对应的父节点
如果一个数没有父节点,特别地设置该数的父节点为自身
数组用parent[]来表示
查找
要查找一个数所属的集合(通过祖先来表示) 可以递归向上找到祖先结点(父节点等于自身的)
public int find(int x) {
if (parent[x] != x) {
return find(parent[x]);
}
return x;
}
路径压缩
上面这种方法就是单纯的树结构,但是这种方法在查找的时候需要逐层向上查询,但是实际上想知道数x的祖先是多少并不需要知道x的父亲是多少,类似给叶子节点加上一个指向根节点的指针的思想,直接将x的父亲设置为x的祖先
public int find(x) {
if (parent[x] != x) {
// 构建的同时做了压缩
parent[x] = find(parent[x]);
}
return parent[x];
}
合并
合并两个数所在的集合不需要考虑合并的方法,只要合并后两个数能够找到同一个祖先即可,再根据路径压缩的思想 直接把其中一个的祖先设置为另一个祖先的儿子
public void union(int x, int y) {
int f1 = find(x);
int f2 = find(y);
// 在同一个集合内 不需要合并
if (f1 == f2) {
return;
}
parent[f1] = f2;
}
启发式合并
上面的合并是两者间随意合并,现有树t1和t2有以下两种情况,合并后的树为t3
- t1合并到t2
- t2合并到t1
假设t1的高度比t2的高度高
那么情况1相比于情况2合并后的树t3的高度会增加,也就会增大下次查找的路径长度
因此可以根据树的高度 选择较小的合并到较大的树上
因此在初始化的时候引入新的数组rank[]用来表示每棵树的高度,这种方法也称按秩合并
public void union(int x, int y) {
int f1 = find(x);
int f2 = find(y);
if (f1 == f2) {
return;
}
int r1 = rank[f1];
int r2 = rank[f2];
if (r1 > r2) {
parent[f2] = f1;
} else if (r1 == r2) {
parent[f2] = f1;
rank[f1]++;
} else {
parent[f1] = f2;
}
}
也可以换一个标准,按照树的结点数作为评估标准,此时rank[]表示每棵树的结点个数
public void union(int x, int y) {
int f1 = find(x);
int f2 = find(y);
if (f1 == f2) {
return;
}
int r1 = rank[f1];
int r2 = rank[f2];
if (r1 > r2) {
parent[r2] = r1;
rank[f1] += r2;
} else {
parent[r1] = r2;
rank[f2] += r1;
}
}
时间复杂度
使用路径压缩和启发式合并之后,并查集的每个操作平均时间为 O ( α ( n ) ) O(\alpha(n)) O(α(n)),只说下结论可以看成常数时间
空间复杂度
两个数组,复杂度 O ( n ) O(n) O(n)