Java详解LeetCode 热题 100(32):LeetCode 138. 随机链表的复制
文章目录
- 第1章:题目描述
- 1.1 题目原文
- 1.2 示例分析
- 示例1:
- 示例2:
- 示例3:
- 1.3 约束条件
- 1.4 链表节点定义
- 第2章:理解题目
- 2.1 核心概念
- 2.1.1 深拷贝 vs 浅拷贝
- 2.1.2 随机指针的挑战
- 2.2 问题可视化
- 2.3 核心挑战
- 第3章:解法一 - 哈希表映射法(两次遍历)
- 3.1 算法思路
- 3.2 算法步骤
- 3.3 Java实现
- 3.4 执行演示
- 3.5 优缺点分析
- 第4章:解法二 - 递归+哈希表法
- 4.1 算法思路
- 4.2 算法特点
- 4.3 Java实现
- 4.4 递归过程可视化
- 4.5 处理循环引用
- 4.6 优缺点分析
- 第5章:解法三 - 原地复制法(O(1)空间)
- 5.1 算法思路
- 5.2 三步骤详解
- 步骤1:插入新节点
- 步骤2:设置random指针
- 步骤3:分离链表
- 5.3 Java实现
- 5.4 详细执行演示
- 5.5 关键技巧解析
- 5.5.1 为什么要先插入所有节点?
- 5.5.2 random指针的巧妙设置
- 5.6 优缺点分析
- 第6章:完整测试用例
- 6.1 测试框架
- 6.2 基础测试用例
- 6.3 边界测试用例
- 第7章:算法复杂度对比
- 7.1 时间复杂度分析
- 7.2 空间复杂度分析
- 7.3 实际性能测试
- 7.4 选择建议
- 第8章:常见错误与调试技巧
- 8.1 常见错误类型
- 8.1.1 指针错误
- 8.1.2 映射错误
- 8.1.3 原地复制法特有错误
- 8.2 调试技巧
- 8.2.1 可视化调试
- 8.2.2 单步调试示例
- 8.3 测试驱动开发
- 第9章:相关题目与拓展
- 9.1 LeetCode相关题目
- 9.1.1 链表复制类题目
- 9.1.2 链表操作类题目
- 9.2 算法模式扩展
- 9.2.1 深拷贝模式
- 9.2.2 原地算法模式
- 9.3 实际应用场景
- 9.3.1 对象序列化
- 9.3.2 缓存系统
- 9.3.3 状态管理
- 第10章:学习建议与总结
- 10.1 学习步骤建议
- 10.1.1 初学者路径
- 10.1.2 进阶学习
- 10.2 面试要点
- 10.2.1 常见面试问题
- 10.2.2 回答技巧
- 10.3 实际应用价值
- 10.3.1 软件开发中的应用
- 10.3.2 算法思维的培养
- 10.4 总结
第1章:题目描述
1.1 题目原文
给你一个长度为 n
的链表,每个节点包含一个额外增加的随机指针 random
,该指针可以指向链表中的任何节点或空节点。
构造这个链表的 深拷贝。深拷贝应该正好由 n
个 全新 节点组成,其中每个新节点的值都设为其对应的原节点的值。新节点的 next
指针和 random
指针也都应指向复制链表中的新节点,并使原链表和复制链表中的这些指针能够表示相同的链表状态。复制链表中的指针都不应指向原链表中的节点。
例如,如果原链表中有 X
和 Y
两个节点,其中 X.random --> Y
。那么在复制链表中对应的两个节点 x
和 y
,同样有 x.random --> y
。
返回复制链表的头节点。
1.2 示例分析
示例1:
输入:head = [[7,null],[13,0],[11,4],[10,2],[1,0]]
输出:[[7,null],[13,0],[11,4],[10,2],[1,0]]
可视化表示:
原链表:
7 -> 13 -> 11 -> 10 -> 1
| | | | |
null 7 1 11 7复制链表:
7' -> 13' -> 11' -> 10' -> 1'
| | | | |
null 7' 1' 11' 7'
示例2:
输入:head = [[1,1],[2,1]]
输出:[[1,1],[2,1]]
可视化表示:
原链表:
1 -> 2
| |
2 2复制链表:
1' -> 2'
| |
2' 2'
示例3:
输入:head = [[3,null],[3,0],[3,null]]
输出:[[3,null],[3,0],[3,null]]
可视化表示:
原链表:
3 -> 3 -> 3
| | |
null 3 null复制链表:
3' -> 3' -> 3'
| | |
null 3' null
1.3 约束条件
0 <= n <= 1000
-10^4 <= Node.val <= 10^4
Node.random
为null
或指向链表中的节点
1.4 链表节点定义
// 链表节点定义
class Node {int val;Node next;Node random;public Node(int val) {this.val = val;this.next = null;this.random = null;}
}
第2章:理解题目
2.1 核心概念
2.1.1 深拷贝 vs 浅拷贝
- 浅拷贝:只复制对象的引用,新旧对象共享内存
- 深拷贝:创建全新的对象,包括所有嵌套对象
// 浅拷贝示例(错误做法)
Node shallowCopy = originalNode; // 只是复制引用// 深拷贝示例(正确做法)
Node deepCopy = new Node(originalNode.val);
deepCopy.next = copyRandomList(originalNode.next);
deepCopy.random = copyRandomList(originalNode.random);
2.1.2 随机指针的挑战
随机指针可能指向:
- 链表中的任意节点(包括自己)
null
- 已经遍历过的节点
- 尚未遍历到的节点
2.2 问题可视化
让我们通过一个具体例子来理解问题:
原链表结构:
节点索引: 0 1 2 3 4
节点值: 7 -> 13 -> 11 -> 10 -> 1
random: ↓ ↓ ↓ ↓ ↓null 0 4 2 0详细分析:
- 节点0(值7): random指向null
- 节点1(值13): random指向节点0(值7)
- 节点2(值11): random指向节点4(值1)
- 节点3(值10): random指向节点2(值11)
- 节点4(值1): random指向节点0(值7)
2.3 核心挑战
- 节点映射问题:如何建立原节点与新节点的对应关系?
- 前向引用问题:当前节点的random可能指向还未创建的节点
- 循环引用问题:节点可能相互引用,形成环
- 内存管理:确保不会内存泄漏
第3章:解法一 - 哈希表映射法(两次遍历)
3.1 算法思路
使用HashMap建立原节点与新节点的映射关系,分两次遍历:
- 第一次遍历:创建所有新节点,建立映射关系
- 第二次遍历:设置next和random指针
3.2 算法步骤
步骤1:第一次遍历 - 创建节点映射
for each node in original list:create new node with same valuemap[original_node] = new_node步骤2:第二次遍历 - 设置指针
for each node in original list:new_node = map[original_node]new_node.next = map[original_node.next]new_node.random = map[original_node.random]
3.3 Java实现
import java.util.*;public class Solution {/*** 解法一:哈希表映射法(两次遍历)* 时间复杂度:O(n)* 空间复杂度:O(n)*/public Node copyRandomList(Node head) {if (head == null) {return null;}// 第一次遍历:创建所有新节点并建立映射Map<Node, Node> nodeMap = new HashMap<>();Node current = head;while (current != null) {// 创建新节点Node newNode = new Node(current.val);// 建立原节点到新节点的映射nodeMap.put(current, newNode);current = current.next;}// 第二次遍历:设置next和random指针current = head;while (current != null) {Node newNode = nodeMap.get(current);// 设置next指针if (current.next != null) {newNode.next = nodeMap.get(current.next);}// 设置random指针if (current.random != null) {newNode.random = nodeMap.get(current.random);}current = current.next;}return nodeMap.get(head);}
}
3.4 执行演示
让我们用示例1来演示执行过程:
// 原链表:7->13->11->10->1
// random: null,0,4,2,0// 第一次遍历 - 创建节点映射
Map<Node, Node> nodeMap = new HashMap<>();// 遍历节点7
Node node7 = new Node(7);
nodeMap.put(original_7, node7);// 遍历节点13
Node node13 = new Node(13);
nodeMap.put(original_13, node13);// 遍历节点11
Node node11 = new Node(11);
nodeMap.put(original_11, node11);// 遍历节点10
Node node10 = new Node(10);
nodeMap.put(original_10, node10);// 遍历节点1
Node node1 = new Node(1);
nodeMap.put(original_1, node1);// 第二次遍历 - 设置指针
// 设置节点7
node7.next = nodeMap.get(original_13) = node13;
node7.random = null; // 原节点random为null// 设置节点13
node13.next = nodeMap.get(original_11) = node11;
node13.random = nodeMap.get(original_7) = node7;// 设置节点11
node11.next = nodeMap.get(original_10) = node10;
node11.random = nodeMap.get(original_1) = node1;// 设置节点10
node10.next = nodeMap.get(original_1) = node1;
node10.random = nodeMap.get(original_11) = node11;// 设置节点1
node1.next = null; // 链表末尾
node1.random = nodeMap.get(original_7) = node7;
3.5 优缺点分析
优点:
- 思路清晰,易于理解
- 代码简洁,不易出错
- 时间复杂度最优O(n)
缺点:
- 需要额外的O(n)空间存储映射
- 需要遍历两次链表
第4章:解法二 - 递归+哈希表法
4.1 算法思路
使用递归的方式深度优先创建节点,同时用哈希表避免重复创建。
4.2 算法特点
- 递归创建:遇到节点就递归创建其next和random指向的节点
- 记忆化:用哈希表记录已创建的节点,避免重复创建
- 延迟绑定:在递归返回时自然完成指针绑定
4.3 Java实现
import java.util.*;public class Solution {// 用于记录已创建的节点映射private Map<Node, Node> visited = new HashMap<>();/*** 解法二:递归+哈希表法* 时间复杂度:O(n)* 空间复杂度:O(n) - 包括递归栈和哈希表*/public Node copyRandomList(Node head) {return copyNode(head);}private Node copyNode(Node node) {// 基础情况:空节点if (node == null) {return null;}// 如果节点已经被复制过,直接返回if (visited.containsKey(node)) {return visited.get(node);}// 创建新节点Node newNode = new Node(node.val);// 先将映射关系存入map,防止循环引用导致无限递归visited.put(node, newNode);// 递归复制next和random指针newNode.next = copyNode(node.next);newNode.random = copyNode(node.random);return newNode;}
}
4.4 递归过程可视化
让我们用一个简单例子来理解递归过程:
原链表:A -> B -> null| |B A递归调用栈:
copyNode(A) {创建 A'visited[A] = A'A'.next = copyNode(B) {创建 B'visited[B] = B'B'.next = copyNode(null) = nullB'.random = copyNode(A) {// A已在visited中,直接返回A'return A'}return B'}A'.random = copyNode(B) {// B已在visited中,直接返回B'return B'}return A'
}
4.5 处理循环引用
递归法的一个重要优势是能够优雅地处理循环引用:
// 示例:节点自引用
Node selfRef = new Node(1);
selfRef.random = selfRef; // 指向自己// 递归处理过程
copyNode(selfRef) {创建 selfRef'visited[selfRef] = selfRef' // 关键:先存储映射selfRef'.next = copyNode(null) = nullselfRef'.random = copyNode(selfRef) {// selfRef已在visited中,返回selfRef'return selfRef'}return selfRef'
}
4.6 优缺点分析
优点:
- 代码简洁优雅
- 自然处理循环引用
- 一次遍历完成
缺点:
- 递归深度可能很大(最坏O(n))
- 空间复杂度包括递归栈
- 对于很长的链表可能栈溢出
第5章:解法三 - 原地复制法(O(1)空间)
5.1 算法思路
这是最巧妙的解法,通过在原链表中插入新节点来避免使用额外的哈希表空间。
核心思想:
- 在每个原节点后面插入对应的新节点
- 利用这种结构来设置新节点的random指针
- 分离原链表和新链表
5.2 三步骤详解
步骤1:插入新节点
原链表:A -> B -> C
插入后:A -> A' -> B -> B' -> C -> C'
步骤2:设置random指针
如果A.random = C,则A'.random = C.next = C'
步骤3:分离链表
恢复原链表:A -> B -> C
新链表:A' -> B' -> C'
5.3 Java实现
public class Solution {/*** 解法三:原地复制法(O(1)空间)* 时间复杂度:O(n)* 空间复杂度:O(1)*/public Node copyRandomList(Node head) {if (head == null) {return null;}// 步骤1:在每个节点后插入复制节点insertCopyNodes(head);// 步骤2:设置复制节点的random指针setRandomPointers(head);// 步骤3:分离原链表和复制链表return separateLists(head);}/*** 步骤1:在每个原节点后插入复制节点*/private void insertCopyNodes(Node head) {Node current = head;while (current != null) {// 创建复制节点Node copyNode = new Node(current.val);// 插入到当前节点后面copyNode.next = current.next;current.next = copyNode;// 移动到下一个原节点current = copyNode.next;}}/*** 步骤2:设置复制节点的random指针*/private void setRandomPointers(Node head) {Node current = head;while (current != null) {Node copyNode = current.next;// 如果原节点有random指针,设置复制节点的randomif (current.random != null) {copyNode.random = current.random.next;}// 移动到下一个原节点current = copyNode.next;}}/*** 步骤3:分离原链表和复制链表*/private Node separateLists(Node head) {Node copyHead = head.next;Node current = head;Node copyCurrent = copyHead;while (current != null) {// 恢复原链表current.next = copyCurrent.next;current = current.next;// 构建复制链表if (current != null) {copyCurrent.next = current.next;copyCurrent = copyCurrent.next;}}return copyHead;}
}
5.4 详细执行演示
让我们用示例来演示整个过程:
// 原链表:7->13->11,random: null,0,0// 步骤1:插入复制节点
// 原链表:7 -> 13 -> 11
// 插入后:7 -> 7' -> 13 -> 13' -> 11 -> 11'System.out.println("步骤1完成后的链表结构:");
// 7(random:null) -> 7'(random:null) -> 13(random:7) -> 13'(random:null)
// -> 11(random:7) -> 11'(random:null)// 步骤2:设置random指针
// 对于节点7:random为null,所以7'.random = null
// 对于节点13:random指向7,所以13'.random = 7.next = 7'
// 对于节点11:random指向7,所以11'.random = 7.next = 7'System.out.println("步骤2完成后的random指针:");
// 7'(random:null), 13'(random:7'), 11'(random:7')// 步骤3:分离链表
// 原链表恢复:7 -> 13 -> 11
// 新链表:7' -> 13' -> 11'
5.5 关键技巧解析
5.5.1 为什么要先插入所有节点?
// 错误做法:边插入边设置random
Node copy = new Node(current.val);
copy.random = current.random.next; // 错误!random.next可能还不存在// 正确做法:先插入所有节点,再设置random
// 这样确保所有节点的next都指向对应的复制节点
5.5.2 random指针的巧妙设置
// 原节点的random指向某个节点X
// 复制节点的random应该指向X的复制节点
// 而X的复制节点就是X.next(因为我们在X后面插入了复制节点)
copyNode.random = current.random.next;
5.6 优缺点分析
优点:
- 空间复杂度O(1)(不计算返回值)
- 时间复杂度O(n)
- 不需要额外的数据结构
缺点:
- 算法较复杂,容易出错
- 临时修改了原链表结构
- 代码可读性相对较差
第6章:完整测试用例
6.1 测试框架
public class RandomListTest {/*** 创建测试链表的辅助方法*/public static Node createTestList(int[] values, int[] randomIndices) {if (values.length == 0) return null;// 创建所有节点Node[] nodes = new Node[values.length];for (int i = 0; i < values.length; i++) {nodes[i] = new Node(values[i]);}// 设置next指针for (int i = 0; i < values.length - 1; i++) {nodes[i].next = nodes[i + 1];}// 设置random指针for (int i = 0; i < values.length; i++) {if (randomIndices[i] != -1) {nodes[i].random = nodes[randomIndices[i]];}}return nodes[0];}/*** 验证复制结果的辅助方法*/public static boolean validateCopy(Node original, Node copy) {Map<Node, Integer> originalMap = new HashMap<>();Map<Node, Integer> copyMap = new HashMap<>();// 建立节点到索引的映射int index = 0;Node curr = original;while (curr != null) {originalMap.put(curr, index++);curr = curr.next;}index = 0;curr = copy;while (curr != null) {copyMap.put(curr, index++);curr = curr.next;}// 验证结构一致性Node origCurr = original;Node copyCurr = copy;while (origCurr != null && copyCurr != null) {// 验证值相同if (origCurr.val != copyCurr.val) {return false;}// 验证random指针指向的位置相同Integer origRandomIndex = origCurr.random == null ? null : originalMap.get(origCurr.random);Integer copyRandomIndex = copyCurr.random == null ? null : copyMap.get(copyCurr.random);if (!Objects.equals(origRandomIndex, copyRandomIndex)) {return false;}// 验证没有指向原链表if (copyMap.containsKey(origCurr) || originalMap.containsKey(copyCurr)) {return false;}origCurr = origCurr.next;copyCurr = copyCurr.next;}return origCurr == null && copyCurr == null;}
}
6.2 基础测试用例
public class TestCases {@Testpublic void testEmptyList() {Solution solution = new Solution();Node result = solution.copyRandomList(null);assertNull(result);}@Testpublic void testSingleNode() {// 测试单节点,random指向自己Node node = new Node(1);node.random = node;Solution solution = new Solution();Node result = solution.copyRandomList(node);assertNotNull(result);assertEquals(1, result.val);assertEquals(result, result.random);assertNotSame(node, result);}@Testpublic void testTwoNodes() {// 测试两个节点相互指向int[] values = {1, 2};int[] randomIndices = {1, 0};Node original = createTestList(values, randomIndices);Solution solution = new Solution();Node copy = solution.copyRandomList(original);assertTrue(validateCopy(original, copy));}@Testpublic void testComplexCase() {// 测试复杂情况:[[7,null],[13,0],[11,4],[10,2],[1,0]]int[] values = {7, 13, 11, 10, 1};int[] randomIndices = {-1, 0, 4, 2, 0}; // -1表示nullNode original = createTestList(values, randomIndices);Solution solution = new Solution();Node copy = solution.copyRandomList(original);assertTrue(validateCopy(original, copy));}@Testpublic void testAllRandomNull() {// 测试所有random都为null的情况int[] values = {1, 2, 3, 4, 5};int[] randomIndices = {-1, -1, -1, -1, -1};Node original = createTestList(values, randomIndices);Solution solution = new Solution();Node copy = solution.copyRandomList(original);assertTrue(validateCopy(original, copy));}@Testpublic void testAllRandomSelf() {// 测试所有random都指向自己int[] values = {1, 2, 3};int[] randomIndices = {0, 1, 2};Node original = createTestList(values, randomIndices);Solution solution = new Solution();Node copy = solution.copyRandomList(original);assertTrue(validateCopy(original, copy));}
}
6.3 边界测试用例
@Test
public void testLargeList() {// 测试大链表(1000个节点)int n = 1000;int[] values = new int[n];int[] randomIndices = new int[n];for (int i = 0; i < n; i++) {values[i] = i;randomIndices[i] = (i + 500) % n; // 随机指向}Node original = createTestList(values, randomIndices);Solution solution = new Solution();long startTime = System.currentTimeMillis();Node copy = solution.copyRandomList(original);long endTime = System.currentTimeMillis();assertTrue(validateCopy(original, copy));System.out.println("大链表测试耗时: " + (endTime - startTime) + "ms");
}@Test
public void testDuplicateValues() {// 测试重复值int[] values = {1, 1, 1, 1, 1};int[] randomIndices = {4, 3, 2, 1, 0};Node original = createTestList(values, randomIndices);Solution solution = new Solution();Node copy = solution.copyRandomList(original);assertTrue(validateCopy(original, copy));
}
第7章:算法复杂度对比
7.1 时间复杂度分析
解法 | 时间复杂度 | 遍历次数 | 说明 |
---|---|---|---|
哈希表法(两次遍历) | O(n) | 2次 | 第一次创建映射,第二次设置指针 |
递归+哈希表法 | O(n) | 1次 | 递归遍历,每个节点访问一次 |
原地复制法 | O(n) | 3次 | 插入、设置random、分离各一次 |
7.2 空间复杂度分析
解法 | 空间复杂度 | 额外空间 | 说明 |
---|---|---|---|
哈希表法(两次遍历) | O(n) | HashMap | 存储n个节点映射 |
递归+哈希表法 | O(n) | HashMap + 递归栈 | 映射+最坏O(n)递归深度 |
原地复制法 | O(1) | 无 | 只使用常数额外空间 |
7.3 实际性能测试
public class PerformanceTest {public static void performanceComparison() {int[] sizes = {100, 500, 1000, 5000};for (int size : sizes) {Node testList = generateRandomList(size);// 测试解法一long start1 = System.nanoTime();new Solution1().copyRandomList(testList);long time1 = System.nanoTime() - start1;// 测试解法二long start2 = System.nanoTime();new Solution2().copyRandomList(testList);long time2 = System.nanoTime() - start2;// 测试解法三long start3 = System.nanoTime();new Solution3().copyRandomList(testList);long time3 = System.nanoTime() - start3;System.out.printf("链表大小: %d\n", size);System.out.printf("哈希表法: %.2f ms\n", time1 / 1_000_000.0);System.out.printf("递归法: %.2f ms\n", time2 / 1_000_000.0);System.out.printf("原地法: %.2f ms\n", time3 / 1_000_000.0);System.out.println("---");}}
}
7.4 选择建议
推荐使用场景:
-
哈希表法(两次遍历):
- 代码面试首选
- 逻辑清晰,不易出错
- 适合大多数实际应用
-
递归+哈希表法:
- 代码简洁优雅
- 适合函数式编程风格
- 注意递归深度限制
-
原地复制法:
- 内存受限环境
- 追求极致空间效率
- 有充足时间调试
第8章:常见错误与调试技巧
8.1 常见错误类型
8.1.1 指针错误
// 错误1:忘记处理null情况
public Node copyRandomList(Node head) {Map<Node, Node> map = new HashMap<>();// 错误:没有检查head是否为nullNode current = head;while (current != null) {// ...}
}// 正确做法
public Node copyRandomList(Node head) {if (head == null) return null; // 必须的null检查// ...
}
// 错误2:random指针可能为null
newNode.random = map.get(current.random); // 错误:如果current.random为null会出错// 正确做法
if (current.random != null) {newNode.random = map.get(current.random);
}
8.1.2 映射错误
// 错误3:使用值作为key(当有重复值时会出错)
Map<Integer, Node> map = new HashMap<>(); // 错误:应该用Node作为key
map.put(current.val, newNode);// 正确做法
Map<Node, Node> map = new HashMap<>();
map.put(current, newNode);
8.1.3 原地复制法特有错误
// 错误4:分离链表时指针处理错误
while (current != null) {current.next = current.next.next; // 错误:可能导致空指针current = current.next;
}// 正确做法
while (current != null) {Node copyNode = current.next;current.next = copyNode.next;current = current.next;if (current != null) {copyNode.next = current.next;}
}
8.2 调试技巧
8.2.1 可视化调试
public class DebugHelper {/*** 打印链表结构(包括random指针)*/public static void printList(Node head, String name) {System.out.println("=== " + name + " ===");// 建立节点到索引的映射Map<Node, Integer> nodeIndex = new HashMap<>();Node current = head;int index = 0;while (current != null) {nodeIndex.put(current, index++);current = current.next;}// 打印每个节点的信息current = head;index = 0;while (current != null) {Integer randomIndex = current.random == null ? null : nodeIndex.get(current.random);System.out.printf("节点%d: val=%d, random->%s\n", index, current.val, randomIndex == null ? "null" : "节点" + randomIndex);current = current.next;index++;}System.out.println();}/*** 验证链表完整性*/public static boolean validateIntegrity(Node head) {Set<Node> visited = new HashSet<>();Node current = head;while (current != null) {if (visited.contains(current)) {System.out.println("检测到next指针环!");return false;}visited.add(current);current = current.next;}return true;}
}
8.2.2 单步调试示例
public Node copyRandomListWithDebug(Node head) {if (head == null) return null;Map<Node, Node> map = new HashMap<>();Node current = head;System.out.println("开始第一次遍历...");while (current != null) {Node newNode = new Node(current.val);map.put(current, newNode);System.out.printf("创建节点: val=%d, 映射大小=%d\n", current.val, map.size());current = current.next;}System.out.println("开始第二次遍历...");current = head;while (current != null) {Node newNode = map.get(current);if (current.next != null) {newNode.next = map.get(current.next);System.out.printf("设置next: %d -> %d\n", newNode.val, newNode.next.val);}if (current.random != null) {newNode.random = map.get(current.random);System.out.printf("设置random: %d -> %d\n", newNode.val, newNode.random.val);}current = current.next;}return map.get(head);
}
8.3 测试驱动开发
// 先写测试,再写实现
@Test
public void testSpecificCase() {// 构造特定的测试用例Node node1 = new Node(1);Node node2 = new Node(2);node1.next = node2;node1.random = node2;node2.random = node1;// 调试输出DebugHelper.printList(node1, "原链表");Solution solution = new Solution();Node result = solution.copyRandomList(node1);// 调试输出DebugHelper.printList(result, "复制链表");// 验证结果assertTrue(validateCopy(node1, result));
}
第9章:相关题目与拓展
9.1 LeetCode相关题目
9.1.1 链表复制类题目
- LeetCode 133. 克隆图:类似的深拷贝问题,但是图结构
- LeetCode 1485. 克隆含随机指针的二叉树:树结构的随机指针复制
9.1.2 链表操作类题目
- LeetCode 206. 反转链表:基础链表操作
- LeetCode 92. 反转链表II:部分反转
- LeetCode 25. K个一组翻转链表:分组操作
- LeetCode 143. 重排链表:复杂链表重构
9.2 算法模式扩展
9.2.1 深拷贝模式
/*** 通用深拷贝接口*/
public interface DeepCopyable<T> {T deepCopy();
}/*** 图的深拷贝*/
public class GraphNode implements DeepCopyable<GraphNode> {int val;List<GraphNode> neighbors;public GraphNode deepCopy() {Map<GraphNode, GraphNode> visited = new HashMap<>();return dfs(this, visited);}private GraphNode dfs(GraphNode node, Map<GraphNode, GraphNode> visited) {if (node == null) return null;if (visited.containsKey(node)) return visited.get(node);GraphNode copy = new GraphNode(node.val);visited.put(node, copy);for (GraphNode neighbor : node.neighbors) {copy.neighbors.add(dfs(neighbor, visited));}return copy;}
}
9.2.2 原地算法模式
/*** 原地算法的通用思路:* 1. 利用现有空间存储额外信息* 2. 分阶段处理* 3. 最后恢复原始状态*/// 示例:原地合并两个有序数组
public void merge(int[] nums1, int m, int[] nums2, int n) {// 从后往前合并,避免覆盖int i = m - 1, j = n - 1, k = m + n - 1;while (i >= 0 && j >= 0) {nums1[k--] = nums1[i] > nums2[j] ? nums1[i--] : nums2[j--];}while (j >= 0) {nums1[k--] = nums2[j--];}
}
9.3 实际应用场景
9.3.1 对象序列化
/*** 对象深拷贝在序列化中的应用*/
public class SerializationExample {public static <T> T deepCopy(T original) {try {ByteArrayOutputStream bos = new ByteArrayOutputStream();ObjectOutputStream oos = new ObjectOutputStream(bos);oos.writeObject(original);ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());ObjectInputStream ois = new ObjectInputStream(bis);return (T) ois.readObject();} catch (Exception e) {throw new RuntimeException("深拷贝失败", e);}}
}
9.3.2 缓存系统
/*** 缓存系统中的对象复制*/
public class CacheSystem {private Map<String, Node> cache = new HashMap<>();public Node get(String key) {Node cached = cache.get(key);if (cached == null) return null;// 返回深拷贝,避免外部修改影响缓存return copyRandomList(cached);}public void put(String key, Node value) {// 存储深拷贝,避免外部修改影响缓存cache.put(key, copyRandomList(value));}
}
9.3.3 状态管理
/*** 游戏状态的快照和回滚*/
public class GameState {private Node gameData;private Stack<Node> snapshots = new Stack<>();public void saveSnapshot() {// 保存当前状态的深拷贝snapshots.push(copyRandomList(gameData));}public void rollback() {if (!snapshots.isEmpty()) {gameData = snapshots.pop();}}
}
第10章:学习建议与总结
10.1 学习步骤建议
10.1.1 初学者路径
-
理解基础概念
- 深拷贝 vs 浅拷贝
- 链表的基本操作
- 指针和引用的区别
-
掌握第一种解法
- 从哈希表法开始
- 理解映射的概念
- 练习调试技巧
-
逐步进阶
- 尝试递归解法
- 挑战原地算法
- 对比不同解法
10.1.2 进阶学习
-
算法优化
- 分析时间空间复杂度
- 考虑边界情况
- 代码重构和优化
-
模式识别
- 识别深拷贝模式
- 掌握原地算法技巧
- 理解递归的应用
-
实际应用
- 在项目中应用
- 解决实际问题
- 扩展到其他数据结构
10.2 面试要点
10.2.1 常见面试问题
-
基础问题
- “请解释什么是深拷贝?”
- “为什么不能简单地复制指针?”
- “如何处理循环引用?”
-
算法问题
- “能否用O(1)空间解决?”
- “递归解法的优缺点是什么?”
- “如何优化时间复杂度?”
-
扩展问题
- “如果是图结构怎么办?”
- “如何处理更复杂的数据结构?”
- “在实际项目中如何应用?”
10.2.2 回答技巧
-
思路清晰
面试官:请实现随机链表的深拷贝回答框架: 1. 确认题目理解(什么是深拷贝,random指针的含义) 2. 分析核心难点(节点映射,前向引用) 3. 提出解决方案(哈希表映射) 4. 编写代码实现 5. 分析复杂度 6. 讨论优化方案
-
代码规范
- 变量命名清晰
- 添加必要注释
- 处理边界情况
- 代码结构清晰
10.3 实际应用价值
10.3.1 软件开发中的应用
- 对象克隆:Java中的Cloneable接口实现
- 状态管理:游戏、编辑器的撤销重做功能
- 缓存系统:防止缓存对象被意外修改
- 并发编程:线程安全的对象复制
10.3.2 算法思维的培养
- 分治思想:将复杂问题分解为子问题
- 空间换时间:哈希表映射的经典应用
- 原地算法:在有限空间内解决问题
- 递归思维:自然处理复杂的引用关系
10.4 总结
随机链表的复制是一道经典的链表操作题目,它不仅考查了基本的链表操作能力,更重要的是培养了以下几个方面的能力:
- 问题分析能力:如何将复杂问题分解为可解决的子问题
- 数据结构应用:哈希表在建立映射关系中的巧妙应用
- 算法优化思维:从O(n)空间到O(1)空间的优化过程
- 代码实现能力:处理复杂指针关系的编程技巧
通过深入学习这道题目,我们不仅掌握了三种不同的解法,更重要的是理解了深拷贝的本质,学会了在面对复杂数据结构时如何系统性地分析和解决问题。
这些技能在实际的软件开发中有着广泛的应用,无论是系统设计、算法优化,还是日常的编程工作,都能从中受益。