携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第11天,点击查看活动详情
一、前言
并查集(Union Find
): 是一种用来管理元素分组情况的数据结构。
并查集的关键在于如何把原问题转化成图的动态连通性问题。
连通是一种等价关系,具有如下 3 个性质:
- 自反性:节点
p
和p
是连通的。 - 对称性:如果节点
p
和q
连通,那么q
和p
也连通。 - 传递性:如果节点
p
和q
连通,q
和r
连通,那么p
和r
也连通。
并查集算法主要实现这两个功能:
union(p, q)
将p
和q
两节点连通起来: 只需将它们的根节点连通起来即可。connect(p, q)
判断p
和q
两节点是否连通: 判断是否有相同的根节点。
实现此功能的方法:记录每个节点的父节点。可以用数组表示。
平衡性优化:为避免生成单边树,降低树高。
union(p, q)
时,小树接到大树下: 需要额外数组size[]
来记录每棵树的节点数量。- 进行路径压缩: 尽量挂在根节点下。(每次查找时,进行压缩)
扫描二维码关注公众号,回复:
14445103 查看本文章
![](/qrcode.jpg)
完整代码结构如下:
public class UnionFind {
private int[] parent, size;
public UnionFind(int n) {
parent = new int[n]; // 存储每个节点的父节点
size = new int[n]; // 记录每棵树的 “重量”
for (int i = 0; i < n; ++i) {
parent[i] = i;
size[i] = 1;
}
}
// 1. 查找
public int find(int x) {
return findPathCompressionIterative(x);
}
// 迭代查找,伴随路径压缩
private int findPathCompressionIterative(int x) {
int root = x;
// 1. 查找根节点
while (parent[root] != root) root = parent[root];
// 2. 路径压缩:把查找路径上所有节点的父节点都更新为 根节点root
while (parent[x] != root) {
int tmp = parent[x];
parent[x] = root;
x = tmp;
}
return root;
}
// 2. 联合
public void union(int x, int y) {
int xRoot = find(x);
int yRoot = find(y);
if (xRoot == yRoot) return;
// 小树接到大树下面,较平衡
if (size[xRoot] < size[yRoot]) {
parent[xRoot] = yRoot;
size[yRoot] += size[xRoot];
} else {
parent[yRoot] = xRoot;
size[xRoot] += size[yRoot];
}
}
}
复制代码
总结下: 算法3个关键点
- 用
parent[]
记录每个节点的父节点: 相当于指向父节点的指针,所以parent[]
内实际存储着一个森林 - 用
size[]
数组记录每棵树的重量: 目的是调用union
后树依然拥有平衡性,而不会退化成链表,影响操作效率。 - 在
find
函数中进行路径压缩: 保证任意树的高度保持在常数,使得union
和connected
API
时间复杂度为O(1)
。
二、题目
(2)省份数量(中)
题干分析
这个题目说的是,给你 0 到 n-1 共 n 个城市,城市之间有的相互连接,有的则不相连。如果城市 0 与城市 1 直接相连,城市 1 与城市 2 直接相连,那么城市 0 与城市 2 称为间接相连。
直接相连或间接相连的一组城市定义为一个省份。现在给你一个 n x n 的矩阵 a 表示城市之间的连接情况。a(i, j) 等于 1 表示第 i 个城市和第 j 个城市直接相连,a(i, j) 等于 0 则表示这两个城市不直接相连。你要计算出,这 n 个城市一共构成了多少个省份。
# 比如说,给你 3 个城市:
0, 1, 2
# 它们对应的连接矩阵 a 是:
1, 0, 0
0, 1, 1
0, 1, 1
# 根据这个矩阵,我们可以知道城市 0 不与城市 1 或城市 2 相连,它自成一个省份。城市 1 与城市 2 相互连接,构成一个省份。
# 因此,这 3 个城市构成了 2 个省份。
复制代码
思路解法
思路有两种: 并查集 和 暴力法(DFS)
方法一:并查集
- 初始化并查集
- 构建并查集
- 查看
i
和j
是否相连 - 相连就合并
- 查看
AC
代码:
// 方法一: 并查集
// Time: O(n^2), Space: O(n), Faster: 86.14%
public int findCircleNum(int[][] isConnected) {
if (isConnected == null || isConnected.length == 0 ||
isConnected[0] == null || isConnected[0].length == 0) {
return 0;
}
// 1. 初始化并查集
int n = isConnected.length;
UnionFind uf = new UnionFind(n);
// 2. 构建并查集
for (int i = 0; i < n; ++i) {
// 2.1 查看 i 和 j 是否相连
for (int j = i + 1; j < n; ++j) {
// 2.2 相连就合并
if (isConnected[i][j] == 1) uf.union(i, j);
}
}
return uf.count();
}
public class UnionFind {
private int[] parent, size;
private int cnt; // 统计省数量
public UnionFind(int n) {
parent = new int[n];
size = new int[n];
cnt = n;
for (int i = 0; i < n; ++i) {
parent[i] = i;
size[i] = 1;
}
}
public int find(int x) {
if (parent[x] == x) return x;
parent[x] = find(parent[x]);
return parent[x];
}
// Time: O(a(n)), Space: O(1)
public void union(int x, int y) {
int xRoot = find(x);
int yRoot = find(y);
if (xRoot == yRoot) return;
if (size[xRoot] < size[yRoot]) {
parent[xRoot] = yRoot;
size[yRoot] += size[xRoot];
} else {
parent[yRoot] = xRoot;
size[xRoot] += size[yRoot];
}
--cnt; // 每合并一次,说明能省的 -1
}
public int count() {
// 或者看 parent[x] == x 有几个
return cnt;
}
}
复制代码
方法二:暴力法 DFS
,使用额外辅助
AC
代码:
// 方法二: 暴力法 DFS
// Time: O(n^2), Space: O(n), Faster: 86.14%
public int findCircleNumDFS(int[][] isConnected) {
if (isConnected == null || isConnected.length == 0 ||
isConnected[0] == null || isConnected[0].length == 0) {
return 0;
}
int n = isConnected.length;
boolean[] isVisited = new boolean[n];
int cnt = 0;
for (int i = 0; i < n; ++i) {
if (!isVisited[i]) {
++ cnt;
dfs(n, i, isConnected, isVisited);
}
}
return cnt;
}
private void dfs(int n, int i, int[][] isConnected, boolean[] isVisited) {
if (i >= n || i < 0) return;
for (int j = 0; j < n; ++j) {
if (j == i) continue;
if (isConnected[i][j] == 1 && !isVisited[j]) {
isVisited[j] = true;
dfs(n, j, isConnected, isVisited);
}
}
}
复制代码