title: All Elements in Two Binary Search Trees slug: all-elements-in-two-binary-search-trees difficulty: medium leetcode_id: 1305 leetcode_url: https://leetcode.com/problems/all-elements-in-two-binary-search-trees/ categories: - trees - sorting patterns: - tree-traversal - two-pointers function_signature: "def get_all_elements(root1: TreeNode, root2: TreeNode) -> list[int]:" test_cases: visible: - input: { root1: [2, 1, 4], root2: [1, 0, 3] } expected: [0, 1, 1, 2, 3, 4] - input: { root1: [1, null, 8], root2: [8, 1] } expected: [1, 1, 8, 8] hidden: - input: { root1: [], root2: [5, 1, 7] } expected: [1, 5, 7] - input: { root1: [1], root2: [] } expected: [1] - input: { root1: [], root2: [] } expected: [] - input: { root1: [0, -10, 10], root2: [5, 1, 7, 0, 2] } expected: [-10, 0, 0, 1, 2, 5, 7, 10] description: | Given two binary search trees `root1` and `root2`, return *a list containing all the integers from both trees sorted in **ascending** order*. constraints: | - The number of nodes in each tree is in the range `[0, 5000]` - `-10^5 <= Node.val <= 10^5` examples: - input: "root1 = [2,1,4], root2 = [1,0,3]" output: "[0,1,1,2,3,4]" explanation: "Tree 1 contains [1,2,4] and tree 2 contains [0,1,3]. Merged in sorted order: [0,1,1,2,3,4]." - input: "root1 = [1,null,8], root2 = [8,1]" output: "[1,1,8,8]" explanation: "Tree 1 contains [1,8] and tree 2 contains [1,8]. Merged in sorted order: [1,1,8,8]." explanation: intuition: | The key insight is recognising that **binary search trees have a special property**: an in-order traversal (left → node → right) visits nodes in sorted ascending order. Think of it like this: each BST is already a "sorted container" in disguise. If you perform an in-order traversal, you get a sorted list for free. So the problem transforms into: **merge two sorted lists into one sorted list**. This is exactly the merge step from merge sort! You compare the front elements of both lists and take the smaller one, repeating until both lists are exhausted. The elegant solution combines these two insights: 1. Use BST's in-order property to extract sorted sequences 2. Use two-pointer merge to combine them efficiently approach: | We solve this using **In-Order Traversal + Two-Pointer Merge**: **Step 1: Perform in-order traversal on both trees** - Traverse each BST using in-order DFS (left → node → right) - This produces two sorted lists: `list1` from `root1` and `list2` from `root2` - Each traversal is O(n) time and produces elements in ascending order   **Step 2: Merge the two sorted lists** - Use two pointers, `i` for `list1` and `j` for `list2` - Compare `list1[i]` and `list2[j]`, append the smaller to the result - Advance the pointer of whichever list contributed the element - Continue until one list is exhausted   **Step 3: Handle remaining elements** - If `list1` has remaining elements, append them all - If `list2` has remaining elements, append them all - One list may be longer or one tree may be empty   **Step 4: Return the merged result** - The result list contains all elements from both trees in sorted order common_pitfalls: - title: Forgetting the BST Property description: | A common mistake is to collect all values from both trees and then sort the combined list. While this works, it's inefficient: sorting takes O((m+n) log(m+n)) time. By leveraging the BST's in-order property, we get sorted lists in O(m+n) time, and merging is also O(m+n). This is asymptotically better. wrong_approach: "Collect all values, then sort with sorted() or list.sort()" correct_approach: "In-order traversal gives sorted lists, then merge in O(m+n)" - title: Not Handling Empty Trees description: | Either `root1` or `root2` (or both) could be empty trees. Your in-order traversal should handle `None` roots gracefully by returning an empty list. The merge step naturally handles this since merging with an empty list just returns the other list. wrong_approach: "Assuming both trees have at least one node" correct_approach: "Check for None roots, return empty list from traversal" - title: Inefficient Merge with List Concatenation description: | Using `result = result + [value]` inside a loop creates a new list each iteration, leading to O(n²) time complexity. Use `result.append(value)` which is O(1) amortized, keeping the merge at O(m+n). wrong_approach: "result = result + [smaller_value] in loop" correct_approach: "result.append(smaller_value)" key_takeaways: - "**BST in-order property**: In-order traversal of a BST always produces elements in sorted ascending order" - "**Problem transformation**: Recognise when a problem can be reduced to a simpler, well-known problem (merge two sorted lists)" - "**Two-pointer merge**: The merge step from merge sort is a fundamental pattern for combining sorted sequences" - "**Foundation for harder problems**: This pattern extends to problems like merge k sorted lists, external sorting, and stream merging" time_complexity: "O(m + n). We traverse each tree once (O(m) + O(n)) and merge the two lists once (O(m + n)), where m and n are the number of nodes in each tree." space_complexity: "O(m + n). We store all elements from both trees in lists, plus O(h1 + h2) recursion stack space for the traversals, where h1 and h2 are the tree heights." solutions: - approach_name: In-Order Traversal + Merge is_optimal: true code: | class TreeNode: def __init__(self, val=0, left=None, right=None): self.val = val self.left = left self.right = right def get_all_elements(root1: TreeNode, root2: TreeNode) -> list[int]: def inorder(root: TreeNode) -> list[int]: """In-order traversal returns BST values in sorted order.""" if not root: return [] # Left subtree + current node + right subtree return inorder(root.left) + [root.val] + inorder(root.right) # Get sorted lists from both BSTs list1 = inorder(root1) list2 = inorder(root2) # Merge two sorted lists using two pointers result = [] i, j = 0, 0 while i < len(list1) and j < len(list2): if list1[i] <= list2[j]: result.append(list1[i]) i += 1 else: result.append(list2[j]) j += 1 # Append remaining elements from either list result.extend(list1[i:]) result.extend(list2[j:]) return result explanation: | **Time Complexity:** O(m + n) — In-order traversal is O(m) + O(n), merge is O(m + n). **Space Complexity:** O(m + n) — We store all elements plus recursion stack. We leverage the BST property that in-order traversal produces sorted output. Then we apply the classic two-pointer merge from merge sort to combine the two sorted lists efficiently. - approach_name: Iterative In-Order with Stack is_optimal: true code: | def get_all_elements(root1: TreeNode, root2: TreeNode) -> list[int]: result = [] stack1, stack2 = [], [] # Helper to push all left children onto stack def push_left(node, stack): while node: stack.append(node) node = node.left # Initialise stacks with leftmost paths push_left(root1, stack1) push_left(root2, stack2) while stack1 or stack2: # Choose which stack to pop from if not stack2 or (stack1 and stack1[-1].val <= stack2[-1].val): # Pop from stack1 node = stack1.pop() result.append(node.val) # Push left path of right child push_left(node.right, stack1) else: # Pop from stack2 node = stack2.pop() result.append(node.val) push_left(node.right, stack2) return result explanation: | **Time Complexity:** O(m + n) — Each node is pushed and popped exactly once. **Space Complexity:** O(h1 + h2) — Only stores nodes on the current paths (tree heights). This approach interleaves the in-order traversals, avoiding the need to materialise both full lists. We maintain two stacks representing the "frontier" of each traversal, always taking the smaller current element. - approach_name: Collect and Sort is_optimal: false code: | def get_all_elements(root1: TreeNode, root2: TreeNode) -> list[int]: def collect(root: TreeNode, values: list[int]): """Collect all values from tree (any order).""" if not root: return values.append(root.val) collect(root.left, values) collect(root.right, values) values = [] collect(root1, values) collect(root2, values) # Sort all collected values return sorted(values) explanation: | **Time Complexity:** O((m + n) log(m + n)) — Dominated by the sorting step. **Space Complexity:** O(m + n) — Stores all elements. This approach ignores the BST property and simply collects all values, then sorts. While correct and simple, it's less efficient than leveraging the inherent ordering of BSTs. Included to illustrate why understanding data structure properties matters.