Advanced Search Trees

Table of Contents

1 Reading

After watching the video, read Bailey 14.5 for an overview of one advanced variation of a binary search tree: the splay tree. Optionally read 14.6 for splay-tree implementation details and 14.7 for a description of a type of self-balancing tree: the red-black tree.

2 AVL Trees

An AVL tree (named after inventors Adelson-Velsky and Landis) is a self-balancing binary search tree.

  • Structural properties
    • Binary search tree property (nodes to the left are smaller, nodes to the right are larger)
    • NEW: balance property
      • The difference in heights between the left subtree and the right subtree must be at most 1
  • Result:
    • Worst-case height is \(O(\log n)\)

A valid AVl tree (blue numbers are the height at each node):

avl1.png

Not a valid AVL tree (red height numbers indicate nodes with children out of balance)

avl2.png

How do we enforce the balance property?

  • As we insert and delete elements, we need to:
    • Track balance
    • Detect imbalance
    • Restore balance

We will need to store height at each node:

avl-node.png

Searching the tree works exactly the same as it does for a BST For adding

  • we first add as normal for a BST
  • then fix any resulting imbalance

avl-insert.png

More specifically,

  • Insert the new node as in a BST (a new leaf)
  • For each node on the path from the root to the new leaf, the insertion may (or may not) have changed the node's height
  • So after recursive insertion in a subtree, detect height imbalance and perform a rotation to restore balance at that node

All the action is in defining the correct rotations to restore balance!

  • Single rotation: The basic operation we'll use to rebalance
    • Move child of unbalanced node into parent position
    • Parent becomes the "other" child (always okay in a BST!)
    • Other subtrees move in only way BST allows.

avl-rotation.png

A rotation is a constant time (\(O(1)\)) operation! Like removing from a BST, there are several different cases to handle when restoring balance to an AVL Tree. We won't go through them in detail, but we'll look at some animations to get a sense of what's going on.

There are also other kinds of balanced binary search trees. Two common ones, splay trees and red-black trees, are discussed in Bailey. AVL Trees provide the fastest searches because they are strictly balanced. Red-black trees and splay trees have more relaxed invariants, and so are faster at adding and removing nodes with fewer rotations. Java's TreeMap class uses a red-black tree.

2.1 Animations

https://visualgo.net/en/bst (select AVL Tree in the bar at the top of the page)

2.2 Analysis

Now that we have guaranteed that a tree with \(n\) nodes will have height \(\log n\), our search tree performance is looking pretty good! Not as efficient as a hash table for some operations, but able to efficiently provide operations related to the sorted order of the keys that a hash table is unsuited for. Here's a chart of the big-O running time for various structures on different Map operations:

Operation Unsorted array Sorted array Hash table Balanced BST (e.g., AVL Tree)
contains(Key key) \(O(n)\) \(O(\log n)\) \(O(1)\) \(O(\log n)\)
get(Key key) \(O(n)\) \(O(\log n)\) \(O(1)\) \(O(\log n)\)
put(Key key, Value val) \(O(1)\) \(O(n)\) \(O(1)\) \(O(\log n)\)
minKey() \(O(n)\) \(O(1)\) \(O(n)\) \(O(\log n)\)
maxKey() \(O(n)\) \(O(1)\) \(O(n)\) \(O(\log n)\)
iterate over the keys in order \(O(n\log n)\) \(O(n)\) \(O(n\log n)\) \(O(n)\)

3 Tries

Also called a prefix tree, a trie is a tree used for location specific keys within a set.

package student;

import java.util.HashMap;
import java.util.List;

public class Trie {
    private TrieNode overallRoot;

    private class TrieNode {
        String content;
        HashMap<Character, TrieNode> children;
        boolean isWord;

        public TrieNode(String v) {
            content = v;
            children = new HashMap<>();
        }
    }

    public Trie() {
        overallRoot = new TrieNode("");
    }

    /**
     * Insert a new word into the Trie, creating any necessary nodes.
     * @param word The word to be inserted.
     */
    public void insert(String word) {
        TrieNode current = overallRoot;

        for (char l: word.toCharArray()) {
            if (!current.children.containsKey(l)) {
                current.children.put(l, new TrieNode(Character.toString(l)));
            }
            current = current.children.get(l);
        }
        current.isWord = true;
    }

    public static void main(String[] args) {
        Trie trie = new Trie();

        trie.insert("house");
        trie.insert("home");
        trie.insert("homeland");
        trie.insert("hope");
        trie.insert("hound");
        trie.insert("hose");
    }
}

4 Practice Problems1

  1. Give five orderings of the keys A X C S E R H that, when inserted into an initially empty BST, produce the best-case (i.e., balanced) tree.
  2. Give nonrecursive implementations of get and put for a BST.
  3. Write a method isBST(Node node) that takes a Node as argument and returns true if the argument node is the root of a binary search tree, false otherwise.
  4. How many nodes are in a full binary tree of height \(x\)?
  5. Which of the following trees are valid AVL trees?

    (A)

    avl-practice1.png

    (B)

    avl-practice2.png

    (C)

    avl-practice3.png

    (D)

    avl-practice4.png

    (E)

    avl-practice5.png

Footnotes:

1

Solutions:

  1. Any sequence that inserts H first; C before A and E; S before R and X.
  2. // Insert key-value pair into symbol table (nonrecursive version).
    public void put(Key key, Value val) {
        Node z = new Node(key, val);
        if (root == null) {
            root = z;
            return;
        }
    
        Node parent = null;
        Node x = root;
        while (x != null) {
            parent = x;
            int cmp = key.compareTo(x.key);
            if      (cmp < 0) x = x.left;
            else if (cmp > 0) x = x.right;
            else {
                x.value = val;
                return; 
            }
        }
        int cmp = key.compareTo(parent.key);
        if (cmp < 0) parent.left  = z;
        else         parent.right = z;
    }
    
    // Search BST for given key, nonrecursive version.
    public Value get(Key key) {
        Node x = root;
        while (x != null) {
            int cmp = key.compareTo(x.key);
            if      (cmp < 0) x = x.left;
            else if (cmp > 0) x = x.right;
            else return x.value;
        }
        return null;
    }   
    
  3. public boolean isBST(Node node) {
        if (node == null) return true; // an empty tree is a valid BST
        // each child must be empty or have an appropriate key and itself be a valid BST
        boolean leftCheck = node.left == null || (node.left.key < node.key && isBST(node.left));
        boolean rightCheck = node.right == null || (node.right.key > node.key && isBST(node.right));
        return leftCheck && rightCheck;
    }
    
  4. A full tree of height \(x\) will have \(2^{x+1} - 1\) nodes
  5. (B) and (E) are AVL trees, as they follow the balance property (all five follow the BST ordering property)