Java详解LeetCode 热题 100(32):LeetCode 138. 随机链表的复制

发布于:2025-06-15 ⋅ 阅读:(14) ⋅ 点赞:(0)

文章目录

第1章:题目描述

1.1 题目原文

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

构造这个链表的 深拷贝。深拷贝应该正好由 n全新 节点组成,其中每个新节点的值都设为其对应的原节点的值。新节点的 next 指针和 random 指针也都应指向复制链表中的新节点,并使原链表和复制链表中的这些指针能够表示相同的链表状态。复制链表中的指针都不应指向原链表中的节点

例如,如果原链表中有 XY 两个节点,其中 X.random --> Y。那么在复制链表中对应的两个节点 xy,同样有 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.randomnull 或指向链表中的节点

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 随机指针的挑战

随机指针可能指向:

  1. 链表中的任意节点(包括自己)
  2. null
  3. 已经遍历过的节点
  4. 尚未遍历到的节点

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 核心挑战

  1. 节点映射问题:如何建立原节点与新节点的对应关系?
  2. 前向引用问题:当前节点的random可能指向还未创建的节点
  3. 循环引用问题:节点可能相互引用,形成环
  4. 内存管理:确保不会内存泄漏

第3章:解法一 - 哈希表映射法(两次遍历)

3.1 算法思路

使用HashMap建立原节点与新节点的映射关系,分两次遍历:

  1. 第一次遍历:创建所有新节点,建立映射关系
  2. 第二次遍历:设置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 算法思路

这是最巧妙的解法,通过在原链表中插入新节点来避免使用额外的哈希表空间。

核心思想:

  1. 在每个原节点后面插入对应的新节点
  2. 利用这种结构来设置新节点的random指针
  3. 分离原链表和新链表

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 选择建议

推荐使用场景:

  1. 哈希表法(两次遍历)

    • 代码面试首选
    • 逻辑清晰,不易出错
    • 适合大多数实际应用
  2. 递归+哈希表法

    • 代码简洁优雅
    • 适合函数式编程风格
    • 注意递归深度限制
  3. 原地复制法

    • 内存受限环境
    • 追求极致空间效率
    • 有充足时间调试

第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 初学者路径
  1. 理解基础概念

    • 深拷贝 vs 浅拷贝
    • 链表的基本操作
    • 指针和引用的区别
  2. 掌握第一种解法

    • 从哈希表法开始
    • 理解映射的概念
    • 练习调试技巧
  3. 逐步进阶

    • 尝试递归解法
    • 挑战原地算法
    • 对比不同解法
10.1.2 进阶学习
  1. 算法优化

    • 分析时间空间复杂度
    • 考虑边界情况
    • 代码重构和优化
  2. 模式识别

    • 识别深拷贝模式
    • 掌握原地算法技巧
    • 理解递归的应用
  3. 实际应用

    • 在项目中应用
    • 解决实际问题
    • 扩展到其他数据结构

10.2 面试要点

10.2.1 常见面试问题
  1. 基础问题

    • “请解释什么是深拷贝?”
    • “为什么不能简单地复制指针?”
    • “如何处理循环引用?”
  2. 算法问题

    • “能否用O(1)空间解决?”
    • “递归解法的优缺点是什么?”
    • “如何优化时间复杂度?”
  3. 扩展问题

    • “如果是图结构怎么办?”
    • “如何处理更复杂的数据结构?”
    • “在实际项目中如何应用?”
10.2.2 回答技巧
  1. 思路清晰

    面试官:请实现随机链表的深拷贝
    
    回答框架:
    1. 确认题目理解(什么是深拷贝,random指针的含义)
    2. 分析核心难点(节点映射,前向引用)
    3. 提出解决方案(哈希表映射)
    4. 编写代码实现
    5. 分析复杂度
    6. 讨论优化方案
    
  2. 代码规范

    • 变量命名清晰
    • 添加必要注释
    • 处理边界情况
    • 代码结构清晰

10.3 实际应用价值

10.3.1 软件开发中的应用
  1. 对象克隆:Java中的Cloneable接口实现
  2. 状态管理:游戏、编辑器的撤销重做功能
  3. 缓存系统:防止缓存对象被意外修改
  4. 并发编程:线程安全的对象复制
10.3.2 算法思维的培养
  1. 分治思想:将复杂问题分解为子问题
  2. 空间换时间:哈希表映射的经典应用
  3. 原地算法:在有限空间内解决问题
  4. 递归思维:自然处理复杂的引用关系

10.4 总结

随机链表的复制是一道经典的链表操作题目,它不仅考查了基本的链表操作能力,更重要的是培养了以下几个方面的能力:

  1. 问题分析能力:如何将复杂问题分解为可解决的子问题
  2. 数据结构应用:哈希表在建立映射关系中的巧妙应用
  3. 算法优化思维:从O(n)空间到O(1)空间的优化过程
  4. 代码实现能力:处理复杂指针关系的编程技巧

通过深入学习这道题目,我们不仅掌握了三种不同的解法,更重要的是理解了深拷贝的本质,学会了在面对复杂数据结构时如何系统性地分析和解决问题。

这些技能在实际的软件开发中有着广泛的应用,无论是系统设计、算法优化,还是日常的编程工作,都能从中受益。