一、128. 最长连续序列
🔗 题目链接
📝 题目描述
给定一个未排序的整数数组 nums,找出数字连续的最长序列(不要求序列元素在原数组中连续)的长度。要求设计并实现时间复杂度为 O(n) 的算法。
示例:
输入:nums = [100,4,200,1,3,2] 输出:4 解释:最长连续序列是 [1,2,3,4],长度为 4。
🧠 思路分析
这道题最经典的解法是用哈希表,但用并查集同样能解,且能加深对并查集理解。并查集的核心是合并连续的数字,每个数字自成一个集合,遍历数组时,如果 x+1 在数组中,就合并 x 和 x+1 所在的集合,并统计合并后的集合大小。由于数组元素可能很大,不能用直接开数组的方式初始化,需要用哈希表来存储每个数字对应的父节点。合并时路径压缩和按秩合并两个优化都要加上,这样每次 find 和 union 操作近似 O(1)。最后遍历所有数字,找到最大的集合大小即可。这个解法的时间复杂度是 O(n·α(n)),近似 O(n)。
💻 代码实现(C++)
cpp
class UnionFind {
unordered_map<int, int> parent;
unordered_map<int, int> size;
public:
int find(int x) {
if (!parent.count(x)) return x;
if (parent[x] != x) parent[x] = find(parent[x]);
return parent[x];
}
void unite(int x, int y) {
x = find(x); y = find(y);
if (x == y) return;
if (size[x] < size[y]) swap(x, y);
parent[y] = x;
size[x] += size[y];
}
void add(int x) {
if (!parent.count(x)) {
parent[x] = x;
size[x] = 1;
}
}
int getSize(int x) {
return size[find(x)];
}
};
class Solution {
public:
int longestConsecutive(vector<int>& nums) {
if (nums.empty()) return 0;
UnionFind uf;
unordered_set<int> numSet(nums.begin(), nums.end());
for (int x : numSet) {
uf.add(x);
if (numSet.count(x + 1)) uf.unite(x, x + 1);
}
int maxLen = 1;
for (int x : numSet) {
maxLen = max(maxLen, uf.getSize(x));
}
return maxLen;
}
};
📚 相关学习资源
-
文章 :LeetCode 128. 最长连续序列------并查集实现(力扣题解)------ 详细展示了用并查集解决此题的过程
-
文章 :128. 最长连续序列(哈希+并查集)(E-COM-NET)------ 提供了完整的并查集实现,讲解清晰
免责声明:以上链接均来自公开网络。若存在侵权问题,请联系删除。
⏱ 复杂度分析
-
时间复杂度:O(n·α(n)),近似 O(n)。
-
空间复杂度:O(n),存储父节点和集合大小。
二、200. 岛屿数量
🔗 题目链接
📝 题目描述
给你一个由 '1'(陆地)和 '0'(水)组成的二维网格,请你计算网格中岛屿的数量。岛屿总是被水包围,并且每座岛屿只能由水平方向和/或竖直方向上相邻的陆地连接形成。你可以假设网格的四个边均被水包围。
示例:
输入:grid = [ ["1","1","1","1","0"], ["1","1","0","1","0"], ["1","1","0","0","0"], ["0","0","0","0","0"] ] 输出:1
🧠 思路分析
并查集解法将每个值为 '1' 的格子看成一个节点,将上下左右相邻的格子合并到同一个集合中。初始化时,每个 '1' 格子自成一个集合。遍历整个网格,对于每个 '1',检查它上方和左方是否有 '1',有则合并,因为从上到下从左到右遍历时,每个格子只需要检查上方和左方就能覆盖所有相邻关系。最终,集合的数量就是岛屿的数量。这种解法与 DFS/BFS 的不同之处在于,并查集能更好地处理动态添加陆地的情况,比如在线算法场景。
💻 代码实现(C++)
cpp
class UnionFind {
vector<int> parent;
vector<int> rank;
int count;
public:
UnionFind(int n) : parent(n), rank(n, 1), count(n) {
for (int i = 0; i < n; i++) parent[i] = i;
}
int find(int x) {
if (parent[x] != x) parent[x] = find(parent[x]);
return parent[x];
}
void unite(int x, int y) {
int rx = find(x), ry = find(y);
if (rx == ry) return;
if (rank[rx] < rank[ry]) parent[rx] = ry;
else if (rank[rx] > rank[ry]) parent[ry] = rx;
else { parent[ry] = rx; rank[rx]++; }
count--;
}
int getCount() { return count; }
};
class Solution {
public:
int numIslands(vector<vector<char>>& grid) {
int m = grid.size(), n = grid[0].size();
UnionFind uf(m * n);
int waterCount = 0;
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
if (grid[i][j] == '0') {
waterCount++;
continue;
}
int idx = i * n + j;
if (i > 0 && grid[i-1][j] == '1') uf.unite(idx, (i-1)*n + j);
if (j > 0 && grid[i][j-1] == '1') uf.unite(idx, i*n + (j-1));
}
}
return uf.getCount() - waterCount;
}
};
📚 相关学习资源
- 文章 :LeetCode 200. 岛屿数量(DFS/BFS/并查集)(力扣官方题解)------ 提供了三种解法的完整对比
免责声明:以上链接均来自公开网络。若存在侵权问题,请联系删除。
⏱ 复杂度分析
-
时间复杂度:O(m×n·α(m×n)),每个格子最多被访问两次。
-
空间复杂度:O(m×n),存储父节点数组。
三、547. 省份数量
🔗 题目链接
📝 题目描述
有 n 个城市,其中一些彼此相连,另一些没有相连。如果城市 a 与城市 b 直接相连,且城市 b 与城市 c 直接相连,那么城市 a 与城市 c 间接相连。省份是一组直接或间接相连的城市,组内不含其他没有相连的城市。给你一个 n × n 的矩阵 isConnected,其中 isConnected[i][j] = 1 表示第 i 个城市和第 j 个城市直接相连,isConnected[i][j] = 0 表示二者不直接相连。返回矩阵中省份的数量。
示例:
输入:isConnected = [[1,1,0],[1,1,0],[0,0,1]] 输出:2
🧠 思路分析
这道题是并查集最直接的入门题,本质上就是求图中的连通分量数。初始化时,n 个城市各自独立成一个集合。遍历对称矩阵的上三角部分,如果 isConnected[i][j] == 1,就将 i 和 j 合并到同一个集合中。遍历结束后,统计有多少个集合的根节点是自己,这个数量就是省份的数量。并查集的查找和合并都要用路径压缩,否则在极端情况下会退化到 O(n²)。这道题也可以 DFS 或 BFS 做,但并查集解法更直观地体现了"集合合并"的语义。
💻 代码实现(C++)
cpp
class UnionFind {
vector<int> parent;
vector<int> rank;
public:
UnionFind(int n) : parent(n), rank(n, 1) {
for (int i = 0; i < n; i++) parent[i] = i;
}
int find(int x) {
if (parent[x] != x) parent[x] = find(parent[x]);
return parent[x];
}
void unite(int x, int y) {
int rx = find(x), ry = find(y);
if (rx == ry) return;
if (rank[rx] < rank[ry]) parent[rx] = ry;
else if (rank[rx] > rank[ry]) parent[ry] = rx;
else { parent[ry] = rx; rank[rx]++; }
}
int countProvinces() {
int cnt = 0;
for (int i = 0; i < parent.size(); i++) {
if (parent[i] == i) cnt++;
}
return cnt;
}
};
class Solution {
public:
int findCircleNum(vector<vector<int>>& isConnected) {
int n = isConnected.size();
UnionFind uf(n);
for (int i = 0; i < n; i++) {
for (int j = i + 1; j < n; j++) {
if (isConnected[i][j]) uf.unite(i, j);
}
}
return uf.countProvinces();
}
};
📚 相关学习资源
-
文章 :算法动画秒懂并查集(547. 省份数量)(腾讯云开发者社区)------ 配有动画演示并查集的工作过程
-
文章 :547. 省份数量(并查集套路)(腾讯云开发者社区)------ 提供了标准的三步并查集模板:初始化、查询、合并
免责声明:以上链接均来自公开网络。若存在侵权问题,请联系删除。
⏱ 复杂度分析
-
时间复杂度:O(n²·α(n)),遍历矩阵的上三角部分。
-
空间复杂度:O(n),存储父节点和秩数组。
四、684. 冗余连接
🔗 题目链接
📝 题目描述
在本问题中,树指的是一个连通且无环的无向图。输入一个图,该图由一个有着 N 个节点(节点值不重复 1, 2, ..., N)的树及一条附加的边构成。附加的边的两个顶点包含在 1 到 N 中间,这条附加的边不属于树中已存在的边。结果图是一个以边组成的二维数组。返回一条可以删去的边,使得结果图是一个有着 N 个节点的树。如果有多个答案,则返回二维数组中最后出现的边。
示例:
输入:[[1,2], [1,3], [2,3]] 输出:[2,3]
🧠 思路分析
这道题完美展现了并查集在图环检测中的威力。一棵树有 N 个节点和 N-1 条边,题目给出的图有 N 条边,因此有且只有一个环。遍历所有边,对于每条边 [u, v],检查 u 和 v 是否已经在同一个集合中。如果不在,说明这条边不会形成环,合并这两个节点;如果已经在同一个集合中,说明这条边会形成环,它就是我们要找的冗余连接。这道题的精妙之处在于,不需要额外判断哪条边是最后一条,只需按顺序处理,第一条导致环的边就是答案(因为题目要求返回最后出现的边,但实际处理中第一条检测到环的边就是那个闭环的边)。
💻 代码实现(C++)
cpp
class UnionFind {
vector<int> parent;
public:
UnionFind(int n) : parent(n + 1) {
for (int i = 1; i <= n; i++) parent[i] = i;
}
int find(int x) {
if (parent[x] != x) parent[x] = find(parent[x]);
return parent[x];
}
bool unite(int x, int y) {
int rx = find(x), ry = find(y);
if (rx == ry) return false;
parent[rx] = ry;
return true;
}
};
class Solution {
public:
vector<int> findRedundantConnection(vector<vector<int>>& edges) {
int n = edges.size();
UnionFind uf(n);
for (auto& edge : edges) {
if (!uf.unite(edge[0], edge[1])) return edge;
}
return {};
}
};
📚 相关学习资源
-
文章 :LeetCode冗余连接:并查集的环检测应用终极指南(CSDN)------ 详细讲解了冗余连接问题的解题思路和代码实现
-
文章 :LeetCode 684. 冗余连接(并查集)(腾讯云开发者社区)------ 提供了 C++ 实现和复杂度分析
免责声明:以上链接均来自公开网络。若存在侵权问题,请联系删除。
⏱ 复杂度分析
-
时间复杂度:O(n·α(n)),遍历每条边。
-
空间复杂度:O(n),存储父节点数组。
五、990. 等式方程的可满足性
🔗 题目链接
📝 题目描述
给定一个由表示变量之间关系的字符串方程组成的数组,每个字符串方程 equations[i] 的长度为 4,并采用两种不同的形式之一:"a==b" 或 "a!=b"。其中 a 和 b 是小写字母(不一定不同),表示单字母变量名。只有当可以将整数分配给变量名,以便满足所有给定的方程时才返回 true,否则返回 false。
示例:
输入:["a==b","b!=a"] 输出:false
🧠 思路分析
这道题是用并查集处理约束关系的最典型例子。等式具有传递性:a==b 且 b==c 可以推出 a==c,因此可以用并查集将所有相等的变量合并到同一个集合中。具体做法是:先遍历所有等式方程,将等式两边的字母合并;然后遍历所有不等式方程,检查不等式两边的字母是否在同一个集合中------如果在,说明它们既被要求相等又被要求不相等,产生了矛盾,直接返回 false。如果所有不等式都通过检查,返回 true。由于只有 26 个小写字母,可以用固定大小的数组实现并查集,非常高效。
💻 代码实现(C++)
cpp
class UnionFind {
vector<int> parent;
public:
UnionFind() : parent(26) {
for (int i = 0; i < 26; i++) parent[i] = i;
}
int find(int x) {
if (parent[x] != x) parent[x] = find(parent[x]);
return parent[x];
}
void unite(int x, int y) {
parent[find(x)] = find(y);
}
};
class Solution {
public:
bool equationsPossible(vector<string>& equations) {
UnionFind uf;
for (string& eq : equations) {
if (eq[1] == '=') {
uf.unite(eq[0] - 'a', eq[3] - 'a');
}
}
for (string& eq : equations) {
if (eq[1] == '!') {
if (uf.find(eq[0] - 'a') == uf.find(eq[3] - 'a')) return false;
}
}
return true;
}
};
📚 相关学习资源
-
文章 :LeetCode 990. 等式方程的可满足性(并查集)(腾讯云开发者社区)------ 提供了标准的 C++ 实现和详细的解题步骤
-
文章 :LeetCode-990. 等式方程的可满足性(E-COM-NET)------ 先构建等式并查集,再检查不等式,思路清晰
免责声明:以上链接均来自公开网络。若存在侵权问题,请联系删除。
⏱ 复杂度分析
-
时间复杂度:O(n·α(26)),n 为方程的数量。
-
空间复杂度:O(26),并查集大小固定。
六、并查集模板代码
并查集(Disjoint Set Union)是处理动态连通性问题最优雅的数据结构。核心操作有两个:find 查找元素所属集合的根节点,union 合并两个集合。为了提高效率,通常配合两种优化手段:路径压缩 (在查找过程中将沿途节点的父节点直接指向根节点)和按秩合并(将节点数较少的集合合并到节点数较多的集合中)。以下是 C++ 的标准模板:
cpp
class UnionFind {
private:
vector<int> parent;
vector<int> rank; // 按秩合并:记录树的高度
public:
UnionFind(int n) {
parent.resize(n);
rank.resize(n, 1);
for (int i = 0; i < n; i++) parent[i] = i;
}
// 查找根节点 + 路径压缩
int find(int x) {
if (parent[x] != x) {
parent[x] = find(parent[x]);
}
return parent[x];
}
// 合并两个集合
bool unite(int x, int y) {
int rx = find(x), ry = find(y);
if (rx == ry) return false; // 已属于同一集合
// 按秩合并:将高度较低的树接到高度较高的树上
if (rank[rx] < rank[ry]) {
parent[rx] = ry;
} else if (rank[rx] > rank[ry]) {
parent[ry] = rx;
} else {
parent[ry] = rx;
rank[rx]++;
}
return true;
}
// 判断两个元素是否属于同一集合
bool connected(int x, int y) {
return find(x) == find(y);
}
// 统计集合数量
int count() {
int cnt = 0;
for (int i = 0; i < parent.size(); i++) {
if (parent[i] == i) cnt++;
}
return cnt;
}
};
📚 相关学习资源
-
文章 :分享|(建议收藏)LeetCoder 并查集模板(力扣讨论区)------ 详细介绍了并查集的两个优化策略和时间复杂度分析
-
文章 :Disjointset 并查集(按秩合并,与路径压缩)的模板(E-COM-NET)------ 提供了完整的模板代码和两种实现方式
免责声明:以上链接均来自公开网络。若存在侵权问题,请联系删除。
结语
并查集篇的五道题覆盖了并查集的几种典型应用场景:连续序列合并 (最长连续序列)、二维网格连通 (岛屿数量)、连通分量计数 (省份数量)、环检测 (冗余连接)、约束满足问题(等式方程)。并查集的核心在于处理"动态连通性"问题------两个元素是否属于同一个集合,以及如何高效地合并集合。加上路径压缩和按秩合并两种优化后,单次操作近乎 O(1),可以轻松应对大规模数据。
建议刷题顺序:先做 547 和 200 熟悉并查集的基本模板,再做 684 理解环检测的应用,然后做 128 挑战哈希映射版并查集,最后用 990 收尾体会约束问题的建模思路。
如果本文对你有帮助,欢迎点赞、收藏、转发,你的支持是我持续创作的动力 ❤️
免责声明:本文部分解题思路参考了力扣官方题解及社区优秀文章,相关链接均来自公开网络。若存在侵权问题,请联系删除。