数据结构学笔记之并查集
由于博客未满15篇所以无法开设专栏,以后会把数据结构方面的知识整合成一个专栏。
目录
并查集简介
并查集,在一些有N个元素的集合应用问题中,我们通常是在开始时让每个元素构成一个单元素的集合,然后按一定顺序将属于同一组的元素所在的集合合并,其间要反复查找一个元素在哪个集合中。这一类问题近几年来反复出现在信息学的国际国内赛题中,其特点是看似并不复杂,但数据量极大,若用正常的数据结构来描述的话,往往在空间上过大,计算机无法承受;即使在空间上勉强通过,运行的时间复杂度也极高,根本就不可能在比赛规定的运行时间(1~3秒)内计算出试题需要的结果,只能用并查集来描述。
并查集解决的问题:
网络中节点的连接问题。
数学中集合的问题。
并查集主要支持两个动作:
union(p,q) 并,将两个元素连接起来
isConnected(p,q) 查,查两个元素是否在同一个组中
并查集中最关键的函数:
find(p) 查找p元素所属的集合
- 并查集两个关键动作的时间复杂度完全依赖于find函数的效率
并查集的渐进优化方案
UF1
#include<iostream>
#include<cassert>
using namespace std;
namespace UF1{
class UnionFind {
private:
int* id;
int count;
public:
UnionFind(int n){
count=n;
id=new int[n];
for(int i=0;i<n;i++){
id[i]=i;
}
}
~UnionFind(){
delete [] id;
}
int find(int q){
assert(q<count&&q>=0);
return id[q];
}
bool isConnected(int p,int q){
return find(p)==find(q);
}
void UnionElement(int q,int p){
int IDP=find(p);
int IDQ=find(q);
for(int i=0;i<count;i++){
if(find(i)==IDP){
id[i]=IDQ;
}
}
}
};
}
UF2
#include<iostream>
#include<cassert>
using namespace std;
namespace UF2{
class UnionFind {
private:
int* parent;//元素i的父节点
int count;
public:
UnionFind(int n){
count=n;
parent=new int[n];
for(int i=0;i<n;i++){
parent[i]=i;
}
}
~UnionFind(){
delete [] parent;
}
int find(int q){
assert(q>=0&&q<count);
while(q!=parent[q]){
q=parent[q];
}
return q;
}
bool isConnected(int p,int q){
return find(p)==find(q);
}
void UnionElement(int p,int q){
int qRoot=find(q);
int pRoot=find(p);
if(qRoot==pRoot)
return;
parent[qRoot]=pRoot;
}
};
}
UF3
#include<iostream>
#include<cassert>
using namespace std;
namespace UF3{
class UnionFind {
private:
int* parent;//元素i的父节点
int* size; //以i为根的集合中元素的个数
int count;
public:
UnionFind(int n){
count=n;
parent=new int[n];
size=new int[n];
for(int i=0;i<n;i++){
parent[i]=i;
size[i]=1;
}
}
~UnionFind(){
delete [] parent;
delete [] size;
}
int find(int q){
assert(q>=0&&q<count);
while(q!=parent[q]){
q=parent[q];
}
return q;
}
bool isConnected(int p,int q){
return find(p)==find(q);
}
void UnionElement(int p,int q){
int qRoot=find(q);
int pRoot=find(p);
if(qRoot==pRoot)
return;
if(size[pRoot]<size[qRoot]){
parent[pRoot]=qRoot;
size[qRoot]+=size[pRoot];
}
else{
parent[qRoot]=pRoot;
size[pRoot]+=size[qRoot];
}
}
};
}
UF4
#include<iostream>
#include<cassert>
using namespace std;
namespace UF4{
class UnionFind {
private:
int* parent;//元素i的父节点
int* rank; //以i为根节点的集合的高度
int count;
public:
UnionFind(int n){
count=n;
parent=new int[n];
rank=new int[n];
for(int i=0;i<n;i++){
parent[i]=i;
rank[i]=1;
}
}
~UnionFind(){
delete [] parent;
delete [] rank;
}
int find(int q){
assert(q>=0&&q<count);
while(q!=parent[q]){
q=parent[q];
}
return q;
}
bool isConnected(int p,int q){
return find(p)==find(q);
}
void UnionElement(int p,int q){
int qRoot=find(q);
int pRoot=find(p);
if(qRoot==pRoot)
return;
if(rank[pRoot]<rank[qRoot]){
parent[pRoot]=qRoot;
}
else if(rank[pRoot]>rank[qRoot]){
parent[qRoot]=pRoot;
}
else{
parent[pRoot]=qRoot;
rank[qRoot]++;
}
}
};
}
UF5
#include<iostream>
#include<cassert>
using namespace std;
namespace UF5{
class UnionFind {
private:
int* parent;//元素i的父节点
int* rank; //以i为根节点的集合的高度
int count;
public:
UnionFind(int n){
count=n;
parent=new int[n];
rank=new int[n];
for(int i=0;i<n;i++){
parent[i]=i;
rank[i]=1;
}
}
~UnionFind(){
delete [] parent;
delete [] rank;
}
int find(int q){
assert(q>=0&&q<count);
while(q!=parent[q]){
q=parent[parent[q]];
}
return q;
// if(q!=parent[q]){
// parent[q]=find(parent[q]);
// }
// return parent[q];
}
bool isConnected(int p,int q){
return find(p)==find(q);
}
void UnionElement(int p,int q){
int qRoot=find(q);
int pRoot=find(p);
if(qRoot==pRoot)
return;
if(rank[pRoot]<rank[qRoot]){
parent[pRoot]=qRoot;
}
else if(rank[pRoot]>rank[qRoot]){
parent[qRoot]=pRoot;
}
else{
parent[pRoot]=qRoot;
rank[qRoot]++;
}
}
};
}
总结:
UF1:UF1的union操作时间复杂度为O(n),所以对大量数据进行并操作时时间复杂度就变成了O(n^2)级别,这是需要进行优化的地方。
UF2:UF2改变了给数组中每个元素设置一个标志位的集合表示方法,它给每个元素设置一个parent属性,指向该节点的父节点,这样集合就变成了一个树形结构,极大地提升了查找效率。UF2相对UF1的提升是巨大的,查询时间减少了80%到90%。但是UF2在进行union操作时随机把一个集合的根节点指向另一个集合的根节点,这就会导致有的集合层数过多,查询用时过长。
UF3:在UF3中我们比较两个集合的元素个数,把元素少的集合归入元素多的集合以解决集合层数过多的问题。经过这个优化之后并查集才有能力处理百万级别的数组数据,相比较UF2,速度提高了两个数量级以上,可谓是质的飞越。
UF4:但是,元素个数与集合层数有时候并没有线性相关,我们可以直接根据集合的层数处理union的决策,虽然实际程序中多了一层判断逻辑,在有些情况下可能会比UF3慢一些,但UF4可以避免出现极端情况下的问题,综合来说更稳定。
UF5:路径压缩
- 方案一:在find过程中,我们实际已经遍历了集合的一个分支,我们应该尽量榨取遍历过程中所可以创造的价值,所以我们可以想办法在遍历过程中尝试优化集合的结构。如果节点i的父节点不为根节点,我们就让节点i的parent指向父节点的父节点。在一百万和一千万数量级速度提高20%-30%左右。
- 方案二:使一个寻找路径上的所有节点的父节点都指向根节点。
实现方法是利用递归找到集合的根节点然后返回并赋值给每个节点的parent。在一百万到一千万数量级速度可以提高30%-40%左右,并且随着数据的增多会逐渐拉大与方案一的差距。
- 并查集的操作,时间复杂度近乎是O(1)。