结构
并查集是一种树形的数据结构。它有以下几个特点:
1.每个元素都唯一的对应一个结点;
2.每一组数据中的多个元素都在同一颗树中;
3.一个组中的数据对应的树和另外一个组中的数据对应的树之间没有任何联系;
4.元素在树中并没有子父级关系的硬性要求;
作用
1.查询元素p和q是否在同一组中
2.合并元素p和q所在的组
构造方法
1.初始情况下,每个元素都在一个独立的分组中,所以,初始情况下,并查集中的数据默认分为N个组;
2.初始化数组eleAndGroup;
3.把eleAndGroup数组的索引看做是每个结点存储的元素,把eleAndGroup数组每个索引处的值看做是该结点所在的分组,那么初始化情况下,i索引处存储的值就是i。
代码实现
//记录节点元素和该元素所在分组
private int[] eleAndGroup;
//记录并查集中数据的分组个数
private int count;
//初始化并查集
public UF(int N){
//初始化分组的数量,默认情况下,有N个分组
this.count=N;
//初始化eleAndGroup数组
this.eleAndGroup=new int[N];
//初始化eleAndGroup中的元素及所在租的标识符,eleAndGroup数组的索引就是并查集中的元素,并让每个索引处的值等于该索引
for (int i = 0; i < eleAndGroup.length; i++) {
eleAndGroup[i]=i;
}
}
查找p元素所在的分组
public int find(int p){
return eleAndGroup[p];
}
把元素p和q所在的组合并
public void nuion(int p,int q){
//判断p,q是否已经在同一分组中
if (connected(p,q)){
return;
}
//找到p所在分组的标识符,
int pGroup = find(p);
//找到q所在的标识符
int qGroup = find(q);
//合并:让p所在组所有元素的标识符变为q元素的标识符
for (int i = 0; i < eleAndGroup.length; i++) {
if (eleAndGroup[i]==pGroup){
eleAndGroup[i]=qGroup;
}
}
//分组个数-1
this.count--;
}
完整代码
public class UF {
//记录节点元素和该元素所在分组
private int[] eleAndGroup;
//记录并查集中数据的分组个数
private int count;
//初始化并查集
public UF(int N){
//初始化分组的数量,默认情况下,有N个分组
this.count=N;
//初始化eleAndGroup数组
this.eleAndGroup=new int[N];
//初始化eleAndGroup中的元素及所在租的标识符,eleAndGroup数组的索引就是并查集中的元素,并让每个索引处的值等于该索引
for (int i = 0; i < eleAndGroup.length; i++) {
eleAndGroup[i]=i;
}
}
//获取当前并查集中有多少分组
public int count(){
return count;
}
//元素p所在分组的标识
public int find(int p){
return eleAndGroup[p];
}
//判断元素p和q是否在同一分组中
public boolean connected(int p,int q){
return eleAndGroup[p]==eleAndGroup[q];
}
//把p元素和q元素所在的分组合并
public void nuion(int p,int q){
//判断p,q是否已经在同一分组中
if (connected(p,q)){
return;
}
//找到p所在分组的标识符,
int pGroup = find(p);
//找到q所在的标识符
int qGroup = find(q);
//合并:让p所在组所有元素的标识符变为q元素的标识符
for (int i = 0; i < eleAndGroup.length; i++) {
if (eleAndGroup[i]==pGroup){
eleAndGroup[i]=qGroup;
}
}
//分组个数-1
this.count--;
}
}
如果我们想让计算机网络中任意两台计算机相连,则一共需调用N-1次union方法,而union方法中又使用了for循环,因此时间复杂度是O(n^2),这对于解决大型问题是不合适的,因此需要进行优化。
并查集的优化
我们首先要对eleAndGroup数组的含义进行重新定义。
1.我们仍然让eleAndGroup数组的索引作为某个结点的元素;
2.eleAndGroup的值不再是当前结点所在的分组标识,而是该结点的父结点;
查找元素p所在的分组
public int find(int p){
while (true){
if (p==eleAndGroup[p]){
return p;
}
p=eleAndGroup[p];
}
}
将元素p和q所在的组合并到一起
public void nuion(int p,int q){
//判断p,q是否已经在同一分组中
if (connected(p,q)){
return;
}
//找到p所在分组的标识符,
int pGroup = find(p);
//找到q所在的标识符
int qGroup = find(q);
//合并:让p所在组所有元素的标识符变为q元素的标识符
eleAndGroup[pGroup]=qGroup;
//分组个数-1
this.count--;
}
完整代码
public class UF_Tree {
//记录节点元素和该元素所在分组
private int[] eleAndGroup;
//记录并查集中数据的分组个数
private int count;
//初始化并查集
public UF_Tree(int N){
//初始化分组的数量,默认情况下,有N个分组
this.count=N;
//初始化eleAndGroup数组
this.eleAndGroup=new int[N];
//初始化eleAndGroup中的元素及所在租的标识符,eleAndGroup数组的索引就是并查集中的元素,并让每个索引处的值等于该索引
for (int i = 0; i < eleAndGroup.length; i++) {
eleAndGroup[i]=i;
}
}
//获取当前并查集中有多少分组
public int count(){
return count;
}
//元素p所在分组的标识
public int find(int p){
while (true){
if (p==eleAndGroup[p]){
return p;
}
p=eleAndGroup[p];
}
}
//判断元素p和q是否在同一分组中
public boolean connected(int p,int q){
return find(p)==find(q);
}
//把p元素和q元素所在的分组合并
public void nuion(int p,int q){
//判断p,q是否已经在同一分组中
if (connected(p,q)){
return;
}
//找到p所在分组的标识符,
int pGroup = find(p);
//找到q所在的标识符
int qGroup = find(q);
//合并:让p所在组所有元素的标识符变为q元素的标识符
eleAndGroup[pGroup]=qGroup;
//分组个数-1
this.count--;
}
}
从上面代码可以看出,union中已经没有for循环,时间复杂度变为O(N),但是find()的时间复杂度也改变了。原来时间复杂度是O(1),但是修改之后,在最坏情况下为O(N),因为union方法调用了find方法,因此时间复杂度仍为O(N^2)。因此我们需要进行路径压缩,避免最坏情况的出现。
最坏情况
路径压缩
以后每次合并时,都把小树添加到大树上,就能压缩合并后新树的路径,从而提高了find方法的效率。
为了达到这个目的,我们需要引入一个记录每个根结点对应的树中的元素的数组。
构造方法
//记录节点元素和该元素所在分组
private int[] eleAndGroup;
//记录并查集中数据的分组个数
private int count;
//存储每一个根结点对应的树中的元素
private int[] sz;
//初始化并查集
public UF_Tree_Weighted(int N){
//初始化分组的数量,默认情况下,有N个分组
this.count=N;
//初始化eleAndGroup数组
this.eleAndGroup=new int[N];
//初始化eleAndGroup中的元素及所在租的标识符,eleAndGroup数组的索引就是并查集中的元素,并让每个索引处的值等于该索引
for (int i = 0; i < eleAndGroup.length; i++) {
eleAndGroup[i]=i;
}
this.sz=new int[N];
for (int i = 0; i < sz.length; i++) {
sz[i]=1;
}
}
查找元素p所在的分组
public int find(int p){
while (true){
if (p==eleAndGroup[p]){
return p;
}
p=eleAndGroup[p];
}
}
把p元素和q元素所在的分组合并
public void nuion(int p,int q){
//判断p,q是否已经在同一分组中
if (connected(p,q)){
return;
}
//找到p所在分组的标识符,
int pGroup = find(p);
//找到q所在的标识符
int qGroup = find(q);
//判断qGroup的树大。还是pGroup树大,然后将较小的树合并到较大的树中
if (sz[pGroup]<sz[qGroup]){
eleAndGroup[pGroup]=qGroup;
sz[qGroup]+=sz[pGroup];
}else{
eleAndGroup[qGroup]=pGroup;
sz[pGroup]+=sz[qGroup];
}
//分组个数-1
this.count--;
}
完整代码
public class UF_Tree_Weighted {
//记录节点元素和该元素所在分组
private int[] eleAndGroup;
//记录并查集中数据的分组个数
private int count;
//存储每一个根结点对应的树中的元素
private int[] sz;
//初始化并查集
public UF_Tree_Weighted(int N){
//初始化分组的数量,默认情况下,有N个分组
this.count=N;
//初始化eleAndGroup数组
this.eleAndGroup=new int[N];
//初始化eleAndGroup中的元素及所在租的标识符,eleAndGroup数组的索引就是并查集中的元素,并让每个索引处的值等于该索引
for (int i = 0; i < eleAndGroup.length; i++) {
eleAndGroup[i]=i;
}
this.sz=new int[N];
for (int i = 0; i < sz.length; i++) {
sz[i]=1;
}
}
//获取当前并查集中有多少分组
public int count(){
return count;
}
//元素p所在分组的标识
public int find(int p){
while (true){
if (p==eleAndGroup[p]){
return p;
}
p=eleAndGroup[p];
}
}
//判断元素p和q是否在同一分组中
public boolean connected(int p,int q){
return find(p)==find(q);
}
//把p元素和q元素所在的分组合并
public void nuion(int p,int q){
//判断p,q是否已经在同一分组中
if (connected(p,q)){
return;
}
//找到p所在分组的标识符,
int pGroup = find(p);
//找到q所在的标识符
int qGroup = find(q);
//判断qGroup的树大。还是pGroup树大,然后将较小的树合并到较大的树中
if (sz[pGroup]<sz[qGroup]){
eleAndGroup[pGroup]=qGroup;
sz[qGroup]+=sz[pGroup];
}else{
eleAndGroup[qGroup]=pGroup;
sz[pGroup]+=sz[qGroup];
}
//分组个数-1
this.count--;
}
}
b站详细讲解网址:http://yun.itheima.com/course/639.html