CSC148H1/Unit 7
Unit 7 · Introduction to Computer Science

Trees

A structure that is recursive by construction: a tree is a value plus a list of trees. Every operation on it is the Unit 6 pattern with "subtree" in place of "element", and this is where recursion stops being optional and becomes the only sensible approach.

By the end you can
  • Define a tree recursively and implement it.
  • Compute size, height and membership recursively.
  • Distinguish preorder, postorder and level order.
  • Handle the empty tree deliberately.
Why it matters & where it comes from
Why it exists

Hierarchies are everywhere and lists cannot express them. A tree captures "this contains those" directly, and because the definition is recursive, every algorithm on it is too.

In the real world

File systems, the DOM behind every web page, organisation charts, decision trees in machine learning, and the parse trees every compiler builds from source code.

Story & open questions

The upside-down convention — root at the top, leaves at the bottom — is universal in computing and backwards from botany. It stuck because it reads naturally top to bottom on a page.

Definition and implementation

Definition

A tree is either empty, or a root value together with a list of subtrees, each itself a tree.

A node with no subtrees is a leaf.
The size is the number of values.
The height of a one-node tree is $1$; the empty tree has height $0$.
Example The class
python
class Tree:
    """A tree with a root value and a list of subtrees.

    Attributes:
        _root: the value at this node, or None if the tree is empty
        _subtrees: the subtrees of this node

    Representation invariants:
        - if self._root is None then self._subtrees == []
    """
    _root: object
    _subtrees: list

    def __init__(self, root, subtrees: list) -> None:
        """Initialize a tree with the given root and subtrees.

        An empty tree is Tree(None, []).
        """
        self._root = root
        self._subtrees = subtrees

    def is_empty(self) -> bool:
        """Return whether this tree is empty."""
        return self._root is None
The invariant matters: an empty tree cannot have subtrees. Without it, Tree(None, [something]) would be a nonsensical object every method would then have to cope with.
A leaf is Tree(value, []) — a root with an empty subtree list, not an empty tree.

Recursive operations

Rule The tree pattern

Every method follows the same three steps:

python
def method(self):
    if self.is_empty():
        return something for the empty tree
    # combine self._root with method() on each subtree

Compare Unit 6. The only change is that "each element" has become "each subtree".

Example Size, height, membership
python
def __len__(self) -> int:
    """Return the number of values in this tree.

    >>> t = Tree(1, [Tree(2, []), Tree(3, [])])
    >>> len(t)
    3
    """
    if self.is_empty():
        return 0
    return 1 + sum(sub.__len__() for sub in self._subtrees)


def height(self) -> int:
    """Return the height of this tree.

    An empty tree has height 0; a leaf has height 1.

    >>> Tree(1, [Tree(2, [])]).height()
    2
    """
    if self.is_empty():
        return 0
    if self._subtrees == []:
        return 1
    return 1 + max(sub.height() for sub in self._subtrees)


def __contains__(self, item) -> bool:
    """Return whether item is a value in this tree.

    >>> 2 in Tree(1, [Tree(2, [])])
    True
    """
    if self.is_empty():
        return False
    if self._root == item:
        return True
    return any(item in sub for sub in self._subtrees)
height needs the leaf case separately, because max cannot handle an empty subtree list — the same trap as Unit 6.
__contains__ stops early: any short-circuits, so it will not search subtrees once the value is found.
Empty tree versus leaf

These are different and both need handling. An empty tree has no root at all; a leaf has a root and no subtrees. Conflating them produces methods that crash on one or return nonsense on the other.

Traversal order

Definition

Visiting every value, in one of several orders:

Preorder: root first, then the subtrees.
Postorder: subtrees first, then the root.
Level order: all depth-1 values, then all depth-2, and so on.
Example The first two, and why they differ
python
def preorder(self) -> list:
    """Return the values of this tree, root before subtrees.

    >>> Tree(1, [Tree(2, []), Tree(3, [])]).preorder()
    [1, 2, 3]
    """
    if self.is_empty():
        return []
    values = [self._root]
    for sub in self._subtrees:
        values.extend(sub.preorder())
    return values


def postorder(self) -> list:
    """Return the values of this tree, subtrees before root.

    >>> Tree(1, [Tree(2, []), Tree(3, [])]).postorder()
    [2, 3, 1]
    """
    if self.is_empty():
        return []
    values = []
    for sub in self._subtrees:
        values.extend(sub.postorder())
    values.append(self._root)
    return values
The only difference is where self._root is added. One line, entirely different output order.
Postorder is what you want for deleting a tree or computing folder sizes — children must be handled before their parent.
Preorder suits copying or printing an outline, where the parent must appear before what it contains.
Example Level order needs a queue
python
def level_order(self) -> list:
    """Return the values of this tree, one depth level at a time."""
    if self.is_empty():
        return []
    values = []
    pending = [self]
    while pending != []:
        current = pending.pop(0)      # take from the front: FIFO
        values.append(current._root)
        pending.extend(current._subtrees)
    return values
This one is not recursive. Level order needs a queue, exactly as Unit 3 showed — taking from the front gives breadth-first, and there is no natural recursive formulation.
Swapping pop(0) for pop() turns it into a stack and gives depth-first order instead. The same one-character difference as before.
Common pitfalls

• Treating the empty tree and a leaf as the same case.
• Calling max on an empty subtree list in height.
• Using append where extend is needed when gathering subtree results.
• Off-by-one on height. Decide whether a leaf is $0$ or $1$ and document it.
• Expecting level order to fall out of recursion. It needs a queue.

Practice

Write out each answer before opening it. Original exercises written for this guide in the style of the course's weekly work.

Practice 1 · Definition

Define a tree recursively.

Answer
Either empty, or a root value together with a list of subtrees, each a tree.
Practice 2 · Two empties

What is the difference between an empty tree and a leaf?

Answer
An empty tree has no root. A leaf has a root and an empty list of subtrees.
Practice 3 · Count leaves

Count the leaves of a tree.

Solution
python
def leaf_count(self) -> int:
    """Return the number of leaves in this tree."""
    if self.is_empty():
        return 0
    if self._subtrees == []:
        return 1
    return sum(sub.leaf_count() for sub in self._subtrees)
Three cases: empty, leaf, and internal node.
Practice 4 · Order

For a root $1$ with children $2$ and $3$, give preorder and postorder.

Answer
Preorder $[1, 2, 3]$; postorder $[2, 3, 1]$.
Practice 5 · Which order

Computing the total size of a folder tree — which traversal?

Answer
Postorder. A folder's size depends on its contents, so children must be computed first.
Practice 6 · Sum the values

Sum all values in a tree of integers.

Solution
python
def total(self) -> int:
    """Return the sum of the values in this tree."""
    if self.is_empty():
        return 0
    return self._root + sum(sub.total() for sub in self._subtrees)
Practice 7 · Height convention

Why does height need a separate leaf case?

Answer
Because max over an empty list of subtrees raises. A leaf must return $1$ directly.
Practice 8 · Depth-first from level order

What single change makes level_order depth-first?

Answer
Replace pop(0) with pop(), turning the queue into a stack. Same code, different traversal — the Unit 3 point again.

Expanded topic checklist

Course coverage. This unit is part of the complete CSC148H1 topic map. It should cover the following items before you move on:

  • Definition. Recursive structure, and the empty tree versus a leaf.
  • Recursive methods. Size, height, membership, and the three-case pattern.
  • Traversals. Preorder, postorder, and what each is for.
  • Level order. Why it needs a queue rather than recursion.
Textbook/source alignment

Trees are the main application of "recursion" in the calendar's topic list. Check Quercus for the assigned exercises; tree methods are standard assignment and exam material.

Core idea map

Recursive data, recursive code. Handle the empty case, do something with the root, recurse on each subtree, combine. Unit 8 constrains the shape to two subtrees and adds an ordering rule, which is what makes fast search possible.

Study routine. For every tree method, write the three cases before any code: empty, leaf, internal. Most tree bugs are a missing one of the three.

Deep practice extensions

Use these after the main practice set. They push on definitions and edge cases rather than routine calculation.

Extension 1 · Why recursive

Why is a tree naturally handled recursively?

Guide
Because its definition is recursive — each subtree is a tree — so the code mirrors the data.
Extension 2 · Traversal choice

When does postorder matter?

Guide
When a node's result depends on its children: computing sizes, freeing memory, evaluating an expression tree.
Extension 3 · Level order

Why is it not recursive?

Guide
It visits across the tree rather than down it, so it needs a queue of pending nodes rather than the call stack.
Extension 4 · The invariant

Why require that an empty tree has no subtrees?

Guide
To rule out a contradictory object every method would otherwise have to handle defensively.