并查集实现思想优化及代码实现
前言
并查集是一种树型的数据结构 ,并查集可以高效地进行如下操作:(1)查询元素p和元素q是否属于同一组(2)合并元素p和元素q所在的组。本文对并查集的的实现思路不断优化,提出3种并查集的实现思想,最后一种是并查集最优的实现思想,并对3种实现思想进行代码实现。
一、并查集实现思想一
1. 整体实现思想
-
不通过树的思想进行实现,仅仅依据数组进行实现 (本质就将每个数组作为一组,然后通过一个辅助数组分别将不同组标识为辅助数组中不同的值)
-
将所有组的元素用连续索引标识,如两组数据分别为a[3],b[4],则0,1,2代表第一组的数据,3,4,5,6代表第二组的数据,然后辅助数组assist[7]包括7个元素,
-
辅助数组中每个元素的值对应存储该索引标识的数组属于哪个分组,以a和b数组为例,assist初始值为:assist=[1,1,1,2,2,2,2],代表前3个元素为第一组,后四个元素为第二组。
-
初始情况下,每个元素单独为一个组
2. 查找分组和合并思想
- 由1可知,查找数据属于哪个分组,只需要在辅助数组中找到数据标识对应的值即可。
- 由1可知,实现数据的合并,即将两组数据合并为同一个分组,只需要将辅助数组中第二组数据对应的分组标识与第一组数据的分组标识修改一致即可。
3. 思想一代码实现
/**
* 该种并查集实现结构,如果想要将所有组合并为一个组,时间复杂度为O(N^2)。
* 因为,合并为一个组最坏情况下需要n-1次union操作,而每次union操作需要遍历n个元素。
*/
public class UF {
private int count;//分组个数(注意,不是元素个数)
private int[] assist;//辅助数组,记录结点元素和该元素所在分组的标识
public UF(int N)
{
this.count=N;//初始情况下,有几个元素就有几个分组,即一个元素一个组
assist=new int[N]; //辅助数组对应存储所有元素的分组
for (int i = 0; i < N; i++) {
assist[i]=i;
}
}
//获取当前并查集中的数据有多少个分组
public int count()
{
return count;
}
//判断并查集中索引为p的元素和索引为q的元素是否在同一分组中
public boolean connected(int p,int q)
{
return assist[p]==assist[q];//判断索引为p的元素和索引为q的元素,在辅助数组中的值是否一样即可
}
//索引为p的元素所在分组的标识符
public int find(int p)
{
return assist[p];
}
//把索引为p的元素所在分组和索引为q的元素所在分组合并
public void union(int p,int q)
{
// 如果索引为p的元素和索引为q的元素本身在一个分组,则结束
if (connected(p,q))
return;
//1.找到p所在的分组
int pGroup = find(p);
//2.找到q所在的分组
int qGroup = find(q);
//3.让索引p所在的分组标识,修改为索引q所在分组的标识
for (int i = 0; i < assist.length; i++) {
if (assist[i]==pGroup) {
assist[i]= qGroup;
}
}
//4.分组个数-1
this.count--;
}
二、并查集实现思想二
1. 整体思想二
通过树的思想进行实现,此时,仍然通过一个辅助数组进行实现,辅助数据的索引仍然代表不同的元素,但辅助数组中的值不再代表元素所对应的分组标识,而是代表元素所在树的父节点。即通过根节点是否相同判断是否在同一颗树,如果在同一颗树,则代码在同一组
2. 查找分组和合并思想
- 由1可知,查找当前元素p所在分组思路如下:
1.判断当前元素p的父结点assist[p]是不是自己,如果是自己则证明已经是根结点了;
2.如果当前元素p的父结点不是自己,则让p=assist[p],继续找父结点的父结点,直到找到根结点为止;
- 由1可知,合并两颗树的思路如下:
1.找到p元素所在树的根结点
2.找到q元素所在树的根结点
3.如果p和q已经在同一个树中,则无需合并;
4.如果p和q不在同一个分组,则只需要将p元素所在树根结点的父结点设置为q元素的根结点即可;
5.分组数量-1
3. 思想二代码实现
/**
* 该种并查集实现结构,如果想要将所有组合并为一个组,时间复杂度也是O(N^2)。
* 因为,合并为一个组最坏情况下需要n-1次union操作,而每次union操作的时间复杂度虽然为O(1),
* 但是union方法中的find操作,最坏情况下可能需要n次,即时间复杂度为O(N),则想要将所有组合并为一个组的时间复杂度仍然为O(N^2)。
*/
public class UF_Tree {
private int count;//分组(树)个数(注意,不是元素个数)
private int[] assist;//辅助数组,记录元素所在树的根节点(通过根节点标识一颗树)
public UF_Tree(int N)
{
this.count=N;//初始情况下,有几个元素就有几个分组,即一个元素一个组
assist=new int[N]; //辅助数组对应存储所有元素的所在树的根节点标识,初始情况下,每个元素本身就是根节点,将辅助数组索引对应的值赋值为该索引
for (int i = 0; i < N; i++) {
assist[i]=i;
}
}
//获取当前并查集中的数据有多少个分组(多少棵树)
public int count()
{
return count;
}
//判断并查集中索引为p的结点和索引为q的结点是否在同一分组(同一颗树)中
public boolean connected(int p,int q)
{
return find(p)==find(q);//判断索引为p的元素和索引为q的元素,在辅助数组中的值是否一样即可
}
//索引为p的结点所在分组(树)的标识符
public int find(int p)
{
//查找p索引所在树的根节点的值,是否与p索引相同,如果相同则查找到,
//否则,继续查找p索引对应的值所在树的根节点,循环往复,直至找到索引与其值相同,则该值就是p索引所对应结点所在的根节点的标识
while (true){
if (assist[p]==p)
return p;
p=assist[p];
}
}
//把索引为p的元素所在分组和索引为q的元素所在分组合并
public void union(int p,int q)
{
//1.找到索引p对应结点所在树的根节点标识
int pRootID = find(p);
//2.找到索引q对应结点所在树的根节点标识
int qRootID = find(q);
//3.如果p和q在同一颗树,则结束
if (pRootID==qRootID)
return;
//4.将p所在树的根节点的标识修改为q所在树的根节点的标识,使得p所在的树和q所在的树合并为同一颗树,合并后树的根节点的标识为q所在树根节点的标识
assist[pRootID]=qRootID;
//5.分组(树)个数-1
count--;
}
三、并查集实现思想三
1.整体思想三
思想二利用树实现并查集存在一个问题,那就是查找的时间复杂度可能太长,导致并查集完成所有组合并为一组的时间复杂度仍然为O(N2)。那么思想三就是考虑如何缩短查找的时间。而查找的时间之所以会长,是因为子树的高度太高,那么缩短子树的高度,也就达到了查找时间缩短的目的。如果每次将两颗子树合并为一个树时 ,把高度低的树(小树)合并到高度高的树(大树)上可以有效降低树的高度。
2. 查找分组和合并思想
相比思想二,区别点就是在合并的时候判断两颗树哪个小,然后把小树合并到大树上。为了判断树的大小就需要记录每颗树的结点个数,通过另一个辅助数组记录每棵树的结点个数。
3. 思想三代码实现
/**
* 该种并查集实现结构,如果想要将所有组合并为一个组,时间复杂度也是O(N^2)。
* 因为,合并为一个组最坏情况下需要n-1次union操作,而每次union操作的时间复杂度虽然为O(1),
* 但是union方法中的find操作,最坏情况下可能需要n次,即时间复杂度为O(N),则想要将所有组合并为一个组的时间复杂度仍然为O(N^2)。
*/
public class UF_Tree_Weighted {
private int count;//分组(树)个数(注意,不是元素个数)
private int[] assist;//辅助数组,记录元素所在树的根节点(通过根节点标识一颗树)
private int[] nodeSize; //记录每棵树的结点个数
public UF_Tree_Weighted(int N)
{
this.count=N;//初始情况下,有几个元素就有几个分组,即一个元素一个组
assist=new int[N]; //辅助数组对应存储所有元素的所在树的根节点标识,初始情况下,每个元素本身就是根节点,将辅助数组索引对应的值赋值为该索引
for (int i = 0; i < N; i++) {
assist[i]=i;
}
nodeSize=new int[N];//初始化时,每棵树是一个数据,因此每棵树中结点的个数为1
for (int i = 0; i < nodeSize.length; i++) {
nodeSize[i]=1;
}
}
//获取当前并查集中的数据有多少个分组(多少棵树)
public int count()
{
return count;
}
//判断并查集中索引为p的结点和索引为q的结点是否在同一分组(同一颗树)中
public boolean connected(int p,int q)
{
return find(p)==find(q);//判断索引为p的元素和索引为q的元素,在辅助数组中的值是否一样即可
}
//索引为p的结点所在分组(树)的标识符
public int find(int p)
{
//查找p索引所在树的根节点的值,是否与p索引相同,如果相同则查找到,
//否则,继续查找p索引对应的值所在树的根节点,循环往复,直至找到索引与其值相同,则该值就是p索引所对应结点所在的根节点的标识
while (true){
if (assist[p]==p)
return p;
p=assist[p];
}
}
//把索引为p的元素所在分组和索引为q的元素所在分组合并
public void union(int p,int q)
{
//1.找到索引p对应结点所在树的根节点标识
int pRootID = find(p);
//2.找到索引q对应结点所在树的根节点标识
int qRootID = find(q);
//3.如果p和q在同一颗树,则结束
if (pRootID==qRootID)
return;
//4.将p所在树的根节点的标识修改为q所在树的根节点的标识,使得p所在的树和q所在的树合并为同一颗树,合并后树的根节点的标识为q所在树根节点的标识
// 此时不是简单粗暴的随便合并两棵树,而是通过判断两棵树的大小,将小树合并到大树上,达到合并后树的高度尽量不增高的目的,减少find方法所需时间。
if (nodeSize[pRootID]<nodeSize[qRootID])
{
assist[pRootID]=qRootID;
nodeSize[qRootID]+=nodeSize[pRootID];
}
else
{
assist[qRootID]=pRootID;
nodeSize[pRootID]+=nodeSize[qRootID];
}
//5.分组(树)个数-1
count--;
}