Jc-alt logo
jc
data structures and algorithms

LeetCode: Trees I BST

LeetCode: Trees I BST
23 min read
#data structures and algorithms
Table Of Contents

Binary Search Tree Intro

What is a Binary Search Tree

Trees are hierarchical data structures representing relationships between entities, often in a parent-child format. A binary search tree is a type of tree where:

235. Lowest Common Ancestor of a Binary Search Tree ::2:: - Medium

Topics: Tree, Depth First Search, Binary Search Tree, Binary Tree

Intro

Given a binary search tree (BST), find the lowest common ancestor (LCA) node of two given nodes in the BST. According to the definition of LCA on Wikipedia: “The lowest common ancestor is defined between two nodes p and q as the lowest node in T that has both p and q as descendants (where we allow a node to be a descendant of itself).”

Example InputOutput
root = [6,2,8,0,4,7,9,null,null,3,5], p = 2, q = 86
root = [6,2,8,0,4,7,9,null,null,3,5], p = 2, q = 42
root = [2,1], p = 2, q = 12

Constraints:

The number of nodes in the root tree is in the range [1, 105].

-104 ≤ root.val ≤ 104

All Node.val are unique.

p != q

p and q will exist in the BST.

Abstraction

Find lowest node that has both p and q in their subtree.

Space & Time Complexity

SolutionTime ComplexitySpace ComplexityTime RemarkSpace Remark
BugError

Brute Force: iterative

AspectTime ComplexitySpace ComplexityTime RemarksSpace Remarks

Find the Bug:

Solution 1: [BST] Recursive BST Traversal - Tree/BST Guided Recursive Traversal

    def lowestCommonAncestor(self, root: TreeNode, p: TreeNode, q: TreeNode) -> TreeNode:
    
        # Lowest Common Ancestor:
        # Lowest node height/depth wise in a tree that has both p and q above it 

        # BST Traversal: (root -> left or right)
        # BST property (left < root < right) guides us toward the LCA at each node
        #   - If both p and q are smaller, LCA must be in left subtree
        #   - If both p and q are larger, LCA must be in right subtree
        #   - Otherwise root is the split point — root is the LCA

        # Both p and q are smaller: LCA in left subtree
        if p.val < root.val and q.val < root.val:
            return self.lowestCommonAncestor(root.left, p, q)

        # Both p and q are larger: LCA in right subtree
        elif p.val > root.val and q.val > root.val:
            return self.lowestCommonAncestor(root.right, p, q)

        # p and q are on different sides: root is the LCA
        else:
            return root

        # Guaranteed to have the lowest point:
        # The moment p and q are on different sides, 
        # any node deeper would only contain one of them, not both.
        # So the split node is the lowest node that 
        # still has both p and q as descendants

        # overall: tc O(log n) for balanced / O(n) for skewed trees
        # overall: sc O(log n) for balanced / O(n) for skewed trees
AspectTime ComplexitySpace ComplexityTime RemarksSpace Remarks
BST TraversalO(log n) / O(n)O(log n) / O(n)Guided by BST property, worst case height of treeGuided by BST property, worst case height of tree
Per Node WorkO(1)O(1)Two ComparisonsNo extra allocation
OverallO(log n) / O(n)O(log n) / O(n)Balanced vs skewed treesBalanced vs skewed trees

Solution 2: [BST] Iterative BST Traversal - Tree/BST Guided Iterative Traversal

    def lowestCommonAncestor(self, root: TreeNode, p: TreeNode, q: TreeNode) -> TreeNode:
        
        # BST Traversal: (root -> left or right) using Iteration
        #   - BST property guides us toward the LCA at each node
        #   - If both p and q are smaller, LCA must be in left subtree
        #   - If both p and q are larger, LCA must be in right subtree
        #   - Otherwise root is the split point — root is the LCA

        # Tree Iterator
        # sc: O(1)
        node = root

        # Traverse until LCA is found
        while node:

            # Both p and q are smaller — LCA in left subtree
            if p.val < node.val and q.val < node.val:
                node = node.left

            # Both p and q are larger — LCA in right subtree
            elif p.val > node.val and q.val > node.val:
                node = node.right

            # p and q are on different sides — node is the LCA
            else:
                return node

            # Guaranteed to have the lowest point:
            # The moment p and q are on different sides, 
            # any node deeper would only contain one of them, not both.
            # So the split node is the lowest node that 
            # still has both p and q as descendants

        # overall: tc O(log n) for balanced / O(n) for skewed trees
        # overall: sc O(1)
AspectTime ComplexitySpace ComplexityTime RemarksSpace Remarks
BST TraversalO(log n) / O(n)O(1)Guided by BST property, worst case height of treeSingle pointer
Per Node WorkO(1)O(1)Two ComparisonsNo extra allocation
OverallO(log n) / O(n)O(1)Balanced vs skewed treesNo call stack

701. Insert into a Binary Search Tree ::2:: - Medium

Topics: Tree, Binary Search Tree, Binary Tree

Intro

You are given the root node of a binary search tree (BST) and a value to insert into the tree. Return the root node of the BST after the insertion. It is guaranteed that the new value does not exist in the original BST. Notice that there may exist multiple valid ways for the insertion, as long as the tree remains a BST after insertion. You can return any of them.

Example InputOutput
root = [4,2,7,1,3], val = 5[4,2,7,1,3,5]
root = [40,20,60,10,30,50,70], val = 25[40,20,60,10,30,50,70,null,null,25]
root = [4,2,7,1,3,null,null,null,null,null,null], val = 5[4,2,7,1,3,5]

Constraints:

The number of nodes in the tree is in the range [1, 104].

-108 ≤ Node.val ≤ 108

All Node.val are unique.

-1010 ≤ val ≤ 108

It's guaranteed that val does not exist in the original BST.

Abstraction

Given a node, find its place in the binary tree

Space & Time Complexity

SolutionTime ComplexitySpace ComplexityTime RemarkSpace Remark
BugError

Brute Force: iterative

AspectTime ComplexitySpace ComplexityTime RemarksSpace Remarks

Find the Bug:

Solution 1: [BST] Recursive Traversal - Tree/BST Guided Recursive Traversal

    def insertIntoBST(self, root: Optional[TreeNode], val: int) -> Optional[TreeNode]:
        
        # Binary Search Tree:
        # BST property: For any node:
        #    left subtree nodes < node.val < right subtree nodes

        # Recursive DFS:
        #   1. Compare val with root.val
        #   2. If val < root.val, recurse left
        #   3. If val > root.val, recurse right
        #   4. If reached None, insert node here
        
        # Base case: reached a leaf, insert new node here
        if root == None:
            # tc O(1), sc O(1) for node creation
            return TreeNode(val)
        
        # Decide whether to insert left or right
        if val < root.val:
            # Insert into left subtree
            root.left = self.insertIntoBST(root.left, val)
        else:
            # Insert into right subtree
            root.right = self.insertIntoBST(root.right, val)
        
        # Return root node to maintain BST structure upwards
        # tc: O(h) for traversing height of tree
        # sc: O(h) recursion stack (O(log n) balanced, O(n) skewed)
        return root
AspectTime ComplexitySpace ComplexityTime RemarksSpace Remarks

Solution 2: [BST] Iterative Traversal - Tree/BST Guided Iterative Traversal

    def insertIntoBST(self, root: Optional[TreeNode], val: int) -> Optional[TreeNode]:
        
        # Iterative BST insertion:
        # Traverse tree from root iteratively to avoid recursive stack
        #   - At each node, compare node.val with val
        #   - Move left or right to maintain the BST property
        #   - When reached a 'None' child, have found insertion point for val
        

        # Empty Case:
        # If tree is empty, new node becomes root
        if not root:
            return TreeNode(val)
        
        # Tree Traversal
        # sc: O(1)
        current = root
        
        # Iterate until we have reached insertion point for val
        while True:

            # Check left
            if val < current.val:
                
                # Go left if left child exists
                if current.left:
                    current = current.left

                # Reached 'None', insert val here  
                else:
                    current.left = TreeNode(val)
                    break

            # Check right
            else:

                # Go right if right child exists
                if current.right:
                    current = current.right
                
                # Reached 'None', insert val here  
                else:
                    current.right = TreeNode(val)
                    break
        
        # Return original root to maintain BST structure

        # overall: tc O(log n) for balanced / O(n) for skewed trees
        # overall: sc: O(1) 
        return root
AspectTime ComplexitySpace ComplexityTime RemarksSpace Remarks

450. Delete Node in a BST ::2:: - Medium

Topics: Tree, Binary Search Tree, Binary Tree

Intro

Given a root node reference of a BST and a key, delete the node with the given key in the BST. Return the root node reference (possibly updated) of the BST. Basically, the deletion can be divided into two stages:

  1. Search for a node to remove.
  2. If the node is found, delete the node. Follow up: Could you solve it with time complexity O(height of tree)?
Example InputOutput
root = [5,3,6,2,4,null,7], key = 3[5,4,6,2,null,null,7]
root = [5,3,6,2,4,null,7], key = 0[5,3,6,2,4,null,7]
root = [], key = 0[]

Constraints:

The number of nodes in the tree is in the range [0, 104].

-105 ≤ Node.val ≤ 105

Each node has a unique value

root is a valid binary search tree

-105 ≤ key ≤ 105

Abstraction

Given a target node, remove it from a binary tree if it exists.

Space & Time Complexity

SolutionTime ComplexitySpace ComplexityTime RemarkSpace Remark
BugError

Brute Force: iterative

AspectTime ComplexitySpace ComplexityTime RemarksSpace Remarks

Find the Bug:

Solution 1: [BST] Recursive Traversal - Tree/BST Guided Recursive Traversal

    def deleteNode(self, root: Optional[TreeNode], key: int) -> Optional[TreeNode]:
        
        # BST property: left < node.val < right
        
        # Binary Search Tree Deletion: 3 main cases

        # 1. Node to delete has no children:
        #       a. remove directly

        # 2. Node has one child:
        #       a. replace node with child

        # 3. Node has two children: 
        #       a. find inorder successor (smallest node in right subtree)
        #       b. replace node value with successor
        #       c. delete successor node recursively
        
        # Base case: 
        # Reached 'None' child, key was not found
        if root == None:
            return None
        
        # Traverse left or right depending on BST property
        
        if key < root.val:
            # Key may be in left subtree
            root.left = self.deleteNode(root.left, key)
        
        elif key > root.val:
            # Key may be in right subtree
            root.right = self.deleteNode(root.right, key)
        
        # Key matches current node,
        # check 3 BST deletion cases
        else:

            # Case 1: Node has no children
            #   - remove by returning 'None' up the recursion calls
            if root.left == None and root.right == None:
                return None

            # Case 2: Node only has 1 child
            #   - remove by returning the 1 child
            elif root.left == None:
                return root.right
            elif root.right == None:
                return root.left

            # Case 3: Node has 2 children
            #   - find inorder successor (smallest node in right subtree)
            #   - replace node value with successor
            #   - delete successor node recursively
            else:

                # Grab right subtree
                successor = root.right

                # Find smallest node in right subtree
                # tc O(h) worst case
                while successor.left:
                    successor = successor.left
                
                # Replace key successor's value
                root.val = successor.val
                
                # Delete successor node from right subtree recursively
                # tc: O(h) for deletion path
                root.right = self.deleteNode(root.right, successor.val)
                 
        # Return original root to maintain BST structure

        # overall: tc O(log n) for balanced / O(n) for skewed trees
        # overall: sc O(log n) for balanced / O(n) for skewed trees
        return root
AspectTime ComplexitySpace ComplexityTime RemarksSpace Remarks

Solution 2: [BST] Iterative Traversal - Tree/BST Guided Iterative Traversal

    def deleteNode(self, root: Optional[TreeNode], key: int) -> Optional[TreeNode]:

        # Iterative BST Deletion
        # Use pointers to track current node and parent to avoid recursive calls

        # BST property: left < node.val < right
        
        # Binary Search Tree Deletion: 3 main cases

        # 1. Node to delete has no children:
        #       a. remove directly

        # 2. Node has one child:
        #       a. replace node with child

        # 3. Node has two children: 
        #       a. find inorder successor (smallest node in right subtree)
        #       b. replace node value with successor
        #       c. delete successor node recursively

        # Base case: 
        # Reached 'None' child, key was not found
        if not root:
            return None
        
        # Dummyhead to keep track of original  parent for convenience
        # sc: O(1)
        dummyHead = TreeNode(0)
        dummyHead.left = root
        parent = dummyHead

        # BST Iterator
        # sc: O(1)
        current = root

        # BST Info:
        # Track whether current is left or right child of parent,
        # to validate 3 edge cases
        # sc: O(1)
        is_left_child = True
        
        # Step 1: Iterate over BST to find node to delete
        while current and current.val != key:
            parent = current
            if key < current.val:
                current = current.left
                is_left_child = True
            else:
                current = current.right
                is_left_child = False
        
        # Early Exit: 
        # Key was not found, return original root
        if not current:
            return root
        
        
        # Step 2: Node has two children
        if current.left and current.right:
            # Find inorder successor (leftmost node of right subtree)
            successor_parent = current
            successor = current.right
            while successor.left:
                successor_parent = successor
                successor = successor.left  # tc O(h)
            
            # Replace current value with successor value
            current.val = successor.val
            
            # Prepare to delete successor node
            parent = successor_parent
            current = successor
            is_left_child = (successor_parent.left == successor)
        
        # Step 3: Node has at most one child
        child = current.left if current.left else current.right  # could be None
        
        # Attach child to parent
        if is_left_child:
            parent.left = child
        else:
            parent.right = child
        
        # Return root original root

        # overall: tc O(h)
        # overall: sc O(1)
        return dummyHead.left
AspectTime ComplexitySpace ComplexityTime RemarksSpace Remarks

98. Validate Binary Search Tree ::2:: - Medium

Topics: Tree, Depth First Search, Binary Search Tree, Binary Tree

Intro

Given the root of a binary tree, determine if it is a valid binary search tree (BST). The left subtree of a node contains only nodes with keys strictly less than the node's key. The right subtree of a node contains only nodes with keys strictly greater than the node's key. Both the left and right subtrees must also be binary search trees.

Example InputOutput
root = [2,1,3]true
root = [5,1,4,null,null,3,6]false

Constraints:

The number of nodes in the root tree is in the range [1, 104].

-231 ≤ Node.val ≤ 231-1

Abstraction

Given a tree, determine if tree is a valid BST. Where value of nodes is always: left.val ≤ root.val ≤ right.val

Space & Time Complexity

SolutionTime ComplexitySpace ComplexityTime RemarkSpace Remark
BugError

Brute Force: iterative

AspectTime ComplexitySpace ComplexityTime RemarksSpace Remarks

Find the Bug:

Solution 1: DFS Pre Order Recursive Passing Range Limits - Tree/DFS Pre order Traversal

    def isValidBST(self, root: Optional[TreeNode]) -> bool:
        
        # Note:
        # DFS pre order: root -> left -> right
        # 1. Process root -> :
        #    validate order
        # 2. Process -> left -> right : 
        # Result: validated BST
        
        def dfs(node, low, high):

            # Process root -> : 
            if not node:
                return True

            # Process root -> : validate order
            if not (low < node.val < high):
                return False

            # Process -> left -> right :
            left_valid = dfs(node.left, low, node.val)
            right_valid = dfs(node.right, node.val, high)

            # Process -> left -> right :
            subtreeValid = left_valid and right_valid

            return subtreeValid

        # Start with infinite bounds
        treeValid = dfs(root, float('-inf'), float('inf'))

        # overall: time complexity
        # overall: space complexity
        return treeValid
AspectTime ComplexitySpace ComplexityTime RemarksSpace Remarks

Solution 2: BFS Pre Order Iterative Passing Queuing Range Limits - Tree/DFS Pre order Traversal

    def isValidBST(self, root: Optional[TreeNode]) -> bool:

        # Note:
        # BFS pre order: process level: root -> left -> right
        # 1. Process level: 
        #      Process root -> :
        #        validate range limits
        # 2. Process -> left -> right : 
        # Result: validated BST

        # iterative queue
        queue = deque([(root, float('-inf'), float('inf'))])

        while queue:

            # Process root -> :
            node, low, high = queue.popleft()

            # Process root -> : 
            if not node:
                continue

            # Process root -> : validate order
            if not (low < node.val < high):
                return False

            # Process -> left -> right :
            queue.append((node.left, low, node.val))
            queue.append((node.right, node.val, high))

        # overall: time complexity
        # overall: space complexity
        return True

230. Kth Smallest Element in a BST ::2:: - Medium

Topics: Tree, Depth First Search, Binary Search Tree, Binary Tree

Intro

Given the root of a binary search tree, and an integer k, return the kth smallest value (1-indexed) of all the values of the nodes in the tree. Follow up: If the BST is modified often (i.e., we can do insert and delete operations) and you need to find the kth smallest frequently, how would you optimize?

Example InputOutput
root = [3,1,4,null,2], k = 11
root = [5,3,6,2,4,null,null,1], k = 33

Constraints:

The number of nodes in the root tree is n

1 ≤ k ≤ n ≤ 104

0 ≤ Node.val ≤ 104

Abstraction

Given a BST, find the kth smallest node.

Space & Time Complexity

SolutionTime ComplexitySpace ComplexityTime RemarkSpace Remark
BugError

Brute Force: iterative

AspectTime ComplexitySpace ComplexityTime RemarksSpace Remarks

Find the Bug:

Solution 1: DFS In Order Recursive Early Stop with Global Element Counter - Tree/DFS Pre order Traversal

    def isValidBST(self, root: Optional[TreeNode]) -> bool:

        # Note:
        # DFS in order: left -> root -> right
        # 1. Process left -> : 
        # 2. Process -> root -> :
        #    if reached kth node, return
        # 3. Process -> right :
        # Result: kth smallest found

        # global counter
        k = k
        # global pointer
        result = None
        
        def inorder(node):
            nonlocal k
            nonlocal result

            # Process left -> :
            if not node or result is not None:
                return
            
            # Process left -> :
            inorder(node.left)
            
            # Process root -> : 
            # if kth node, return
            k -= 1
            if k == 0:
                result = node.val
                return
            
            # Process right -> : 
            inorder(node.right)
        
        inorder(root)

        # overall: time complexity
        # overall: space complexity
        return result
AspectTime ComplexitySpace ComplexityTime RemarksSpace Remarks

Solution 2: DFS In Order Iterative with Element Counter - Tree/DFS Pre order Traversal

    def isValidBST(self, root: Optional[TreeNode]) -> bool:

        # Note:
        # DFS in order: left -> root -> right
        # 1. Process left -> : 
        # 2. Process -> root -> :
        #    if reached kth node, return
        # 3. Process -> right :
        # Result: kth smallest found

        # iterative stack
        stack = []
        
        while True:

            # Process left -> :
            while root:
                stack.append(root)
                root = root.left
            
            # Process -> root -> :
            root = stack.pop()

            # Process -> root -> : 
            # if kth node, return
            k -= 1
            if k == 0:
                return root.val
            
            # Process -> right :
            root = root.right

        # overall: time complexity
        # overall: space complexity
AspectTime ComplexitySpace ComplexityTime RemarksSpace Remarks

669. Trim a Binary Search Tree ::1:: - Medium

Topics: Tree, Depth First Search, Binary Search Tree, Binary Tree

Intro

Given the root of a binary search tree and the lowest and highest boundaries as low and high, trim the tree so that all its elements lies in [low, high]. Trimming the tree should not change the relative structure of the elements that will remain in the tree (i.e., any node's descendant should remain a descendant). It can be proven that there is a unique answer. Return the root of the trimmed binary search tree. Note that the root may change depending on the given bounds.

Example InputOutput
root = [1,0,2], low = 1, high = 2[1,null,2]
root = [3,0,4,null,2,null,null,1], low = 1, high = 3[3,2,null,1]

Constraints:

The number of nodes in the tree is in the range [1, 10^4]

0 ≤ Node.val ≤ 10^4

The value of each node in the tree is unique

root is guaranteed to be a valid binary search tree

0 ≤ low ≤ high ≤ 10^4

Abstraction

something!

Space & Time Complexity

SolutionTime ComplexitySpace ComplexityTime RemarkSpace Remark
BugError

Brute Force:

AspectTime ComplexitySpace ComplexityTime RemarksSpace Remarks

Find the Bug:

Solution 1: [DFS] DFS And BFS Pruning Optimization - Tree/DFS Post Order Recursive Two Sided Bottom Up

    def trimBST(self, root: Optional[TreeNode], low: int, high: int) -> Optional[TreeNode]:
        
        # DFS + BST Optimization
        #
        # Idea:
        #   - Use BST property to prune entire subtrees.
        #
        # Rules:
        #   1. If node.val < low:
        #        - left subtree is invalid
        #        - return trimmed right subtree
        #
        #   2. If node.val > high:
        #        - right subtree is invalid
        #        - return trimmed left subtree
         
        #   3. Otherwise:
        #        - node is valid
        #        - recursively trim both children
        #
        # Complexity:
        #   - Time: O(n)
        #   - Space: O(h) (recursion stack)

        if not root:
            return None

        # Node too small → discard left side
        if root.val < low:
            return self.trimBST(root.right, low, high)

        # Node too large → discard right side
        if root.val > high:
            return self.trimBST(root.left, low, high)

        # Node is valid → trim children
        root.left = self.trimBST(root.left, low, high)
        root.right = self.trimBST(root.right, low, high)

        return root