Replication Magic: A Guide to Deep Copying Graphs and Binary Trees (Part 2: Binary Trees)

Sharwari Phadnis
4 min readJul 16, 2024

--

I hope you enjoyed diving into the world of graph cloning in the previous article (here)! Now, let’s branch out and explore the art of cloning binary trees.

Clone Binary Tree With Random Pointer
A binary tree is given such that each node contains an additional random pointer which could point to any node in the tree or null.

Return a deep copy of the tree.

The tree is represented in the same input/output way as normal binary trees where each node is represented as a pair of [val, random_index] where:

val: an integer representing Node.val

random_index: the index of the node (in the input) where the random pointer points to, or null if it does not point to any node.

You will be given the tree in class Node and you should return the cloned tree in class NodeCopy. NodeCopy class is just a clone of Node class with the same attributes and constructors.

Input: root = [[1,null],null,[4,3],[7,0]]
Output: [[1,null],null,[4,3],[7,0]]
Binary Tree with an additional random pointer

You’ll receive the tree in class Node and send back the replicated binary tree in class NodeCopy. NodeCopy? It’s not just any clone; it mirrors Node with the same attributes and constructors.

Now, this problem seems like déjà vu from our previous challenge (Clone graph), so let’s seek inspiration from there. But before we dive in, let’s explore any hidden concepts and constraints.

Constraints:

  • The number of nodes in the tree is in the range [0, 1000].
  • 1 <= Node.val <= 106

Some crucial details that we should consider: each node’s random pointer can point to a random node in the tree, and don’t forget — the new nodes must be of type NodeCopy.

First, let’s create a class NodeCopy (with the same attributes as Node).

class Node(object):
def __init__(self, val=0, left=None, right=None, random=None):
self.val = val
self.left = left
self.right = right
self.random = random
class NodeCopy(object):
def __init__(self, val=0, left=None, right=None, random=None):
self.val = val
self.left = left
self.right = right
self.random = random

To make it easy and to ensure the binary tree structure is correctly duplicated, we can first clone the nodes using the left and right pointers.

Once the basic binary tree structure is formed, we can then set up the random pointers, leveraging the cloned nodes’ mapping to the original nodes.

To solve this problem, we can first perform a Depth-First Search (DFS) traversal on the original tree to create the basic structure of the new tree.

During this traversal, we clone each node and store the mapping between the original node and its clone in a hashmap. This mapping allows us to efficiently assign the random pointers later.

# Initialize a dictionary to store mappings from original nodes to their copies.
node_map = {}
# Depth-first search function to create deep copies of nodes.
def dfs(node):
if not node:
return None
# If the node has already been copied, return its copy from node_map.
if node in node_map:
return node_map[node]
# Create a new copy of the current node.
node_copy = NodeCopy(node.val)
node_map[node] = node_copy

# Recursively copy the left and right subtrees.
node_copy.left = dfs(node.left)
node_copy.right = dfs(node.right)
# Return the copy of the current node.
return node_copy
root_copy = dfs(root)

After cloning the tree structure, we can iterate through the original nodes and their corresponding clones using the hashmap.

For each node, we set the random pointer of the cloned node to point to the clone of the original node’s random pointer.

# Now set the random pointers
for original_node, copied_node in node_map.items():
if original_node.random:
copied_node.random = node_map[original_node.random]
return root_copy

Bringing it all together, you can find the complete code to clone a Binary Tree:

# Definition for Node.
# class Node(object):
# def __init__(self, val=0, left=None, right=None, random=None):
# self.val = val
# self.left = left
# self.right = right
# self.random = random
class NodeCopy(object):
def __init__(self, val=0, left=None, right=None, random=None):
self.val = val
self.left = left
self.right = right
self.random = random

class Solution(object):
def copyRandomBinaryTree(self, root):
"""
:type root: Node
:rtype: NodeCopy
"""
if not root:
return None
node_map = {}
def dfs(node):
if not node:
return None
if node in node_map:
return node_map[node]

node_copy = NodeCopy(node.val)
node_map[node] = node_copy

node_copy.left = dfs(node.left)
node_copy.right = dfs(node.right)
return node_copy
root_copy = dfs(root)
# Now set the random pointers
for original_node, copied_node in node_map.items():
if original_node.random:
copied_node.random = node_map[original_node.random]
return root_copy

Let’s analyze the time and space complexity of the above code consisting of N nodes

Time complexity:O(N)
where N is the number of nodes in the original binary tree. Each node is processed once during the DFS traversal and once when setting the random pointers.

Space complexity: O(N)
We are storing a clone for each node in the hashmap. Plus, the recursion stack for DFS also requires O(N) space, but the overall space complexity remains O(N).

And that wraps up our journey into cloning binary trees! Hope you enjoyed the article!

References:

https://leetcode.com/explore/

https://leetcode.com/problems/clone-binary-tree-with-random-pointer

https://leetcode.com/problems/clone-graph/description

--

--

Sharwari Phadnis
Sharwari Phadnis

Written by Sharwari Phadnis

Software Engineer in the Bay Area—yes, the tech cliché is real!