并查集是一种并查集是一种树型的数据结构,用于处理一些不交集的合并及查询问题。在一些有N个元素的集合应用问题中,我们通常在开始时让每个元素构成一个单元素的集合,然后按一定顺序将同一组的元素所在的集合合并,期间要反复查询一个元素在哪个集合中,并查集就是完成这种操作的数据结构。
并查集使用树来表示集合。集合中的每一个元素由一个节点表示,处于同一个集合中的元素共同构成一棵树;每一棵树的根节点作为该集合的代表,我们在查找一个元素所在的集合时就是查找这个集合的代表,即表示这个集合的树的根节点。
上图中有两个树,根节点由红色节点表示;它们分别代表2个集合,其中第一个集合为{1, 2, 3, 4},代表元素为1;第二个集合为{5, 6, 7},代表元素为5。
树的节点表示集合中的元素,节点的指针指向其父节点,根节点不指向任何节点;我们在查找一个元素所在集合时,只要顺着每个节点的父节点一直向上查找,就可以找到这棵树的根节点,即该集合的代表集合。
我们使用一个father
数组来存储每个节点的父节点,
f
a
t
h
e
r
[
i
]
father[i]
father[i]代表节点i的父节点编号;我们规定,如果节点i是根节点,则father[i]
的值为-1
。
创建并查集
void init(int m) //初始化一个并查集,其中包含m个单元素集合
{
for(int i = 1; i <= m; i++)
father[i] = -1; //将每个节点的fahter值设为-1,即每个节点都是所在集合的根节点
}
基本操作
查询集合的代表
如前言所述,我们只需要顺着每个节点的父节点逐步向上查找,就可以找到集合的代表元素。
代码如下,有递归和非递归两个版本。
//递归
int find(int x)
{
if(father[x] != -1) //如果当前节点不是根节点
return find(x); //继续向上查找
return x; //否则返回根节点编号
}
//非递归
int find(int x)
{
if(father[x] != -1) //同上
x = father[x];
return x;
}
合并集合
将元素 x x x和元素 y y y所在的集合合并,要求两个元素不在同一个集合,如果处在同一个集合则不合并。将一个集合的树的树根指向另外一个集合的树的树根即可。
代码如下:
void union(int x, int y)
{
x = find(x); //将x替换为x所在树的根节点
y = find(y); //同理
if(x == y) //如果两个元素在同一个集合
return; //不合并,直接返回
father[y] = x; //将y集合合并到x集合中
}
并查集的优化
并查集做单次查询的时间复杂度为 O ( 树 的 高 度 ) O(树的高度) O(树的高度),如果树过高的话查询效率将会降低,最坏的情况下的时间复杂度能达到 O ( n ) O(n) O(n),所以我们可以对其做一些优化。
路径压缩
路径压缩的思路很简单:每次查找时令查找路径上的每个父节点都指向根节点,如图所示:利用这种方法,可以缩短查找路径,从而减少查找时间。
代码如下,分为递归和非递归两个版本。
//递归
int find(int x)
{
if(x != father[x])
return father[x] = find(father[x]); //向上查找+路径压缩
else
return x;
}
//非递归
int find(int x)
{
int p = x, t;
while(father[p] != -1) // 查找根节点
p = father[p];
while(x != p) //顺着查找路径进行路径压缩
{
t = father[x];
father[x] = p;
x = t;
}
}
按秩合并
故名思意,给每一个根节点定一个秩(用rank
数组存储,rank[i]
代表节点i
的秩),合并集合的时候根节点的秩小的集合合并到秩大的集合,意图是使得树不至于过深以优化查询复杂度。
较好的一个做法是将当前节点子树的的深度作为树的秩,如果是根节点,秩即为树的深度。合并集合时将深度低的树合并到深度高的树。
这样能将树的深度保持在 l o g 2 n log _2 n log2n以内,但是不能和路径压缩共用,原因请读者自行思考。
优化后的代码如下:
void init(int m)
{
for(int i = 1; i <= m; i++)
{
father[i] = -1;
rank[i] = 1; //初始时每个单元素集合的树的深度都为1
}
}
int find(int x); //find函数没有变化,此处省略
void union(int x, int y)
{
x = find(x);
y = find(y);
if(x == y)
return;
if(rank[x] > rank[y]) //如果x所在树比y所在树深
{
father[y] = x; //y所在集合合并到x所在集合
}
else //如果y所在树比x所在树深或者两棵树一样深
{
father[x] = y
rank[y] = max{rank[y], rank[x] + 1} //如果两棵树深度相同,y所在树的深度+1
}
}
还有一种按秩合并方式是将每棵树的节点数量作为这棵树的秩来进行合并,这种方法不能保证每次合并都是最优解,但是可以和路径压缩共用。
代码如下所示,其中不使用rank
数组,而是将根节点的father[i]
设置为树的节点数量的相反数。例:若father[i] = -8
,则i
节点是所在树的根节点,且这棵树有8个节点。
void init(int m)
{
for(int i = 1; i <= m; i++)
{
father[i] = -1; //初始时每个节点都是所在树的根节点且每棵树的节点数量都是-1
}
}
int find(int x)
{
if(father[x] < 0) //如果x是根节点
return x;
return father[x] = find(father[x]);
}
void union(int x, int y)
{
x = find(x);
y = find(y);
if(x == y)
return;
if(father[x] < father[y]) //如果x所在树节点数大于y所在树节点数
{
father[x] += father[y] //更新x所在树节点个数
father[y] = x; //y所在集合合并到x所在集合
}
else //如果y所在树节点数大于等于x所在树节点数
{
father[y] += father[x];
father[x] = y;
}
}