(leetcode)力扣100 32随机链表的复制(回溯+哈希/迭代+节点拆分)

题目

给你一个长度为 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 作为传入参数。

数据范围

0 <= n <= 1000

-104 <= Node.val <= 104

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

测试用例

示例1

java 复制代码
输入:head = [[7,null],[13,0],[11,4],[10,2],[1,0]]
输出:[[7,null],[13,0],[11,4],[10,2],[1,0]]

示例2

java 复制代码
输入:head = [[1,1],[2,1]]
输出:[[1,1],[2,1]]

示例3

java 复制代码
输入:head = [[3,null],[3,0],[3,null]]
输出:[[3,null],[3,0],[3,null]]

代码(官解1 哈希回溯,时空复杂度On)

java 复制代码
class Solution {
    Map<Node, Node> cachedNode = new HashMap<Node, Node>();

    public Node copyRandomList(Node head) {
        if (head == null) {
            return null;
        }
        if (!cachedNode.containsKey(head)) {
            Node headNew = new Node(head.val);
            cachedNode.put(head, headNew);
            headNew.next = copyRandomList(head.next);
            headNew.random = copyRandomList(head.random);
        }
        return cachedNode.get(head);
    }
}

题解2(官解2,迭代加节点拆分,空间On,时间O1)

java 复制代码
/*
// Definition for a Node.
class Node {
    int val;
    Node next;
    Node random;

    public Node(int val) {
        this.val = val;
        this.next = null;
        this.random = null;
    }
}
*/

class Solution {
    public Node copyRandomList(Node head) {
        // 0. 特殊情况处理:如果是空链表,直接返回 null
        if(head == null)
            return head;

        // ------------------------------------------------------------
        // 第一步:复制每个节点,并将新节点插入到原节点后面
        // ------------------------------------------------------------
        // 变换前:A -> B -> C
        // 变换后:A -> A' -> B -> B' -> C -> C'
        // 注意循环步长:node = node.next.next,因为中间插了一个新节点,要跳两步
        for(Node node = head; node != null; node = node.next.next){
            Node tnode = new Node(node.val); // 创建新节点(拷贝值)
            tnode.next = node.next;          // 新节点指向原节点的下一个节点
            node.next = tnode;               // 原节点指向新节点
        }

        // ------------------------------------------------------------
        // 第二步:构建新节点的 random 指针
        // ------------------------------------------------------------
        // 此时链表结构是:原 -> 新 -> 原 -> 新
        // 核心逻辑:因为 A' 在 A 后面,B' 在 B 后面。
        // 如果 A.random 指向 B,那么 A'.random 应该指向 B' (即 B.next)
        for(Node node = head; node != null; node = node.next.next){
            Node tnode = node.next; // tnode 就是上面的 A' (新节点)
            if(node.random != null){
                // node.random 是原目标节点,node.random.next 就是该目标的副本
                tnode.random = node.random.next;
            } else {
                tnode.random = null;
            }
        }

        // ------------------------------------------------------------
        // 第三步:拆分链表(还原原链表,提取新链表)
        // ------------------------------------------------------------
        // 目标:
        // 1. 恢复原链表:A -> B -> C
        // 2. 提取新链表:A' -> B' -> C'
        
        Node res = head.next; // 保存新链表的头节点(即 A'),用于最后返回

        // 这里循环的更新条件是 node = node.next
        // 因为在循环体内,我们已经把 node.next 修改回了原链表的下一个节点
        for(Node node = head; node != null; node = node.next){
            Node tnode = node.next; // 当前的新节点
            
            // 1. 恢复原链表的 next 指针
            // 让 A 直接指向 B (跳过 A')
            node.next = node.next.next; 
            
            // 2. 链接新链表的 next 指针
            // 让 A' 指向 B'。如果后面没有节点了,就指向 null
            // (这就是你刚才问的判空逻辑,防止空指针异常)
            tnode.next = (tnode.next != null) ? tnode.next.next : null;
        }

        // 返回新链表的头节点
        return res;
    }
}

思路

这道题反而比昨天的困难标注的题更像一个困难题,主要在之前的链表题中,都只是普通的迭代或者递归,很少设计其他知识,这道题中涉及到了回溯与节点拆分。回溯代码更简单,节点拆分思路更清晰空间更小,建议大家都掌握。

递归虽然代码简单,但是对没有递归思维的人来说很麻烦这里简单拆解一下:

java 复制代码
递归逻辑拆解
函数 copyRandomList(head) 的定义是:给我一个原节点 head,我返回拷贝好的新节点。

代码执行流程如下:

终止条件(Base Case):

如果 head 是 null,说明走到头了,直接返回 null。

查缓存(Memoization):

问:head 这个节点之前来过吗?(在 cachedNode 里吗?)

答:如果来过,说明它对应的副本已经创建好了。直接返回缓存里的副本,千万不要再递归了(否则就死循环了)。

创建与递归(Recursive Step):

如果没来过,说明这是第一次遇到 head。

Create: 立刻创建一个新节点 headNew,值等于 head.val。

Put: (关键) 在继续递归之前,先把这个新老节点的映射关系存入 cachedNode。

为什么要先存? 因为后续递归 head.next 或 head.random 时,它们可能会指回到 head。如果不先存进去,后面递归回来找 head 时发现没有,又会创建一个新的,导致死循环或逻辑错误。

Connect:

headNew.next = copyRandomList(head.next); (去吧,把后面那一串复制好连上)

headNew.random = copyRandomList(head.random); (去吧,把 random 指向的那一串复制好连上)

返回结果:

返回当前创建好的 headNew。
相关推荐
橘颂TA4 小时前
【剑斩OFFER】算法的暴力美学——两数之和
数据结构·算法·leetcode·力扣·结构与算法
云里雾里!4 小时前
力扣 268. 缺失数字 ✅ 【位运算(异或)最优解法】深度解析
算法·leetcode
梭七y5 小时前
【力扣hot100题】(122)回文链表
算法·leetcode·链表
alphaTao6 小时前
LeetCode 每日一题 2025/12/29-2026/1/4
算法·leetcode
ShaderJoy6 小时前
ShaderJoy —— 《对称镜面下的绞肉机》【算法悬疑短文】【Python】
算法·leetcode·面试
有一个好名字6 小时前
力扣-盛最多水的容器
算法·leetcode·职场和发展
im_AMBER7 小时前
Leetcode 95 分割链表
数据结构·c++·笔记·学习·算法·leetcode·链表
梭七y7 小时前
【力扣hot100题】(121)反转链表
算法·leetcode·链表
qq_433554547 小时前
C++字符串hash
c++·算法·哈希算法
黎雁·泠崖7 小时前
【线性表系列入门篇】从顺序表到链表:解锁数据结构的进化密码
c语言·数据结构·链表