题目描述

✅ 1485. 克隆含随机指针的二叉树

题意分析

复制一棵二叉树,每个原节点除了值和左右孩子,还有一个 random 指针,可以指向树中的任意节点或为空。结果中要为每个原节点创建独立的新节点,并保留三种指针之间的对应关系。

这是深拷贝:新节点的 left、right、random 都只能指向新树节点,不能继续引用原树。不同节点的值可能相同,仍需要各自的副本;多个原指针指向同一节点时,新指针也要共享同一个副本。随机指针可以形成自环或相互指向,不要求顺着树的父子方向。

解法:两趟 DFS + 身份映射

核心思路

[!blue]

如果创建节点时就尝试连接随机目标,目标副本可能尚未创建。把工作分成两遍:第一遍只创建全部新节点并建立对应关系,第二遍再统一连接指针,就能完全消除创建顺序的影响。

用映射 copies 保存原节点对象到副本节点对象的对应。第一遍沿原树的 left、right 做迭代 DFS,为每个节点创建只含值的 NodeCopy。树的左右孩子结构本身没有环,每个节点只有一个父节点,因此沿这两种边就能把全部节点创建一次,不需要沿 random 寻找节点。

映射的键必须表达节点身份,而不是数值。Java 使用 IdentityHashMap 明确按对象身份区分,Go 使用节点指针作为键。这样同值节点不会被合并,每个原对象始终对应唯一副本。

第一遍结束后,所有非空随机目标都已经在映射里。第二遍再次沿左右孩子遍历,对每个原节点取出其副本,把三种指针分别设为原目标在映射中的副本。空目标查不到条目,自然得到空指针。

随机关系此时只需要一次查表,不触发新的遍历。自环会查到副本自身,多个来源指向同一目标会查到同一个副本,相互随机指向也都能同时保留。每条新引用都经由映射取得,因此不会残留原树引用,原树本身也不被修改。

两遍使用显式栈完成 DFS,第一遍栈耗尽后重新压入原根即可开始第二遍。最后返回原根在映射中的副本,空树在入口直接返回空。

解题步骤

  1. 原根为空时直接返回空,非空时创建节点身份映射和遍历栈。
  2. 先创建根的副本,再沿左右孩子遍历,为全部原节点建立对应的新节点。
  3. 第一遍结束后,再次把原根放入栈,开始第二次遍历。
  4. 对当前原节点查出副本,再分别用映射设置副本的左孩子、右孩子和随机指针。
  5. 继续沿原树左右孩子推进,不沿随机指针递归。
  6. 返回原根对应的新根。

代码实现

class Solution {
    public NodeCopy copyRandomBinaryTree(Node root) {
        if (root == null) {
            return null;
        }

        // 按原节点身份创建副本,相同值的不同节点不能合并。
        Map<Node, NodeCopy> copies = new IdentityHashMap<>();
        Deque<Node> stack = new ArrayDeque<>();

        copies.put(root, new NodeCopy(root.val));
        stack.push(root);

        while (!stack.isEmpty()) {
            Node node = stack.pop();

            if (node.left != null) {
                copies.put(node.left, new NodeCopy(node.left.val));
                stack.push(node.left);
            }

            if (node.right != null) {
                copies.put(node.right, new NodeCopy(node.right.val));
                stack.push(node.right);
            }
        }

        stack.push(root);

        while (!stack.isEmpty()) {
            Node node = stack.pop();
            NodeCopy copy = copies.get(node);

            // 全部副本已创建,统一映射三个指针;空目标自然对应空指针。
            copy.left = copies.get(node.left);
            copy.right = copies.get(node.right);
            // 只连接副本,不沿随机指针继续遍历;自环也指向副本自身。
            copy.random = copies.get(node.random);

            if (node.left != null) {
                stack.push(node.left);
            }

            if (node.right != null) {
                stack.push(node.right);
            }
        }

        return copies.get(root);
    }
}
func copyRandomBinaryTree(root *Node) *NodeCopy {
    if root == nil {
        return nil
    }

    // 按原节点身份创建副本,相同值的不同节点不能合并。
    copies := map[*Node]*NodeCopy{
        root: {Val: root.Val},
    }
    stack := []*Node{
        root,
    }

    for len(stack) > 0 {
        node := stack[len(stack)-1]
        stack = stack[:len(stack)-1]

        if node.Left != nil {
            copies[node.Left] = &NodeCopy{Val: node.Left.Val}
            stack = append(stack, node.Left)
        }
        if node.Right != nil {
            copies[node.Right] = &NodeCopy{Val: node.Right.Val}
            stack = append(stack, node.Right)
        }
    }

    stack = append(stack, root)
    for len(stack) > 0 {
        node := stack[len(stack)-1]
        stack = stack[:len(stack)-1]
        copy := copies[node]

        // 全部副本已创建,统一映射三个指针;空目标自然对应空指针。
        copy.Left = copies[node.Left]
        copy.Right = copies[node.Right]
        // 只连接副本,不沿随机指针继续遍历;自环也指向副本自身。
        copy.Random = copies[node.Random]

        if node.Left != nil {
            stack = append(stack, node.Left)
        }
        if node.Right != nil {
            stack = append(stack, node.Right)
        }
    }
    return copies[root]
}

复杂度分析

  • 时间复杂度:O(n)。两遍各访问所有节点一次,每个节点创建一次并连接固定三个指针。
  • 空间复杂度:辅助空间 O(n)。映射保存全部对应关系,显式 DFS 栈至多为树高 O(h);返回的新树另占 O(n)。

关键点总结

[!green]

  • 左右孩子定义遍历范围,随机指针只定义需要复制的额外关系。
  • 全部副本先创建,第二遍连接时任意合法目标都已经可以查到。
  • 按对象身份建立一一对应,同时保留不同同值节点和共享目标。
  • 所有新指针都指向映射得到的副本,自环与交叉随机引用无需特殊分支。

易错点总结

[!yellow]

  • 按节点值作为映射键:不同同值节点会被合并,破坏树结构和随机关系。
  • 直接把原随机指针赋给副本:结果仍然引用原树,不满足深拷贝。
  • 第一遍就假定随机目标副本已存在:目标可能在尚未访问的其他分支,应该先建完全部节点。
  • 沿随机指针无记录地继续搜索:随机引用可能成环,原左右孩子已经足以覆盖整棵树。
  • 只复制值或只连接左右孩子:随机指针同样属于需要恢复的结构。
  • 为每次随机引用另建目标节点:会把应当共享的目标拆成多份,必须复用统一映射。

相似题目

题目 难度 关联与区别
138. 随机链表的复制 中等 随机指针要求保存旧节点到新节点映射,才能保留共享引用,不能只复制值。
133. 克隆图 中等 加上随机边后也可视为图克隆,先登记副本再递归邻居以处理环或共享。
转载与许可
作者
链接 https://hgnulb.github.io/blog/2024/16372136
许可 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处!