Binary Search Tree

CSCI 1913 – Introduction to Algorithms, Data Structures, and Program Development
Adriana Picoral

Binary Search Trees

  • Arrays provide fast random access but inserting/deleting is slow
  • Linked lists make insertion easy but searching is slow

What if we want something that’s reasonably fast at both searching and inserting?

We will start by taking a look at binary search trees and will look at different kinds of balanced trees and set/dictionary implementations later

Binary Search Trees

A Binary Search Tree (BST) is a linked structure where:

  • each node has at most two children
  • everything in the left subtree is smaller than the parent node
  • everything in the right subtree is larger than the parent node

Binary Search Trees

Each node has at most two children

BST 10 10 5 5 10--5 15 15 10--15 3 3 5--3 8 8 5--8 12 12 15--12 20 20 15--20 1 1 3--1 4 4 3--4 6 6 8--6 9 9 8--9 11 11 12--11 16 16 20--16 25 25 20--25 28 28 25--28

Binary Search Trees

everything in a left subtree is smaller than its parent node

BST 5 5 3 3 5--3 8 8 5--8 1 1 3--1 4 4 3--4 10 10 10--5 15 15 10--15 12 12 15--12 20 20 15--20 6 6 8--6 9 9 8--9 11 11 12--11 16 16 20--16 25 25 20--25 28 28 25--28

Binary Search Trees

everything in a right subtree is larger than its parent node

BST 10 10 5 5 10--5 15 15 10--15 3 3 5--3 8 8 5--8 12 12 15--12 20 20 15--20 1 1 3--1 4 4 3--4 6 6 8--6 9 9 8--9 11 11 12--11 16 16 20--16 25 25 20--25 28 28 25--28

Binary Search Trees

We will look at 6 functions (5 public and 1 private)

  • add
  • findMin
  • findMax
  • search
  • remove

Add

Data in Node that we want to add: 7

What do we have to do to add Node 7 to the right spot?

BST 10 10 5 5 10--5 15 15 10--15 3 3 5--3 8 8 5--8 12 12 15--12 20 20 15--20 1 1 3--1 4 4 3--4 6 6 8--6 9 9 8--9 11 11 12--11 16 16 20--16 25 25 20--25 28 28 25--28

Add

BST 7 7 10 10 5 5 10--5 15 15 10--15 3 3 5--3 8 8 5--8 12 12 15--12 20 20 15--20 1 1 3--1 4 4 3--4 6 6 8--6 9 9 8--9 11 11 12--11 16 16 20--16 25 25 20--25 28 28 25--28

Add

BST 7 7 10 10 5 5 10--5 15 15 10--15 3 3 5--3 8 8 5--8 12 12 15--12 20 20 15--20 1 1 3--1 4 4 3--4 6 6 8--6 9 9 8--9 11 11 12--11 16 16 20--16 25 25 20--25 28 28 25--28

Add

BST 7 7 10 10 5 5 10--5 15 15 10--15 3 3 5--3 8 8 5--8 12 12 15--12 20 20 15--20 1 1 3--1 4 4 3--4 6 6 8--6 9 9 8--9 11 11 12--11 16 16 20--16 25 25 20--25 28 28 25--28

Add

BST 7 7 10 10 5 5 10--5 15 15 10--15 3 3 5--3 8 8 5--8 12 12 15--12 20 20 15--20 1 1 3--1 4 4 3--4 6 6 8--6 9 9 8--9 11 11 12--11 16 16 20--16 25 25 20--25 28 28 25--28

Add

BST 10 10 5 5 10--5 15 15 10--15 3 3 5--3 8 8 5--8 12 12 15--12 20 20 15--20 1 1 3--1 4 4 3--4 6 6 8--6 9 9 8--9 null 6--null 7 7 6--7 11 11 12--11 16 16 20--16 25 25 20--25 28 28 25--28

Binary Search Trees

BSTs are only efficient when they’re balanced.

In the worst case you get O(n) operations – how?

You will learn about self-balancing trees like AVL trees and Red-Black trees (later)

Binary Search Trees

In the worst case you get O(n) operations – how?

BST 100 100 leftNull 100--leftNull rightNull 100--rightNull

Binary Search Trees

In the worst case you get O(n) operations – how?

BST 100 100 90 90 100--90 null 100--null

Binary Search Trees

In the worst case you get O(n) operations – how?

BST 100 100 90 90 100--90 null 100--null 80 80 90--80 null2 90--null2

Binary Search Trees

In the worst case you get O(n) operations – how?

BST 100 100 90 90 100--90 null 100--null 80 80 90--80 null2 90--null2 79 79 80--79 null3 80--null3

Binary Search Trees

In the worst case you get O(n) operations – how?

BST 100 100 90 90 100--90 null 100--null 80 80 90--80 null2 90--null2 79 79 80--79 null3 80--null3 65 65 79--65 null4 79--null4

Binary Search Trees

In the worst case you get O(n) operations – how?

BST 100 100 90 90 100--90 null 100--null 80 80 90--80 null2 90--null2 79 79 80--79 null3 80--null3 65 65 79--65 null4 79--null4 42 42 65--42 null5 65--null5

Binary Search Trees

In the worst case you get O(n) operations – how?

BST 100 100 90 90 100--90 null 100--null 80 80 90--80 null2 90--null2 79 79 80--79 null3 80--null3 65 65 79--65 null4 79--null4 42 42 65--42 null5 65--null5 26 26 42--26 null6 42--null6

Quiz 9

You have 10 minutes to complete the quiz.

Whether you check one or more options is up to you.

Java implementation

Node implementation

As a reminder, here’s what we had for our generic Node class:

public class Node<T> {
    private T data;
    private Node<T> left;
    private Node<T> right;

    // constructor with data as argument
    public Node(T data) {
        this.data = data;
        left = null;
        right = null;
    }

    // getters
    public T getData() {
        return data;
    }

    public Node<T> getLeft() {
        return left;
    }

    public Node<T> getRight() {
        return right;
    }

    // setters
    public void setData(T data) {
        this.data = data;
    }

    public void setLeft(Node<T> node) {
        left = node;
    }

    public void setRight(Node<T> node) {
        right = node;
    }
}

Node Comparable implementation

We need to implement extend Comparable to be able to go left or right (use compareTo from the data object)

public class Node<T extends Comparable<T>> {
    private T data;
    private Node<T> left;
    private Node<T> right;

    public Node(T data) {
        this.data = data;
        left = null;
        right = null;
    }

    public T getData() {
        return data;
    }

    public Node<T> getLeft() {
        return left;
    }

    public Node<T> getRight() {
        return right;
    }

    public void setData(T data) {
        this.data = data;
    }

    public void setLeft(Node<T> node) {
        left = node;
    }

    public void setRight(Node<T> node) {
        right = node;
    }

    public String toString() { return data.toString(); }

}

Tree implementation (basics)

public class Tree<T extends Comparable<T>> {
    private Node<T> root;

    public Tree() {
        root = null;
    }
  
    public Node<T> getRoot() {
        return root;
    }
    
}

Add – strategy

  • Check if tree is empty (is root null?), if so the node to add is the root
  • If tree is not empty, find the spot to add (I’ll do that recursively)

public addNode

public void addNode(Node<T> node) {
  if (root == null) {
    root = node;
  } else {
    addNode(root, node);
  }
}

private addNode

This cursive method should be private.

private void addNode(Node<T> current, Node<T> nodeToAdd)

Strategy:

  • Decide if you need to go left or right (use compareTo)
  • Check if left/right (whatever path you need to take) child is null, if so, set current node child to nodeToAdd
  • If left/right child is not null, call addNode recursively with left/right child as current node

addNode

private void addNode(Node<T> current, Node<T> nodeToAdd) {
  if (current.getData().compareTo(nodeToAdd.getData()) > 0) {
    if (current.getLeft() == null) current.setLeft(nodeToAdd);
    else addNode(current.getLeft(), nodeToAdd);
  } else if(current.getData().compareTo(nodeToAdd.getData()) < 0) {
    if (current.getRight() == null) current.setRight(nodeToAdd);
    else addNode(current.getRight(), nodeToAdd);
  }
}

public void addNode(Node<T> node) {
  if (root == null) root = node;
  else addNode(root, node);
}

Test it

Tree<Character> tree = new Tree<>();

tree.addNode(new Node<>('M'));
System.out.println(tree.getRoot().getData());

tree.addNode(new Node<>('O'));
System.out.println(tree.getRoot().getLeft());
System.out.println(tree.getRoot().getRight().getData());

toString

It’s going to be easy to test adding multiple nodes if we implement toString

Here’s the public toString:

public String toString() {
  return toString(root).trim();
}

Implement a private toString(Node)

Depth-First Traversal (DFS)

These traverse as far down a branch as possible before backtracking:

Inorder (Left → Root → Right)

  • Visits left subtree, then root, then right subtree
  • For binary search trees, this produces values in sorted order

Preorder (Root → Left → Right)

  • Visits root first, then left subtree, then right subtree
  • Useful for creating a copy of the tree or getting prefix expressions

Depth-First Traversal (DFS)

These traverse as far down a branch as possible before backtracking:

Postorder (Left → Right → Root)

  • Visits left subtree, then right subtree, then root last
  • Useful for deleting trees or evaluating postfix expressions

Breadth-First Traversal (BFS)

Level-order

  • Visits nodes level by level, from top to bottom and left to right
  • Typically implemented using a queue (we will see more of this later)
  • Useful for finding shortest paths or level-based operations

toString

One way to implement this that I find useful for debugging:

private String toString(Node<T> current) {
        if (current == null) {
            return "";
        }
        String message = "";

        if (current.getLeft() != null) {
            message += current.getData() + " left child is " + current.getLeft().getData() + "\n";
        }
        if (current.getRight() != null) {
            message += current.getData() + " right child is " + current.getRight().getData() + "\n";
        }

        if (current.getLeft() != null) {
            message += toString(current.getLeft());
        }
        if (current.getRight() != null) {
            message += toString(current.getRight());
        }
        return message;
    }

Other methods

  • findMin
  • findMax

Before you start writing code, what is the strategy here?

findMin

public Node<T> findMin() {
  return findMin(root);
}

private Node<T> findMin(Node<T> current) {
  if (current == null) return current;
  if (current.getLeft() == null) return current;
  return findMin(current.getLeft());
}

findMax

public Node<T> findMax() {
  return findMax(root);
}

private Node<T> findMax(Node<T> current) {
  if (current == null) return current;
  if (current.getRight() == null) return current;
  return findMax(current.getRight());
}

Search

What’s the runtime?

public Node<T> search(T value) {
  return search(root, value);
}

private Node<T> search(Node<T> current, T value) {
  if (current == null) return null;
  if (current.getData().compareTo(value) == 0) return current;
  if (current.getData().compareTo(value) > 0) {
    if (current.getLeft() == null) return null;
    return search(current.getLeft(), value);
  }
  if (current.getRight() == null) return null;
  return search(current.getRight(), value);
}

Search runtime

Best/Average case: \(O(\log n)\) when the tree is balanced

  • In a balanced BST the number of iterations equals the height of the tree.
  • The height of the tree is \(\log n\)

Worst case: \(O(n)\) (why?)

Remove

Before you start writing code, what is the strategy here?

Remove

General idea - search for node to remove and its parent.

Remove

Leaf

  • Can be removed without breaking tree

One child (two cases really, left/right)

  • Cannot be removed without breaking tree
  • Can be removed with only locally available pointers

Remove

Two children

  • Cannot be removed without breaking tree
  • Cannot be fixed with locally available pointers
  • Costs an additional O(H) work to find a way to fix.

Remove

BST lemon lemon guava guava lemon--guava raspberry raspberry lemon--raspberry coconut coconut guava--coconut kiwi kiwi guava--kiwi lychee lychee raspberry--lychee strawberry strawberry raspberry--strawberry avocado avocado coconut--avocado banana banana coconut--banana lime lime lychee--lime pomelo pomelo lychee--pomelo starfruit starfruit strawberry--starfruit tomato tomato strawberry--tomato null1 null2

Remove

A leaf can be removed without breaking the tree

BST lemon lemon guava guava lemon--guava raspberry raspberry lemon--raspberry coconut coconut guava--coconut kiwi kiwi guava--kiwi lychee lychee raspberry--lychee strawberry strawberry raspberry--strawberry avocado avocado coconut--avocado banana banana coconut--banana lime lime lychee--lime pomelo pomelo lychee--pomelo starfruit starfruit strawberry--starfruit tomato tomato strawberry--tomato null1 null2

Remove

Node with one child can be removed with only locally available pointers

BST lemon lemon guava guava lemon--guava raspberry raspberry lemon--raspberry coconut coconut guava--coconut null0 guava--null0 lychee lychee raspberry--lychee strawberry strawberry raspberry--strawberry avocado avocado coconut--avocado banana banana coconut--banana lime lime lychee--lime pomelo pomelo lychee--pomelo starfruit starfruit strawberry--starfruit tomato tomato strawberry--tomato null1 null2

Remove

Node with one child can be removed with only locally available pointers

BST lemon lemon coconut coconut lemon--coconut raspberry raspberry lemon--raspberry avocado avocado coconut--avocado banana banana coconut--banana lychee lychee raspberry--lychee strawberry strawberry raspberry--strawberry lime lime lychee--lime pomelo pomelo lychee--pomelo starfruit starfruit strawberry--starfruit tomato tomato strawberry--tomato null1 null2

Remove

Node with two children

BST lemon lemon coconut coconut lemon--coconut raspberry raspberry lemon--raspberry avocado avocado coconut--avocado banana banana coconut--banana lychee lychee raspberry--lychee strawberry strawberry raspberry--strawberry lime lime lychee--lime pomelo pomelo lychee--pomelo starfruit starfruit strawberry--starfruit tomato tomato strawberry--tomato null1 null2

Remove

Node with two children – replace node to remove data with min to the right data (or max to the left data)

BST lemon lemon coconut coconut lemon--coconut raspberry raspberry lemon--raspberry avocado avocado coconut--avocado banana banana coconut--banana lychee lychee raspberry--lychee strawberry strawberry raspberry--strawberry lime lime lychee--lime pomelo pomelo lychee--pomelo starfruit starfruit strawberry--starfruit tomato tomato strawberry--tomato null1 null2

Remove

Node with two children – replace node to remove data with min to the right data (or max to the left data)

BST lemon lemon coconut coconut lemon--coconut starfruit starfruit lemon--starfruit avocado avocado coconut--avocado banana banana coconut--banana lychee lychee starfruit--lychee strawberry strawberry starfruit--strawberry lime lime lychee--lime pomelo pomelo lychee--pomelo null2 strawberry--null2 tomato tomato strawberry--tomato null1

Remove – steps

  • find parent of node to remove

  • modify parent.left/right to not reference node to remove

  • count how many children node to remove has

    • 0 children – modify parent.left/right to not reference node to remove
    • 1 child – the “orphan” child replaces the node to remove
    • 2 children – special node (we need to find it) replaces it (can’t just swap nodes because of references, switch data instead)

remove implementation

public void remove(T value) {
  // find node to remove and its parent
  Node<T> parent = null;
  Node<T> current = root;
  
  while (current != null && current.getData().compareTo(value) != 0) {
    parent = current;
    if (current.getData().compareTo(value) > 0) {
      current = current.getLeft();
    } else {
      current = current.getRight();
    }
  }
  if (current != null) {
    remove(parent, current);
  }
}

private void remove(Node<T> parent, Node<T> nodeToRemove) {
  // count number of children
  int children = 0;
  Node<T> child = null;
  
  if (nodeToRemove.getLeft() != null) {
    children++;
    child = nodeToRemove.getLeft();
  }
  if (nodeToRemove.getRight() != null) {
    children++;
    child = nodeToRemove.getRight();;
  }
  
  // address each case
  if (children <= 1) {
    if (parent == null) root = child;
    else if (parent.getLeft() == nodeToRemove) parent.setLeft(child);
    else parent.setRight(child);
  }
  
  if (children == 2) {
    // find minimum to the right
    // or find max to the left
    Node<T> previous = nodeToRemove;
    Node<T> replacement = previous.getRight();
    while (replacement.getLeft() != null) {
      previous = replacement;
      replacement = replacement.getLeft();
    }
    nodeToRemove.setData(replacement.getData());
    if (previous == nodeToRemove) previous.setRight(null);
    else previous.setLeft(null);
    
  }
  
}

Recap

  • What is a binary search tree?
  • How do you traverse a binary search tree?
  • How do you search a binary search tree?
  • How do you add to a binary search tree?
  • How do you remove from a binary search tree?
  • What is the runtime of these operations?

Tree Implementation

public class Tree<T extends Comparable<T>> {
    private Node<T> root;

    public Tree() { root = null; }

    private void addNode(Node<T> current, Node<T> nodeToAdd) {
        if (current.getData().compareTo(nodeToAdd.getData()) > 0) {
            if (current.getLeft() == null) {
                current.setLeft(nodeToAdd);
            } else {
                addNode(current.getLeft(), nodeToAdd);
            }
        } else if(current.getData().compareTo(nodeToAdd.getData()) < 0) {
            if (current.getRight() == null) {
                current.setRight(nodeToAdd);
            } else {
                addNode(current.getRight(), nodeToAdd);
            }
        }
    }

    public Node<T> getRoot() { return root; }

    public void addNode(Node<T> node) {
        if (root == null) root = node;
        else addNode(root, node);
    }

    private String toString(int level, Node<T> current) {
        if (current == null) return "";

        String retVal = toString(level + 1, current.getLeft());

        for (int i = 0; i < level; i++) retVal += "\t";

        retVal += current.getData() + "<";
        retVal += "\n";

        retVal += toString(level + 1, current.getRight());
        return retVal;
    }

    private String toString(Node<T> current) {
        if (current == null) return "";

        String message = "";

        if (current.getLeft() != null) {
            message += current.getData() + " left child is " + current.getLeft().getData() + "\n";
        }
        if (current.getRight() != null) {
            message += current.getData() + " right child is " + current.getRight().getData() + "\n";
        }

        if (current.getLeft() != null) {
            message += toString(current.getLeft());
        }
        if (current.getRight() != null) {
            message += toString(current.getRight());
        }
        return message;
    }


    public String toString() {
        return toString(0, root);
        //return root.getData() + " is the root\n" + toString(root).trim();
    }

    public Node<T> findMin() {
        return findMin(root);
    }

    private Node<T> findMin(Node<T> current) {
        if (current == null) return current;
        if (current.getLeft() == null) return current;
        return findMin(current.getLeft());
    }

    public Node<T> findMax() {
        return findMax(root);
    }

    private Node<T> findMax(Node<T> current) {
        if (current == null) return current;
        if (current.getRight() == null) return current;
        return findMax(current.getRight());
    }

    public Node<T> search(T value) {
        return search(root, value);
    }

    private Node<T> search(Node<T> current, T value) {
        if (current == null) return null;
        if (current.getData().compareTo(value) == 0) return current;
        if (current.getData().compareTo(value) > 0) {
            if (current.getLeft() == null) return null;
            return search(current.getLeft(), value);
        }
        if (current.getRight() == null) return null;
        return search(current.getRight(), value);
    }

    private void remove(Node<T> parent, Node<T> nodeToRemove) {
        // count number of children
        int children = 0;
        Node<T> child = null;

        if (nodeToRemove.getLeft() != null) {
            children++;
            child = nodeToRemove.getLeft();
        }
        if (nodeToRemove.getRight() != null) {
            children++;
            child = nodeToRemove.getRight();;
        }

        // address each case
        if (children <= 1) {
            if (parent == null) root = child;
            else if (parent.getLeft() == nodeToRemove) parent.setLeft(child);
            else parent.setRight(child);
        }

        if (children == 2) {
            // find minimum to the right
            // or find max to the left
            Node<T> previous = nodeToRemove;
            Node<T> replacement = previous.getRight();
            while (replacement.getLeft() != null) {
                previous = replacement;
                replacement = replacement.getLeft();
            }
            nodeToRemove.setData(replacement.getData());
            if (previous == nodeToRemove) previous.setRight(null);
            else previous.setLeft(null);

        }

    }

    public void remove(T value) {
        // find node to remove and its parent
        Node<T> parent = null;
        Node<T> current = root;

        while (current != null && current.getData().compareTo(value) != 0) {
            parent = current;
            if (current.getData().compareTo(value) > 0) {
                current = current.getLeft();
            } else {
                current = current.getRight();
            }
        }
        if (current != null) {
            remove(parent, current);
        }
    }
}

BST operations runtime

Tree height \(h\) is \(\log n\) for balanced tree.

Search, Insert, Delete:

  • Average case: O(log n) - when the tree is reasonably balanced
  • Worst case: O(n) - when the tree degenerates into a linked list (all nodes in a single chain)

Find Minimum/Maximum:

  • Average case: O(log n)
  • Worst case: O(n)

Traversal (inorder, preorder, postorder):

  • Always O(n) - must visit every node