Facebook Pixel

1379. Find a Corresponding Node of a Binary Tree in a Clone of That Tree

Problem Description

You are given two binary trees: original and cloned. The cloned tree is an exact copy of the original tree with the same structure and node values. You are also given a reference to a specific node called target that exists in the original tree.

Your task is to find and return a reference to the corresponding node in the cloned tree that matches the target node from the original tree.

The key constraints are:

  • You cannot modify either tree
  • You cannot modify the target node
  • Your answer must be a reference to the actual node in the cloned tree, not just a node with the same value

Since the cloned tree has the exact same structure as the original tree, the corresponding node will be at the same position in the cloned tree as the target node is in the original tree.

For example, if the target node is the left child of the root in the original tree, then you need to return the left child of the root in the cloned tree.

The solution uses a depth-first search (DFS) approach to traverse both trees simultaneously. When the traversal finds the target node in the original tree, it returns the corresponding node from the cloned tree at the same position. The function explores both left and right subtrees recursively until it finds the match.

Flowchart Walkthrough

First, let's pin down the algorithm using the Flowchart. Here's a step-by-step walkthrough:

Is it a graph?

  • Yes: A binary tree is a special type of graph where each node has at most two children and there are no cycles.

Is it a tree?

  • Yes: The problem explicitly states we're working with binary trees (original and cloned).

DFS

  • Since we answered "yes" to the tree question, the flowchart leads us directly to DFS (Depth-First Search).

Conclusion: The flowchart suggests using DFS for this problem.

This makes perfect sense for our problem because:

  1. We need to traverse two trees simultaneously (original and cloned)
  2. We need to maintain the correspondence between nodes in both trees as we traverse
  3. When we find the target node in the original tree, we can immediately return the corresponding node from the cloned tree
  4. DFS allows us to explore each path deeply, checking if the current node in original matches target, and returning the corresponding cloned node if it does

The DFS approach naturally maintains the structural correspondence between the two trees as we traverse them in parallel, making it the ideal algorithm for finding the corresponding node in the cloned tree.

Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

Intuition

The key insight is that the cloned tree has the exact same structure as the original tree. This means that if we traverse both trees in the same way at the same time, we'll always be at corresponding positions in both trees.

Think of it like having two identical mazes and walking through them step-by-step in sync. If you turn left in the first maze, you turn left in the second maze. If you go straight in the first maze, you go straight in the second maze. When you reach your destination in the first maze, you'll be at the corresponding location in the second maze.

This parallel traversal approach naturally emerges when we consider:

  1. We can't compare nodes by value alone (there might be duplicate values)
  2. We need to find the exact node object reference, not just a similar node
  3. The position of the target node in the original tree directly maps to the position of its corresponding node in the cloned tree

The solution becomes elegantly simple: traverse both trees simultaneously using DFS. At each step, we're looking at two nodes - one from original and one from cloned that are at the same structural position. When we find that the current node in original is our target, we know that the current node in cloned is exactly what we're looking for.

The beauty of this approach is that we don't need to store any path information or node mappings. The recursive DFS naturally maintains the correspondence between nodes as we explore the trees. If we go left in original, we go left in cloned. If we go right in original, we go right in cloned. This synchronized movement guarantees that when we find target in original, we're standing at its twin in cloned.

Learn more about Tree, Depth-First Search, Breadth-First Search and Binary Tree patterns.

Solution Approach

The implementation uses a recursive DFS helper function dfs(root1, root2) that traverses both trees simultaneously. Here's how it works:

Function Structure: The dfs function takes two parameters:

  • root1: Current node in the original tree
  • root2: Corresponding node in the cloned tree

Base Case:

if root1 is None:
    return None

If we reach a null node in the original tree, there's nothing to search, so we return None.

Target Found:

if root1 == target:
    return root2

This is the key moment - when we find that the current node in original is the target node, we immediately return the corresponding node root2 from the cloned tree. Since we've been traversing both trees in sync, root2 is guaranteed to be at the same structural position as target.

Recursive Exploration:

return dfs(root1.left, root2.left) or dfs(root1.right, root2.right)

If the current node isn't the target, we recursively search both subtrees:

  • First, we explore the left subtrees of both trees together
  • Then, we explore the right subtrees of both trees together

The or operator is clever here - it returns the first non-None result. If the target is found in the left subtree, that result is returned immediately without exploring the right subtree (short-circuit evaluation). If the left subtree returns None, then we check the right subtree.

Time Complexity: O(n) where n is the number of nodes, as we might need to visit every node in the worst case.

Space Complexity: O(h) where h is the height of the tree, representing the recursion stack depth. In the worst case of a skewed tree, this could be O(n).

The elegance of this solution lies in its simplicity - by maintaining synchronized traversal of both trees, we automatically preserve the structural correspondence between nodes without any additional bookkeeping.

Ready to land your dream job?

Unlock your dream job with a 3-minute evaluator for a personalized learning plan!

Start Evaluator

Example Walkthrough

Let's walk through a concrete example to see how the solution works.

Consider these two identical trees where we need to find the node with value 3 (marked with *) in the cloned tree:

Original Tree:        Cloned Tree:
      7                    7
     / \                  / \
    3*  4                3   4
   /   / \              /   / \
  2   6   9            2   6   9

Our target is the node with value 3 in the original tree (left child of root).

Step-by-step traversal:

  1. Start at roots: dfs(original:7, cloned:7)

    • Is original:7 == target:3? No
    • Explore left subtree: dfs(original:3, cloned:3)
  2. At left children: dfs(original:3, cloned:3)

    • Is original:3 == target:3? Yes!
    • Return cloned:3 ← This is our answer

The function returns immediately when we find the target. We never even explore the right subtree of the root because the or operator short-circuits when the left recursion returns a non-None value.

What if the target was deeper in the tree? Let's say target is the node with value 6:

Original Tree:        Cloned Tree:
      7                    7
     / \                  / \
    3   4                3   4
   /   / \              /   / \
  2   6*  9            2   6   9

The traversal would proceed as:

  1. dfs(7, 7) - Not target, explore left
  2. dfs(3, 3) - Not target, explore left
  3. dfs(2, 2) - Not target, no children, return None
  4. Back to step 2, explore right: dfs(None, None) - Returns None
  5. Back to step 1, left returned None, explore right: dfs(4, 4) - Not target
  6. dfs(6, 6) - Found target! Return cloned:6

The synchronized traversal ensures that when we find node 6 in the original tree, we're at the corresponding node 6 in the cloned tree, maintaining the exact structural position throughout the search.

Solution Implementation

1# Definition for a binary tree node.
2# class TreeNode:
3#     def __init__(self, x):
4#         self.val = x
5#         self.left = None
6#         self.right = None
7
8
9class Solution:
10    def getTargetCopy(
11        self, original: TreeNode, cloned: TreeNode, target: TreeNode
12    ) -> TreeNode:
13        """
14        Find and return the reference to the target node in the cloned tree.
15      
16        Args:
17            original: Root of the original binary tree
18            cloned: Root of the cloned binary tree (identical structure to original)
19            target: Reference to a node in the original tree
20          
21        Returns:
22            Reference to the corresponding node in the cloned tree
23        """
24      
25        def dfs(original_node: TreeNode, cloned_node: TreeNode) -> TreeNode:
26            """
27            Traverse both trees simultaneously to find the target node.
28          
29            Args:
30                original_node: Current node in the original tree
31                cloned_node: Corresponding node in the cloned tree
32              
33            Returns:
34                The corresponding node in cloned tree if target is found, None otherwise
35            """
36            # Base case: if we reach a null node, return None
37            if original_node is None:
38                return None
39          
40            # If current node in original tree is the target,
41            # return the corresponding node in cloned tree
42            if original_node == target:
43                return cloned_node
44          
45            # Recursively search in left subtree
46            left_result = dfs(original_node.left, cloned_node.left)
47            if left_result:
48                return left_result
49          
50            # If not found in left subtree, search in right subtree
51            return dfs(original_node.right, cloned_node.right)
52      
53        # Start the DFS traversal from the roots of both trees
54        return dfs(original, cloned)
55
1/**
2 * Definition for a binary tree node.
3 * public class TreeNode {
4 *     int val;
5 *     TreeNode left;
6 *     TreeNode right;
7 *     TreeNode(int x) { val = x; }
8 * }
9 */
10
11class Solution {
12    // Instance variable to store the target node reference
13    private TreeNode targetNode;
14
15    /**
16     * Finds and returns the corresponding node in the cloned tree that matches the target node in the original tree.
17     * 
18     * @param original The root of the original binary tree
19     * @param cloned The root of the cloned binary tree (identical structure to original)
20     * @param target The target node to find in the original tree
21     * @return The corresponding node in the cloned tree
22     */
23    public final TreeNode getTargetCopy(
24            final TreeNode original, final TreeNode cloned, final TreeNode target) {
25        // Store the target node for comparison during traversal
26        this.targetNode = target;
27        // Start the depth-first search traversal
28        return dfsTraversal(original, cloned);
29    }
30
31    /**
32     * Performs parallel depth-first search on both trees to find the target node.
33     * Traverses both trees simultaneously to maintain position correspondence.
34     * 
35     * @param originalNode Current node in the original tree
36     * @param clonedNode Corresponding node in the cloned tree
37     * @return The cloned node corresponding to the target, or null if not found
38     */
39    private TreeNode dfsTraversal(TreeNode originalNode, TreeNode clonedNode) {
40        // Base case: if we reach a null node, target is not in this path
41        if (originalNode == null) {
42            return null;
43        }
44      
45        // Check if current node in original tree is the target
46        if (originalNode == targetNode) {
47            return clonedNode;
48        }
49      
50        // Recursively search in the left subtree
51        TreeNode leftResult = dfsTraversal(originalNode.left, clonedNode.left);
52      
53        // If target found in left subtree, return it; otherwise, search right subtree
54        return leftResult != null ? leftResult : dfsTraversal(originalNode.right, clonedNode.right);
55    }
56}
57
1/**
2 * Definition for a binary tree node.
3 * struct TreeNode {
4 *     int val;
5 *     TreeNode *left;
6 *     TreeNode *right;
7 *     TreeNode(int x) : val(x), left(NULL), right(NULL) {}
8 * };
9 */
10
11class Solution {
12public:
13    /**
14     * Find the corresponding node in the cloned tree that matches the target node in the original tree.
15     * @param original - The root of the original binary tree
16     * @param cloned - The root of the cloned binary tree (identical structure to original)
17     * @param target - The target node to find in the original tree
18     * @return The corresponding node in the cloned tree
19     */
20    TreeNode* getTargetCopy(TreeNode* original, TreeNode* cloned, TreeNode* target) {
21        // Lambda function for depth-first search traversal
22        // Traverses both trees simultaneously to find the target
23        function<TreeNode*(TreeNode*, TreeNode*)> dfs = [&](TreeNode* originalNode, TreeNode* clonedNode) -> TreeNode* {
24            // Base case: if we've reached a null node, return nullptr
25            if (originalNode == nullptr) {
26                return nullptr;
27            }
28          
29            // If we found the target node in the original tree,
30            // return the corresponding node from the cloned tree
31            if (originalNode == target) {
32                return clonedNode;
33            }
34          
35            // Recursively search the left subtree
36            TreeNode* leftResult = dfs(originalNode->left, clonedNode->left);
37          
38            // If target was found in left subtree, return it
39            // Otherwise, continue searching in the right subtree
40            return leftResult == nullptr ? dfs(originalNode->right, clonedNode->right) : leftResult;
41        };
42      
43        // Start the DFS traversal from the roots of both trees
44        return dfs(original, cloned);
45    }
46};
47
1/**
2 * Definition for a binary tree node.
3 * class TreeNode {
4 *     val: number
5 *     left: TreeNode | null
6 *     right: TreeNode | null
7 *     constructor(val?: number, left?: TreeNode | null, right?: TreeNode | null) {
8 *         this.val = (val===undefined ? 0 : val)
9 *         this.left = (left===undefined ? null : left)
10 *         this.right = (right===undefined ? null : right)
11 *     }
12 * }
13 */
14
15/**
16 * Finds the corresponding node in the cloned tree that matches the target node in the original tree.
17 * @param original - The root of the original binary tree
18 * @param cloned - The root of the cloned binary tree (exact copy of original)
19 * @param target - The target node to find in the original tree
20 * @returns The corresponding node in the cloned tree, or null if not found
21 */
22function getTargetCopy(
23    original: TreeNode | null,
24    cloned: TreeNode | null,
25    target: TreeNode | null,
26): TreeNode | null {
27    /**
28     * Performs depth-first search on both trees simultaneously.
29     * When the target node is found in the original tree, returns the corresponding node from the cloned tree.
30     * @param originalNode - Current node in the original tree
31     * @param clonedNode - Corresponding node in the cloned tree
32     * @returns The cloned node corresponding to the target, or null if not found
33     */
34    const depthFirstSearch = (
35        originalNode: TreeNode | null, 
36        clonedNode: TreeNode | null
37    ): TreeNode | null => {
38        // Base case: if original node is null, return null
39        if (!originalNode) {
40            return null;
41        }
42      
43        // Target found: return the corresponding cloned node
44        if (originalNode === target) {
45            return clonedNode;
46        }
47      
48        // Recursively search left subtree, then right subtree
49        // Returns the first non-null result (short-circuit evaluation)
50        return depthFirstSearch(originalNode.left, clonedNode!.left) || 
51               depthFirstSearch(originalNode.right, clonedNode!.right);
52    };
53  
54    // Start the search from the roots of both trees
55    return depthFirstSearch(original, cloned);
56}
57

Time and Space Complexity

Time Complexity: O(n)

The algorithm performs a depth-first search (DFS) traversal on both trees simultaneously. In the worst case, we need to visit every node in the tree before finding the target node (or the target could be the last node visited). Since we visit each node at most once, the time complexity is O(n), where n is the number of nodes in the tree.

Space Complexity: O(n)

The space complexity is determined by the recursive call stack. In the worst case, the tree could be completely unbalanced (essentially a linked list), where each node has only one child. This would result in a recursion depth equal to the number of nodes n, giving us O(n) space complexity. In the best case of a perfectly balanced tree, the recursion depth would be O(log n), but we consider the worst-case scenario for complexity analysis.

Learn more about how to find time and space complexity quickly.

Common Pitfalls

Pitfall 1: Comparing Node Values Instead of Node References

The Problem: A common mistake is comparing node values (root1.val == target.val) instead of node references (root1 == target). This would be incorrect because multiple nodes might have the same value, and we need to find the exact corresponding node, not just any node with the same value.

Incorrect Implementation:

# WRONG: This compares values, not references
if root1.val == target.val:
    return root2

Why This Fails: Consider a tree where multiple nodes have the value 5. If the target is the second occurrence of 5, comparing values would incorrectly return the first node with value 5 in the cloned tree.

Correct Implementation:

# CORRECT: This compares object references
if root1 == target:
    return root2

Pitfall 2: Not Handling the Short-Circuit Properly

The Problem: Using just or without understanding its behavior, or incorrectly implementing the recursive calls can lead to missing the target node.

Incorrect Implementation:

# WRONG: This always explores both subtrees
left_result = dfs(root1.left, root2.left)
right_result = dfs(root1.right, root2.right)
return left_result or right_result

Why This is Inefficient: While this works, it unnecessarily explores the entire right subtree even when the target is found in the left subtree, wasting computational resources.

Better Implementation:

# Efficient: Short-circuits when target is found
return dfs(root1.left, root2.left) or dfs(root1.right, root2.right)

# Or explicitly with early return:
left_result = dfs(root1.left, root2.left)
if left_result:
    return left_result
return dfs(root1.right, root2.right)

Pitfall 3: Traversing Only One Tree

The Problem: Beginners might try to find the target in the original tree first, then separately traverse the cloned tree to find the corresponding node. This requires additional logic to track the path or position.

Incorrect Approach:

# WRONG: Trying to find path first, then apply it
def getTargetCopy(self, original, cloned, target):
    # Find path to target in original
    path = self.findPath(original, target)
    # Apply path to cloned tree
    return self.followPath(cloned, path)

Why This is Complex: This approach requires maintaining path information and implementing two separate functions, making the solution unnecessarily complicated.

Correct Approach: Traverse both trees simultaneously, maintaining the structural correspondence naturally through synchronized recursion.

Discover Your Strengths and Weaknesses: Take Our 3-Minute Quiz to Tailor Your Study Plan:

What's the output of running the following function using input [30, 20, 10, 100, 33, 12]?

1def fun(arr: List[int]) -> List[int]:
2    import heapq
3    heapq.heapify(arr)
4    res = []
5    for i in range(3):
6        res.append(heapq.heappop(arr))
7    return res
8
1public static int[] fun(int[] arr) {
2    int[] res = new int[3];
3    PriorityQueue<Integer> heap = new PriorityQueue<>();
4    for (int i = 0; i < arr.length; i++) {
5        heap.add(arr[i]);
6    }
7    for (int i = 0; i < 3; i++) {
8        res[i] = heap.poll();
9    }
10    return res;
11}
12
1class HeapItem {
2    constructor(item, priority = item) {
3        this.item = item;
4        this.priority = priority;
5    }
6}
7
8class MinHeap {
9    constructor() {
10        this.heap = [];
11    }
12
13    push(node) {
14        // insert the new node at the end of the heap array
15        this.heap.push(node);
16        // find the correct position for the new node
17        this.bubble_up();
18    }
19
20    bubble_up() {
21        let index = this.heap.length - 1;
22
23        while (index > 0) {
24            const element = this.heap[index];
25            const parentIndex = Math.floor((index - 1) / 2);
26            const parent = this.heap[parentIndex];
27
28            if (parent.priority <= element.priority) break;
29            // if the parent is bigger than the child then swap the parent and child
30            this.heap[index] = parent;
31            this.heap[parentIndex] = element;
32            index = parentIndex;
33        }
34    }
35
36    pop() {
37        const min = this.heap[0];
38        this.heap[0] = this.heap[this.size() - 1];
39        this.heap.pop();
40        this.bubble_down();
41        return min;
42    }
43
44    bubble_down() {
45        let index = 0;
46        let min = index;
47        const n = this.heap.length;
48
49        while (index < n) {
50            const left = 2 * index + 1;
51            const right = left + 1;
52
53            if (left < n && this.heap[left].priority < this.heap[min].priority) {
54                min = left;
55            }
56            if (right < n && this.heap[right].priority < this.heap[min].priority) {
57                min = right;
58            }
59            if (min === index) break;
60            [this.heap[min], this.heap[index]] = [this.heap[index], this.heap[min]];
61            index = min;
62        }
63    }
64
65    peek() {
66        return this.heap[0];
67    }
68
69    size() {
70        return this.heap.length;
71    }
72}
73
74function fun(arr) {
75    const heap = new MinHeap();
76    for (const x of arr) {
77        heap.push(new HeapItem(x));
78    }
79    const res = [];
80    for (let i = 0; i < 3; i++) {
81        res.push(heap.pop().item);
82    }
83    return res;
84}
85

Recommended Readings

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More