CS 70

Phase 3: Implementing lower_bound in TreeSet<T>

  • Duck speaking

    I was looking ahead, and saw we'll soon be learning about hash tables. They perform lookups in expected \( \mathrm{O}(1) \) time, and they're what Python dictionaries use, so why are we bothering with binary search trees that only do lookups in \( \mathrm{O}(\log n) \) time?

  • LHS Cow speaking

    The ordered nature of BSTs lets us do things that other schemes for storing sets can't do efficiently. You've already seen that your TreeSet<T> iterator gives you the elements in sorted order. But another useful operation is finding the “nearest” element to a given value.

  • Dog speaking

    So, if I look for bone, and that's not there, but bones is, I'll get bones back?

  • LHS Cow speaking

    Exactly! Let's express the idea more formally.

The lower_bound operation takes a value and returns an iterator to the first element in the tree that is equal to or comes after that value in sorted order. If all elements in the tree are less than the given value, it returns an iterator equal to end().

  • Cat speaking

    Ah ha, so if I have the values apple, banana, cherry, and I call lower_bound("blueberry"), I should get an iterator to cherry back. Cool.

  • Horse speaking

    Hay! Why is it called lower bound if it gives an element that's greater than or equal to the value?

  • LHS Cow speaking

    Good question! There are two ways we can consider that this is the right name. We can say that the element we pass in is the bound, and all the elements returned are greater than or equal to that bound. Or we can say that the element returned is the smallest of all the elements that are greater than or equal to the value we passed in.

For this part, we're continuing the theme of merging code. Much as you merged the two solutions from Homework 6 into a single templated TreeSet<T> class, now you'll be merging a provided implementation of lower_bound into your existing TreeSet<T> class. This way, you can have the fun of using and testing lower_bound without having to design and implement it from scratch.

A Complete Implementation of lower_bound

What's interesting about this code is that it shows how we can return an iterator to an arbitrary position in the tree without needing to start at the smallest element and incrementing up to the desired position. Instead, we can directly navigate the tree to find the desired position (in the normal recursive way), building the iterator's stack as we go.

template <typename T>
TreeSet<T>::const_iterator TreeSet<T>::lower_bound(
    const T& key) const {
    // We use end() to make an iterator with an empty stack, and then
    // the helper function builds the iterator's final value by pushing
    // nodes onto the stack as it searches for the lower bound.
    const_iterator iter = end();
    return nodeLowerBound(root_, key, iter);
}

template <typename T>
TreeSet<T>::const_iterator TreeSet<T>::nodeLowerBound(
    Node* currentNode, const T& key, const_iterator& iter) {
    // The goal:
    // - Find the first node in the tree whose value is >= key and return
    //   an iterator pointing to it.
    // How it works:
    // - Recall that an iterator is represented by a stack consisting of all
    //   the parent nodes that are greater than (or equal to for the top of the
    //   stack) the node where the iterator currently is.
    // - Imagine our tree holds ints, we're looking for lower_bound(7), and
    //   suppose the current node is
    //     - 5: since 5 < 7, we know that all nodes in the left subtree
    //          are also < 7, so we can skip them and go right.
    //     - 10: since 10 >= 7, we know that 10 is a potential lower bound,
    //          so we push it onto the stack and go left to see if there's
    //          a smaller candidate.
    //     - 7: we could have an explicit check for equality, push 7, and
    //          terminate the search early, but it's not necessary; we can just
    //          treat it like the 10 case above, pushing and going left. All
    //          nodes in the left subtree will be < 7, so we'll eventually
    //          exhaust that subtree and return the iterator with 7 on top
    //          of the stack.
    //     - nullptr: we've reached the end of a subtree; we return the
    //          iterator we've built so far.

    if (currentNode == nullptr) {
        // Return whatever iterator value we've built so far.
        return iter;
    }

    if (currentNode->value_ < key) {
        // Current node is less than key, but we need a value >= key.
        // Go right to find a larger candidate.
        return nodeLowerBound(currentNode->right_, key, iter);
    } else {
        // Current node is >= key, so it's a potential lower bound.
        // Push it onto the stack and go left to see if there's a smaller
        // candidate, since we want the least such node.
        iter.stack_.push(currentNode);
        return nodeLowerBound(currentNode->left_, key, iter);
    }
}

Your Task

Adapt the Code into Your TreeSet<T> Class Template

Adapt this code to fit into your TreeSet<T> class template. Things you might want or need to do include:

  • Changing the name of the helper function to match your naming conventions for helper functions.
  • Changing the names of data members (e.g., value_, left_, right_) to match your existing code.
  • Adding const to Node* if your other search helper functions use const Node* parameters.
  • If the comment block seems excessive, you can trim it down.

But you should not need to change the fundamental logic of the code.

Test lower_bound

Add tests to your test suite to verify that lower_bound works correctly. You should test a variety of cases, including:

  • Calling lower_bound on an empty tree.
  • Calling lower_bound with a value less than all elements in the tree.
  • Calling lower_bound with a value greater than all elements in the tree.
  • Calling lower_bound with a value equal to an element in the tree.
  • Calling lower_bound with a value between two elements in the tree.
  • Checking that the iterator returned by lower_bound can be used to access the correct element and the ones that follow it in sorted order.

But don't go too crazy; a few well-chosen tests should be sufficient to give you confidence that your implementation is correct.

To Complete This Part of the Assignment…

You'll know you're done with this part of the assignment when you've done all of the following:

(When logged in, completion status appears here.)