一、题目简述(其实主要是知道什么是深拷贝,深拷贝就是自己申请一块内存去把要拷贝的值放进去,而不是像浅拷贝那样,深拷贝 = 两个对象各有一份自己的堆内存,里面的数据内容一样,但地址不同,互不影响。浅拷贝 = 两个对象各有一块自己的内存,但它们内部的指针成员指向同一块堆内存。)
题目给出的是一张无向连通图,节点结构为:
cpp
class Node {
public:
int val;
vector<Node*> neighbors;
Node() {
val = 0;
neighbors = vector<Node*>();
}
Node(int _val) {
val = _val;
neighbors = vector<Node*>();
}
Node(int _val, vector<Node*> _neighbors) {
val = _val;
neighbors = _neighbors;
}
};
函数签名为:
cpp
Node* cloneGraph(Node* node);
给定原图中的一个起点 node,要求返回这张图的深拷贝(完全独立的一份图)。
二、思路整体概括
要"克隆图",其实就是两件事:
-
每个旧节点对应创建一个新节点;
-
把新节点之间的边连接起来,结构与原图一致。
由于图中可能存在环,比如 1-2-3-4 再回到 1,如果不做"访问记录",递归会在环里绕圈。所以我们需要:
-
一个"映射表":记录"原节点 → 克隆节点";
-
当再次访问到某个已经克隆过的原节点时,直接返回对应的克隆节点,而不是重新 new。
常见写法是用 unordered_map<Node*, Node*> 做映射,你这份代码用的是一个"定长数组映射",利用题目里节点 val 在 1~100 之间的约束,代码会更简单、跑得也更快。
三、代码实现
代码如下:
cpp
class Solution {
private:
Node* vim_[101]; // 下标是节点值 val,存对应的克隆节点指针
public:
Node* cloneGraph(Node* node) {
if (!node) return nullptr;
memset(vim_, 0, sizeof(vim_)); // 初始化映射表
return dfs(node);
}
private:
Node* dfs(Node* node) {
// 如果数组里已经有这个 val,对应的克隆节点说明已经建过了,直接返回
if (vim_[node->val] != nullptr) return vim_[node->val];
// 1. 创建当前节点的克隆节点
Node* clone = new Node(node->val);
// 2. 建立映射:原 node -> 克隆 clone
vim_[node->val] = clone;
// 3. 递归克隆所有邻居,并挂到 clone->neighbors 上
for (auto neighbor : node->neighbors) {
clone->neighbors.push_back(dfs(neighbor));
}
return clone;
}
};
四、逐步讲解
1. 为什么要用 vim_ 这个数组?
题目中节点的 val 范围是 1~100,因此可以用一个大小为 101 的数组:
Node* vim_[101];
含义:
-
vim_[v] == nullptr:表示值为v的节点还没有被克隆过; -
vim_[v] != nullptr:表示原图中所有val == v的节点,都对应这一个克隆节点指针。
这样就同时完成了:
-
"是否访问过"的标记;
-
"原节点 → 新节点"的映射。
不再需要单独的 visited 数组或者 unordered_map。
2. cloneGraph 的入口逻辑
cpp
Node* cloneGraph(Node* node) {
if (!node) return nullptr;
memset(vim_, 0 , sizeof(vim_));
return dfs(node);
}
-
先处理空图的情况:如果
node == nullptr,直接返回空; -
memset把数组清零,确保每次调用都是一个干净的映射表; -
然后从起点
node开始做深度优先搜索克隆。
3. dfs 的核心逻辑
cpp
Node* dfs(Node* node) {
if (vim_[node->val] != nullptr) return vim_[node->val];
Node* clone = new Node(node->val);
vim_[node->val] = clone;
for (auto neighbor : node->neighbors) {
clone->neighbors.push_back(dfs(neighbor));
}
return clone;
}
可以理解为三个步骤:
-
已经克隆过:直接返回映射中的节点
if (vim_[node->val] != nullptr) return vim_[node->val];这里避免了重复克隆,也避免了在图中有环时递归无限进行。
-
第一次遇到这个节点:创建克隆节点并记录映射
Node* clone = new Node(node->val); vim_[node->val] = clone;一定要先放到
vim_中,再去递归邻居。否则遇到环形结构(比如 1 的邻居有 2,2 的邻居又有 1),会重复 new 出很多节点。
-
克隆邻居列表
for (auto neighbor : node->neighbors) { clone->neighbors.push_back(dfs(neighbor)); }对原图中当前节点的每一个邻居,递归调用
dfs去拿到对应的克隆节点 ,然后挂到clone->neighbors上,这样就把边的结构也复制出来了。
五、时间复杂度与空间复杂度
-
时间复杂度 :
每个节点只会被
dfs真正处理一次(之后再访问就直接从vim_中返回),每条边也只会被遍历一次(在 for 循环中)。
所以时间复杂度是
O(N),N 为节点数(边数也是同阶)。 -
空间复杂度:
-
映射表
vim_是常数大小(101),可以看作O(1); -
递归调用栈最坏情况下深度为
O(N);所以总体空间复杂度为
O(N)。
-
六、小结
这份解法的特点:
-
思路上仍然是"图的 DFS + 映射表",和经典做法一致;
-
利用了节点值范围有限的条件,用数组代替
unordered_map,实现更简单、常数更小; -
关键点是:
-
映射表里存的是克隆节点指针;
-
遇到已访问节点时返回的是克隆节点,而不是原节点。
-