1305. All Elements in Two Binary Search Trees
Problem Description
You are given two binary search trees, root1
and root2
. Your task is to combine all the integers from both trees and return them in a single list sorted in ascending order.
The problem requires you to:
- Extract all values from the first binary search tree
- Extract all values from the second binary search tree
- Combine all these values together
- Return them as a sorted list in ascending order
Since both inputs are binary search trees (BSTs), their in-order traversal naturally gives values in sorted order. The solution leverages this property by:
- Performing an in-order traversal (left → root → right) on each tree to get two sorted arrays
a
andb
- Merging these two sorted arrays using a two-pointer technique to produce the final sorted result
The merging process compares elements from both arrays and adds the smaller one to the result, similar to the merge step in merge sort. After one array is exhausted, any remaining elements from the other array are appended to complete the merged list.
For example, if root1
contains values [1, 4, 6]
and root2
contains values [2, 3, 5]
, the final output would be [1, 2, 3, 4, 5, 6]
.
Flowchart Walkthrough
First, let's pin down the algorithm using the Flowchart. Here's a step-by-step walkthrough:
Is it a graph?
- Yes: Binary search trees are a special type of graph structure where each node has at most two children (left and right), forming a hierarchical tree structure.
Is it a tree?
- Yes: The problem explicitly states we're working with two binary search trees. Trees are connected acyclic graphs with a root node and parent-child relationships.
DFS
- Conclusion reached: Since we're dealing with trees, the flowchart leads us directly to DFS (Depth-First Search).
The flowchart confirms that DFS is the appropriate pattern for this problem. This makes sense because:
- We need to visit every node in both trees to collect all values
- In-order traversal (a type of DFS) on a BST naturally gives us sorted values
- DFS allows us to recursively traverse the tree structure efficiently, visiting left subtree, then root, then right subtree
The solution implements DFS through the recursive dfs
function that performs in-order traversal on each tree, collecting values in sorted order. This leverages the BST property where in-order traversal yields sorted sequences, which can then be merged efficiently.
Intuition
When we need to combine elements from two binary search trees into a single sorted list, the key insight is recognizing that BSTs have a special property: an in-order traversal naturally produces values in sorted order.
Think about how in-order traversal works: we visit the left subtree first (which contains smaller values), then the current node, then the right subtree (which contains larger values). This left-root-right pattern guarantees that we process nodes in ascending order.
Since we have two BSTs, we can:
- Extract a sorted list from the first tree using in-order traversal
- Extract a sorted list from the second tree using in-order traversal
- Now we have two sorted arrays - this transforms our tree problem into a simpler array problem
At this point, we need to combine two sorted arrays into one. This is a classic merge operation, similar to what's done in merge sort. We use two pointers, one for each array, and at each step we compare the current elements and take the smaller one. This ensures our final result maintains sorted order.
Why is this approach efficient? Instead of collecting all values unsorted and then sorting them (which would take O(n log n)
time), we leverage the BST structure to get already-sorted sequences through DFS traversal in O(n)
time. The merge step is also O(n)
, making our overall solution linear in the total number of nodes.
The beauty of this solution lies in decomposing the problem: first use the tree structure to our advantage (getting sorted sequences via DFS), then solve a simpler problem (merging sorted arrays).
Learn more about Tree, Depth-First Search, Binary Search Tree, Binary Tree and Sorting patterns.
Solution Approach
The solution implements the DFS + Merge approach in two main phases:
Phase 1: Extract sorted values using DFS
The dfs
helper function performs an in-order traversal on each tree:
def dfs(root: Optional[TreeNode], nums: List[int]) -> int:
if root is None:
return
dfs(root.left, nums) # Visit left subtree first
nums.append(root.val) # Process current node
dfs(root.right, nums) # Visit right subtree last
This recursive function follows the left-root-right pattern. For a BST, this guarantees values are collected in ascending order. We call this function twice:
dfs(root1, a)
populates arraya
with sorted values from the first treedfs(root2, b)
populates arrayb
with sorted values from the second tree
Phase 2: Merge two sorted arrays
With two sorted arrays a
and b
, we use the two-pointer technique to merge them:
i = j = 0 ans = [] while i < m and j < n: if a[i] <= b[j]: ans.append(a[i]) i += 1 else: ans.append(b[j]) j += 1
The algorithm maintains pointers i
and j
for arrays a
and b
respectively. At each step:
- Compare
a[i]
withb[j]
- Append the smaller value to the result
- Advance the pointer of the array from which we took the element
After one array is exhausted, we append any remaining elements:
while i < m: ans.append(a[i]) i += 1 while j < n: ans.append(b[j]) j += 1
Time Complexity: O(m + n)
where m
and n
are the number of nodes in each tree. We visit each node once during traversal and process each element once during merging.
Space Complexity: O(m + n)
for storing the values in arrays a
, b
, and the final result ans
.
Ready to land your dream job?
Unlock your dream job with a 5-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through a concrete example to see how the solution works step by step.
Given Trees:
Tree 1: Tree 2: 2 1 / \ / \ 1 4 0 3
Phase 1: Extract sorted values using DFS (in-order traversal)
For Tree 1 (root1):
- Start at root (2)
- Go left to node (1)
- Node 1 has no left child
- Add 1 to array:
a = [1]
- Node 1 has no right child
- Back at root (2), add 2:
a = [1, 2]
- Go right to node (4)
- Node 4 has no left child
- Add 4 to array:
a = [1, 2, 4]
- Node 4 has no right child
- Result:
a = [1, 2, 4]
For Tree 2 (root2):
- Start at root (1)
- Go left to node (0)
- Node 0 has no left child
- Add 0 to array:
b = [0]
- Node 0 has no right child
- Back at root (1), add 1:
b = [0, 1]
- Go right to node (3)
- Node 3 has no left child
- Add 3 to array:
b = [0, 1, 3]
- Node 3 has no right child
- Result:
b = [0, 1, 3]
Phase 2: Merge two sorted arrays
Now merge a = [1, 2, 4]
and b = [0, 1, 3]
:
Step | i | j | a[i] | b[j] | Comparison | Action | Result |
---|---|---|---|---|---|---|---|
1 | 0 | 0 | 1 | 0 | 1 > 0 | Add b[0]=0, j++ | [0] |
2 | 0 | 1 | 1 | 1 | 1 ≤ 1 | Add a[0]=1, i++ | [0, 1] |
3 | 1 | 1 | 2 | 1 | 2 > 1 | Add b[1]=1, j++ | [0, 1, 1] |
4 | 1 | 2 | 2 | 3 | 2 ≤ 3 | Add a[1]=2, i++ | [0, 1, 1, 2] |
5 | 2 | 2 | 4 | 3 | 4 > 3 | Add b[2]=3, j++ | [0, 1, 1, 2, 3] |
Now j = 3 equals length of b, so the first while loop exits. Append remaining elements from a: Add a[2]=4
Final Result: [0, 1, 1, 2, 3, 4]
The algorithm efficiently combines both trees by leveraging the BST property to get sorted sequences through DFS, then merging them in linear time.
Solution Implementation
1# Definition for a binary tree node.
2# class TreeNode:
3# def __init__(self, val=0, left=None, right=None):
4# self.val = val
5# self.left = left
6# self.right = right
7
8from typing import List, Optional
9
10class Solution:
11 def getAllElements(
12 self, root1: Optional[TreeNode], root2: Optional[TreeNode]
13 ) -> List[int]:
14 """
15 Get all elements from two binary search trees in sorted order.
16
17 Args:
18 root1: Root of the first binary search tree
19 root2: Root of the second binary search tree
20
21 Returns:
22 List of all elements from both trees in sorted order
23 """
24
25 def inorder_traversal(root: Optional[TreeNode], elements: List[int]) -> None:
26 """
27 Perform inorder traversal to collect elements in sorted order.
28
29 Args:
30 root: Current node in the tree
31 elements: List to store elements in sorted order
32 """
33 if root is None:
34 return
35
36 # Traverse left subtree
37 inorder_traversal(root.left, elements)
38 # Process current node
39 elements.append(root.val)
40 # Traverse right subtree
41 inorder_traversal(root.right, elements)
42
43 # Collect sorted elements from both trees
44 first_tree_elements = []
45 second_tree_elements = []
46 inorder_traversal(root1, first_tree_elements)
47 inorder_traversal(root2, second_tree_elements)
48
49 # Get lengths of both lists
50 len_first = len(first_tree_elements)
51 len_second = len(second_tree_elements)
52
53 # Initialize pointers for merging
54 pointer_first = 0
55 pointer_second = 0
56 merged_result = []
57
58 # Merge two sorted lists
59 while pointer_first < len_first and pointer_second < len_second:
60 if first_tree_elements[pointer_first] <= second_tree_elements[pointer_second]:
61 merged_result.append(first_tree_elements[pointer_first])
62 pointer_first += 1
63 else:
64 merged_result.append(second_tree_elements[pointer_second])
65 pointer_second += 1
66
67 # Add remaining elements from first list
68 while pointer_first < len_first:
69 merged_result.append(first_tree_elements[pointer_first])
70 pointer_first += 1
71
72 # Add remaining elements from second list
73 while pointer_second < len_second:
74 merged_result.append(second_tree_elements[pointer_second])
75 pointer_second += 1
76
77 return merged_result
78
1/**
2 * Definition for a binary tree node.
3 * public class TreeNode {
4 * int val;
5 * TreeNode left;
6 * TreeNode right;
7 * TreeNode() {}
8 * TreeNode(int val) { this.val = val; }
9 * TreeNode(int val, TreeNode left, TreeNode right) {
10 * this.val = val;
11 * this.left = left;
12 * this.right = right;
13 * }
14 * }
15 */
16class Solution {
17 /**
18 * Retrieves all elements from two binary search trees in sorted order.
19 *
20 * @param root1 The root of the first binary search tree
21 * @param root2 The root of the second binary search tree
22 * @return A list containing all elements from both trees in ascending order
23 */
24 public List<Integer> getAllElements(TreeNode root1, TreeNode root2) {
25 // Lists to store inorder traversal of each tree
26 List<Integer> tree1Elements = new ArrayList<>();
27 List<Integer> tree2Elements = new ArrayList<>();
28
29 // Perform inorder traversal on both trees to get sorted elements
30 inorderTraversal(root1, tree1Elements);
31 inorderTraversal(root2, tree2Elements);
32
33 // Get sizes of both lists for iteration
34 int size1 = tree1Elements.size();
35 int size2 = tree2Elements.size();
36
37 // Initialize pointers for merging
38 int pointer1 = 0;
39 int pointer2 = 0;
40
41 // Result list to store merged elements
42 List<Integer> mergedResult = new ArrayList<>();
43
44 // Merge two sorted lists using two-pointer technique
45 while (pointer1 < size1 && pointer2 < size2) {
46 if (tree1Elements.get(pointer1) <= tree2Elements.get(pointer2)) {
47 mergedResult.add(tree1Elements.get(pointer1));
48 pointer1++;
49 } else {
50 mergedResult.add(tree2Elements.get(pointer2));
51 pointer2++;
52 }
53 }
54
55 // Add remaining elements from first list if any
56 while (pointer1 < size1) {
57 mergedResult.add(tree1Elements.get(pointer1));
58 pointer1++;
59 }
60
61 // Add remaining elements from second list if any
62 while (pointer2 < size2) {
63 mergedResult.add(tree2Elements.get(pointer2));
64 pointer2++;
65 }
66
67 return mergedResult;
68 }
69
70 /**
71 * Performs inorder traversal on a binary tree and stores elements in a list.
72 * Inorder traversal visits nodes in ascending order for BST.
73 *
74 * @param root The root node of the binary tree
75 * @param elements The list to store traversed elements
76 */
77 private void inorderTraversal(TreeNode root, List<Integer> elements) {
78 // Base case: if node is null, return
79 if (root == null) {
80 return;
81 }
82
83 // Traverse left subtree
84 inorderTraversal(root.left, elements);
85
86 // Process current node
87 elements.add(root.val);
88
89 // Traverse right subtree
90 inorderTraversal(root.right, elements);
91 }
92}
93
1/**
2 * Definition for a binary tree node.
3 * struct TreeNode {
4 * int val;
5 * TreeNode *left;
6 * TreeNode *right;
7 * TreeNode() : val(0), left(nullptr), right(nullptr) {}
8 * TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
9 * TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
10 * };
11 */
12class Solution {
13public:
14 /**
15 * Merges all elements from two binary search trees into a sorted list
16 * @param root1 - Root of the first binary search tree
17 * @param root2 - Root of the second binary search tree
18 * @return A sorted vector containing all elements from both trees
19 */
20 vector<int> getAllElements(TreeNode* root1, TreeNode* root2) {
21 vector<int> firstTreeElements;
22 vector<int> secondTreeElements;
23 vector<int> mergedResult;
24
25 // Perform in-order traversal to get sorted elements from both trees
26 inorderTraversal(root1, firstTreeElements);
27 inorderTraversal(root2, secondTreeElements);
28
29 // Merge two sorted arrays using two-pointer technique
30 int firstIndex = 0;
31 int secondIndex = 0;
32
33 // Compare and add smaller element to result
34 while (firstIndex < firstTreeElements.size() && secondIndex < secondTreeElements.size()) {
35 if (firstTreeElements[firstIndex] <= secondTreeElements[secondIndex]) {
36 mergedResult.push_back(firstTreeElements[firstIndex]);
37 firstIndex++;
38 } else {
39 mergedResult.push_back(secondTreeElements[secondIndex]);
40 secondIndex++;
41 }
42 }
43
44 // Add remaining elements from first tree (if any)
45 while (firstIndex < firstTreeElements.size()) {
46 mergedResult.push_back(firstTreeElements[firstIndex]);
47 firstIndex++;
48 }
49
50 // Add remaining elements from second tree (if any)
51 while (secondIndex < secondTreeElements.size()) {
52 mergedResult.push_back(secondTreeElements[secondIndex]);
53 secondIndex++;
54 }
55
56 return mergedResult;
57 }
58
59private:
60 /**
61 * Performs in-order traversal of binary search tree
62 * In-order traversal of BST gives elements in sorted order
63 * @param root - Current node being processed
64 * @param elements - Vector to store traversed elements in sorted order
65 */
66 void inorderTraversal(TreeNode* root, vector<int>& elements) {
67 // Base case: if node is null, return
68 if (root == nullptr) {
69 return;
70 }
71
72 // Traverse left subtree first
73 inorderTraversal(root->left, elements);
74
75 // Process current node
76 elements.push_back(root->val);
77
78 // Traverse right subtree last
79 inorderTraversal(root->right, elements);
80 }
81};
82
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 * Performs in-order traversal on a binary tree and collects values in sorted order
17 * @param root - The root node of the binary tree
18 * @param values - Array to store the traversed values
19 */
20function inOrderTraversal(root: TreeNode | null, values: number[]): void {
21 if (!root) {
22 return;
23 }
24
25 // Traverse left subtree
26 inOrderTraversal(root.left, values);
27
28 // Process current node
29 values.push(root.val);
30
31 // Traverse right subtree
32 inOrderTraversal(root.right, values);
33}
34
35/**
36 * Returns all elements from two binary search trees in sorted order
37 * @param root1 - Root of the first binary search tree
38 * @param root2 - Root of the second binary search tree
39 * @returns Array containing all elements from both trees in ascending order
40 */
41function getAllElements(root1: TreeNode | null, root2: TreeNode | null): number[] {
42 // Arrays to store sorted values from each tree
43 const firstTreeValues: number[] = [];
44 const secondTreeValues: number[] = [];
45
46 // Perform in-order traversal on both trees to get sorted values
47 inOrderTraversal(root1, firstTreeValues);
48 inOrderTraversal(root2, secondTreeValues);
49
50 // Get lengths of both arrays
51 const firstLength: number = firstTreeValues.length;
52 const secondLength: number = secondTreeValues.length;
53
54 // Result array to store merged sorted values
55 const mergedResult: number[] = [];
56
57 // Two pointers for merging sorted arrays
58 let firstIndex: number = 0;
59 let secondIndex: number = 0;
60
61 // Merge two sorted arrays using two-pointer technique
62 while (firstIndex < firstLength && secondIndex < secondLength) {
63 if (firstTreeValues[firstIndex] < secondTreeValues[secondIndex]) {
64 mergedResult.push(firstTreeValues[firstIndex]);
65 firstIndex++;
66 } else {
67 mergedResult.push(secondTreeValues[secondIndex]);
68 secondIndex++;
69 }
70 }
71
72 // Add remaining elements from first array, if any
73 while (firstIndex < firstLength) {
74 mergedResult.push(firstTreeValues[firstIndex]);
75 firstIndex++;
76 }
77
78 // Add remaining elements from second array, if any
79 while (secondIndex < secondLength) {
80 mergedResult.push(secondTreeValues[secondIndex]);
81 secondIndex++;
82 }
83
84 return mergedResult;
85}
86
Time and Space Complexity
Time Complexity: O(n + m)
The algorithm consists of three main phases:
- In-order traversal of the first tree: The
dfs
function visits each node inroot1
exactly once, takingO(n)
time wheren
is the number of nodes in the first tree. - In-order traversal of the second tree: Similarly, the
dfs
function visits each node inroot2
exactly once, takingO(m)
time wherem
is the number of nodes in the second tree. - Merging two sorted arrays: The while loops iterate through arrays
a
andb
exactly once. Each element from both arrays is processed exactly once, takingO(n + m)
time.
Therefore, the total time complexity is O(n) + O(m) + O(n + m) = O(n + m)
.
Space Complexity: O(n + m)
The space usage includes:
- Array
a
: Stores alln
values from the first tree, usingO(n)
space. - Array
b
: Stores allm
values from the second tree, usingO(m)
space. - Result array
ans
: Stores alln + m
elements from both trees, usingO(n + m)
space. - Recursion call stack: The in-order traversal uses recursion with maximum depth equal to the height of each tree. In the worst case (skewed tree), this could be
O(n)
for the first tree andO(m)
for the second tree. However, this doesn't add to the overall complexity since it's dominated by the array storage.
The dominant space usage comes from storing all elements, resulting in a space complexity of O(n + m)
.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Forgetting to Handle Empty Trees
A common mistake is not properly handling cases where one or both trees are empty. While the provided solution handles this correctly through the base case if root is None: return
, developers often forget to test edge cases like:
- Both trees are empty (should return
[]
) - Only one tree is empty (should return all elements from the non-empty tree)
Solution: Always include null checks in your traversal function and test with empty tree inputs.
2. Using Wrong Tree Traversal Order
Since we need sorted output and are working with BSTs, using pre-order or post-order traversal instead of in-order traversal would give incorrectly ordered results.
Incorrect approach:
def preorder_traversal(root, elements):
if root is None:
return
elements.append(root.val) # Wrong: Processing node before left subtree
preorder_traversal(root.left, elements)
preorder_traversal(root.right, elements)
Solution: Always use in-order traversal (left → root → right) for BSTs to get sorted order.
3. Inefficient Merging by Sorting Combined Arrays
Some developers might combine both arrays and then sort the result:
# Inefficient approach - O((m+n)log(m+n)) time
all_elements = first_tree_elements + second_tree_elements
return sorted(all_elements)
This approach has O((m+n)log(m+n)) time complexity instead of the optimal O(m+n).
Solution: Leverage the fact that both arrays are already sorted and use the two-pointer merge technique for O(m+n) complexity.
4. Modifying Input Lists During Traversal
Passing the same list reference across multiple function calls without clearing it can lead to accumulating values from previous test cases:
# Bug-prone if reusing the same list
elements = [] # Defined outside function
def getAllElements(self, root1, root2):
inorder_traversal(root1, elements) # elements might contain old values
inorder_traversal(root2, elements)
Solution: Always create fresh lists for each function call as shown in the correct implementation.
5. Off-by-One Errors in Merge Logic
Using incorrect comparison operators or loop conditions can cause missing elements or index out of bounds errors:
# Common mistakes: while pointer_first <= len_first: # Should be < not <= merged_result.append(first_tree_elements[pointer_first]) # IndexError!
Solution: Use strict less-than (<
) when comparing indices with list length, and carefully verify loop boundaries.
What is the best way of checking if an element exists in a sorted array once in terms of time complexity? Select the best that applies.
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
Binary Search Tree Intro Definition A Binary Search Tree or BST is a rooted binary tree with the value of each internal node being greater than all the values in the respective node's left subtree and less than the ones in its right subtree An empty tree is a BST since it satisfies the
Want a Structured Path to Master System Design Too? Don’t Miss This!