LeetCode 1305 - All Elements in Two Binary Search Trees
The problem gives us two binary search trees, root1 and root2. A binary search tree, commonly abbreviated as BST, has the important property that for every node: - All values in the left subtree are smaller than the node's value - All values in the right subtree are larger…
Difficulty: 🟡 Medium
Topics: Tree, Depth-First Search, Binary Search Tree, Sorting, Binary Tree
Solution
Problem Understanding
The problem gives us two binary search trees, root1 and root2. A binary search tree, commonly abbreviated as BST, has the important property that for every node:
- All values in the left subtree are smaller than the node's value
- All values in the right subtree are larger than the node's value
We need to collect every integer stored in both trees and return them in one sorted list in ascending order.
The key detail is that the input trees are already BSTs. That means each tree individually contains values that can be retrieved in sorted order by performing an inorder traversal. An inorder traversal visits nodes in this order:
- Left subtree
- Current node
- Right subtree
For a BST, this traversal naturally produces sorted values.
The input consists of two tree roots. Either tree may be empty, represented by None in Python or nil in Go. The output must contain every value from both trees, including duplicates, sorted from smallest to largest.
The constraints tell us that each tree can contain up to 5000 nodes. This size is large enough that we should avoid unnecessarily expensive sorting operations if we can leverage BST properties directly. However, it is still small enough that linear or near-linear algorithms are perfectly acceptable.
Several edge cases are important:
- One or both trees may be empty
- Trees may contain duplicate values across the two trees
- Trees may be extremely unbalanced, effectively behaving like linked lists
- Negative values are allowed
- One tree may contain significantly more nodes than the other
A naive implementation that ignores BST ordering would miss an opportunity for optimization.
Approaches
Brute Force Approach
The most straightforward solution is to collect all values from both trees into one array, then sort the final array.
We can traverse each tree using any traversal method, such as preorder or inorder, because the brute-force approach does not rely on BST ordering. After gathering every value, we use a standard sorting algorithm to sort the combined list.
This approach is correct because it eventually places every value into the array, and sorting guarantees ascending order.
However, it performs unnecessary work. Since BSTs already contain ordered structure, sorting again wastes computation.
If the total number of nodes is n, sorting requires O(n log n) time.
Optimal Approach
The important insight is that inorder traversal of a BST already produces values in sorted order.
Instead of collecting unsorted values and sorting afterward, we can:
- Perform inorder traversal on both trees separately
- Obtain two individually sorted arrays
- Merge the two sorted arrays using the same technique used in merge sort
This avoids the extra sorting step.
The merge process is linear because both arrays are already sorted. At each step, we compare the current elements from both arrays and append the smaller one.
This gives an overall linear-time solution.
| Approach | Time Complexity | Space Complexity | Notes |
|---|---|---|---|
| Brute Force | O(n log n) | O(n) | Collect all elements and sort afterward |
| Optimal | O(n) | O(n) | Use inorder traversal plus linear merge |
Here, n represents the total number of nodes across both trees.
Algorithm Walkthrough
Optimal Algorithm
- Perform an inorder traversal on the first BST.
During the traversal, recursively visit the left subtree, then the current node, then the right subtree. Because the tree is a BST, this produces values in ascending order. Store these values in an array called values1.
2. Perform an inorder traversal on the second BST.
Use the same traversal strategy and store the results in another sorted array called values2.
3. Initialize two pointers.
Create pointer i for values1 and pointer j for values2. Both start at index 0.
4. Merge the two sorted arrays.
Compare values1[i] and values2[j].
- If
values1[i]is smaller, append it to the result and incrementi - Otherwise, append
values2[j]and incrementj
This works because both arrays are already sorted. 5. Process remaining elements.
After one array is exhausted, append all remaining elements from the other array. 6. Return the merged result.
The final merged array contains all elements in ascending order.
Why it works
The correctness relies on two properties:
- Inorder traversal of a BST always produces sorted values
- Merging two sorted arrays preserves sorted order
Since every node from both trees appears exactly once in the traversal arrays, and the merge step processes all elements while maintaining order, the final result is guaranteed to contain all values in ascending order.
Python Solution
# Definition for a binary tree node.
# class TreeNode:
# def __init__(self, val=0, left=None, right=None):
# self.val = val
# self.left = left
# self.right = right
from typing import List, Optional
class Solution:
def getAllElements(
self,
root1: Optional[TreeNode],
root2: Optional[TreeNode]
) -> List[int]:
def inorder(node: Optional[TreeNode], values: List[int]) -> None:
if not node:
return
inorder(node.left, values)
values.append(node.val)
inorder(node.right, values)
values1 = []
values2 = []
inorder(root1, values1)
inorder(root2, values2)
merged = []
i = 0
j = 0
while i < len(values1) and j < len(values2):
if values1[i] <= values2[j]:
merged.append(values1[i])
i += 1
else:
merged.append(values2[j])
j += 1
while i < len(values1):
merged.append(values1[i])
i += 1
while j < len(values2):
merged.append(values2[j])
j += 1
return merged
The implementation starts by defining a helper function called inorder. This function recursively traverses the BST in inorder sequence and appends node values into a list.
Two arrays, values1 and values2, store the sorted traversal results from the two trees.
After obtaining both sorted arrays, the algorithm uses two pointers to merge them efficiently. The merge logic is identical to the merge phase of merge sort. Since both arrays are already sorted, comparing the current smallest unused elements guarantees that the next appended element is globally smallest.
Finally, any remaining elements are appended after one array becomes exhausted.
Go Solution
/**
* Definition for a binary tree node.
* type TreeNode struct {
* Val int
* Left *TreeNode
* Right *TreeNode
* }
*/
func getAllElements(root1 *TreeNode, root2 *TreeNode) []int {
var inorder func(node *TreeNode, values *[]int)
inorder = func(node *TreeNode, values *[]int) {
if node == nil {
return
}
inorder(node.Left, values)
*values = append(*values, node.Val)
inorder(node.Right, values)
}
values1 := []int{}
values2 := []int{}
inorder(root1, &values1)
inorder(root2, &values2)
merged := []int{}
i := 0
j := 0
for i < len(values1) && j < len(values2) {
if values1[i] <= values2[j] {
merged = append(merged, values1[i])
i++
} else {
merged = append(merged, values2[j])
j++
}
}
for i < len(values1) {
merged = append(merged, values1[i])
i++
}
for j < len(values2) {
merged = append(merged, values2[j])
j++
}
return merged
}
The Go version follows the same algorithmic structure as the Python solution. The main implementation difference is that slices are passed by reference during inorder traversal so that recursive calls can append values efficiently.
Go uses nil instead of None for empty nodes. Slice appending is handled using the built-in append function.
There are no integer overflow concerns because node values remain well within Go's standard integer range.
Worked Examples
Example 1
Input:
root1 = [2,1,4]
root2 = [1,0,3]
Tree 1:
2
/ \
1 4
Tree 2:
1
/ \
0 3
Step 1: Inorder traversal of root1
| Traversal Step | Current Node | values1 |
|---|---|---|
| Visit left of 2 | 1 | [1] |
| Visit 2 | 2 | [1, 2] |
| Visit right of 2 | 4 | [1, 2, 4] |
Final:
values1 = [1, 2, 4]
Step 2: Inorder traversal of root2
| Traversal Step | Current Node | values2 |
|---|---|---|
| Visit left of 1 | 0 | [0] |
| Visit 1 | 1 | [0, 1] |
| Visit right of 1 | 3 | [0, 1, 3] |
Final:
values2 = [0, 1, 3]
Step 3: Merge arrays
| i | j | Compare | Append | Result |
|---|---|---|---|---|
| 0 | 0 | 1 vs 0 | 0 | [0] |
| 0 | 1 | 1 vs 1 | 1 | [0, 1] |
| 1 | 1 | 2 vs 1 | 1 | [0, 1, 1] |
| 1 | 2 | 2 vs 3 | 2 | [0, 1, 1, 2] |
| 2 | 2 | 4 vs 3 | 3 | [0, 1, 1, 2, 3] |
Remaining element from values1:
4
Final result:
[0,1,1,2,3,4]
Example 2
Input:
root1 = [1,null,8]
root2 = [8,1]
Tree 1:
1
\
8
Tree 2:
8
/
1
Traversal Results
| Tree | Inorder Result |
|---|---|
| root1 | [1, 8] |
| root2 | [1, 8] |
Merge Process
| Compare | Append | Result |
|---|---|---|
| 1 vs 1 | 1 | [1] |
| 8 vs 1 | 1 | [1, 1] |
| 8 vs 8 | 8 | [1, 1, 8] |
Append remaining:
8
Final result:
[1,1,8,8]
Complexity Analysis
| Measure | Complexity | Explanation |
|---|---|---|
| Time | O(n) | Each node is visited once, and merging is linear |
| Space | O(n) | Arrays store all node values |
The inorder traversals collectively visit every node exactly once. If the total number of nodes across both trees is n, traversal requires O(n) time.
The merge step also processes each element exactly once, which is another O(n) operation.
The auxiliary space comes primarily from the traversal arrays and the output array. In the worst case, all node values are stored simultaneously, giving O(n) space usage. Recursive traversal also adds stack space proportional to tree height.
Test Cases
# Helper TreeNode class for testing
class TreeNode:
def __init__(self, val=0, left=None, right=None):
self.val = val
self.left = left
self.right = right
solution = Solution()
# Example 1
root1 = TreeNode(2, TreeNode(1), TreeNode(4))
root2 = TreeNode(1, TreeNode(0), TreeNode(3))
assert solution.getAllElements(root1, root2) == [0,1,1,2,3,4] # standard merge case
# Example 2
root1 = TreeNode(1, None, TreeNode(8))
root2 = TreeNode(8, TreeNode(1), None)
assert solution.getAllElements(root1, root2) == [1,1,8,8] # duplicate values across trees
# Both trees empty
assert solution.getAllElements(None, None) == [] # completely empty input
# One tree empty
root1 = TreeNode(2, TreeNode(1), TreeNode(3))
assert solution.getAllElements(root1, None) == [1,2,3] # single-tree behavior
# Negative values
root1 = TreeNode(-10, TreeNode(-20), TreeNode(-5))
root2 = TreeNode(-15, None, TreeNode(0))
assert solution.getAllElements(root1, root2) == [-20,-15,-10,-5,0] # negative number ordering
# Highly unbalanced tree
root1 = TreeNode(1)
root1.right = TreeNode(2)
root1.right.right = TreeNode(3)
root2 = TreeNode(0)
assert solution.getAllElements(root1, root2) == [0,1,2,3] # linked-list-like BST
# Duplicate values within and across trees
root1 = TreeNode(2, TreeNode(2), TreeNode(2))
root2 = TreeNode(2)
assert solution.getAllElements(root1, root2) == [2,2,2,2] # repeated values
# Large value range
root1 = TreeNode(100000)
root2 = TreeNode(-100000)
assert solution.getAllElements(root1, root2) == [-100000,100000] # extreme constraint values
| Test | Why |
|---|---|
| Standard balanced trees | Validates normal inorder and merge behavior |
| Duplicate values across trees | Ensures duplicates are preserved |
| Both trees empty | Confirms empty input handling |
| One tree empty | Verifies single BST traversal works |
| Negative values | Ensures ordering with negatives |
| Highly unbalanced tree | Tests recursion on skewed structures |
| Repeated identical values | Verifies duplicates are not removed |
| Extreme value range | Confirms constraint boundary handling |
Edge Cases
Both Trees Are Empty
A common bug source is forgetting that either tree may be None. If both trees are empty, the traversal arrays remain empty and the merge loop never executes.
The implementation handles this naturally because the inorder traversal immediately returns when given None. The final merged array is simply an empty list.
One Tree Is Much Larger Than the Other
One BST may contain thousands of nodes while the other contains only a few. Incorrect merge logic can accidentally skip remaining elements after one pointer reaches the end.
The implementation avoids this by using two additional loops after the main merge loop. These loops append all remaining elements from whichever array still has unused values.
Highly Unbalanced Trees
A BST can degenerate into a linked-list-like structure if nodes are inserted in sorted order. In this case, recursion depth becomes proportional to the number of nodes.
The algorithm still produces correct output because inorder traversal logic does not depend on tree balance. Even in a skewed tree, visiting left subtree, current node, and right subtree still yields sorted order.