克隆图(LeetCode 133)——用数组做映射的 DFS 解法

一、题目简述(其实主要是知道什么是深拷贝,深拷贝就是自己申请一块内存去把要拷贝的值放进去,而不是像浅拷贝那样,深拷贝 = 两个对象各有一份自己的堆内存,里面的数据内容一样,但地址不同,互不影响。浅拷贝 = 两个对象各有一块自己的内存,但它们内部的指针成员指向同一块堆内存。)

题目给出的是一张无向连通图,节点结构为:

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. 把新节点之间的边连接起来,结构与原图一致

由于图中可能存在环,比如 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;
}

可以理解为三个步骤:

  1. 已经克隆过:直接返回映射中的节点

    复制代码
    if (vim_[node->val] != nullptr) return vim_[node->val];

    这里避免了重复克隆,也避免了在图中有环时递归无限进行。

  2. 第一次遇到这个节点:创建克隆节点并记录映射

    复制代码
    Node* clone = new Node(node->val);
    vim_[node->val] = clone;

    一定要先放到 vim_ 中,再去递归邻居。

    否则遇到环形结构(比如 1 的邻居有 2,2 的邻居又有 1),会重复 new 出很多节点。

  3. 克隆邻居列表

    复制代码
    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)


六、小结

这份解法的特点:

  1. 思路上仍然是"图的 DFS + 映射表",和经典做法一致;

  2. 利用了节点值范围有限的条件,用数组代替 unordered_map,实现更简单、常数更小;

  3. 关键点是:

    • 映射表里存的是克隆节点指针

    • 遇到已访问节点时返回的是克隆节点,而不是原节点

相关推荐
朔北之忘 Clancy12 小时前
2020 年 6 月青少年软编等考 C 语言一级真题解析
c语言·开发语言·c++·学习·算法·青少年编程·题解
_codemonster12 小时前
计算机视觉入门到实战系列(九) SIFT算法(尺度空间、极值点判断)
深度学习·算法·计算机视觉
御承扬12 小时前
鸿蒙原生系列之动画效果(帧动画)
c++·harmonyos·动画效果·ndk ui·鸿蒙原生
梭七y12 小时前
【力扣hot100题】(133)LRU缓存
leetcode·缓存·哈希算法
sinat_2869451913 小时前
AI Coding LSP
人工智能·算法·prompt·transformer
星马梦缘13 小时前
算法与数据结构
数据结构·c++·算法·动态规划·克鲁斯卡尔·kahn
你的冰西瓜13 小时前
C++中的array容器详解
开发语言·c++·stl
2501_9434691513 小时前
【无标题】
数据结构·算法
_codemonster13 小时前
计算机视觉入门到实战系列(八)Harris角点检测算法
python·算法·计算机视觉
Snow_day.14 小时前
有关排列排列组合(1)
数据结构·算法·贪心算法·动态规划·图论