【LeetCode Hot100 刷题日记 (32/100)】138. 随机链表的复制 —— 链表、哈希表、原地操作、深拷贝🧠

46 阅读7分钟

📌 题目链接:138. 随机链表的复制 - 力扣(LeetCode) 🔍 难度:中等 | 🏷️ 标签:链表、哈希表、原地操作、深拷贝

⏱️ 目标时间复杂度:O(n)

💾 空间复杂度:O(n)(方法一)或 O(1)(方法二,不计返回值)


在 LeetCode Hot100 中,「随机链表的复制」 是一道非常经典的链表深拷贝问题。它不仅考察你对链表结构的理解,还深入测试了你在指针操作、内存管理、算法空间优化等方面的综合能力。

这道题看似简单,实则暗藏玄机——因为每个节点除了 next 指针外,还有一个 random 指针,可能指向任意位置(包括 null 或自身)。如何在不知道后续节点是否已创建的情况下,正确建立 random 指向? 这就是本题的核心难点。

面试中,此题常被用于考察:

  • 对“深拷贝”与“浅拷贝”的理解;
  • 递归思维与哈希缓存的结合;
  • 空间优化技巧(原地修改链表) —— 这是区分普通选手和高手的关键!

🧩 题目分析

给定一个带有 random 指针的链表,要求返回其完全独立的深拷贝。关键约束:

  • 新链表必须由 全新节点 构成;
  • 所有指针(nextrandom)必须指向新链表中的节点;
  • 不能引用原链表的任何节点
  • random 可能为 null,也可能形成环或自指。

💡 注意:题目输入以 [val, random_index] 形式给出,但函数参数只传入 head 节点,因此我们无法直接通过索引访问,必须依赖指针遍历。


🔑 核心算法及代码讲解

本题有两种主流解法,各有千秋:

✅ 方法一:回溯 + 哈希表(推荐理解)

核心思想
使用 递归 + 哈希缓存 实现“按需创建”。每当访问一个节点,若其拷贝未创建,则新建;否则直接返回已有拷贝。这样可避免重复创建,同时自然处理 random 指向尚未遍历到的节点的问题。

为什么可行?
因为递归天然具有“延迟求值”特性:即使 random 指向后面的节点,递归调用会自动创建它,并通过哈希表缓存结果。

📜 代码详解(含行注释)

// 哈希表:记录原节点 → 新节点的映射
unordered_map<Node*, Node*> cachedNode;

Node* copyRandomList(Node* head) {
    // 基线条件:空节点直接返回
    if (head == nullptr) {
        return nullptr;
    }
    // 若当前节点尚未拷贝
    if (!cachedNode.count(head)) {
        // 创建新节点(仅赋值 val)
        Node* headNew = new Node(head->val);
        // 立即缓存,防止后续递归重复创建(关键!)
        cachedNode[head] = headNew;
        // 递归构建 next 和 random(即使它们还没创建也没关系)
        headNew->next = copyRandomList(head->next);
        headNew->random = copyRandomList(head->random);
    }
    // 返回已缓存的新节点
    return cachedNode[head];
}

优点:逻辑清晰,易于理解,天然处理任意 random 指向。
⚠️ 缺点:需要 O(n) 额外空间存储哈希表。


✅ 方法二:迭代 + 原地拆分(空间最优)

核心思想
不使用额外空间,通过“穿插复制”将新旧节点交织在一起,利用结构关系间接建立 random 映射。

三步走策略

  1. 穿插复制:对每个原节点 A,插入其拷贝 A' 在 A 后面 → A → A' → B → B' ...
  2. 设置 random:A'.random = A.random ? A.random.next : nullptr(因为 A.random 的拷贝就在其后!)
  3. 拆分链表:将原链表与新链表分离,恢复原链表结构,返回新链表头。

📜 代码详解(含行注释)

Node* copyRandomList(Node* head) {
    if (head == nullptr) return nullptr;

    // Step 1: 穿插复制 —— 每个原节点后插入其拷贝
    for (Node* node = head; node != nullptr; node = node->next->next) {
        Node* nodeNew = new Node(node->val);      // 创建拷贝
        nodeNew->next = node->next;               // 新节点指向原节点的下一个
        node->next = nodeNew;                     // 原节点指向新节点
    }

    // Step 2: 设置 random 指针
    for (Node* node = head; node != nullptr; node = node->next->next) {
        Node* nodeNew = node->next;               // 获取拷贝节点
        // 原节点的 random 若存在,则其拷贝是 random->next
        nodeNew->random = (node->random != nullptr) ? node->random->next : nullptr;
    }

    // Step 3: 拆分链表
    Node* headNew = head->next;                   // 新链表头
    for (Node* node = head; node != nullptr; node = node->next) {
        Node* nodeNew = node->next;               // 当前拷贝节点
        node->next = node->next->next;            // 恢复原链表的 next
        // 设置新链表的 next:跳过原节点
        nodeNew->next = (nodeNew->next != nullptr) ? nodeNew->next->next : nullptr;
    }

    return headNew;
}

优点:空间复杂度 O(1)(不计返回值),极致优化!
⚠️ 缺点:代码较复杂,需小心处理指针边界(如 nullptr)。

💬 面试加分点:能手写方法二,说明你对链表操作非常熟练,且具备空间优化意识!


🧭 解题思路(分步拆解)

方法一(哈希回溯)步骤:

  1. 定义全局哈希表 cachedNode,记录原节点到新节点的映射。
  2. 递归函数入口:若 head == nullptr,返回 nullptr
  3. 若当前 head 未被拷贝:
    • 创建新节点 headNew,值为 head->val
    • 立即存入哈希表(防止循环引用或重复创建);
    • 递归设置 headNew->nextheadNew->random
  4. 返回 cachedNode[head]

方法二(原地拆分)步骤:

  1. 第一遍遍历:在每个原节点后插入其拷贝,形成交织链表。
  2. 第二遍遍历:利用 原节点.random.next 即为 拷贝节点.random 的特性,设置所有 random
  3. 第三遍遍历:将交织链表拆分为原链表和新链表,恢复原链表结构,返回新链表头。

📊 算法分析

方法时间复杂度空间复杂度是否修改原链表面试推荐度
哈希回溯O(n)O(n)⭐⭐⭐⭐(易理解)
原地拆分O(n)O(1)临时修改,最终恢复⭐⭐⭐⭐⭐(高阶技巧)

📌 注意:方法二虽然“修改”了原链表,但在最后一步完全恢复了原始结构,因此符合题目“只读输入”的隐含要求。


💻 完整代码(保留你的模板)

#include <bits/stdc++.h>
using namespace std;
using ll = long long;

// Definition for a Node.
class Node {
public:
    int val;
    Node* next;
    Node* random;
    
    Node(int _val) {
        val = _val;
        next = NULL;
        random = NULL;
    }
};

// 方法一:哈希回溯
class Solution1 {
public:
    unordered_map<Node*, Node*> cachedNode;

    Node* copyRandomList(Node* head) {
        if (head == nullptr) {
            return nullptr;
        }
        if (!cachedNode.count(head)) {
            Node* headNew = new Node(head->val);
            cachedNode[head] = headNew;
            headNew->next = copyRandomList(head->next);
            headNew->random = copyRandomList(head->random);
        }
        return cachedNode[head];
    }
};

// 方法二:原地拆分(空间最优)
class Solution2 {
public:
    Node* copyRandomList(Node* head) {
        if (head == nullptr) {
            return nullptr;
        }
        for (Node* node = head; node != nullptr; node = node->next->next) {
            Node* nodeNew = new Node(node->val);
            nodeNew->next = node->next;
            node->next = nodeNew;
        }
        for (Node* node = head; node != nullptr; node = node->next->next) {
            Node* nodeNew = node->next;
            nodeNew->random = (node->random != nullptr) ? node->random->next : nullptr;
        }
        Node* headNew = head->next;
        for (Node* node = head; node != nullptr; node = node->next) {
            Node* nodeNew = node->next;
            node->next = node->next->next;
            nodeNew->next = (nodeNew->next != nullptr) ? nodeNew->next->next : nullptr;
        }
        return headNew;
    }
};

// 测试
signed main(){
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    cout.tie(nullptr);

    // 构建示例1: [[7,null],[13,0],[11,4],[10,2],[1,0]]
    Node* n1 = new Node(7);
    Node* n2 = new Node(13);
    Node* n3 = new Node(11);
    Node* n4 = new Node(10);
    Node* n5 = new Node(1);
    n1->next = n2; n2->next = n3; n3->next = n4; n4->next = n5;
    n1->random = nullptr;
    n2->random = n1;
    n3->random = n5;
    n4->random = n3;
    n5->random = n1;

    Solution2 sol;
    Node* copied = sol.copyRandomList(n1);

    // 简单验证:输出 val 和 random->val(若存在)
    Node* cur = copied;
    while (cur) {
        cout << "Val: " << cur->val << ", Random: ";
        if (cur->random) cout << cur->random->val;
        else cout << "null";
        cout << "\n";
        cur = cur->next;
    }

    return 0;
}

🌟 本期完结,下期见!🔥

👉 点赞收藏加关注,新文更新不迷路。关注专栏【算法】LeetCode Hot100刷题日记,持续为你拆解每一道热题的底层逻辑与面试技巧!
💬 欢迎留言交流你的解法或疑问!一起进步,冲向 Offer!💪


📣 下一期预告:LeetCode 热题 100 第33题 —— 39.合并两个有序链表(简单)

🔹 题目:将两个升序链表合并为一个新的升序链表并返回。
🔹 核心思路:虚拟头节点 + 双指针迭代 / 递归合并。
🔹 考点:链表基础操作、递归思维、哨兵技巧。
🔹 难度:简单,但高频出现在链表面试题开头,务必手写无 bug!

💡 提示:不要新建节点!直接重连指针,实现 O(1) 空间合并!


📌 记住:当你在刷题时,不要只看答案,要像写这篇文章一样,深入思考每一步背后的原理、优化空间和面试价值。这才是真正提升算法能力的方式!