🚧 We are still building the lighthouse. Hang tight!

projectlighthouse logoprojectlighthouse

Sign in with your Google or GitHub account to get started

BST Insert and Remove

Implementing insertion and deletion in binary search trees. Handle all deletion cases including nodes with two children.

Insertion and deletion in Binary Search Trees must maintain the BST property. Insertion is straightforward, but deletion has several cases to handle.

BST Insertion

To insert a value:

  1. If tree is empty, create root
  2. Otherwise, find the correct position by comparing with each node
  3. Insert as a leaf
def insert(self, val):
    if self.root is None:
        self.root = TreeNode(val)
    else:
        self._insert(self.root, val)

def _insert(self, node, val):
    if val < node.val:
        if node.left is None:
            node.left = TreeNode(val)
        else:
            self._insert(node.left, val)
    else:  # val >= node.val
        if node.right is None:
            node.right = TreeNode(val)
        else:
            self._insert(node.right, val)
Inserting 5 into BST:

       8                        8
      / \                      / \
     3   10                   3   10
    / \                      / \
   1   6         →          1   6
                               / \
                              5   (existing subtree)

5 < 8 → go left
5 > 3 → go right
5 < 6 → go left
6.left is None → insert here

Iterative Insert

def insert_iterative(self, val):
    new_node = TreeNode(val)

    if self.root is None:
        self.root = new_node
        return

    parent = None
    current = self.root

    while current:
        parent = current
        if val < current.val:
            current = current.left
        else:
            current = current.right

    if val < parent.val:
        parent.left = new_node
    else:
        parent.right = new_node

BST Deletion

Deletion is more complex. There are three cases:

Case 1: Deleting a Leaf

Simply remove the node.

Delete 4:

       8                        8
      / \                      / \
     3   10                   3   10
    / \                      / \
   1   6         →          1   6
      /
     4                        (4 removed)

Case 2: Node with One Child

Replace the node with its child.

Delete 6 (has left child 4):

       8                        8
      / \                      / \
     3   10                   3   10
    / \                      / \
   1   6         →          1   4
      /
     4

Case 3: Node with Two Children

Replace with either:

  • Inorder successor: Smallest node in right subtree
  • Inorder predecessor: Largest node in left subtree

Then delete the successor/predecessor.

Delete 3 (has two children):

       8                        8
      / \                      / \
     3   10                   4   10
    / \                      / \
   1   6         →          1   6
      /
     4

Inorder successor of 3 is 4.
Replace 3 with 4, delete original 4.

Complete Delete Implementation

def delete(self, val):
    self.root = self._delete(self.root, val)

def _delete(self, node, val):
    if node is None:
        return None

    # Find the node to delete
    if val < node.val:
        node.left = self._delete(node.left, val)
    elif val > node.val:
        node.right = self._delete(node.right, val)
    else:
        # Found the node to delete

        # Case 1 & 2: No child or one child
        if node.left is None:
            return node.right
        if node.right is None:
            return node.left

        # Case 3: Two children
        # Find inorder successor (smallest in right subtree)
        successor = self._find_min(node.right)

        # Copy successor's value to this node
        node.val = successor.val

        # Delete the successor
        node.right = self._delete(node.right, successor.val)

    return node

def _find_min(self, node):
    while node.left:
        node = node.left
    return node
func (bst *BST) Delete(val int) {
    bst.root = deleteNode(bst.root, val)
}

func deleteNode(node *TreeNode, val int) *TreeNode {
    if node == nil {
        return nil
    }

    if val < node.Val {
        node.Left = deleteNode(node.Left, val)
    } else if val > node.Val {
        node.Right = deleteNode(node.Right, val)
    } else {
        // Found node to delete
        if node.Left == nil {
            return node.Right
        }
        if node.Right == nil {
            return node.Left
        }

        // Two children: find successor
        successor := findMin(node.Right)
        node.Val = successor.Val
        node.Right = deleteNode(node.Right, successor.Val)
    }

    return node
}

func findMin(node *TreeNode) *TreeNode {
    for node.Left != nil {
        node = node.Left
    }
    return node
}

Successor vs Predecessor

You can use either the inorder successor (min of right subtree) or inorder predecessor (max of left subtree) for case 3. The choice is arbitrary, but being consistent simplifies debugging.

Visualizing Case 3

Delete 8 (root with two children):

Original:
         8
        / \
       3   10
      / \  / \
     1  6 9  14

Inorder successor of 8 is 9 (min of right subtree)

Step 1: Copy 9's value to 8's position
         9
        / \
       3   10
      / \  / \
     1  6 9  14

Step 2: Delete original 9 (it's a leaf or has one child)
         9
        / \
       3   10
      / \    \
     1  6    14

Iterative Delete

def delete_iterative(self, val):
    parent = None
    current = self.root

    # Find the node and its parent
    while current and current.val != val:
        parent = current
        if val < current.val:
            current = current.left
        else:
            current = current.right

    if current is None:
        return  # Value not found

    # Case 1 & 2: At most one child
    if current.left is None or current.right is None:
        child = current.left if current.left else current.right

        if parent is None:
            self.root = child
        elif parent.left == current:
            parent.left = child
        else:
            parent.right = child

    # Case 3: Two children
    else:
        # Find inorder successor
        succ_parent = current
        successor = current.right
        while successor.left:
            succ_parent = successor
            successor = successor.left

        # Copy successor value
        current.val = successor.val

        # Delete successor
        if succ_parent.left == successor:
            succ_parent.left = successor.right
        else:
            succ_parent.right = successor.right

Building a BST from Array

def build_bst(arr):
    bst = BST()
    for val in arr:
        bst.insert(val)
    return bst

# Order matters!
arr1 = [5, 3, 7, 2, 4, 6, 8]  # Balanced
arr2 = [2, 3, 4, 5, 6, 7, 8]  # Skewed (sorted input)

To build a balanced BST from sorted array:

def sorted_array_to_bst(arr):
    if not arr:
        return None

    mid = len(arr) // 2

    root = TreeNode(arr[mid])
    root.left = sorted_array_to_bst(arr[:mid])
    root.right = sorted_array_to_bst(arr[mid + 1:])

    return root

Insertion Order Matters

Inserting sorted data creates a skewed tree (linked list). To get a balanced tree, either insert in random order, use a balanced tree variant, or build from sorted array using the middle element as root.

Complete BST Class

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

class BST:
    def __init__(self):
        self.root = None

    def insert(self, val):
        if self.root is None:
            self.root = TreeNode(val)
        else:
            self._insert(self.root, val)

    def _insert(self, node, val):
        if val < node.val:
            if node.left is None:
                node.left = TreeNode(val)
            else:
                self._insert(node.left, val)
        else:
            if node.right is None:
                node.right = TreeNode(val)
            else:
                self._insert(node.right, val)

    def search(self, val):
        return self._search(self.root, val)

    def _search(self, node, val):
        if node is None or node.val == val:
            return node
        if val < node.val:
            return self._search(node.left, val)
        return self._search(node.right, val)

    def delete(self, val):
        self.root = self._delete(self.root, val)

    def _delete(self, node, val):
        if node is None:
            return None

        if val < node.val:
            node.left = self._delete(node.left, val)
        elif val > node.val:
            node.right = self._delete(node.right, val)
        else:
            if node.left is None:
                return node.right
            if node.right is None:
                return node.left

            successor = node.right
            while successor.left:
                successor = successor.left
            node.val = successor.val
            node.right = self._delete(node.right, successor.val)

        return node

    def inorder(self):
        result = []
        self._inorder(self.root, result)
        return result

    def _inorder(self, node, result):
        if node:
            self._inorder(node.left, result)
            result.append(node.val)
            self._inorder(node.right, result)

Time Complexity

| Operation | Balanced | Skewed | |-----------|----------|--------| | Insert | O(log n) | O(n) | | Delete | O(log n) | O(n) | | Search | O(log n) | O(n) |

Key Takeaways

  1. Insert: Find correct position using BST property, add as leaf
  2. Delete has three cases: leaf, one child, two children
  3. Two-child deletion uses inorder successor or predecessor
  4. Insertion order affects tree balance
  5. Sorted input creates worst-case O(n) height
  6. Build balanced tree from sorted array using middle element

Understanding BST insert and delete operations is essential before moving to self-balancing trees like AVL and Red-Black trees, which automate maintaining balance.

Notes & Highlights

© 2025 projectlighthouse. All rights reserved.