克隆图(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. 关键点是:

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

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

相关推荐
不知所云,1 小时前
1. 开篇简介
c++·vulkan
欧阳x天1 小时前
类和对象(三)
c++
iiiiii111 小时前
【论文阅读笔记】IDAQ:离线元强化学习中的分布内在线适应
论文阅读·人工智能·笔记·学习·算法·机器学习·强化学习
秋深枫叶红1 小时前
嵌入式第二十三篇——数据结构基本概念
linux·数据结构·学习·算法
lilv661 小时前
visual studio 2026中C4996错误 ‘operator <<‘: 被声明为已否决
c++·ide·visual studio
Zsy_0510031 小时前
【数据结构】二叉树介绍及C语言代码实现
c语言·数据结构·算法
Ayanami_Reii1 小时前
基础数学算法-移棋子游戏
数学·算法·游戏·博弈论·sg函数
谁刺我心1 小时前
蓝桥杯C++常用STL
c++·算法·蓝桥杯
wubba lubba dub dub7501 小时前
第二十七周 学习周报
学习·算法·机器学习