Saturday, June 21, 2025

Binary Search Trees

 We’ve covered that a binary tree is a special kind of tree, in which each node splits into exactly 0, 1, or 2 others—and never more. We’ve also initially seen binary search done on an array: go to the middle index, decide which half, discard the “wrong” one, go to the middle, decide which half, etc., and keep doing this recursively until either you can prove that you’ve found the element you want, or that it definitely isn’t in your collection.

Now, let’s take those ideas and merge them: create a tree purporpse-built to execute binary search.  

Remember that one of the conditions for binary search to work—as opposed to the simpler linear search—is that the contents of the array (now a tree) need to be sorted.

When something is sorted, it helps to keep an eye on 3 reference points: the first element, the median element (half are “less/before” and half are “greater/after”), and the last element. (Which of the first versus last is the biggest and which is the smallest, of course, depends on the direction of the sort—ascending or descending.)

Knowing how a binary tree works, where would these three values go?

·       The median has half the data set to its left and half the data set to its right

·       The min has all the rest of the data set to its right

·       The max has all the rest of the data set to its left

Take a moment to pause, draw out a tree, and think about where these values should go.

Let’s start with the median, because this value actually occupies a special place. Where is the “center of gravity” (this isn’t a real term) of the tree? Naturally, the root—half of the data less than it is in the left subtree, and half of the data greater than it is in the right subtree!

Where then do the min and max live? Let’s start with the min. The min occupies the place that takes a left at every fork from the root (graphically, as far to the bottom left as possible). Likewise, the max takes a right at every fork from the root (and therefore is found as far to the bottom right as possible).

Suppose this is our tree:
Data Structure: Binary Search Tree | Learn. Share. Improve.


How did we build it?

  • 8 becomes the root
  • Then we add 3
    • 3 < 8, so go left
  • Then we add 1
    • 1 < 8, so go left
    • 1 < 3, so go left
  • Then we add 6
    • 6 < 8, so go left
    • 6 > 3, so go right
  • Then we add 4
    • 4 < 8, so go left
    • 4 > 3, so go right
    • 4 < 6, so go left
  • Then we add 7
    • 7 < 8, so go left
    • 7 > 3, so go right
    • 7 > 6, so go right
  • Then we add 10
    • 8 > 10, so go right
  • Then we add 14
    • 14 > 8, so go right
    • 14 > 10, so go right
  • Then we add 13
    • 13 > 8, so go right
    • 13 > 10, so go right
    • 13 < 14, so go left

To add 0 and 20:

Adding 0:

  • 0 < 8, so go left
  • 0 < 3, so go left
  • 0 < 1, so go left

Adding 20:

  • 20 > 8, so go right
  • 20 > 10, so go right
  • 20 > 14, so go right

Here is that process in pseudocode:

function insert(node, key):

    if node is null:

        return new Node(key)

 

    if key < node.value:

        node.left = insert(node.left, key)

    else if key > node.value:

        node.right = insert(node.right, key)

    // If key == node.value, do nothing, or handle duplicates as needed

 

    return node

 

Adding nodes in this way is easy, but how about removing, with the caveat that a binary search tree, as its name implies, is used for binary search, which requires a sorted data set?

We find ourselves in one of 3 cases:

  • The node to remove is a leaf, i.e., has no children—this is trivial
  • The node to remove is an interior node, i.e., having both children and parents
  • The node to remove is the root, i.e., having no parents, but having children

 

Here is the pseudocode for the removal of an interior node:

function deleteNode(root, key):

    if root is null:

        return null

 

    if key < root.value:

        root.left = deleteNode(root.left, key)

    else if key > root.value:

        root.right = deleteNode(root.right, key)

    else:

        // Node found

        if root.left is null:

            temp = root.right

            free(root)

            return temp

        else if root.right is null:

            temp = root.left

            free(root)

            return temp

        else:

            // Node has two children

            successor = findMin(root.right)

            root.value = successor.value

            root.right = deleteNode(root.right, successor.value)

    return root

 

function findMin(node):

    while node.left is not null:

        node = node.left

    return node

and here is the pseudocode to remove the root:

function deleteRoot(root):

    if root is null:

        return null

 

    // Case 1: Root has no children (tree becomes empty)

    if root.left is null and root.right is null:

        free(root)

        return null

 

    // Case 2: Root has only one child

    if root.left is null:

        temp = root.right

        free(root)

        return temp

    if root.right is null:

        temp = root.left

        free(root)

        return temp

 

    // Case 3: Root has two children

    // Find inorder successor (smallest in right subtree)

    successor = findMin(root.right)

    root.value = successor.value

    root.right = deleteNode(root.right, successor.value)

    return root

 

function findMin(node):

    while node.left is not null:

        node = node.left

    return node

No comments:

Post a Comment

Switch

 Other than if/if-else/if-else if-else and the ternary operator, there is yet another common and important conditional expression in Java th...