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

    &nbsp;

    **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

    &nbsp;

    **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

    &nbsp;

    **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.
