leetcode / Trees / Binary search trees core

Binary search trees

A binary search tree is a binary tree with one extra promise: at every node, everything in the left subtree is smaller and everything in the right subtree is larger. That promise is binary search spread across pointers. It lets you find, insert and delete by discarding half the remaining tree at each step, and it makes an inorder walk produce the values in sorted order. Most search-tree questions are the same promise used in one of four ways: check it, walk it in order, descend by it, or repair it after a change.

On this page
  1. Problem
  2. Intuition
  3. Mechanics
  4. The invariant, stated carefully
  5. Validating with intervals
  6. Searching, inserting and the descent loop
  7. Deleting a node
  8. Worked example
  9. Implementation
  10. Variations and edge cases
  11. Complexity and tradeoffs
  12. Problems
  13. Medium
  14. Also uses this pattern
  15. Connections
  16. Read first
  17. Read next
  18. Check yourself
  19. References

Problem

A plain tree cannot answer "is 13 in here?" without looking at every node. A search tree can: compare with the root, go left or right, repeat. If the tree is reasonably balanced that takes about log n steps, the same as binary search on a sorted array, but the tree also supports cheap insertion and deletion, which a sorted array does not. The interview questions test whether you hold the invariant precisely. The common failure is checking only a node against its parent, when the promise is about every ancestor.

SignalPatternWhy
"Is this a valid search tree?"Pass an open interval (low, high) downEach value must respect the bounds set by every ancestor, not only its parent.
"The k-th smallest / largest", "next larger value"Inorder walk, stop earlyInorder visits values in sorted order, so the k-th visit is the k-th smallest.
"Lowest common ancestor in a search tree"Descend until the two targets splitWhile both are on one side, the answer is on that side; the first node between them is the answer.
"Insert a value", "find a value", "closest value"Descend by comparison, O(h)Every comparison discards one whole subtree.
"Delete a node"Three cases: leaf, one child, two childrenTwo children: replace with the inorder successor, then delete that successor.
"Convert a sorted array / list to a balanced tree"Pick the middle as the root, recurse on halvesThe middle keeps both sides equal, so height stays at about log n.
"Inorder successor", "iterator over a search tree"Stack of left spine or parent-free descentThe successor is the leftmost node of the right subtree, or the lowest ancestor you turned left at.
"Repair" or "recover" a tree with two swapped valuesInorder walk, spot the descentsA sorted list with two swapped entries has one or two places where it goes down.

Intuition

Picture a library whose shelves are arranged so that, at every shelf, books to your left come earlier in the alphabet and books to your right come later. To find a title you never scan; you ask at each shelf "left or right?" and walk away from the wrong half. That works because the rule holds for the whole half, not just the next shelf over. If one shelf deep on the right there is a book that belongs far to the left, the search will walk right past it and conclude it is missing.

This is why the invariant is stated for subtrees, and why validation passes a range down. Each time you step left, the node's value becomes the new upper limit; each time you step right, it becomes the new lower limit. The range only ever shrinks, and every node must lie strictly inside the range it inherits. The inorder walk is the same fact told as a list: reading left subtree, node, right subtree gives a strictly increasing sequence exactly when the promise holds everywhere, by induction on the subtree.

A search tree drawn so that left to right is sorted, with the search path for a common ancestor A binary search tree with 9 values. Nodes are placed left to right by inorder position, so the values read 1, 3, 4, 6, 7, 8, 10, 13, 14 from left to right. The filled nodes 8, 3, 6 are the path taken to find the lowest common ancestor of 4 and 7, which is 6. 8 3 1 6 4 7 10 14 13

Every node sits at its sorted position from left to right, so reading the bottom edge of the picture gives the inorder list. The amber path is how you find the common ancestor of 4 and 7: go left of 8 (both are smaller), go right of 3 (both are larger), and stop at 6, where they split.

Mechanics

The invariant, stated carefully

For every node x: all values in x's left subtree are less than x.val, and all values in x's right subtree are greater. Whether duplicates are allowed, and on which side, changes by problem; the strict version used here has none. The local test "left child < node < right child" is necessary but not sufficient: it misses a value that is correct against its parent but wrong against a grandparent.

Validating with intervals

Walk the tree carrying (low, high), starting at (-inf, +inf). A node is acceptable if low < val < high. Its left child inherits (low, val) and its right child (val, high). If every node passes, every ancestor-descendant pair has been checked, because each ancestor contributed a bound to every node below it. The alternative, checking that the inorder sequence is strictly increasing by remembering the previous value, is equivalent and often shorter.

Find, insert and the common ancestor all share one loop: compare, then go to one child. Insertion walks until it falls off the tree and attaches the new node where the search ended, which is the only place the invariant allows it. None of these need recursion; a while loop uses O(1) extra space. The cost is the height, which is log n for a balanced tree and n for a tree built from already-sorted input, since every insert then goes right.

Deleting a node

Find the node, then handle three cases. A leaf is simply removed. A node with one child is replaced by that child. A node with two children cannot be replaced by either child without breaking something, so copy the value of its inorder successor (the smallest value in the right subtree: go right once, then left as far as possible) into it, and delete the successor from the right subtree, where it has at most one child. The successor is the only value that is larger than everything on the left and no larger than everything else on the right, which is why swapping it in keeps the invariant.

A tree that passes the parent check but is not a search tree A tree with root 5. Under each node is the interval of values allowed there. Node 3 is a left child of 6 and smaller than it, so it looks fine locally, but it sits in the right subtree of 5 and its interval is (5, 6). It is outside that interval, so the tree is not a search tree. 5 (-inf, +inf) 4 (-inf, 5) 6 (5, +inf) 3 (5, 6) 7

Allowed interval under each node. The walk stops at the amber node, so the nodes after it get no interval. Locally, 3 < 6 < 7 looks fine; globally, 3 is on the right of 5 and must be larger than 5.

Worked example

Validation. The tree in the second diagram, walked in preorder with the inherited interval:

StepNodeAllowed intervalInside?
15(-inf, +inf)yes
24(-inf, 5)yes
36(5, +inf)yes
43(5, 6)no

Node 3 fails its interval (5, 6), so the whole tree is rejected after 4 steps without looking at the rest.

k-th smallest and common ancestor. On the valid tree in the first diagram, the inorder list is 1, 3, 4, 6, 7, 8, 10, 13, 14. Asking for the 4th smallest value, the walk visits 1, 3, 4, 6 and stops. The 4th smallest value is 6, and nothing to the right of it was touched. For the common ancestor of 4 and 7, the descent visits 8, 3, 6: at 8 both are smaller so go left, at 3 both are larger so go right, and at 6 one is on each side or equal, so the lowest common ancestor is 6.

Deleting. Removing the leaf 13 leaves inorder 1, 3, 4, 6, 7, 8, 10, 14. Removing 3, which has two children, copies in its successor 4 (the leftmost value of the right subtree) and deletes the old 4 below it: the root's left child is now 4 and the inorder list is 1, 4, 6, 7, 8, 10, 13, 14, still sorted.

Implementation

class TreeNode:
    def __init__(self, val=0, left=None, right=None):
        self.val, self.left, self.right = val, left, right


def is_valid_bst(root):
    def ok(node, low, high):
        if not node:
            return True
        if not (low < node.val < high):
            return False
        return ok(node.left, low, node.val) and ok(node.right, node.val, high)

    return ok(root, float("-inf"), float("inf"))


def kth_smallest(root, k):
    stack, cur = [], root
    while stack or cur:
        while cur:
            stack.append(cur)
            cur = cur.left
        cur = stack.pop()
        k -= 1
        if k == 0:
            return cur.val
        cur = cur.right


def lca_bst(root, p, q):
    node = root
    while node:
        if p.val < node.val and q.val < node.val:
            node = node.left
        elif p.val > node.val and q.val > node.val:
            node = node.right
        else:
            return node


def insert(root, val):
    if not root:
        return TreeNode(val)
    if val < root.val:
        root.left = insert(root.left, val)
    else:
        root.right = insert(root.right, val)
    return root


def search(root, val):
    while root and root.val != val:
        root = root.left if val < root.val else root.right
    return root


def delete(root, key):
    if not root:
        return None
    if key < root.val:
        root.left = delete(root.left, key)
    elif key > root.val:
        root.right = delete(root.right, key)
    else:
        if not root.left:
            return root.right
        if not root.right:
            return root.left
        succ = root.right
        while succ.left:
            succ = succ.left
        root.val = succ.val
        root.right = delete(root.right, succ.val)
    return root


def inorder_successor(root, p):
    succ = None
    while root:
        if p.val < root.val:
            succ = root                              # last node where we turned left
            root = root.left
        else:
            root = root.right
    return succ


def sorted_array_to_bst(nums):
    if not nums:
        return None
    mid = len(nums) // 2
    return TreeNode(nums[mid], sorted_array_to_bst(nums[:mid]), sorted_array_to_bst(nums[mid + 1:]))

is_valid_bst uses infinities so the first comparison needs no special case; if node values can be as extreme as the integer limits, use None bounds instead of sentinels. kth_smallest is the iterative inorder from the previous page with an early return. sorted_array_to_bst copies slices for clarity, which costs O(n log n); pass index bounds to make it O(n).

Variations and edge cases

  • Previous-value validation. Do an inorder walk and keep the previous value; the tree is valid when each value is greater than the one before. It needs one variable but must compare against the previous visited value, not the parent.
  • Augmented trees. To answer many k-th smallest queries, store each node's subtree size and descend: if the left size is at least k go left, if it equals k-1 stop here, otherwise go right with k reduced. That gives O(h) per query.
  • Recover a swapped pair. Inorder walk, record the places where a value is smaller than its predecessor; the first descent's larger value and the last descent's smaller value are the swapped nodes.
  • Balancing. Interview BSTs are usually unbalanced and their cost is O(h). Self-balancing trees (red-black, AVL) keep h at O(log n); you describe them rather than implement them.
  • Edge cases: an empty tree (valid), a single node, duplicates (confirm the rule and make the comparison strict or non-strict on one side only), a skewed tree of 105 nodes that exceeds the recursion limit, deleting the root, and deleting a key that is absent.

Complexity and tradeoffs

  • Time: search, insert, delete and common ancestor take O(h). Validation visits every node: O(n). The k-th smallest takes O(h + k) because the walk goes down the left spine first. In the example, the 4th smallest of 9 values needed 4 visits plus the spine.
  • Space: O(h) for a stack or recursion, O(1) for the descent loops that need none.
  • Baseline. Searching an unordered tree is O(n); searching a sorted array is O(log n) but insert and delete are O(n) because elements shift. A balanced search tree gives O(log n) for all three.
  • When it fails. An unbalanced tree degrades every O(h) bound to O(n). Heavy repeated insertions of sorted data are the classic cause. Also, a search tree orders by one key; for "k-th smallest" under many updates, a balanced tree with subtree sizes or a Fenwick tree is the better fit.

Problems

The home problems use the invariant in four ways: checking it (validate), reading it in order (k-th smallest, inorder successor), descending by it (lowest common ancestor, insert) and repairing it (delete). Converting a sorted array into a balanced tree builds a tree and lives with tree construction; it appears below.

Medium

  • 98. Validate Binary Search Tree Blind 75 Grind 169 NeetCode 150 NeetCode 250
    Pass down the open interval each node must fall inside, tightening the upper bound going left and the lower bound going right.
  • 230. Kth Smallest Element in a BST Blind 75 Grind 169 NeetCode 150 NeetCode 250
    An inorder traversal visits a BST in sorted order, so stop at the kth node visited.
  • 235. Lowest Common Ancestor of a Binary Search Tree Blind 75 Grind 169 NeetCode 150 NeetCode 250
    Walk down from the root: if both values are smaller go left, if both are larger go right, otherwise this node is where they split.
  • 285. Inorder Successor in BST Grind 169
    Walk down from the root, and every time you go left remember that node, since it is the smallest value seen so far that is larger than the target.
  • 450. Delete Node in a BST NeetCode 250
    To delete a node with two children, replace its value with its inorder successor's and delete that successor from the right subtree.
  • 701. Insert into a Binary Search Tree NeetCode 250
    Walk down the BST comparing values until you reach an empty child, and attach the new node there.

Also uses this pattern

These problems have a different home page, but this idea is part of their solution.

Connections

Read first

Read next

No page builds on this one yet.

This page is the tree version of binary search: the same "discard half" argument, with pointers instead of indices. It reuses the traversals from the depth-first page, especially inorder. The iterator and successor ideas return in heap and ordered-structure problems, and the sorted-array-to-tree construction appears again when you build trees from traversals.

Check yourself

Why does checking "left child < node < right child" at every node fail to validate a search tree?

It only compares each node with its parent, but the rule covers every ancestor. In the example the node 3 is below 6 and passes against its parent, yet it sits in the right subtree of 5 and must exceed 5, so its true interval is (5, 6).

When deleting a node with two children, why use the inorder successor?

It is the smallest value larger than everything in the left subtree, and no larger than anything else in the right subtree, so placing it at the node keeps both sides on the correct side. It also has no left child, so removing it from the right subtree is an easy one-child or leaf case. The inorder predecessor works symmetrically.

The k-th smallest took 4 visits for k = 4. Why is the worst case O(h + k), not O(k)?

The first visit is the leftmost node, which can be h steps below the root, so the walk must descend the left spine before it can count anything. After that each visit is amortised constant work.

Why is the lowest common ancestor in a search tree O(h) with O(1) space, while in a general tree it needs a full traversal?

Values tell you which side each target is on, so you never have to look: while both are smaller go left, both larger go right, otherwise you are at the split. A general tree has no ordering, so you must search both subtrees for the targets.

References

  • Cormen, Leiserson, Rivest and Stein, Introduction to Algorithms, chapter 12 (binary search trees: querying, successor and predecessor, insertion and deletion) and chapter 13 (red-black trees).
  • Sedgewick and Wayne, Algorithms, section 3.2: binary search trees, with rank and select via subtree sizes.
  • Skiena, The Algorithm Design Manual, the dictionary data structures chapter: binary search trees and their balance.
  • Adelson-Velsky and Landis, "An algorithm for the organization of information" (1962), the original self-balancing search tree.
  • The NeetCode roadmap and the Blind 75, Grind 169, NeetCode 150 and NeetCode 250 lists, used as the index of topics and problems.