并查集是一种很不一样的数据结构。可以解决连接问题,路径问题,主要有两个操作:
- union(p, q)//将p和q所在的集合合并在一起
- find(p) //找到p属于哪个集合
可以非常容易的实现判断p和q是否相连,是否在一个集合。
1. 并查集实现思路1(quick find)
由quick find可以看出,此种实现思路会在执行find操作是具有较高的效率。
其实思路很简单,假设建立了一个大小为n的并查集,此时说明并查集中有n个集合。设置一个id数组,初始化时id[i]=i。
由此执行find(p)操作时,可以直接返回id[p]即可,可以看出时间复杂度在O(1)即可完成操作
但是在执行union(p, q)是,需要遍历整个id数组,将所有p的id值等于q的id值(反过来也行)达成合并的操作,时间复杂度为O(n)
#include <iostream>
#include <cassert>
namespace UF1 {
class UnionFind {
private:
int count;
int *id;
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 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;
for(int i = 0; i < count; i++)
if(pId == id[i])
id[i] = qId;
}
};
}
2. 并查集实现思路2(quick union)
相比于第一种思路,这一种思路才是最常规的并查集实现思路。
使用一个parent数组,parent[i]存储的是节点i的父节点。
parent[i] = i, 代表父指针指向自己,该节点为当前集合的根节点。如何判断p和q是否属于一个集合,只需要分别找到p和q的根节点即可,如果是一个根节点,说明p和q在一个集合,否则不再一个根节点。
find(p)操作
从p向上不断的找父节点,直到找到根节点即可:
int find(int p) {
assert(p >= 0 && p < count);
while(p != parent[p])
p = parent[p];
return p;
}
union(p, q)操作
找到p和q的根节点,然后将q(p)的父指针指向p(q)即可。
void unionElements(int p, int q) {
int pRoot = find(p);
int qRoot = find(q);
if(pRoot == qRoot)
return;
parent[pRoot] = qRoot;
}
完整实现如下:
class UnionFind {
private:
int *parent;
int count;
public:
UnionFind(int n) {
parent = new int[n];
this->count = n;
for(int i = 0; i < n; 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(p);
}
void unionElements(int p, int q) {
int pRoot = find(p);
int qRoot = find(q);
if(pRoot == qRoot)
return;
parent[pRoot] = qRoot;
}
};
3. 并查集的优化一(size)
由前面的实现来看,在执行union(p, q)的时候,因为我们是将p或q任意执行另一集合的根节点,这样非常容易导致一个问题------有可能会导致一棵树的高度过高,在执行find操作效率变低。
我们应该尽量去平衡每一个集合,通过size数组来实现, sz[i]来表示以i为根的集合的元素个数,这样在执行union操作时可以先比较两个集合的size,将size小的集合根节点指向size大的集合的根节点。
class UnionFind {
private:
int *parent;
int *sz;//sz[i]表示以i为根的集合中元素的个数
int count;
public:
UnionFind(int n) {
parent = new int[n];
sz = new int[n];
this->count = n;
for(int i = 0; i < n; i++) {
parent[i] = i;
sz[i] = 1;
}
}
~UnionFind() {
delete [] parent;
delete [] sz;
}
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(p);
}
//此方法使得一个集合的树更矮一些
void unionElements(int p, int q) {
int pRoot = find(p);
int qRoot = find(q);
if(pRoot == qRoot)
return;
if(sz[pRoot] < sz[qRoot]) {
parent[pRoot] = qRoot;
sz[qRoot] += sz[pRoot];
}
else {
parent[qRoot] = pRoot;
sz[pRoot] += sz[qRoot];
}
}
};
3. 并查集的优化二(rank)
在有些情况你可能会发现用size来优化效果并不好,比如,有些集合的树形结构并不高,但是节点多(size大);有些树形结构高,但是节点少(size小),然后将size小的指向了size大的集合,即将树高的集合根节点指向了矮树的根节点,实际上增加了树的高度,在执行find的时候,当查找元素p处在很高的那个分支,需要执行非常多的递归操作,所以可以通过评判两颗树的深度(高度)来选择合并时的指向。
如果rank[p] > rank[q], 那么将q集合根节点执行p节点根节点。
class UnionFind {
private:
int *parent;
int *rank;//rank[i]表示以i为根的树的高度
int count;
public:
UnionFind(int n) {
parent = new int[n];
rank = new int[n];
this->count = n;
for(int i = 0; i < n; 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]){
parent[p] = parent[parent[p]];//路径压缩
p = parent[p];
}
return p;
}
bool isConnected(int p, int q) {
return find(p) == find(p);
}
//此方法使得一个集合的树更矮一些
void unionElements(int p, int q) {
int pRoot = find(p);
int qRoot = find(q);
if(pRoot == qRoot)
return;
//高度不会变化
if(rank[pRoot] < rank[qRoot]) {
parent[pRoot] = qRoot;
}
else if(rank[pRoot] > rank[qRoot]){
parent[qRoot] = pRoot;
}
else {
parent[pRoot] = qRoot;
rank[qRoot]++;
}
}
};
4. 并查集的优化三(路径压缩)
我们可以尽量使find的执行此时少,即所在集合形成的树个深度越小越好,最为理想的情况是,所有节点的值直接执行根节点。
通过每次find操作,实现路径压缩,将p指向根节点
int find(int p) {
assert(p >= 0 && p < count);
if(p != parent[p])
parent[p] = find(parent[p]);
return parent[p];
}