泰安市网站建设_网站建设公司_虚拟主机_seo优化
2026/1/16 13:59:32 网站建设 项目流程

Java版LeetCode热题100之「随机链表的复制」详解

本文约9200字,全面深入剖析 LeetCode 第138题《随机链表的复制》。涵盖题目解析、两种解法(哈希表回溯法 & 节点拆分法)、复杂度分析、面试高频问答、实际开发应用场景、相关题目推荐等,助你彻底掌握深拷贝与指针操作的核心技巧。


一、原题回顾

题目描述:
给你一个长度为n的链表,每个节点包含一个额外增加的随机指针random,该指针可以指向链表中的任何节点或空节点。

构造这个链表的深拷贝

  • 深拷贝应由n全新节点组成;
  • 每个新节点的值等于原节点的值;
  • 新节点的nextrandom指针都应指向复制链表中的新节点
  • 不能指向原链表中的任何节点。

节点定义:

classNode{intval;Nodenext;Noderandom;publicNode(intval){this.val=val;this.next=null;this.random=null;}}

示例 1:

输入:head = [[7,null],[13,0],[11,4],[10,2],[1,0]] 输出:[[7,null],[13,0],[11,4],[10,2],[1,0]]

示例 2:

输入:head = [[1,1],[2,1]] 输出:[[1,1],[2,1]]

提示:

  • 0 <= n <= 1000
  • -10⁴ <= Node.val <= 10⁴
  • Node.randomnull或指向链表中的节点。

二、原题分析

这是一道经典的深拷贝(Deep Copy)问题,难点在于随机指针的指向关系重建

核心挑战:

  1. 依赖顺序不确定
    当拷贝节点 A 时,其random可能指向尚未创建的节点 B;
  2. 指针映射关系
    必须建立“原节点 → 新节点”的映射,才能正确设置random
  3. 空间效率要求
    是否能在 O(1) 额外空间内完成?(进阶要求)

关键观察:

  • 普通链表拷贝只需按顺序创建节点并连接next
  • 但随机链表中,random打破了线性依赖,需全局视角处理;
  • 有两种主流思路:
    • 方法一:用哈希表记录映射关系(直观但需 O(n) 空间);
    • 方法二:通过“节点拆分”技巧,在原链表中嵌入新节点(O(1) 空间)。

三、答案构思

方法一:回溯 + 哈希表(推荐初学者)

  • 思想:递归拷贝每个节点,利用哈希表缓存已创建的节点;
  • 流程
    1. 若当前节点为空,返回null
    2. 若当前节点已拷贝,直接从哈希表返回;
    3. 否则,创建新节点,存入哈希表;
    4. 递归拷贝nextrandom,赋值给新节点;
  • 优点:逻辑清晰,代码简洁;
  • 缺点:空间复杂度 O(n)。

方法二:迭代 + 节点拆分(工业级推荐)

  • 思想:在原链表中“穿插”新节点,利用结构隐式存储映射关系;
  • 三步走
    1. 复制节点:将每个原节点后插入其拷贝(A → A’ → B → B’);
    2. 设置 random:A’.random = A.random.next(若 A.random 存在);
    3. 拆分链表:分离原链表和拷贝链表;
  • 优点:O(1) 额外空间,高效;
  • 缺点:修改了原链表结构(需恢复),逻辑稍复杂。

📌两种方法均满足“深拷贝”要求,但方法二更符合进阶要求。


四、完整答案(Java 实现)

方法一:回溯 + 哈希表

/* // Definition for a Node. class Node { int val; Node next; Node random; public Node(int val) { this.val = val; this.next = null; this.random = null; } } */classSolution{// 哈希表:原节点 → 新节点privateMap<Node,Node>cache=newHashMap<>();publicNodecopyRandomList(Nodehead){if(head==null){returnnull;}// 如果已拷贝,直接返回if(cache.containsKey(head)){returncache.get(head);}// 创建新节点NodenewNode=newNode(head.val);cache.put(head,newNode);// 递归拷贝 next 和 randomnewNode.next=copyRandomList(head.next);newNode.random=copyRandomList(head.random);returnnewNode;}}

方法二:迭代 + 节点拆分(O(1) 空间)

classSolution{publicNodecopyRandomList(Nodehead){if(head==null){returnnull;}// 第一步:复制每个节点,并插入到原节点之后// A → B → C => A → A' → B → B' → C → C'Nodecurrent=head;while(current!=null){Nodecopy=newNode(current.val);copy.next=current.next;current.next=copy;current=copy.next;// 跳过新节点,处理下一个原节点}// 第二步:设置新节点的 random 指针current=head;while(current!=null){Nodecopy=current.next;// copy.random = current.random 的下一个节点(即其拷贝)copy.random=(current.random!=null)?current.random.next:null;current=copy.next;// 移动到下一个原节点}// 第三步:拆分链表,恢复原链表,提取拷贝链表NodenewHead=head.next;// 拷贝链表的头current=head;while(current!=null){Nodecopy=current.next;// 恢复原链表:current → nextOriginalcurrent.next=copy.next;// 设置拷贝链表的 nextcopy.next=(copy.next!=null)?copy.next.next:null;current=current.next;// 移动到下一个原节点}returnnewHead;}}

方法二满足进阶要求:O(1) 额外空间!


五、代码分析

方法一:哈希表回溯

  • 缓存机制:避免重复创建同一节点;
  • 递归顺序:先创建节点,再设置指针,天然处理依赖;
  • 边界处理head == null直接返回。

执行过程示例([[1,1],[2,1]]):

  1. 拷贝节点1 → 创建 new1,缓存 {1: new1}
  2. 拷贝 node1.next(即节点2)→ 创建 new2,缓存 {1:new1, 2:new2}
  3. 拷贝 node1.random(即节点2)→ 从缓存取 new2
  4. 拷贝 node2.random(即节点2)→ 从缓存取 new2
  5. 返回 new1

方法二:节点拆分(重点!)

第一步:复制节点
while(current!=null){Nodecopy=newNode(current.val);copy.next=current.next;current.next=copy;current=copy.next;}
  • 原链表:A → B → C
  • 操作后:A → A' → B → B' → C → C'
第二步:设置 random
copy.random=(current.random!=null)?current.random.next:null;
  • 关键洞察:current.random.next就是current.random的拷贝!
  • 例如:若A.random = C,则A'.random = C.next = C'
第三步:拆分链表
// 恢复原链表current.next=copy.next;// A.next = B// 设置拷贝链表copy.next=(copy.next!=null)?copy.next.next:null;// A'.next = B'
  • 最终:原链表恢复为A → B → C,拷贝链表为A' → B' → C'

⚠️注意:方法二临时修改了原链表,但在拆分后已完全恢复,符合题目要求。


六、时间复杂度和空间复杂度分析

方法时间复杂度空间复杂度是否修改原链表
哈希表回溯O(n)O(n)
节点拆分O(n)O(1)临时修改,最终恢复

其中n为链表长度。

  • 时间:两种方法均需遍历链表常数次,O(n)
  • 空间
    • 方法一:哈希表存储 n 个映射 → O(n)
    • 方法二:仅用几个指针 → O(1)(返回值不计入)

💡工程建议:优先使用方法二,尤其在内存受限场景。


七、常见问题解答(FAQ)

Q1:为什么方法二的空间复杂度是 O(1)?


虽然创建了 n 个新节点,但这些是输出所需,不算“额外空间”。
算法本身只用了current,copy,newHead等常数个变量,故空间复杂度 O(1)。


Q2:方法二中,如果原链表有环怎么办?


题目未说明无环,但两种方法均能处理环:

  • 方法一:哈希表可检测重复节点,避免无限递归;
  • 方法二:穿插和拆分操作在环上同样有效(只要正确终止循环)。

Q3:能否先遍历一次记录所有 random 关系,再创建节点?

:可以,但本质仍是哈希表思路。
例如:先用数组存储[val, random_index],再重建。但需额外 O(n) 空间,且不如方法一直接。


Q4:为什么不用 clone() 方法?


Java 的Object.clone()默认是浅拷贝,不会递归拷贝引用对象。
Node类未实现Cloneable接口,无法直接调用。


八、优化思路

1. 迭代替代递归(方法一)

可改写方法一为迭代+栈,避免递归栈开销,但空间仍 O(n),意义不大。

2. 合并步骤(方法二)

可尝试在第二步同时设置nextrandom,减少一次遍历,但代码可读性下降。

3. 工程化增强

  • 输入校验:虽然题目保证范围,但生产代码应检查head有效性;
  • 日志记录:调试时打印映射关系;
  • 单元测试:覆盖空链表、单节点、环形、random=null 等 case。
// 示例测试用例@TestvoidtestCopyRandomList(){Nodehead=newNode(1);head.random=head;Nodecopy=solution.copyRandomList(head);assertNotSame(head,copy);assertSame(copy,copy.random);}

九、数据结构与算法基础知识点回顾

1. 深拷贝 vs 浅拷贝

类型对象本身引用对象
浅拷贝新实例共享引用
深拷贝新实例递归新建

2. 哈希表的应用

  • 作用:O(1) 查找,建立“旧→新”映射;
  • 适用场景:需要快速查找对应关系的问题(如本题、LRU 缓存)。

3. 链表穿插技巧

  • 思想:利用链表结构隐式存储信息;
  • 优势:节省空间,避免额外数据结构;
  • 风险:临时破坏原结构,需确保恢复。

4. 递归与回溯

  • 回溯:在递归返回时修复状态(本题无需修复,因是拷贝);
  • 记忆化:用缓存避免重复计算(本题避免重复创建节点)。

十、面试官提问环节(模拟)

❓ 问题1:你的方法二修改了原链表,是否违反题目要求?

回答
题目要求“返回深拷贝”,并未禁止临时修改原链表。
我的方法在最后一步完全恢复了原链表结构,因此是合规的。
若面试官要求“绝对不修改原链表”,则选择方法一。


❓ 问题2:如果节点的 val 是对象而非 int,你的解法还适用吗?

回答
适用,但需注意:

  • 方法一:new Node(head.val)需确保val也被深拷贝;
  • 方法二:同理,创建新节点时应对val进行深拷贝。
    本题中val是基本类型,无需考虑。

❓ 问题3:能否用 BFS 或 DFS 实现?

回答
本质上,方法一就是DFS(深度优先搜索)
BFS 也可行:用队列存储待处理节点,配合哈希表记录映射。
但链表是线性结构,DFS 更自然,BFS 无明显优势。


❓ 问题4:这个算法能扩展到图的深拷贝吗?

回答
完全可以!本题本质是有向图的深拷贝(链表是特殊图)。
图的深拷贝标准解法正是“哈希表 + DFS/BFS”,与方法一一致。
方法二依赖链表的线性结构,无法直接用于一般图。


十一、这道算法题在实际开发中的应用

虽然“随机链表”看似抽象,但其思想在实际系统中广泛应用:

1.对象序列化与反序列化

  • 将对象图转换为字节流(序列化)时,需处理循环引用;
  • 反序列化时重建对象图,需深拷贝并恢复引用关系。

2.虚拟机/解释器中的对象复制

  • JavaScript 引擎的structuredClone()需处理复杂对象图;
  • JVM 的对象克隆机制需避免浅拷贝陷阱。

3.游戏开发中的状态快照

  • 保存游戏状态时,需深拷贝整个对象图(包括角色、物品、地图等);
  • 恢复状态时重建完全独立的副本。

4.数据库事务的 undo/redo 日志

  • 记录操作前后的对象状态,需深拷贝以隔离变更。

💡核心价值:训练引用关系重建能力,这是处理复杂数据结构的基础。


十二、相关题目推荐

掌握本题后,可挑战以下 LeetCode 题目:

题号题目关联点
133. 克隆图图的深拷贝本题扩展
148. 排序链表链表操作指针技巧
143. 重排链表链表重组多指针
25. K 个一组翻转链表链表反转结构操作
328. 奇偶链表链表分隔指针分离
707. 设计链表链表实现基础巩固

建议先掌握普通链表操作,再挑战带随机指针的变种。


十三、总结与延伸

✅ 本题核心收获

  1. 深拷贝的本质:不仅要复制数据,更要重建引用关系;
  2. 哈希表的威力:O(1) 映射,解决依赖顺序问题;
  3. 链表穿插技巧:用结构隐式存储信息,实现 O(1) 空间;
  4. 工程权衡意识:方法一直观,方法二高效,根据场景选择。

🔮 延伸思考

  • 多线程安全:若原链表被并发修改,如何保证拷贝一致性?(需加锁)
  • 不可变数据结构:在函数式编程中,深拷贝是常态,但可通过共享子结构优化。
  • 序列化协议:Protocol Buffers、JSON 如何处理循环引用?(通常不支持,或需特殊标记)

🌟 最后建议

  • 手写代码:在白板上写出方法二的三步操作,确保无 bug;
  • 讲清思路:面试时先说“我有两种解法”,再对比优劣;
  • 主动测试:提出测试random=null、自引用、长链表等 case,展现全面性。

“深拷贝,拷的是结构,更是关系。”
掌握本题,你就拥有了处理复杂对象图的钥匙。继续前行,算法之路越走越宽!


需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询