Binary Search Trees

Ordered Trees for Efficient Search

A Binary Search Tree (BST) is a powerful node-based binary tree data structure which has the following properties:

  • The left subtree of a node contains only nodes with keys lesser than the node’s key.
  • The right subtree of a node contains only nodes with keys greater than the node’s key.
  • The left and right subtree each must also be a binary search tree.
  • There must be no duplicate keys.

This structure allows for fast lookup, addition, and removal of data items. On average, these operations take O(log n) time, where n is the number of nodes in the tree. However, the worst-case time complexity can be O(n) if the tree becomes unbalanced.

Binary Search Tree Structure

      8
     / \
    3   10
   / \   \
  1   6   14
     / \   /
    4   7 13

Note: All values in the left subtree of 8 are smaller, and all in the right are larger.

1. Core Operations

Search

To search for a value, you start at the root. If the value is less than the root, you go to the left child; if it's greater, you go to the right child. You repeat this process until you find the value or reach a null pointer.

Time: O(h) | Space: O(h) for recursive (O(1) for iterative), where h is the height.

class Solution:
    def searchBST(self, root: Optional[TreeNode], val: int) -> Optional[TreeNode]:
        curr = root
        while curr:
            if val < curr.val:
                curr = curr.left
            elif val > curr.val:
                curr = curr.right
            else:
                return curr
        return None
Insertion

Insertion is similar to searching. You traverse the tree to find the correct position for the new value and then insert a new node there.

Time: O(h) | Space: O(h) or O(1)

class Solution:
    def insertIntoBST(self, root: Optional[TreeNode], val: int) -> Optional[TreeNode]:
        if not root:
            return TreeNode(val)
        
        curr = root
        while True:
            if val > curr.val:
                if not curr.right:
                    curr.right = TreeNode(val)
                    return root
                curr = curr.right
            else:
                if not curr.left:
                    curr.left = TreeNode(val)
                    return root
                curr = curr.left
Deletion

Deletion is the most complex operation. There are three cases for the node to be deleted:

  1. Node is a leaf: Simply remove the node.
  2. Node has one child: Replace the node with its child.
  3. Node has two children: Find the node's "in-order successor" (the smallest value in its right subtree). Copy the value of the successor to the node, and then recursively delete the successor.

Time: O(h) | Space: O(h) or O(1)

class Solution:
    def deleteNode(self, root: Optional[TreeNode], key: int) -> Optional[TreeNode]:
        if not root:
            return None
        
        if key > root.val:
            root.right = self.deleteNode(root.right, key)
        elif key < root.val:
            root.left = self.deleteNode(root.left, key)
        else: # Found the node
            if not root.left:
                return root.right
            elif not root.right:
                return root.left
            else:
                # Find the in-order successor (min value in right subtree)
                min_node = root.right
                while min_node.left:
                    min_node = min_node.left
                root.val = min_node.val
                root.right = self.deleteNode(root.right, root.val)
        return root

2. Classic Problems

Validate Binary Search Tree

This problem tests your understanding of the BST property. It's not enough to check node.left < node < node.right. You must ensure all nodes in the left subtree are smaller and all in the right are larger. This is done by passing down valid range constraints (min and max) during the traversal.

class Solution:
    def isValidBST(self, root: Optional[TreeNode]) -> bool:
        def validate(node, low=-float('inf'), high=float('inf')):
            if not node:
                return True
            if not (low < node.val < high):
                return False
            
            return (validate(node.left, low, node.val) and
                    validate(node.right, node.val, high))
        
        return validate(root)
Kth Smallest Element in a BST

The key insight here is that an in-order traversal of a BST visits the nodes in sorted ascending order. You can perform an in-order traversal and stop once you have visited k elements.

class Solution:
    def kthSmallest(self, root: Optional[TreeNode], k: int) -> int:
        stack = []
        curr = root
        
        while curr or stack:
            while curr:
                stack.append(curr)
                curr = curr.left
            
            curr = stack.pop()
            k -= 1
            if k == 0:
                return curr.val
            
            curr = curr.right
Construct Binary Search Tree from Preorder Traversal

This problem leverages the properties of both preorder traversal and BSTs. The first element of a preorder traversal is always the root. The subsequent elements are then partitioned into those that belong in the left subtree (smaller than the root) and those that belong in the right subtree (larger than the root). You can solve this recursively.

class Solution:
    def bstFromPreorder(self, preorder: List[int]) -> Optional[TreeNode]:
        self.i = 0
        def build(bound=float('inf')):
            if self.i == len(preorder) or preorder[self.i] > bound:
                return None
            
            node = TreeNode(preorder[self.i])
            self.i += 1
            node.left = build(node.val)
            node.right = build(bound)
            return node
            
        return build()