Facebook Pixel

1902. Depth of BST Given Insertion Order 🔒

Problem Description

You need to construct a binary search tree (BST) by inserting elements one by one from a given array order, then find the maximum depth of the resulting tree.

The array order is a permutation of integers from 1 to n, representing the sequence in which values are inserted into the BST. The first element order[0] becomes the root of the tree. Each subsequent element is inserted following standard BST insertion rules:

  • If the value is less than the current node, go to the left subtree
  • If the value is greater than the current node, go to the right subtree
  • Continue this process until finding an empty position, then insert the node there

The depth of the BST is the number of nodes along the longest path from the root to any leaf node.

For example, if order = [2, 1, 4, 3]:

  1. Insert 2 as the root (depth 1)
  2. Insert 1 as left child of 2 (depth 2)
  3. Insert 4 as right child of 2 (depth 2)
  4. Insert 3 as left child of 4 (depth 3)

The resulting tree has a maximum depth of 3.

The solution uses a SortedDict to efficiently track the depth of each inserted node. For each new value, it finds the closest smaller and larger values already in the tree. The new node will be a child of one of these adjacent nodes, specifically the one with greater depth. The depth of the new node is one more than its parent's depth. The algorithm maintains the maximum depth seen throughout all insertions.

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

Intuition

When inserting a new value into a BST, we need to find where it will be placed. The key insight is that a new node will always become a leaf node - it never gets inserted between existing nodes. This new leaf will be a child of some existing node.

But which existing node will be the parent? When we traverse the BST to insert value v, we compare it with nodes along a path from root to a leaf. The last node we visit before reaching an empty spot becomes the parent. This parent node will be either:

  1. The largest value smaller than v (if v becomes its right child)
  2. The smallest value larger than v (if v becomes its left child)

Here's the crucial observation: between these two adjacent values (the closest smaller and larger values), the new node v will be attached to whichever one was inserted later (has greater depth). Why? Because if the smaller value has greater depth, it means we reached it by going through the larger value first, so v becomes the right child of the smaller value. Conversely, if the larger value has greater depth, we reached it through the smaller value, so v becomes the left child of the larger value.

This means we don't need to build the actual tree structure. We only need to:

  1. Keep track of all inserted values and their depths
  2. For each new value, find its immediate neighbors (closest smaller and larger values)
  3. The new value's depth is 1 + max(depth of smaller neighbor, depth of larger neighbor)

A SortedDict is perfect for this because it maintains values in sorted order and allows efficient binary search to find neighbors. We initialize it with sentinel values 0 and inf at depth 0 to handle edge cases when inserting the minimum or maximum values.

Learn more about Tree, Binary Search Tree and Binary Tree patterns.

Solution Approach

The solution uses a SortedDict data structure to efficiently track inserted values and their corresponding depths without building the actual BST.

Initialization:

sd = SortedDict({0: 0, inf: 0, order[0]: 1})
  • We initialize the SortedDict with sentinel values: 0 and inf both at depth 0
  • These sentinels handle edge cases when inserting minimum or maximum values
  • The root node order[0] is inserted with depth 1
  • Variable ans tracks the maximum depth seen so far, initially 1

Main Algorithm: For each subsequent value v in order[1:]:

  1. Find the closest neighbors:

    lower = sd.bisect_left(v) - 1
    higher = lower + 1
    • bisect_left(v) returns the index where v would be inserted to maintain sorted order
    • lower is the index of the largest value smaller than v
    • higher is the index of the smallest value larger than v
  2. Calculate the depth of the new node:

    depth = 1 + max(sd.values()[lower], sd.values()[higher])
    • The new node will be a child of either the lower or higher neighbor
    • It attaches to whichever neighbor has greater depth (was inserted more recently in that subtree)
    • The new node's depth is one more than its parent's depth
  3. Update the maximum depth and insert the new node:

    ans = max(ans, depth)
    sd[v] = depth
    • Update ans if we found a deeper node
    • Add the new value and its depth to the SortedDict

Time Complexity: O(n log n) where n is the length of the order array. Each insertion into the SortedDict takes O(log n) time.

Space Complexity: O(n) for storing all values and their depths in the SortedDict.

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 trace through the algorithm with order = [3, 1, 2, 5, 4, 6].

Initial Setup:

  • SortedDict: {0: 0, inf: 0, 3: 1}
  • Root node 3 has depth 1
  • ans = 1

Insert 1:

  • Find neighbors of 1: lower = 0 (depth 0), higher = 3 (depth 1)
  • Depth of 1 = 1 + max(0, 1) = 2
  • Node 1 becomes left child of 3
  • SortedDict: {0: 0, 1: 2, 3: 1, inf: 0}
  • ans = 2

Insert 2:

  • Find neighbors of 2: lower = 1 (depth 2), higher = 3 (depth 1)
  • Depth of 2 = 1 + max(2, 1) = 3
  • Node 2 becomes right child of 1
  • SortedDict: {0: 0, 1: 2, 2: 3, 3: 1, inf: 0}
  • ans = 3

Insert 5:

  • Find neighbors of 5: lower = 3 (depth 1), higher = inf (depth 0)
  • Depth of 5 = 1 + max(1, 0) = 2
  • Node 5 becomes right child of 3
  • SortedDict: {0: 0, 1: 2, 2: 3, 3: 1, 5: 2, inf: 0}
  • ans = 3

Insert 4:

  • Find neighbors of 4: lower = 3 (depth 1), higher = 5 (depth 2)
  • Depth of 4 = 1 + max(1, 2) = 3
  • Node 4 becomes left child of 5
  • SortedDict: {0: 0, 1: 2, 2: 3, 3: 1, 4: 3, 5: 2, inf: 0}
  • ans = 3

Insert 6:

  • Find neighbors of 6: lower = 5 (depth 2), higher = inf (depth 0)
  • Depth of 6 = 1 + max(2, 0) = 3
  • Node 6 becomes right child of 5
  • SortedDict: {0: 0, 1: 2, 2: 3, 3: 1, 4: 3, 5: 2, 6: 3, inf: 0}
  • ans = 3

The resulting BST structure:

       3 (depth 1)
      / \
     1   5 (depth 2)
      \  / \
      2 4   6 (depth 3)

Maximum depth = 3

The key insight: when inserting value 4, it finds neighbors 3 and 5. Since 5 has greater depth (2) than 3 (1), node 4 attaches to node 5 as its child. This happens because to reach position 4 in the actual BST, we must traverse through node 5, making 5 the parent of 4.

Solution Implementation

1from sortedcontainers import SortedDict
2from typing import List
3from math import inf
4
5class Solution:
6    def maxDepthBST(self, order: List[int]) -> int:
7        # Initialize sorted dictionary with sentinels at boundaries
8        # 0 and inf act as boundaries with depth 0
9        # First element of order starts at depth 1
10        sorted_dict = SortedDict({0: 0, inf: 0, order[0]: 1})
11      
12        # Track maximum depth seen so far
13        max_depth = 1
14      
15        # Process remaining elements in insertion order
16        for value in order[1:]:
17            # Find the index of the largest key smaller than current value
18            lower_index = sorted_dict.bisect_left(value) - 1
19            higher_index = lower_index + 1
20          
21            # Get the actual depth values at these indices
22            # The new node's depth is 1 plus the maximum depth of its potential parents
23            depth = 1 + max(sorted_dict.values()[lower_index], 
24                           sorted_dict.values()[higher_index])
25          
26            # Update maximum depth if current depth is larger
27            max_depth = max(max_depth, depth)
28          
29            # Insert current value with its calculated depth
30            sorted_dict[value] = depth
31      
32        return max_depth
33
1class Solution {
2    public int maxDepthBST(int[] order) {
3        // TreeMap to store node values as keys and their depths as values
4        // Automatically maintains sorted order of keys
5        TreeMap<Integer, Integer> nodeDepthMap = new TreeMap<>();
6      
7        // Initialize boundaries with depth 0
8        // 0 acts as left boundary (smaller than any node value)
9        nodeDepthMap.put(0, 0);
10        // MAX_VALUE acts as right boundary (larger than any node value)
11        nodeDepthMap.put(Integer.MAX_VALUE, 0);
12      
13        // Insert root node with depth 1
14        nodeDepthMap.put(order[0], 1);
15        int maxDepth = 1;
16      
17        // Process remaining nodes in insertion order
18        for (int i = 1; i < order.length; i++) {
19            int currentValue = order[i];
20          
21            // Find the closest smaller value (left parent candidate)
22            Map.Entry<Integer, Integer> leftParent = nodeDepthMap.lowerEntry(currentValue);
23            // Find the closest larger value (right parent candidate)
24            Map.Entry<Integer, Integer> rightParent = nodeDepthMap.higherEntry(currentValue);
25          
26            // Current node's depth is 1 + depth of its actual parent
27            // The actual parent is the one with greater depth between left and right candidates
28            int currentDepth = 1 + Math.max(leftParent.getValue(), rightParent.getValue());
29          
30            // Update maximum depth encountered so far
31            maxDepth = Math.max(maxDepth, currentDepth);
32          
33            // Store current node with its calculated depth
34            nodeDepthMap.put(currentValue, currentDepth);
35        }
36      
37        return maxDepth;
38    }
39}
40
1class Solution {
2public:
3    int maxDepthBST(vector<int>& order) {
4        // Map to store node values as keys and their depths as values
5        // Automatically maintains sorted order of keys
6        map<int, int> node_depth_map;
7      
8        // Initialize boundaries with depth 0
9        // 0 acts as left boundary (smaller than any node value)
10        node_depth_map[0] = 0;
11        // INT_MAX acts as right boundary (larger than any node value)
12        node_depth_map[INT_MAX] = 0;
13      
14        // Insert root node with depth 1
15        node_depth_map[order[0]] = 1;
16        int max_depth = 1;
17      
18        // Process remaining nodes in insertion order
19        for (int i = 1; i < order.size(); i++) {
20            int current_value = order[i];
21          
22            // Find the closest smaller value (left parent candidate)
23            // lower_bound returns iterator to first element >= current_value
24            // We need the element just before it
25            auto right_it = node_depth_map.lower_bound(current_value);
26            auto left_it = prev(right_it);
27          
28            // left_it points to closest smaller value (left parent candidate)
29            // right_it points to closest larger value (right parent candidate)
30          
31            // Current node's depth is 1 + depth of its actual parent
32            // The actual parent is the one with greater depth between left and right candidates
33            int current_depth = 1 + max(left_it->second, right_it->second);
34          
35            // Update maximum depth encountered so far
36            max_depth = max(max_depth, current_depth);
37          
38            // Store current node with its calculated depth
39            node_depth_map[current_value] = current_depth;
40        }
41      
42        return max_depth;
43    }
44};
45
1function maxDepthBST(order: number[]): number {
2    // Map to store node values as keys and their depths as values
3    // We'll maintain this in sorted order manually
4    const nodeDepthMap: Map<number, number> = new Map();
5  
6    // Initialize boundaries with depth 0
7    // 0 acts as left boundary (smaller than any node value)
8    nodeDepthMap.set(0, 0);
9    // MAX_VALUE acts as right boundary (larger than any node value)
10    nodeDepthMap.set(Number.MAX_VALUE, 0);
11  
12    // Insert root node with depth 1
13    nodeDepthMap.set(order[0], 1);
14    let maxDepth: number = 1;
15  
16    // Process remaining nodes in insertion order
17    for (let i = 1; i < order.length; i++) {
18        const currentValue: number = order[i];
19      
20        // Find the closest smaller value (left parent candidate)
21        let leftParentKey: number = 0;
22        let leftParentDepth: number = 0;
23      
24        // Find the closest larger value (right parent candidate)
25        let rightParentKey: number = Number.MAX_VALUE;
26        let rightParentDepth: number = 0;
27      
28        // Iterate through all entries to find lower and higher entries
29        for (const [key, depth] of nodeDepthMap.entries()) {
30            if (key < currentValue && key > leftParentKey) {
31                // Found a closer smaller value
32                leftParentKey = key;
33                leftParentDepth = depth;
34            }
35            if (key > currentValue && key < rightParentKey) {
36                // Found a closer larger value
37                rightParentKey = key;
38                rightParentDepth = depth;
39            }
40        }
41      
42        // Current node's depth is 1 + depth of its actual parent
43        // The actual parent is the one with greater depth between left and right candidates
44        const currentDepth: number = 1 + Math.max(leftParentDepth, rightParentDepth);
45      
46        // Update maximum depth encountered so far
47        maxDepth = Math.max(maxDepth, currentDepth);
48      
49        // Store current node with its calculated depth
50        nodeDepthMap.set(currentValue, currentDepth);
51    }
52  
53    return maxDepth;
54}
55

Time and Space Complexity

Time Complexity: O(n log n) where n is the length of the input array order.

  • The code iterates through the array once with a for loop that runs n-1 times
  • Inside each iteration:
    • sd.bisect_left(v) performs a binary search operation on the sorted dictionary, which takes O(log k) where k is the current size of the dictionary (at most n+2 elements)
    • sd.values()[lower] and sd.values()[higher] access values by index, which in a SortedDict takes O(log k) time each
    • Inserting a new key-value pair with sd[v] = depth takes O(log k) time to maintain the sorted order
  • Since we perform these operations for each element and the dictionary grows to size O(n), the total time complexity is O(n log n)

Space Complexity: O(n) where n is the length of the input array order.

  • The SortedDict sd stores at most n+2 key-value pairs (all elements from the input array plus the two sentinel values 0 and inf)
  • The variable ans and other local variables use O(1) space
  • Therefore, the overall space complexity is O(n)

Learn more about how to find time and space complexity quickly.

Common Pitfalls

1. Incorrectly Assuming the Parent Node

Pitfall: A common mistake is thinking that the new node's parent is simply the node with the value closest to it (minimum absolute difference). This would lead to incorrect depth calculations.

Why it's wrong: In a BST, when inserting a new value, its parent is determined by the path taken during insertion, not just proximity. The parent must be either:

  • The largest value smaller than the new value (if the new node becomes its right child)
  • The smallest value larger than the new value (if the new node becomes its left child)

Correct approach: The parent is the one among these two candidates that was inserted more recently into that position (has greater depth).

2. Forgetting the Sentinel Values

Pitfall: Omitting the sentinel values 0 and inf from the initial SortedDict setup:

# Incorrect initialization
sorted_dict = SortedDict({order[0]: 1})

Why it fails: Without sentinels, when inserting extreme values:

  • If inserting a value smaller than all existing values, lower_index would be -1
  • If inserting a value larger than all existing values, higher_index would exceed the valid range
  • This causes IndexError when accessing sorted_dict.values()[lower_index] or sorted_dict.values()[higher_index]

Solution: Always initialize with sentinels:

sorted_dict = SortedDict({0: 0, inf: 0, order[0]: 1})

3. Using Regular Dictionary Instead of SortedDict

Pitfall: Attempting to use a regular Python dict and manually finding neighbors:

# Incorrect approach
depths = {order[0]: 1}
for value in order[1:]:
    # Finding neighbors in O(n) time
    smaller = [k for k in depths if k < value]
    larger = [k for k in depths if k > value]
    # ... rest of logic

Why it's inefficient: This approach has O(n²) time complexity because finding neighbors requires scanning all existing keys for each insertion.

Solution: Use SortedDict which maintains keys in sorted order and provides O(log n) bisect operations.

4. Misunderstanding the Depth Calculation

Pitfall: Setting the new node's depth equal to its parent's depth instead of adding 1:

# Incorrect
depth = max(sorted_dict.values()[lower_index], 
           sorted_dict.values()[higher_index])

Correct version:

depth = 1 + max(sorted_dict.values()[lower_index], 
               sorted_dict.values()[higher_index])

The new node is one level deeper than its parent in the tree structure.

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

In a binary min heap, the minimum element can be found in:


Recommended Readings

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

Load More