14.并查集的实现与特性
并查集(Dijoint Set
)属于一种跳跃式数据结构,也就是说你不会就是你压根都不会,你要是一会的就会用就行了,它没有太多让你在上面进行发展的空间,或者是需要像动态规划或者是各种搜索一样有非常强的随机应变和在上面进行自由发挥的空间。所以我们主要就是把它的情景和它的实现代码进行学习掌握。掌握代码模版直接套上去用即可。
并查集 (Dijoint Set)
适用场景
它解决的场景就是组团和配对的问题,也就是说在有些现实的问题中,你需要很快地判断这两个个体是不是在一个集合中,这么讲有点抽象,很多时候就是说你和他是不是朋友这么一个问题,如果是在社交网络里面以及判断两个群组之间,是不是一个群组以及很快速地合并群组,所以需要用这样的数据结构。
- 组团、配对问题
- Group or not?
假设让你自己来实现微信上的好友和所谓的朋友圈的功能,以及分析这两个人是不是好友这么一个问题,思考一下你应该如何实现?
假设我们有 n 个人,从 0 到 n-1,让你非常快地能够判断 a 和 b 到底是不是朋友,以及可以支持一些操作,比如说把 a 变成 b 的朋友。类似这样的问题,如果你在做的话,你可能会想到比如说用一个 set
或者用 dictionary
表示这里面的人都是朋友,这时候你会发现你就会出现一种问题,就是你可能要建很多个 set ,比如说他两两一个朋友,但是互相的话不是朋友的情况,以及在后面你要合并 set 之类的,这样的操作以及很难分辨这一堆朋友的一个群组它是属于比如说第几个群之类的问题,这样的话基于这种原因,当时就发明来一种数据结构,这个数据结构就叫做“并查集”,专门来解决这类问题。
基本操作
并查集要解决这类场景,主要有三个要实现的函数:
- makeSet(s) : 建立一个新的并查集,其中包含 s 个单元素集合。
- unionSet(x,y) : 把元素 x 和元素 y 所在的集合合并,要求 x 和 y 所在的集合不相交,如果相交则不合并。
- find(x) : 找到元素 x 所在的集合的代表,该操作也可以用于判断两个元素是否位于同一个集合,只要将它们各自的代表比较一下就可以了。
并查集原理
初始化
通过图来学习并查集原理,一开始每一个元素,它拥有一个parent数组指向自己,每个元素它都有一个所谓的parent这么一个数组,它指向自己表示它自己的话就是自己的集合或者自己是自己的老大,
查询、合并
接下来所谓的合并和查询的一个操作:
- 如何查询对任何一个元素,看它的parent再看它的parent就一直往上,直到它的parent等于它自己,说明找到来它的领头元素,就是它的集合代表元素,那么就表示这个集合是谁。
-
如何合并,比如说我们要把这两个集合进行合并,要做的一件事情就是找出集合的领头元素,在这里是 a 和另外一个集合它的领头元素是 e,然后将 parent[e] 特意指向 a 或者将 parent[a] 指向 e,这两个都是一样的操作,最后的话就把两个进行所谓的合并了。
路径压缩
还有一个叫做所谓的路径压缩,这里的话我们看到:
- d 的 parent 是 c
- c 的 parent 是 b
-
b 的 parent 是 a
那么我们可以直接把这条路上的所有元素,它的 parent 都指向 a。这样的话还是和原来的表是一样,但是它的查询时间会快不少。
并查集代码模版
Java
// Java
class UnionFind {
private int count = 0;
private int[] parent;
public UnionFind(int n) {
count = n;
parent = new int[n];
for (int i = 0; i < n; i++) {
parent[i] = i;
}
}
public int find(int p) {
while (p != parent[p]) {
parent[p] = parent[parent[p]];
p = parent[p];
}
return p;
}
public void union(int p, int q) {
int rootP = find(p);
int rootQ = find(q);
if (rootP == rootQ) return;
parent[rootP] = rootQ;
count--;
}
}
Python
# Python
def init(p):
# for i = 0 .. n: p[i] = i;
p = [i for i in range(n)]
def union(self, p, i, j):
p1 = self.parent(p, i)
p2 = self.parent(p, j)
p[p1] = p2
def parent(self, p, i):
root = i
while p[root] != root:
root = p[root]
while p[i] != i: # 路径压缩 ?
x = i; i = p[i]; p[x] = root
return root
C/C++
//C/C++
class UnionFind {
public:
UnionFind(vector>& grid) {
count = 0;
int m = grid.size();
int n = grid[0].size();
for (int i = 0; i < m; ++i) {
for (int j = 0; j < n; ++j) {
if (grid[i][j] == '1') {
parent.push_back(i * n + j);
++count;
}
else {
parent.push_back(-1);
}
rank.push_back(0);
}
}
}
//递归
int find(int i) {
if (parent[i] != i) {
parent[i] = find(parent[i]);
}
return parent[i];
}
void unite(int x, int y) {
int rootx = find(x);
int rooty = find(y);
if (rootx != rooty) {
if (rank[rootx] < rank[rooty]) {
swap(rootx, rooty);
}
parent[rooty] = rootx;
if (rank[rootx] == rank[rooty]) rank[rootx] += 1;
--count;
}
}
int getCount() const {
return count;
}
private:
vector parent;
vector rank;
int count;
};
JavaScript
// JavaScript
class unionFind {
constructor(n) {
this.count = n;
this.parent = new Array(n);
for (let i = 0; i < n; i++) {
this.parent[i] = i;
}
}
find(p) {
let root = p;
while (parent[root] !== root) {
root = parent[root];
}
// 压缩路径
while (parent[p] !== p) {
let x = p;
p = this.parent[p];
this.parent[x] = root;
}
return root;
}
union(p, q) {
let rootP = find(p);
let rootQ = find(q);
if (rootP === rootQ) return;
this.parent[rootP] = rootQ;
this.count--;
}
}
实战题目
547.朋友圈
200.岛屿数量
130.被围绕的区域
这里只对朋友圈这道题进行说明,其他的可以再去练习。
547. 朋友圈
方法1: DFS,BFS(类似岛屿问题)
class Solution {
// 1. DFS,BFS (类似岛屿问题)
// 时间O(n^2) 空间O(n)
public int findCircleNum(int[][] M) {
/**
* 使用一个visited数组,依次判断每个节点
* 如果其未访问,朋友圈数加1并对该节点进行dfs搜索标记所有访问到的节点。
*/
boolean[] visited = new boolean[M.length];
int res = 0;
// 遍历所有的学生
for (int i = 0; i < M.length; i++) {
// 当这个学生没有被访问过,那就res++
if (!visited[i]) {
// 同时DFS这个学生和他的所有的朋友(类似于岛屿问题一样)
dfs(M, visited, i);
res++;
}
}
return res;
}
private void dfs(int[][] M, boolean[] visited, int i) {
// 这里的DFS一开始没有所谓的Terminator
// 这也是一个特殊情况,因为它的Terminator就是所有东西都访问过了,不用再扩散了
// 对于所有当前访问的节点 i ,然后遍历其他的节点,也就是学生0把0到M.length重新遍历一遍
for (int j = 0; j < M.length; j++) {
// 如果M[i][j] = 1,意味着i和j是朋友,且j没有被访问过
if (M[i][j] == 1 && !visited[j]) {
visited[j] = true;
dfs(M, visited, j);
}
}
}
}
方法2: 并查集
class Solution {
// 2. 并查集 时间O(n^3) 空间O(n)
public int findCircleNum(int[][] M) {
if (M == null || M.length == 0) return 0;
int n = M.length;
UnionFind uf = new UnionFind(n);
for (int i = 0; i < n - 1; i ++) {
for (int j = i + 1; j < n; j++) {
if (M[i][j] == 1) uf.union(i, j);
}
}
return uf.count;
}
class UnionFind {
public int count = 0;
public int[] parent;
public UnionFind(int n) {
count = n;
parent = new int[n];
for (int i = 0; i < n; i++) {
parent[i] = i;
}
}
public int find(int p) {
while (p != parent[p]) {
parent[p] = parent[parent[p]];
p = parent[p];
}
return p;
}
public void union (int p, int q) {
int rootP = find(p);
int rootQ = find(q);
if (rootP == rootQ) return;
parent[rootP] = rootQ;
count--;
}
}
}
部分图片来源于网络,版权归原作者,侵删。