一、并查集 Union Find
1.不一样的树形结构
2.主要解决连接问题 Connectvity Problem
2.1 网络中结点间的连接状态 网络是个抽象概念,用户之间形成的网络
2.2 数学类中的集合类实现
2.3 比路径问题要回答的问题少,连接问题只用判断是否相连,路径问题还要求出路径
3. 对于一组数据,主要支持两个动作
3.1 union(p,q) 两个元素合并连接
3.2 find§
4.用来回答一个问题
4.1 isConnected(p,q) 是否连接
5. 并查集的基本数据表示
5.1 用数组表示每个元素的连接关系,称为id组,id号相同的为一个组,合并两个组就是把id号改成一样的
总结:其实通俗来看,并查集主要是用来在大数据的时候,判断两个节点是否连接,即例如在100000个人中判断任意两个人之间是否有关系。
二、并查集的实现以及优化
1.并查集的初步实现
由下面代码可知,私有成员变量主要有两个,count表示元素总数,id数组表示每一个元素是哪一个集合。构造函数中初始化id数组是其本身,公有类主要是三种方法,find查找某一个元素是哪一个类别号,isConnected判断两个元素是否相连,即判断两个元素的id号是否相同,unionElements用来合并两个元素,通过改变其中某一个类的类别号进行合并。
namespace UF1{
class UnionFind {
private:
int count;
int* id;
public:
UnionFind(int n) {
this->count = n;
id = new int[n];
for (int i = 0; i < n; i++)
id[i] = i;
}
~UnionFind() {
delete[] id;
}
int find(int p) {
assert(p >= 0 && p < count);
return id[p];
}
bool isConnected(int p, int q) {
return find(p) == find(q);
}
void unionElements(int p, int q) {
int pId = find(p);
int qId = find(q);
if (pId == qId)
return;
else {
for (int i = 0; i < count; i++) {
if (id[i] == pId)
id[i] = qId;
}
}
}
void showUnion() {
for (int i = 0; i < count; i++) {
cout <<i<<": "<< id[i] << endl;
}
}
};
}
2.优化1,对于每一次查找id号都要遍历元素,这里采用了一种链式存储的方式,采用了parent数组,用于存放某一个节点的父亲节点。
namespace UF2 {
class UnionFind {
private:
int count;
int* parent;
public:
UnionFind(int n) {
this->count = n;
parent = new int[n];
for (int i = 0; i < count; i++)
parent[i] = i;
}
~UnionFind() {
delete[] parent;
}
int find(int p) {
assert(p >= 0 && p < count);
while (p != parent[p]) {
p = parent[p];
}
return p;
}
bool isConnected(int p, int q) {
return find(p) == find(q);
}
void unionElements(int p, int q) {
int pRoot = find(p);
int qRoot = find(q);
if (pRoot == qRoot)
return;
else
parent[pRoot] = qRoot;
}
};
3.优化2:在优化1的基础上,增加一个判断以i为根的底下的元素个数,在合并两个集合的时候,通过把元素个数少的增加到元素个数多的集合中,节约查找时间。
namespace UF3 {
class UnionFind {
private:
int count;
int* parent;
int* size;
public:
UnionFind(int n) {
this->count = n;
parent = new int[n];
size = new int[n];
for (int i = 0; i < count; i++) {
parent[i] = i;
size[i] = 1;
}
}
~UnionFind() {
delete[] parent;
delete[] size;
}
int find(int p) {
assert(p >= 0 && p < count);
while (p != parent[p]) {
p = parent[p];
}
return p;
}
bool isConnected(int p, int q) {
return find(p) == find(q);
}
void unionElements(int p, int q) {
assert(p >= 0 && p < count);
assert(p >= 0 && p < count);
int pRoot = find(p);
int qRoot = find(q);
if (size[pRoot] > size[qRoot]) {
parent[qRoot] = pRoot;
size[pRoot] += size[qRoot];
}
else {
parent[pRoot] = qRoot;
size[qRoot] += size[pRoot];
}
}
};
}
4.优化3:在优化1的基础上,再增加一个rank数组表示以i为根的层数,在合并两个集合的时候,通过把层数少的并入到层数多的树中,节约查找时间。
namespace UF4 {
class UnionFind {
private:
int count;
int* parent;
int* rank;//表示以i为根的层数
public:
UnionFind(int n) {
this->count = n;
parent = new int[n];
rank = new int[n];
for (int i = 0; i < count; i++) {
parent[i] = i;
rank[i] = 1;
}
}
~UnionFind() {
delete[] parent;
delete[] rank;
}
int find(int p) {
assert(p >= 0 && p < count);
while (p != parent[p]) {
p = parent[p];
}
return p;
}
bool isConnected(int p, int q) {
return find(p) == find(q);
}
void unionElements(int p,int q) {
assert(p >= 0 && p < count);
assert(q >= 0 && q < count);
int pRoot = find(p);
int qRoot = find(q);
if (rank[pRoot] > rank[qRoot])
parent[qRoot] = pRoot;
else if (rank[pRoot] < rank[qRoot])
parent[pRoot] = qRoot;
else {
parent[qRoot] = pRoot;
rank[pRoot] += 1;
}
}
};
}
5.优化4:在优化3的基础上,对于查找某一个元素的类别号,即查找某一个元素的根,都是逐级遍历,由最底一层到倒数第二层,最后到根节点。为了节约时间这里采用了跳跃查询的方法,即不是逐级判断而是跳跃判断。
namespace UF5 {
class UnionFind {
private:
int count;
int* parent;
int* rank;//表示以i为根的层数
public:
UnionFind(int n) {
this->count = n;
parent = new int[n];
rank = new int[n];
for (int i = 0; i < count; i++) {
parent[i] = i;
rank[i] = 1;
}
}
~UnionFind() {
delete[] parent;
delete[] rank;
}
int find(int p) {
assert(p >= 0 && p < count);
if (p != parent[p])
parent[p] = find(parent[p]);
return parent[p];
}
bool isConnected(int p, int q) {
return find(p) == find(q);
}
void unionElements(int p, int q) {
assert(p >= 0 && p < count);
assert(q >= 0 && q < count);
int pRoot = find(p);
int qRoot = find(q);
if (rank[pRoot] > rank[qRoot])
parent[qRoot] = pRoot;
else if (rank[pRoot] < rank[qRoot])
parent[pRoot] = qRoot;
else {
parent[qRoot] = pRoot;
rank[pRoot] += 1;
}
}
};
}
三、并查集的测试
这里由于空间优先给出一种测试代码,具体实现时只需要更黄testUF5,还有两个UF5空间名称即可测试其他优化的并查集,这里为了体现优化的效果,随机抽取三组数,判断每一组中两个数是否相连,打印出判断时间,比较
void testUF5(int n) {
srand(time(NULL));
UF5::UnionFind uf = UF5::UnionFind(n);
time_t stratTime = clock();
for (int i = 0; i < n; i++) {
int a = rand() % n;
int b = rand() % n;
uf.unionElements(a, b);
}
cout << endl;
for (int i = 0; i < 3; i++) {
int c = rand() % n;
int d = rand() % n;
cout << c << " and " << d << " 是否相连:" << uf.isConnected(c, d) << endl;
}
time_t endTime = clock();
cout << "test5:" << double(endTime - stratTime) / CLOCKS_PER_SEC << "s" << endl;
}
这里是主函数给定n的值,依次调用每一种并查集实现代码,时间比较如下图所示。
int n=100000;
UnionTest::testUF1(n);
其中,test1是未优化的初始并查集,test2是优化1,test3是优化2,
test4是优化3,test5是优化4。