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
andcloned
).
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:
- We need to traverse two trees simultaneously (
original
andcloned
) - We need to maintain the correspondence between nodes in both trees as we traverse
- When we find the
target
node in theoriginal
tree, we can immediately return the corresponding node from thecloned
tree - DFS allows us to explore each path deeply, checking if the current node in
original
matchestarget
, and returning the correspondingcloned
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.
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:
- We can't compare nodes by value alone (there might be duplicate values)
- We need to find the exact node object reference, not just a similar node
- The position of the
target
node in theoriginal
tree directly maps to the position of its corresponding node in thecloned
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 theoriginal
treeroot2
: Corresponding node in thecloned
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 EvaluatorExample 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:
-
Start at roots:
dfs(original:7, cloned:7)
- Is
original:7 == target:3
? No - Explore left subtree:
dfs(original:3, cloned:3)
- Is
-
At left children:
dfs(original:3, cloned:3)
- Is
original:3 == target:3
? Yes! - Return
cloned:3
← This is our answer
- Is
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:
dfs(7, 7)
- Not target, explore leftdfs(3, 3)
- Not target, explore leftdfs(2, 2)
- Not target, no children, return None- Back to step 2, explore right:
dfs(None, None)
- Returns None - Back to step 1, left returned None, explore right:
dfs(4, 4)
- Not target dfs(6, 6)
- Found target! Returncloned: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.
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
Everything About Trees A tree is a type of graph data structure composed of nodes and edges Its main properties are It is acyclic doesn't contain any cycles There exists a path from the root to any node Has N 1 edges where N is the number of nodes in the tree and
https assets algo monster cover_photos dfs svg Depth First Search Prereqs Recursion Review problems recursion_intro Trees problems tree_intro With a solid understanding of recursion under our belts we are now ready to tackle one of the most useful techniques in coding interviews Depth First Search DFS As the name suggests
https assets algo monster cover_photos bfs svg Breadth First Search on Trees Hopefully by this time you've drunk enough DFS Kool Aid to understand its immense power and seen enough visualization to create a call stack in your mind Now let me introduce the companion spell Breadth First Search BFS
Want a Structured Path to Master System Design Too? Don’t Miss This!