并查集
简介
并查集是一种简单的用途广泛的集合,是一种树形的数据结构,可以实现较快的元素合并和元素是否在某一集合的判断,应用很多,如求无向图的连通个数,带限制的作业排序,实现kruskar算法求解最小生成树等。 并查集时钟树形结构,但由于二叉树、红黑树、B数不一样,要求比较简单: 1、每一个元素都唯一的对应一个结点 2、每一组数据中的多个元素都在同一棵树中 3、一个组中的数据对应的树和另外一个组中的树之间没有任何联系 4、元素在树中并没有子父级关系的硬性要求 例如: 这是一组,里面有三个元素 单个元素也可以是一组 因为没有子父级关系,所以可以随便排:
(1)查两个数据是否在同一组可以转换为查看两个数据是否在同一棵树上,而判断两个数据是否在同一棵树上则是判断两个数据结点的根结点是否是同一个 (2)将两个组合并在一起主要是通过将其中一棵树的根结点的指针指向另一棵树的根结点。
主要方法介绍
1、构造方法实现说明: (1)初始情况下,每个元素都在一个独立的组中,所以在初始情况下,并查集中的数据默认分为N个组(传进N个数据就分成N组) (2)初始化数组eleAndGroup,把eleAndGroup数组的索引看做是每个结点存储的元素,把eleAndGroup数组每个索引处的值当做索引所在的分组(初始化情况下,i索引处存储的值就是i)
2、union(int p, int q)合并方法实现 (1)如果p和q已经在同一个分组中,则无需合并 (2)如果p和q不再同一个分组,则只需要将p元素所在组的左右的元素的组标识符改为q元素所在组的表示符 (3)每次合并后,分组的数量就会减少一个。
代码实现:
public class UF {
private int[] eleAndGroup;
private int count;
public UF(int N){
this.count = N;
this.eleAndGroup = new int[N];
for (int i = 0; i < eleAndGroup.length; i++) {
eleAndGroup[i] = i;
}
}
public int count(){
return count;
}
public int find(int p){
return eleAndGroup[p];
}
public boolean connected(int p, int q){
return find(p) == find(q);
}
public void union(int p, int q){
if(connected(p,q)){
return ;
}
int pGroup = find(p);
int qGroup = find(q);
for (int i = 0; i < eleAndGroup.length; i++) {
if(eleAndGroup[i] == pGroup){
eleAndGroup[i] = qGroup;
}
}
this.count--;
}
}
测试代码:
public class UF_test {
public static void main(String[] args) {
UF uf = new UF(5);
Scanner sc = new Scanner(System.in);
while(true){
System.out.println("请输入第一个要合并的元素:");
int p = sc.nextInt();
System.out.println("请输入第二个要合并的元素:");
int q = sc.nextInt();
if(uf.connected(p,q)){
System.out.println("元素" + p + "和元素" + q + "已经在同一组了");
continue;
}
uf.union(p,q);
System.out.println("当前并查集中还有" + uf.count() + "个分组");
}
}
}
结果:
请输入第一个要合并的元素: 1 请输入第二个要合并的元素: 0 当前并查集中还有4个分组 请输入第一个要合并的元素: 1 请输入第二个要合并的元素: 2 当前并查集中还有3个分组 请输入第一个要合并的元素: 1 请输入第二个要合并的元素: 2 元素1和元素2已经在同一组了
直到最后合并到只剩下一组。。。
算法优化
优化原因:union方法中使用for循环遍历了所有的元素,很明显,合并算法的时间复杂度是O(n^2),当N非常大的时候,它是不合适的。
优化过程:为了提升union算法的性能,需要重新设计find方法和union方法,并且要对eleAndGroup数组的含义重新设定: 1、仍然让eleAndGroup数组的索引作为某个结点的元素; 2、eleAndGroup[i]的值不再标识当前结点所在的分组标识,而是该结点的父结点
find方法优化: 1、判断当前元素p的父结点eleAndGroup[p]是不是索引值,是,则证明自己是根结点了 2、如果当前元素p的父结点不是自己,则让q= eleAndGroup[p],继续找父结点的父结点,直到找到根节点位置。
union方法优化 1、找到p元素所在树的根结点 2、找打p元素所在树的根结点 3、如果p和q已经在同一个树中,则无需合并 4、如果p和q不再同一个分组,则只需要将p元素所在树结点的父结点设置为q元素的根结点即可 5、分组数量减一
具体代码 find方法实现:
public int find(int p){
while(true){
if(p == eleAndGroup[p]){
return p;
}
p = eleAndGroup[p];
}
}
union方法实现:
public void union(int p, int q){
int pRoot = find(p);
int qRoot = find(q);
if(pRoot == qRoot){
return ;
}
eleAndGroup[pRoot] = qRoot;
this.count--;
}
可以发现,优化后的union方法中都没有了for循环,所以时间复杂度变为了O(1)。 但你会发现此时find方法中有添加了一个while(true)循环,而由此可能导致find方法的最坏情况下的时间复杂度是O(N),而所谓的最坏情况就是通过union合并所构成的树成线状,这种树的深度很大,如图: union调用了find方法后,最坏情况下union算法的时间复杂度仍为O(N^2)。这就是所谓的优化了还未完全优化嘛,哎。。。于是,我们要再度优化find方法,使其在遇到这类情况时,使得生成的树的深度尽可能的小,则达到了优化的最终效果。如图: 只要保证每次合并都能把小树合并到大树上,就能够压缩合并后的新树的路径。 最终代码如下:
public class UF_tree_last {
private int eleAndGroup[];
private int count;
private int[] sz;
public UF_tree_last(int N) {
this.count = N;
this.eleAndGroup = new int[N];
for (int i = 0; i < eleAndGroup.length; i++) {
this.eleAndGroup[i] = i;
}
this.sz = new int[N];
for (int i = 0; i < sz.length; i++) {
sz[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(true){
if(p == eleAndGroup[p]){
return p;
}
p = eleAndGroup[p];
}
}
public void union(int p, int q){
int pRoot = find(p);
int qRoot = find(q);
if(pRoot == qRoot){
return ;
}
if(sz[pRoot]<sz[qRoot]){
eleAndGroup[pRoot] = qRoot;
sz[qRoot] += sz[pRoot];
}else{
eleAndGroup[qRoot] = pRoot;
sz[pRoot] += sz[qRoot];
}
this.count--;
}
}
|