LeetCode 684题"冗余的边"是一个经典的图论问题,主要考察对无向图环检测的理解和应用。题目给定一个由n个节点组成的树(即无环的无向连通图),额外添加了一条边后形成了环,要求找出这条导致环形成的"冗余边"。
在实际开发中,这类问题常出现在网络拓扑分析、依赖关系检测等场景。比如在构建微服务架构时,我们需要确保服务间的调用关系不会形成循环依赖;在数据库设计中,需要避免外键引用形成闭环。理解这类问题的解法对提升算法思维和解决实际问题都很有帮助。
题目输入是以边列表的形式给出的,每条边表示为[node1, node2]。关键点在于:
最直观的解法是使用深度优先搜索(DFS)来检测环:
这种方法的时间复杂度是O(N^2),因为最坏情况下需要对每条边执行O(N)的DFS检查。对于节点数较多的情况(比如N=1000),这样的复杂度难以接受。
并查集是解决这类连通性问题的利器,它提供了两种高效操作:
对于本问题,我们可以:
并查集的优势在于:
相比DFS/BFS,并查集更适合本问题的原因:
cpp复制class UnionFind {
private:
vector<int> parent; // 父节点数组
vector<int> rank; // 秩(用于按秩合并优化)
public:
// 构造函数:初始化每个元素的父节点为自身
UnionFind(int size) {
parent.resize(size);
rank.resize(size, 0); // 初始秩为0
for(int i = 0; i < size; i++) {
parent[i] = i;
}
}
// 查找操作(带路径压缩)
int find(int x) {
if(parent[x] != x) {
parent[x] = find(parent[x]); // 路径压缩
}
return parent[x];
}
// 合并操作(带按秩合并)
bool unionSet(int x, int y) {
int rootX = find(x);
int rootY = find(y);
if(rootX == rootY) {
return false; // 已在同一集合,合并失败
}
// 按秩合并优化
if(rank[rootX] < rank[rootY]) {
parent[rootX] = rootY;
} else if(rank[rootX] > rank[rootY]) {
parent[rootY] = rootX;
} else {
parent[rootY] = rootX;
rank[rootX]++;
}
return true;
}
};
路径压缩优化:
按秩合并优化:
cpp复制class Solution {
public:
vector<int> findRedundantConnection(vector<vector<int>>& edges) {
int n = edges.size();
UnionFind uf(n + 1); // 节点编号从1开始
for(auto& edge : edges) {
if(!uf.unionSet(edge[0], edge[1])) {
return edge; // 发现冗余边
}
}
return {}; // 无冗余边(根据题意不会执行到这里)
}
};
其中α(N)是反阿克曼函数,对于任何实际应用中的N值,α(N)不超过4。
| 优化方式 | 最坏时间复杂度 | 平均时间复杂度 |
|---|---|---|
| 无优化 | O(logN) | O(logN) |
| 仅路径压缩 | O(α(N)) | O(α(N)) |
| 仅按秩合并 | O(logN) | O(logN) |
| 双重优化 | O(α(N)) | O(α(N)) |
实际测试中,双重优化的并查集在N=1e6时比无优化版本快5-10倍。
虽然题目保证输入有效,但实际工程中应考虑:
python复制[[1,2],[2,3],[3,1]] # 应返回[3,1]
python复制[[1,2],[2,3],[3,4],[4,1],[1,5]] # 应返回[4,1]
python复制[[1,2],[1,3],[1,4],[4,5],[5,1]] # 应返回[5,1]
cpp复制vector<int> findRedundantConnection(vector<vector<int>>& edges) {
if(edges.empty()) return {};
int n = edges.size();
UnionFind uf(n + 1);
for(auto& edge : edges) {
// 验证节点编号有效性
if(edge[0] <= 0 || edge[0] > n || edge[1] <= 0 || edge[1] > n) {
throw invalid_argument("Invalid node index");
}
if(!uf.unionSet(edge[0], edge[1])) {
return edge;
}
}
throw logic_error("No redundant connection found");
}
面试官可能会追问:
cpp复制// 生成测试用例
vector<vector<int>> generateTestcase(int n, bool hasRedundant) {
vector<vector<int>> edges;
for(int i = 1; i < n; i++) {
edges.push_back({i, i+1});
}
if(hasRedundant) {
edges.push_back({1, n});
}
return edges;
}
// 性能测试
void benchmark() {
for(int n = 1e3; n <= 1e6; n *= 10) {
auto testcase = generateTestcase(n, true);
auto start = chrono::high_resolution_clock::now();
Solution().findRedundantConnection(testcase);
auto end = chrono::high_resolution_clock::now();
cout << "n=" << n << " time: "
<< chrono::duration_cast<chrono::microseconds>(end-start).count()
<< "μs" << endl;
}
}
cpp复制// 优化后的UnionFind类
class OptimizedUF {
private:
int* parent;
int* rank;
public:
OptimizedUF(int size) {
parent = new int[size];
rank = new int[size];
for(int i = 0; i < size; i++) {
parent[i] = i;
rank[i] = 0;
}
}
~OptimizedUF() {
delete[] parent;
delete[] rank;
}
// ...其他方法相同...
};
这种优化可以减少vector的开销,在极端性能要求下可考虑使用。
cpp复制// 使用移动语义避免拷贝
class Solution {
public:
vector<int> findRedundantConnection(vector<vector<int>> edges) {
UnionFind uf(edges.size() + 1);
for(const auto& edge : edges) { // 使用const引用
if(!uf.unionSet(edge[0], edge[1])) {
return {edge[0], edge[1]}; // 使用初始化列表
}
}
return {};
}
};
cpp复制#include <gtest/gtest.h>
TEST(RedundantConnectionTest, BasicTest) {
Solution sol;
vector<vector<int>> edges1 = {{1,2},{1,3},{2,3}};
EXPECT_EQ(sol.findRedundantConnection(edges1), vector<int>({2,3}));
vector<vector<int>> edges2 = {{1,2},{2,3},{3,4},{1,4},{1,5}};
EXPECT_EQ(sol.findRedundantConnection(edges2), vector<int>({1,4}));
}
TEST(RedundantConnectionTest, EmptyTest) {
Solution sol;
EXPECT_TRUE(sol.findRedundantConnection({}).empty());
}
cpp复制/**
* @class UnionFind
* @brief 并查集数据结构实现,支持路径压缩和按秩合并优化
*
* @param size 初始元素个数
* @method find 查找元素所在集合的根节点
* @method unionSet 合并两个元素所在的集合
*/
class UnionFind {
// ...实现...
};
/**
* @brief 查找无向图中导致环形成的冗余边
*
* @param edges 边列表,每条边表示为[node1, node2]
* @return vector<int> 冗余边,如果有多条返回输入中最后出现的
*
* @note 时间复杂度O(Nα(N)),空间复杂度O(N)
*/
vector<int> findRedundantConnection(vector<vector<int>>& edges) {
// ...实现...
}
python复制class UnionFind:
def __init__(self, size):
self.parent = list(range(size))
self.rank = [0] * size
def find(self, x):
if self.parent[x] != x:
self.parent[x] = self.find(self.parent[x])
return self.parent[x]
def union(self, x, y):
x_root = self.find(x)
y_root = self.find(y)
if x_root == y_root:
return False
if self.rank[x_root] < self.rank[y_root]:
self.parent[x_root] = y_root
else:
self.parent[y_root] = x_root
if self.rank[x_root] == self.rank[y_root]:
self.rank[x_root] += 1
return True
class Solution:
def findRedundantConnection(self, edges):
uf = UnionFind(len(edges)+1)
for u, v in edges:
if not uf.union(u, v):
return [u, v]
return []
java复制class UnionFind {
private int[] parent;
private int[] rank;
public UnionFind(int size) {
parent = new int[size];
rank = new int[size];
for(int i = 0; i < size; i++) {
parent[i] = i;
}
}
public int find(int x) {
if(parent[x] != x) {
parent[x] = find(parent[x]);
}
return parent[x];
}
public boolean union(int x, int y) {
int xRoot = find(x);
int yRoot = find(y);
if(xRoot == yRoot) return false;
if(rank[xRoot] < rank[yRoot]) {
parent[xRoot] = yRoot;
} else {
parent[yRoot] = xRoot;
if(rank[xRoot] == rank[yRoot]) {
rank[xRoot]++;
}
}
return true;
}
}
class Solution {
public int[] findRedundantConnection(int[][] edges) {
UnionFind uf = new UnionFind(edges.length + 1);
for(int[] edge : edges) {
if(!uf.union(edge[0], edge[1])) {
return edge;
}
}
return new int[0];
}
}
| 特性 | C++ | Python | Java |
|---|---|---|---|
| 初始化语法 | vector |
list(range(size)) | new int[size] |
| 内存管理 | 手动/RAII | GC | GC |
| 性能 | 最高 | 中等 | 较高 |
| 代码简洁度 | 中等 | 最简洁 | 较冗长 |
| 工程化支持 | 强大 | 良好 | 优秀 |
基础阶段:
优化阶段:
应用阶段:
基础应用:
中等难度:
高级应用:
在实际编码练习中,我发现并查集的实现虽然简单,但优化后的版本性能提升非常显著。特别是在处理大规模数据时,双重优化的并查集几乎比朴素版本快一个数量级。这让我深刻理解了算法优化的重要性——有时候简单的几行代码优化,就能带来巨大的性能提升。