文章目录
第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 value
map[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) = null
B'.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) = null
selfRef'.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指针,设置复制节点的random
if (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 {
@Test
public void testEmptyList() {
Solution solution = new Solution();
Node result = solution.copyRandomList(null);
assertNull(result);
}
@Test
public 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);
}
@Test
public 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));
}
@Test
public 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表示null
Node original = createTestList(values, randomIndices);
Solution solution = new Solution();
Node copy = solution.copyRandomList(original);
assertTrue(validateCopy(original, copy));
}
@Test
public 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));
}
@Test
public 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是否为null
Node 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)空间的优化过程
- 代码实现能力:处理复杂指针关系的编程技巧
通过深入学习这道题目,我们不仅掌握了三种不同的解法,更重要的是理解了深拷贝的本质,学会了在面对复杂数据结构时如何系统性地分析和解决问题。
这些技能在实际的软件开发中有着广泛的应用,无论是系统设计、算法优化,还是日常的编程工作,都能从中受益。