力扣解题-138. 复制带随机指针的链表

0 阅读7分钟

力扣解题-138. 复制带随机指针的链表

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

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

例如,如果原链表中有 X 和 Y 两个节点,其中 X.random --> Y 。那么在复制链表中对应的两个节点 x 和 y ,同样有 x.random --> y 。

返回复制链表的头节点。

用一个由 n 个节点组成的链表来表示输入/输出中的链表。每个节点用一个 [val, random_index] 表示:

  • val:一个表示 Node.val 的整数。
  • random_index:随机指针指向的节点索引(范围从 0 到 n-1);如果不指向任何节点,则为 null 。

你的代码 只 接受原链表的头节点 head 作为传入参数。

示例 1:

image.png

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

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

示例 2:

image.png 输入:head = [[1,1],[2,1]]

输出:[[1,1],[2,1]]

示例 3:

image.png

输入:head = [[3,null],[3,0],[3,null]]

输出:[[3,null],[3,0],[3,null]]

提示:

0 <= n <= 1000

-10⁴ <= Node.val <= 10⁴

Node.random 为 null 或指向链表中的节点。

Related Topics

哈希表、链表


第一次解答

解题思路

核心方法:哈希表映射法,通过HashMap建立“原节点→新节点”的映射关系,分两步完成深拷贝:第一步创建所有新节点并存储映射,第二步遍历原链表设置新节点的next和random指针,逻辑直观且易于理解,时间复杂度O(n)、空间复杂度O(n)。

核心逻辑拆解

深拷贝带随机指针链表的核心难点是“random指针无法按顺序直接复制”,哈希表的作用是建立原节点与新节点的一一对应关系:

  1. 边界处理:若原链表头节点head为null,直接返回null(空链表无需拷贝);
  2. 第一步:创建新节点并建立映射
    • 初始化HashMap,定义cur指针从head开始遍历原链表;
    • 遍历过程中,为每个原节点cur创建值相同的新节点new Node(cur.val)
    • cur(原节点)作为key、新节点作为value存入HashMap;
    • 遍历完成后,HashMap中存储了所有原节点与对应新节点的映射;
  3. 第二步:设置新节点的next和random指针
    • 重置cur指针到head,再次遍历原链表;
    • 对每个原节点cur,从HashMap中取出对应的新节点newNode
    • 设置newNode.next:等于HashMap中cur.next对应的新节点(cur.next为null时,map.get返回null,符合要求);
    • 设置newNode.random:等于HashMap中cur.random对应的新节点(同理,null时返回null);
  4. 返回结果:从HashMap中取出head对应的新节点,即为拷贝链表的头节点。
具体步骤(以示例1 head=[[7,null],[13,0],[11,4],[10,2],[1,0]]为例)
步骤原节点新节点HashMap映射next/random设置
17(null)7(null)7→7-
213(0)13(null)13→13-
311(4)11(null)11→11-
410(2)10(null)10→10-
51(0)1(null)1→1-
67(null)7(null)-newNode.next=13,newNode.random=null
713(0)13(null)-newNode.next=11,newNode.random=7
811(4)11(null)-newNode.next=10,newNode.random=1
910(2)10(null)-newNode.next=1,newNode.random=11
101(0)1(null)-newNode.next=null,newNode.random=7
最终拷贝链表与原链表结构完全一致,且所有指针指向新节点。
性能说明
  • 时间复杂度:O(n)(两次线性遍历原链表,HashMap的get/put操作均为O(1));
  • 空间复杂度:O(n)(HashMap存储n个节点的映射关系);
  • 优势:
    1. 逻辑清晰,将“拷贝节点”和“设置指针”拆分为两步,降低问题复杂度;
    2. 无需处理复杂的指针插入/拆分逻辑,新手易实现;
    3. 天然支持random指针指向null或任意节点的场景,无边界遗漏。
    public Node copyRandomList(Node head) {
        if(head==null){
            return null;
        }
        Map<Node,Node> map=new HashMap<>();
        Node cur=head;
        while(cur!=null){
            map.put(cur,new Node(cur.val));
            cur=cur.next;
        }

        cur=head;
        while(cur!=null){
            Node newNode=map.get(cur);
            newNode.next=map.get(cur.next);
            newNode.random=map.get(cur.random);
            cur=cur.next;
        }
        return map.get(head);
    }

示例解答

解题思路

解法1:原地拆分法(最优解,O(1)额外空间)

核心方法:原地插入新节点+拆分链表,无需哈希表,通过在原链表每个节点后插入对应的拷贝节点,利用原节点的指针关系设置拷贝节点的random指针,最后拆分原链表和拷贝链表,时间复杂度O(n)、额外空间复杂度O(1)(满足进阶优化要求)。

核心原理铺垫

该方法的核心是“利用原链表的指针位置,直接关联拷贝节点”:

  1. 第一步:在每个原节点后插入值相同的拷贝节点(如原链表A→B→C变为A→A'→B→B'→C→C');
  2. 第二步:利用原节点的random指针,设置拷贝节点的random(如A.random=B → A'.random=B');
  3. 第三步:拆分链表,将A'→B'→C'从原链表中分离,得到拷贝链表。
核心逻辑拆解
  1. 边界处理:若head为null,返回null;
  2. 第一步:插入拷贝节点
    • 定义cur指针指向head,遍历原链表;
    • 对每个原节点cur,创建拷贝节点copy = new Node(cur.val)
    • copy插入到curcur.next之间(copy.next = cur.next; cur.next = copy);
    • cur移动到cur.next.next(跳过拷贝节点,继续遍历原节点);
  3. 第二步:设置拷贝节点的random指针
    • 重置curhead,再次遍历原链表;
    • 对每个原节点cur,其拷贝节点为cur.next
    • cur.random != null,则cur.next.random = cur.random.next(原节点random的拷贝节点);
    • cur.random == null,拷贝节点的random也为null(无需处理);
  4. 第三步:拆分链表
    • 定义dummy哑节点和copyCur指针指向dummy
    • 重置curhead,遍历原链表,每次取出拷贝节点cur.next接入dummy链表;
    • 恢复原链表的结构(cur.next = cur.next.next),cur继续遍历;
  5. 返回结果:返回dummy.next(拷贝链表的头节点)。
代码实现
public Node copyRandomList(Node head) {
    if (head == null) {
        return null;
    }

    // 第一步:在每个原节点后插入拷贝节点
    Node cur = head;
    while (cur != null) {
        Node copy = new Node(cur.val);
        copy.next = cur.next;
        cur.next = copy;
        cur = copy.next; // 跳过拷贝节点,遍历下一个原节点
    }

    // 第二步:设置拷贝节点的random指针
    cur = head;
    while (cur != null) {
        Node copy = cur.next;
        // 原节点random不为空时,拷贝节点的random指向原random的拷贝节点
        if (cur.random != null) {
            copy.random = cur.random.next;
        }
        cur = copy.next; // 跳过拷贝节点
    }

    // 第三步:拆分原链表和拷贝链表
    cur = head;
    Node dummy = new Node(0);
    Node copyCur = dummy;
    while (cur != null) {
        // 取出拷贝节点
        Node copy = cur.next;
        // 恢复原链表
        cur.next = copy.next;
        // 将拷贝节点接入新链表
        copyCur.next = copy;
        copyCur = copyCur.next;
        // 遍历下一个原节点
        cur = cur.next;
    }

    return dummy.next;
}
性能说明
  • 时间复杂度:O(n)(三次线性遍历原链表,无嵌套操作);
  • 额外空间复杂度:O(1)(仅使用几个指针变量,无哈希表等额外存储);
  • 核心优势:
    1. 无需额外空间,满足进阶优化要求;
    2. 仅操作指针,内存开销极小,执行效率更高;
  • 注意事项:
    1. 拆分链表时需恢复原链表结构,避免破坏输入数据;
    2. 处理random指针时需先判断原节点random是否为null,避免空指针异常。

总结

  1. 哈希表映射法(第一次解答):O(n)时间+O(n)空间,逻辑直观、易于实现,适合新手理解核心思路;
  2. 原地拆分法(最优解):O(n)时间+O(1)额外空间,无需哈希表,通过指针操作完成拷贝,工程首选;
  3. 关键技巧:
    • 核心难点:random指针无法按顺序复制,需通过“映射”或“原地关联”解决;
    • 哈希表法:用空间换时间,降低逻辑复杂度;
    • 原地拆分法:用指针操作替代哈希表,优化空间复杂度,需注意链表拆分时的边界处理。