Facebook Pixel

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:

  1. Extract all values from the first binary search tree
  2. Extract all values from the second binary search tree
  3. Combine all these values together
  4. 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 and b
  • 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:

  1. We need to visit every node in both trees to collect all values
  2. In-order traversal (a type of DFS) on a BST naturally gives us sorted values
  3. 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.

Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

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:

  1. Extract a sorted list from the first tree using in-order traversal
  2. Extract a sorted list from the second tree using in-order traversal
  3. 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 array a with sorted values from the first tree
  • dfs(root2, b) populates array b 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] with b[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 Evaluator

Example 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]:

Stepija[i]b[j]ComparisonActionResult
100101 > 0Add b[0]=0, j++[0]
201111 ≤ 1Add a[0]=1, i++[0, 1]
311212 > 1Add b[1]=1, j++[0, 1, 1]
412232 ≤ 3Add a[1]=2, i++[0, 1, 1, 2]
522434 > 3Add 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:

  1. In-order traversal of the first tree: The dfs function visits each node in root1 exactly once, taking O(n) time where n is the number of nodes in the first tree.
  2. In-order traversal of the second tree: Similarly, the dfs function visits each node in root2 exactly once, taking O(m) time where m is the number of nodes in the second tree.
  3. Merging two sorted arrays: The while loops iterate through arrays a and b exactly once. Each element from both arrays is processed exactly once, taking O(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:

  1. Array a: Stores all n values from the first tree, using O(n) space.
  2. Array b: Stores all m values from the second tree, using O(m) space.
  3. Result array ans: Stores all n + m elements from both trees, using O(n + m) space.
  4. 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 and O(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.

Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

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

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More