题目描述

✅ 138. 随机链表的复制

题意分析

给定带有 next 和 random 两种指针的链表,创建一条全新的链表。每个原节点恰好对应一个新节点,值相同,两个指针的目标关系也要对应;副本的任何指针都不能指回原节点。

random 可以指向前面的节点、后面的节点、自己或空节点,也可能有多个节点指向同一个目标。节点值允许重复,因此不能用值来识别目标,必须保留“原节点对应哪个副本”的关系。

传入的是头指针,题面用下标展示随机指向只是输入输出的表示方式。返回副本头节点,空链表返回空;使用交织法临时改变原链表时,结束前还要恢复原连接。

解法:原地交织复制

核心思路

[!blue]

复制的难点不是创建相同值的节点,而是把每条旧指针的目标换成对应的新节点。交织法把这份对应关系临时存到原链表中:在每个原节点后面插入它的副本,于是对任意原节点 cur,cur.next 都是唯一对应的副本。

第一遍只负责创建节点并插入,暂不处理 random。因为随机目标可能在后面,如果尚未创建其副本,就无法正确连接。遍历时从刚插入副本的 next 走到下一个原节点,避免把副本再次复制。

全部副本就位后,第二遍翻译随机指针。cur.next 是当前节点的副本,cur.random.next 是随机目标的副本,因此将前者的 random 指向后者即可。随机目标为空时保留空;目标为自身或被多个节点共享时,也直接遵循同一对应关系。

第三遍拆分两条链。先保存副本及它后面的下一个原节点,再让当前原节点跳过副本、接回原后继;副本则跳过下一个原节点、接到其副本。按从左到右的顺序拆分时,下一个原节点仍与副本相邻,因此这个位置关系仍可用于接线。所有随机指针已在第二遍连接完成,拆分不会再影响它们。

这种方法省去了额外映射表,但会临时修改原链表。后面的哈希映射法直接保存对应关系,整个复制过程不需要改变原节点。

解题步骤

  1. 头节点为空时直接返回空。
  2. 沿原链表遍历,为每个原节点创建副本并插在其后;保存原后继,继续处理下一个原节点。
  3. 再沿原节点遍历:随机目标非空时,令当前副本的随机指针指向目标原节点后面的副本。
  4. 保存新头 head.next,开始拆分。每次先记录当前副本和下一个原节点,再恢复原节点的 next,并连接副本的 next。
  5. 拆分完成后返回保存的新头,原链表同时恢复原样。

代码实现

class Solution {
    public Node copyRandomList(Node head) {
        if (head == null) {
            return null;
        }

        for (Node cur = head; cur != null; ) {
            // 先为每个原节点插入紧邻副本,建立位置映射。
            Node copy = new Node(cur.val);

            copy.next = cur.next;
            cur.next = copy;
            cur = copy.next;
        }

        for (Node cur = head; cur != null; cur = cur.next.next) {
            if (cur.random != null) {
                // 随机目标原节点的后继,就是它对应的副本。
                cur.next.random = cur.random.next;
            }
        }

        Node newHead = head.next;

        for (Node cur = head; cur != null; ) {
            Node copy = cur.next;
            // 拆分前保存下一个原节点,同时恢复原链与连接副本链。
            Node nextOriginal = copy.next;

            cur.next = nextOriginal;
            copy.next = nextOriginal == null ? null : nextOriginal.next;
            cur = nextOriginal;
        }

        return newHead;
    }
}
func copyRandomList(head *Node) *Node {
    if head == nil {
        return nil
    }

    for cur := head; cur != nil; {
        // 先为每个原节点插入紧邻副本,建立位置映射。
        copyNode := &Node{Val: cur.Val, Next: cur.Next}
        cur.Next = copyNode
        cur = copyNode.Next
    }

    for cur := head; cur != nil; cur = cur.Next.Next {
        if cur.Random != nil {
            // 随机目标原节点的后继,就是它对应的副本。
            cur.Next.Random = cur.Random.Next
        }
    }

    newHead := head.Next
    for cur := head; cur != nil; {
        copyNode := cur.Next
        // 拆分前保存下一个原节点,同时恢复原链与连接副本链。
        nextOriginal := copyNode.Next
        cur.Next = nextOriginal
        if nextOriginal != nil {
            copyNode.Next = nextOriginal.Next
        }
        cur = nextOriginal
    }
    return newHead
}

复杂度分析

  • 时间复杂度:$O(n)$,创建、设置随机指针、拆分各遍历一次原链表,每个节点只做常数次操作。
  • 空间复杂度:$O(1)$,只使用固定数量的辅助指针;深拷贝必须创建的 $n$ 个新节点属于输出空间,另计为 $O(n)$。

关键点总结

[!green]

  • 先建立每个原节点到副本的对应,再按照对应关系复制指针目标。
  • 交织位置临时充当映射,节点值相同也不会混淆身份。
  • 随机指针全部设置完后才能拆链,拆分同时恢复原链表和连接新链表。
  • 深拷贝要求连接结构一致,且两条链表的节点集合完全不相交。

补充解法:哈希表保存节点映射

核心思路

[!blue]

用哈希表直接保存“原节点 → 副本节点”。键是节点本身的引用或指针,不是节点值,这样重复值的不同节点仍然拥有各自独立的副本。

第一遍沿 next 创建所有副本并登记映射;第二遍再设置指针。原节点的 next 指向谁,副本的 next 就指向那个节点对应的副本;random 同理。所有副本已提前建立,所以前指、后指、自指都能直接查表,共享的目标也会映射到同一个副本。

空指针没有对应节点,Java 对不存在的键调用 get 得到 null,Go 查找不存在的指针键得到零值 nil,因此可以统一处理空孩子与空头节点。这个方法始终只读取原链表,代价是额外保存一张线性大小的映射表。

解题步骤

  1. 创建从原节点到副本的映射表。
  2. 第一遍遍历原链表,为每个节点创建同值副本,保存对应关系。
  3. 第二遍取出当前副本,分别查找原 next、random 对应的副本并连接。
  4. 返回原头节点映射到的副本;原头为空时自然返回空。

代码实现

class Solution {
    public Node copyRandomList(Node head) {
        Map<Node, Node> copies = new HashMap<>();
        for (Node cur = head; cur != null; cur = cur.next) {
            copies.put(cur, new Node(cur.val));
        }
        for (Node cur = head; cur != null; cur = cur.next) {
            Node copy = copies.get(cur);
            copy.next = copies.get(cur.next);
            copy.random = copies.get(cur.random);
        }
        return copies.get(head);
    }
}
func copyRandomList(head *Node) *Node {
    copies := make(map[*Node]*Node)
    for cur := head; cur != nil; cur = cur.Next {
        copies[cur] = &Node{Val: cur.Val}
    }
    for cur := head; cur != nil; cur = cur.Next {
        copyNode := copies[cur]
        copyNode.Next = copies[cur.Next]
        copyNode.Random = copies[cur.Random]
    }
    return copies[head]
}

复杂度分析

  • 时间复杂度:$O(n)$,两次线性遍历,哈希表插入和查询平均为 $O(1)$。
  • 空间复杂度:$O(n)$,映射表为每个原节点保存一条记录;副本节点占用的 $O(n)$ 输出空间另计。

关键点总结

[!green]

  • 映射键必须反映节点身份,不能拿节点值代替。
  • 先建立全部副本,再连接指针,避免目标副本尚不存在。
  • 通过同一映射翻译 next 和 random,保留原图中的目标共享关系。
  • 原链表全程不变,与交织法的区别在于对应关系保存在哪里。

易错点总结

[!yellow]

  • 直接把原节点的 random 赋给副本,会让新链表指回原链表,得到的不是深拷贝。
  • 用节点值作为映射键,重复值的不同节点会被合并,随机指针关系随之出错。
  • 插入副本后仍按普通的 cur = cur.next 前进,会进入新插入节点并继续复制;应跳到保存的下一个原节点。
  • 副本尚未全部建立就连接随机目标,可能找不到指向后方节点的副本。
  • 原随机指针为空时仍访问其 next,会触发空指针错误。
  • 只拆出副本而不恢复旧节点连接,会把原链表留在交织状态;两个链表的 next 都要处理。

相似题目

题目 难度 关联与区别
133. 克隆图 中等 两题都必须保持原节点到副本节点的一一对应,才能复制共享引用和环;本题把映射临时存入 next,图克隆使用显式哈希表保存映射。
转载与许可
作者
链接 https://hgnulb.github.io/blog/2026/58473049
许可 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处!