Topics › Binary Search Trees › Practice Problems
Merge 2 BST's
LeetCode ↗ Java source ↗ Edit this article ↗
Jump to code:
Problem
You are given the roots of two binary search trees with m and n nodes. Return a list of all keys from both trees in ascending order. A key that appears in both trees appears twice in the output. Either tree may be empty.
Constraints (LeetCode version): up to 5000 nodes per tree, keys between -10^5 and 10^5. The interesting part is not the size but the follow-up: use the BST ordering instead of a general sort, and keep extra memory proportional to the trees’ heights, not their sizes.
Some versions (for example on GeeksforGeeks) ask you to “merge two BSTs” and return this sorted list; others ask for one balanced BST containing all keys. The sorted list is the hard part of both: a balanced BST is built from it by making the middle element the root and recursing on each half.
Example 1
root1: 2 root2: 1
/ \ / \
1 4 0 3
answer = [0, 1, 1, 2, 3, 4] // 1 is in both trees, so it appears twice
Example 2
root1 = [0, -10, 10], root2 = [5, 1, 7, 0, 2] (level order)
answer = [-10, 0, 0, 1, 2, 5, 7, 10]
Approach 1: Brute force, collect and sort
Intuition. The output is just the multiset of all keys, sorted. Collect every key from both trees into one list and sort it. This treats the trees as bags of numbers and throws away the fact that each one already knows its keys’ order.
Algorithm.
- Traverse both trees and append every key to one list.
- Sort the list and return it.
static List<Integer> bruteForce(TreeNode root1, TreeNode root2) {
List<Integer> all = new ArrayList<>();
collectInorder(root1, all);
collectInorder(root2, all);
Collections.sort(all);
return all;
}
static void collectInorder(TreeNode node, List<Integer> out) {
if (node == null) return;
collectInorder(node.left, out);
out.add(node.val);
collectInorder(node.right, out);
}
Complexity. Time O((m + n) log(m + n)) for the sort. Space O(m + n) for the list.
Approach 2: Better, two in-order lists and a merge
Intuition. In-order traversal of a BST yields its keys already sorted. So we get two sorted lists for linear time, and two sorted lists combine into one with the merge step of merge sort: look at the front of each list, take the smaller, advance that list. Ties can go either way; taking from the first list keeps the result stable.
Algorithm.
- In-order traverse
root1intoaandroot2intob. - With indices
iandj, repeatedly append the smaller ofa[i]andb[j]and advance that index. - When one list runs out, append the rest of the other.
static List<Integer> better(TreeNode root1, TreeNode root2) {
List<Integer> a = new ArrayList<>(), b = new ArrayList<>();
collectInorder(root1, a);
collectInorder(root2, b);
List<Integer> out = new ArrayList<>(a.size() + b.size());
int i = 0, j = 0;
while (i < a.size() && j < b.size()) {
if (a.get(i) <= b.get(j)) out.add(a.get(i++));
else out.add(b.get(j++));
}
while (i < a.size()) out.add(a.get(i++));
while (j < b.size()) out.add(b.get(j++));
return out;
}
Complexity. Time O(m + n): each key is visited once in the traversal and once in the merge. Space O(m + n) for the two intermediate lists, in addition to the output.
The intermediate lists are the waste. The merge only ever looks at the next key of each tree, so we do not need the whole list in advance, only a way to produce keys one at a time in sorted order.
Approach 3: Optimal, two in-order traversals in lockstep
Intuition. An iterative in-order traversal keeps a stack holding the “left spine” of the part of the tree not yet visited. Its top is always the smallest unvisited key. After popping a node, we push the left spine of its right subtree, and the new top is again the smallest unvisited key. Running two such traversals gives us, at every moment, the next key of each tree, which is exactly what the merge step needs. Pop from whichever stack has the smaller top, emit it, and refill that stack.
Each stack holds at most one root-to-leaf path of its tree, so the extra memory is O(h1 + h2) instead of O(m + n).
Algorithm.
- Push the left spine of
root1ontos1and ofroot2ontos2. - While either stack is non-empty: choose
s1ifs2is empty ors1’s top key is<=s2’s top key; otherwise chooses2. - Pop the chosen stack’s top, append its key, and push the left spine of its right child onto the same stack.
static List<Integer> optimal(TreeNode root1, TreeNode root2) {
Deque<TreeNode> s1 = new ArrayDeque<>(), s2 = new ArrayDeque<>();
pushLeft(s1, root1);
pushLeft(s2, root2);
List<Integer> out = new ArrayList<>();
while (!s1.isEmpty() || !s2.isEmpty()) {
Deque<TreeNode> from;
if (s2.isEmpty() || (!s1.isEmpty() && s1.peek().val <= s2.peek().val)) from = s1;
else from = s2;
TreeNode node = from.pop();
out.add(node.val);
pushLeft(from, node.right); // the next key of that tree is the leftmost node of node.right
}
return out;
}
static void pushLeft(Deque<TreeNode> stack, TreeNode node) {
while (node != null) {
stack.push(node);
node = node.left;
}
}
Dry run on Example 1. Initially s1 = [2, 1] and s2 = [1, 0] (bottom to top).
| s1 top | s2 top | take | emitted | refill |
|---|---|---|---|---|
| 1 | 0 | s2 | 0 | 0 has no right child |
| 1 | 1 | s1 (tie) | 1 | 1 has no right child |
| 2 | 1 | s2 | 1 | push spine of 3: s2 = [3] |
| 2 | 3 | s1 | 2 | push spine of 4: s1 = [4] |
| 4 | 3 | s2 | 3 | s2 empty |
| 4 | empty | s1 | 4 | s1 empty |
Result: [0, 1, 1, 2, 3, 4].
Complexity. Time O(m + n): every node is pushed once and popped once across both traversals. Space O(h1 + h2) for the stacks, beyond the output list itself. On balanced trees that is logarithmic; on skewed trees it degrades to linear, which is still no worse than Approach 2.
The same “lazy in-order cursor” is the BST iterator: pushLeft plus “pop, then push the left spine of the right child” is the entire iterator, written inline here for two trees.
Edge cases and pitfalls
- One or both trees empty. The loop condition and the
s2.isEmpty()check handle it: whichever stack still has nodes is drained. The tests cover each tree empty and both empty. - Duplicates across trees. Both copies must appear. Using
<=and taking one node per iteration guarantees nothing is dropped. - Skewed trees. A left chain makes the initial
pushLeftpush every node, so the stack isO(n). The tests include a left chain merged with a right chain. - Comparing boxed values. In Approach 2 the lists hold
Integerobjects.a.get(i) <= b.get(j)unboxes and compares values, buta.get(i) == b.get(j)would compare references and is only reliable for small values in theIntegercache. Use<=,<, orequals. - Negative keys. Nothing special is needed; only comparisons are used.
Related problems
- Iterative Inorder Traversal of Binary Tree is the stack technique that Approach 3 runs twice.
- BST iterator packages the same stack into a reusable
next()cursor. - Two Sum In BST uses an ascending and a descending cursor on one tree.
- Merge two Sorted Lists is the same merge step on linked lists.
- Merge two sorted arrays without extra space pushes the space question further on arrays.