【力扣100题】18.随机链表的复制

一、题目描述

给定一个链表,每个节点包含:

  • val:节点值
  • next:指向下一个节点
  • random:随机指针,可以指向链表中的任意节点或 null

要求构造深拷贝

  • 复制链表中的每个节点要是全新节点
  • nextrandom 都要指向复制链表中的新节点
  • 原链表和复制链表的指针状态要完全一致

注意: 复制链表中的指针不能指向原链表的节点


二、解题方法总览

方法 核心思路 时间复杂度 空间复杂度
方法一:哈希表法 用哈希表记录 原节点 → 新节点 的映射 O(n) O(n)
方法二:原地插入法 把复制节点插入原节点后面,三步完成 O(n) O(1)
方法三:DFS 深搜法 用递归 + 哈希表处理 random 链 O(n) O(n)

三、方法一:哈希表法(最易理解)

3.1 核心思想

两次遍历:

  • 第一次:遍历原链表,创建所有新节点,存入 hash[原节点] = 新节点
  • 第二次:遍历原链表,从哈希表中查找新节点,设置 nextrandom

3.2 流程图

复制代码
┌─────────────────────────────────────────┐
│              开始                        │
└──────────────┬──────────────────────────┘
               ▼
┌─────────────────────────────────────────┐
│  第一次遍历:遍历原链表                  │
│  对每个原节点 cur:                     │
│    创建新节点 new Node(cur->val)        │
│    hash[cur] = new                     │
└──────────────┬──────────────────────────┘
               ▼
┌─────────────────────────────────────────┐
│  第二次遍历:遍历原链表                  │
│  对每个原节点 cur:                     │
│    new->next  = hash[cur->next]        │
│    new->random = hash[cur->random]     │
└──────────────┬──────────────────────────┘
               ▼
┌─────────────────────────────────────────┐
│         返回 hash[head]                  │
└──────────────┬──────────────────────────┘
               ▼
           【结束】

3.3 代码实现

cpp 复制代码
class Solution {
public:
    Node* copyRandomList(Node* head) {
        if (!head) return nullptr;

        unordered_map<Node*, Node*> hash;

        // ========== 第一次遍历:创建新节点 ==========
        Node* cur = head;
        while (cur) {
            hash[cur] = new Node(cur->val);
            cur = cur->next;
        }

        // ========== 第二次遍历:设置指针 ==========
        cur = head;
        while (cur) {
            hash[cur]->next = hash[cur->next];       // cur->next 可能是 nullptr
            hash[cur]->random = hash[cur->random];  // cur->random 可能是 nullptr
            cur = cur->next;
        }

        return hash[head];
    }
};

3.4 图解示例

复制代码
【原链表】
  1 --→ 2 --→ 3 --→ null
  ↑          ↑
  └──────────┘ random

【哈希表】
  1 → 1'     2 → 2'     3 → 3'

【复制链表】
  1' --→ 2' --→ 3' --→ null
  ↑          ↑
  └──────────┘ random

3.5 复杂度分析

指标 复杂度 说明
时间 O(n) 两次遍历
空间 O(n) 哈希表存储 n 个节点的映射

四、方法二:原地插入法(最优空间)

4.1 核心思想

利用原链表的空间,把新节点直接插入原节点后面:

  • Step 1:在每个原节点后插入一个复制节点
  • Step 2 :设置复制节点的 random(利用 原节点.random.next
  • Step 3:拆分新旧链表,恢复原链表

4.2 完整流程图

复制代码
┌─────────────────────────────────────────┐
│              开始                        │
└──────────────┬──────────────────────────┘
               ▼
┌─────────────────────────────────────────┐
│   Step 1:插入复制节点                   │
│   for cur = head; cur; cur = cur->next->next│
│       cur->next = new Node(cur->val)    │
│       new->next = cur->next             │
└──────────────┬──────────────────────────┘
               ▼
        【链表变化示意】
    插入前: 1 → 2 → 3 → null
    插入后: 1 → 1' → 2 → 2' → 3 → 3' → null
               ▼
┌─────────────────────────────────────────┐
│   Step 2:设置 random                    │
│   for cur = head; cur; cur = cur->next->next│
│       if (cur->random)                  │
│           cur->next->random = cur->random->next│
└──────────────┬──────────────────────────┘
               ▼
        【random 变化示意】
    原节点 1.random → 3
    → 1'.random → 3.next = 3'
               ▼
┌─────────────────────────────────────────┐
│   Step 3:拆分链表                       │
│   创建 dummy -> tail                   │
│   for cur = head; cur; cur = cur->next│
│       tail->next = cur->next            │
│       cur->next = cur->next->next       │
│       tail = tail->next                │
└──────────────┬──────────────────────────┘
               ▼
┌─────────────────────────────────────────┐
│     原链表恢复: 1 → 2 → 3 → null        │
│     新链表:     1' → 2' → 3' → null    │
│     返回 dummy->next                     │
└──────────────┬──────────────────────────┘
               ▼
          【结束】

4.3 代码实现

cpp 复制代码
class Solution {
public:
    Node* copyRandomList(Node* head) {
        if (!head) return nullptr;

        // ========== Step 1:插入复制节点 ==========
        // 在每个原节点后插入一个复制节点
        // 1 -> 1' -> 2 -> 2' -> 3 -> 3'
        for (Node* cur = head; cur; cur = cur->next->next) {
            cur->next = new Node(cur->val, cur->next, NULL);
        }

        // ========== Step 2:设置 random ==========
        // 复制节点的 random = 原节点 random 的下一个
        for (Node* cur = head; cur; cur = cur->next->next) {
            if (cur->random) {
                cur->next->random = cur->random->next;
            }
        }

        // ========== Step 3:拆分链表 ==========
        // 用 tail 尾插法构建新链表,同时恢复原链表
        Node* dummy = new Node(0);
        Node* tail = dummy;

        for (Node* cur = head; cur; cur = cur->next) {
            Node* copy = cur->next;
            tail->next = copy;      // 新链表尾插
            cur->next = copy->next; // 恢复原链表
            tail = tail->next;
        }

        return dummy->next;
    }
};

4.4 完整图解示例

初始链表
复制代码
节点:      1          2          3
val:      7          13         11
random:   null       1          4(null)
next:     2          3          null
Step 1 后:插入复制节点
复制代码
1 → [1'] → 2 → [2'] → 3 → [3']

其中:
- 1'.val = 7
- 2'.val = 13
- 3'.val = 11
Step 2 后:设置 random
复制代码
1.random = null      →  1'.random = null
2.random = 1        →  2'.random = 1.next = 1'
3.random = null      →  3'.random = null
Step 3 后:拆分
复制代码
原链表:  1 → 2 → 3 → null    (已恢复)
新链表:  1' → 2' → 3' → null (目标答案)

4.5 复杂度分析

指标 复杂度 说明
时间 O(n) 三次遍历
空间 O(1) 只用了几个指针变量

五、方法三:DFS 深搜法(递归思维)

5.1 核心思想

用递归处理链表的 random 链:

  • 递归遍历链表,遇到一个新节点就创建它
  • 递归处理 nextrandom 两条链
  • 用哈希表记录 visited,防止重复创建

5.2 流程图

复制代码
┌─────────────────────────────────────────┐
│          copyRandomList(head)            │
└──────────────┬──────────────────────────┘
               ▼
┌─────────────────────────────────────────┐
│    if head == nullptr → return nullptr   │
└──────────────┬──────────────────────────┘
               ▼
┌─────────────────────────────────────────┐
│    if hash contains head → return it    │
└──────────────┬──────────────────────────┘
               ▼
┌─────────────────────────────────────────┐
│    创建 newNode = new Node(head->val)   │
│    hash[head] = newNode                 │
└──────────────┬──────────────────────────┘
               ▼
┌─────────────────────────────────────────┐
│    newNode->next = DFS(head->next)      │
│    newNode->random = DFS(head->random)  │
│    return newNode                        │
└──────────────┬──────────────────────────┘
               ▼
           【返回新链表头节点】

5.3 代码实现

cpp 复制代码
class Solution {
public:
    unordered_map<Node*, Node*> hash;

    Node* copyRandomList(Node* head) {
        if (!head) return nullptr;
        if (hash.count(head)) return hash[head];

        Node* newNode = new Node(head->val);
        hash[head] = newNode;

        newNode->next = copyRandomList(head->next);
        newNode->random = copyRandomList(head->random);

        return newNode;
    }
};

5.4 递归展开示意

复制代码
copyRandomList(1)
  → 创建 1'
  → 1'.next  = copyRandomList(2)
  → 1'.random = copyRandomList(null) → null
  → 返回 1'

  其中 copyRandomList(2):
    → 创建 2'
    → 2'.next  = copyRandomList(3)
    → 2'.random = copyRandomList(1) → hash[1] = 1'
    → 返回 2'

    其中 copyRandomList(3):
      → 创建 3'
      → 3'.next  = copyRandomList(null) → null
      → 3'.random = copyRandomList(null) → null
      → 返回 3'

5.5 复杂度分析

指标 复杂度 说明
时间 O(n) 每个节点访问一次
空间 O(n) 哈希表 + 递归栈深度

六、三种方法对比

6.1 横向对比表

维度 方法一 哈希表 方法二 原地插入 方法三 DFS
代码复杂度 ⭐⭐⭐ 清晰直观 ⭐⭐ 需三步操作 ⭐⭐ 递归需理解
时间复杂度 O(n) O(n) O(n)
空间复杂度 O(n) O(1) O(n)
面试推荐度 ⭐⭐⭐ 思路清晰 ⭐⭐⭐ 最优解 ⭐⭐ 回溯思维
适用场景 工程实现 面试空间优化 有向图问题延伸

6.2 核心差异图

复制代码
【方法一:哈希表法】
原链表:  1 → 2 → 3
              ↓
        ┌──────────┐
        ↓          ↓
      1' ← 2' ← 3'  (独立存在,靠哈希表维护关系)

【方法二:原地插入法】
插入后: 1 → 1' → 2 → 2' → 3 → 3'
拆分后: 1' → 2' → 3'        (独立存在)

【方法三:DFS 递归】
原链表:  1 → 2 → 3
              ↓
        ┌──────────┐
        ↓          ↓
      1' ← 2' ← 3'  (通过递归链维护关系)

6.3 面试选择建议

情况 推荐方法 原因
面试被问到最优空间 方法二 唯一 O(1) 空间解法
想写清晰易懂代码 方法一 两遍遍历,逻辑清晰
random 链复杂(如环) 方法三 递归天然适合处理有向依赖

七、常见面试追问

问题 回答要点
为什么方法二的空间是 O(1)? 只用了 dummy、tail、cur 三个指针,没有额外容器
Step 2 为什么 cur->random->next 一定是新节点? Step 1 已将新节点插入原节点后面,原节点.random 的 next 就是对应新节点
Step 3 为什么要恢复原链表? 题目要求原链表保持不变,新链表独立存在
random 指向 null 怎么办? if (cur->random) 判断,为 null 时不设置,默认就是 null
方法三的递归栈会爆吗? n ≤ 1000,栈深度可控;工程中建议用方法一或方法二
三种方法本质相同点? 都用哈希表记录映射关系;方法二将哈希表「隐式」在 next 链中

八、完整代码汇总

cpp 复制代码
// ========== 方法一:哈希表法 ==========
class Solution {
public:
    Node* copyRandomList(Node* head) {
        if (!head) return nullptr;
        unordered_map<Node*, Node*> hash;

        Node* cur = head;
        while (cur) {
            hash[cur] = new Node(cur->val);
            cur = cur->next;
        }

        cur = head;
        while (cur) {
            hash[cur]->next = hash[cur->next];
            hash[cur]->random = hash[cur->random];
            cur = cur->next;
        }

        return hash[head];
    }
};

// ========== 方法二:原地插入法 ==========
class Solution {
public:
    Node* copyRandomList(Node* head) {
        if (!head) return nullptr;

        // Step 1: 插入复制节点
        for (Node* cur = head; cur; cur = cur->next->next) {
            cur->next = new Node(cur->val, cur->next, NULL);
        }

        // Step 2: 设置 random
        for (Node* cur = head; cur; cur = cur->next->next) {
            if (cur->random) {
                cur->next->random = cur->random->next;
            }
        }

        // Step 3: 拆分链表
        Node* dummy = new Node(0);
        Node* tail = dummy;
        for (Node* cur = head; cur; cur = cur->next) {
            Node* copy = cur->next;
            tail->next = copy;
            cur->next = copy->next;
            tail = tail->next;
        }

        return dummy->next;
    }
};

// ========== 方法三:DFS 递归法 ==========
class Solution {
public:
    unordered_map<Node*, Node*> hash;

    Node* copyRandomList(Node* head) {
        if (!head) return nullptr;
        if (hash.count(head)) return hash[head];

        Node* newNode = new Node(head->val);
        hash[head] = newNode;

        newNode->next = copyRandomList(head->next);
        newNode->random = copyRandomList(head->random);

        return newNode;
    }
};

九、总结

要点 内容
核心难点 random 可能指向尚未创建的节点
方法一 哈希表维护映射,两遍遍历,O(n) 空间
方法二 原地插入 + 拆分,O(1) 空间,面试最优解
方法三 递归 + 哈希表,思维巧妙,适合延伸
记忆口诀 「插、设、分」三步走,拿下原地复制法

相关推荐
南宫萧幕1 小时前
规则基 EMS 仿真实战:SOC 区间划分与 Simulink 闭环建模全解
算法·matlab·控制
多加点辣也没关系1 小时前
数据结构与算法|第二十三章:高级数据结构
数据结构·算法
hoiii1874 小时前
孤立森林 (Isolation Forest) 快速异常检测系统
算法
c++之路5 小时前
适配器模式(Adapter Pattern)
java·算法·适配器模式
吴声子夜歌5 小时前
Java——接口的细节
java·开发语言·算法
myheartgo-on5 小时前
Java—方 法
java·开发语言·算法·青少年编程
宝贝儿好6 小时前
【LLM】第三章:项目实操案例:智能输入法项目
人工智能·python·深度学习·算法·机器人
雪碧聊技术7 小时前
上午题_算法
算法·软考·软件设计师