Jc-alt logo
jc
data structures and algorithms

LeetCode: Trees I BST

LeetCode: Trees I BST
19 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:
        
        # Note:
        # BST traversal: root -> either -> left or -> right
        # 1. Process -> root : 
        #    determine if LCA is in left or right
        # 2. Process -> left or -> right :
        # Result: find LCA node
        
        # process root -> :
        # p and q are smaller, LCA in left subtree 
        if p.val < root.val and q.val < root.val:

            # process -> left :
            return self.lowestCommonAncestor(root.left, p, q)

        # process root -> :
        # p and q are larger, LCA in right subtree
        elif p.val > root.val and q.val > root.val:

            # process -> right :
            return self.lowestCommonAncestor(root.right, p, q)
        
        # process root -> :
        # p and q are in different subtrees, root is LCA
        else:
            return root

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

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

    def lowestCommonAncestor(self, root: TreeNode, p: TreeNode, q: TreeNode) -> TreeNode:
        
        # Note:
        # BST traversal: root -> either -> left or -> right
        # 1. Process -> root : 
        #    determine if LCA is in left or right
        # 2. Process -> left or -> right :
        # Result: find LCA node

        # iterative root
        while root:

            # process root -> :
            # p and q are smaller, LCA in left subtree 
            if p.val < root.val and q.val < root.val:

                # process -> left :
                root = root.left

            # process root ->
            # p and q are larger, LCA in right subtree
            elif p.val > curr.val and q.val > curr.val:

                # process -> right :
                curr = curr.right

            # process root -> :
            # p and q are in different subtrees, root is LCA
            else:
                return curr
        
        # overall: time complexity
        # overall: space complexity
AspectTime ComplexitySpace ComplexityTime RemarksSpace Remarks

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
        #
        # Example:
        #        4
        #       / \
        #      2   7
        #     / \
        #    1   3
        #
        # Inserting val = 5:
        #        4
        #       / \
        #      2   7
        #     / \  /
        #    1   3 5

        # 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 not root:
            # 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:
        # Idea:
        #   1. Traverse tree from root iteratively
        #   2. At each node, compare val with node.val
        #   3. Move left or right based on BST property
        #   4. When a None child is found, insert the new node
        
        if not root:
            # If tree is empty, new node becomes root
            # tc O(1), sc O(1)
            return TreeNode(val)
        
        current = root
        
        while True:
            if val < current.val:
                # Go left if left child exists
                if current.left:
                    current = current.left
                else:
                    # Insert new node as left child
                    current.left = TreeNode(val)
                    # tc O(1), sc O(1) for node creation
                    break
            else:
                # Go right if right child exists
                if current.right:
                    current = current.right
                else:
                    # Insert new node as right child
                    current.right = TreeNode(val)
                    # tc O(1), sc O(1) for node creation
                    break
        
        # Return root to maintain BST structure
        # tc: O(h) traversal along height
        # sc: O(1) iterative → no recursion stack
        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]:
        
        # Binary Search Tree Deletion:
        # BST property: left < node.val < right
        #
        # Deletion has 3 main cases:
        # 1. Node to delete has no children -> remove directly
        # 2. Node has one child -> 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
        
        if not root:
            # Base case: key not found
            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)
        else:
            # Found the node to delete
            if not root.left and not root.right:
                # Case 1: No children
                return None  # tc O(1), sc O(1)
            elif not root.left:
                # Case 2a: Only right child
                return root.right  # tc O(1), sc O(1)
            elif not root.right:
                # Case 2b: Only left child
                return root.left  # tc O(1), sc O(1)
            else:
                # Case 3: Two children
                # Find inorder successor: leftmost node in right subtree
                successor = root.right
                while successor.left:
                    successor = successor.left  # tc O(h) worst case
                
                # Replace root value with successor's value
                root.val = successor.val
                
                # Delete successor node in right subtree recursively
                root.right = self.deleteNode(root.right, successor.val)
                # tc: O(h) for deletion path
                
        # Return root to maintain BST structure
        # overall: tc O(h), 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 deleteNode(self, root: Optional[TreeNode], key: int) -> Optional[TreeNode]:

        # Iterative BST Deletion
        # Idea:
        #   1. Use pointers to track current node and parent
        #   2. Traverse the tree to find the node with the key
        #   3. Once found, handle three deletion cases:
        #       a. No children -> just remove
        #       b. One child -> replace node with child
        #       c. Two children -> find inorder successor (smallest in right subtree),
        #          replace value, then remove successor
        
        if not root:
            return None  # tc O(1), sc O(1)
        
        # Dummy parent for convenience
        dummy = TreeNode(0)
        dummy.left = root
        parent = dummy
        current = root
        is_left_child = True  # Track whether current is left or right child of parent
        
        # Step 1: 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
        
        # Key not found
        if not current:
            return root  # tc O(h), sc O(1)
        
        # 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 might not change)
        # overall: tc O(h), sc O(1) iterative
        return dummy.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