Class 24 - More trees
Date: November 9, 2026
Skills: None
Notes (review before class)
Summarizing How to Approach Tree Problems
Tree programs use the same recipe as lists:
- The data definition, including a base (leaf) case.
- Example trees.
- The
defline: the function's name, parameters, and types. - A test function.
- The template, with the cases and recursive calls (as for
in_treein Class 23's notes). - Fill in the template for the problem.
- Run the tests.
Binary Search Trees
- A binary search tree (BST) stores values at ordered keys (often numbers), for quick lookup. Each node has a key, a value, and two subtrees.
- Its invariant, a rule the types can't express (so we write it as a comment): keys are unique; keys in a node's left subtree are less than the node's key, and keys in its right subtree are greater.
- So a lookup that misses at the root searches only one subtree; if the subtrees are about the same size, each step rules out half the nodes.
- The worst case follows the longest path from the root to an empty leaf: the
tree's height (computed with
max(a, b), the larger of two numbers). - In a balanced tree (subtrees of about the same size, at every level), the height is about log₂ n, for n nodes; a list may need to look at all n. For n = 1,000,000, that's about 20 vs. 1,000,000.
Study Questions
- Writing
in_treeon a table (withfilter) vs. on a tree: how many name comparisons might each make? - Why do we need a recursive function to process a tree?
- In what order does
in_treecheck the names?
For practice, count a tree's blue-eyed people, its people, its generations, and the people with a given name.
Recap (20 mins)
After Class 23's river networks, another binary tree: a BST with a number key and a string value at each node. Read through the program:
from dataclasses import dataclass
@dataclass
class EmptyLeaf:
pass
@dataclass
class Node:
key: int
val: str
left: "BST"
right: "BST"
BST = EmptyLeaf | Node
# Invariant: keys are unique, and all nodes in the left
# subtree have keys less than the key in the current node,
# all nodes in the right subtree have keys greater than
# the key in the current node
BST0 = EmptyLeaf()
BST1 = Node(1, "hello", BST0, BST0)
BST2 = Node(5, "bye", BST1, BST0)
BST3 = Node(6, "bye", BST2, Node(10, "cs2000", BST0, BST0))
# def bst_temp(bst: BST) -> ...:
# match bst:
# case EmptyLeaf():
# ...
# case Node(k, v, left, right):
# ... k ...
# ... v ...
# ... bst_temp(left) ...
# ... bst_temp(right) ...
def height(bst: BST) -> int:
"""returns the number of nodes on the longest path from the root of bst
to an EmptyLeaf"""
match bst:
case EmptyLeaf():
return 0
case Node(k, v, left, right):
return 1 + max(height(left), height(right))
def test_height():
assert height(BST0) == 0
assert height(BST1) == 1
assert height(BST2) == 2
assert height(BST3) == 3
- Draw
BST3. Which keys are in its left subtree, and why must they be there? Which nodes would you visit looking for key 10? For key 7? - What is
height(Node(20, "big", BST3, BST0))? - What would
heightcompute if its last line werereturn 1 + height(left) + height(right)?
Class Exercises (35 mins)
-
The main goal of a BST is to be able to look up values at a given key. Design a function
retrievethat takes aBST, a number, and returns astr | None(see Class 20) -- if the given number exists as a key in the tree, your function should returnv, the string value that is on the node with the key. If the given number doesn't exist, your function should returnNone. -
In order for BSTs to have the performance characteristics that define them, they have to be balanced. This means not that each subtree is the same size (this would only allow trees that had number of elements equal to a power of 2), but rather, that each subtree is close to the size of the other. Rather than measuring size, often BSTs compare heights, calculated as we did with our
heightfunction above. Design a functionis_balancedthat should take aBSTand return a boolean that indicates if the tree is balanced: this means that at every level, the height of the left and right subtrees differs by at most 1. -
Programs are also often tree shaped! In this exercise, we consider something simpler than full programs -- arithmetic expressions. Design a data definition
Ariththat allows you to represent numbers, addition of two arithmetic expressions, multiplication of two arithmetic expressions, division of two arithmetic expressions, and subtraction of two arithmetic expressions. This should be a binary tree with one leaf constructor (for numbers) and four node constructors (for the four types of binary operations). -
Define constants using your
Arithto represent the following expressions:
1 + 2
1 * (3/4)
(7-3) * (2 + (5/2))
- Design a function
evaluate(we can't call iteval, since Python already has a built-in function with that name) that takes anArithand returns the single number to which it evaluates. If you use the three examples from exercise 4, your test cases should result in3,0.75, and18.
Wrap-up (5 mins)
- BSTs are a powerful idea, and there are sophisticated algorithms to handle inserting and deleting data from them while maintaining balance. The core of their performance is exactly as you've seen, however.
- Trees also show up in many other problems!