\( \newcommand{\NOT}{\neg} \newcommand{\AND}{\wedge} \newcommand{\OR}{\vee} \newcommand{\XOR}{\oplus} \newcommand{\IMP}{\Rightarrow} \newcommand{\IFF}{\Leftrightarrow} \newcommand{\TRUE}{\text{True}\xspace} \newcommand{\FALSE}{\text{False}\xspace} \newcommand{\IN}{\,{\in}\,} \newcommand{\NOTIN}{\,{\notin}\,} \newcommand{\TO}{\rightarrow} \newcommand{\DIV}{\mid} \newcommand{\NDIV}{\nmid} \newcommand{\MOD}[1]{\pmod{#1}} \newcommand{\MODS}[1]{\ (\text{mod}\ #1)} \newcommand{\N}{\mathbb N} \newcommand{\Z}{\mathbb Z} \newcommand{\Q}{\mathbb Q} \newcommand{\R}{\mathbb R} \newcommand{\C}{\mathbb C} \newcommand{\cA}{\mathcal A} \newcommand{\cB}{\mathcal B} \newcommand{\cC}{\mathcal C} \newcommand{\cD}{\mathcal D} \newcommand{\cE}{\mathcal E} \newcommand{\cF}{\mathcal F} \newcommand{\cG}{\mathcal G} \newcommand{\cH}{\mathcal H} \newcommand{\cI}{\mathcal I} \newcommand{\cJ}{\mathcal J} \newcommand{\cL}{\mathcal L} \newcommand{\cK}{\mathcal K} \newcommand{\cN}{\mathcal N} \newcommand{\cO}{\mathcal O} \newcommand{\cP}{\mathcal P} \newcommand{\cQ}{\mathcal Q} \newcommand{\cS}{\mathcal S} \newcommand{\cT}{\mathcal T} \newcommand{\cV}{\mathcal V} \newcommand{\cW}{\mathcal W} \newcommand{\cZ}{\mathcal Z} \newcommand{\emp}{\emptyset} \newcommand{\bs}{\backslash} \newcommand{\floor}[1]{\left \lfloor #1 \right \rfloor} \newcommand{\ceil}[1]{\left \lceil #1 \right \rceil} \newcommand{\abs}[1]{\left | #1 \right |} \newcommand{\xspace}{} \newcommand{\proofheader}[1]{\underline{\textbf{#1}}} \)

CSC111 Lecture 7: Trees and Recursion

Note: if you wish, you may write the code in this exercise in the same file as your prep from this week.

For your reference, here is our definition for the Tree class:

from __future__ import annotations
from typing import Any, Optional


class Tree:
    """A recursive tree data structure.

    Note the relationship between this class and RecursiveList; the only major
    difference is that _rest has been replaced by _subtrees to handle multiple
    recursive sub-parts.

    Representation Invariants:
        - self._root is not None or self._subtrees == []
    """
    # Private Instance Attributes:
    #   - _root:
    #       The item stored at this tree's root, or None if the tree is empty.
    #   - _subtrees:
    #       The list of subtrees of this tree. This attribute is empty when
    #       self._root is None (representing an empty tree). However, this attribute
    #       may be empty when self._root is not None, which represents a tree consisting
    #       of just one item.
    _root: Optional[Any]
    _subtrees: list[Tree]

    def __init__(self, root: Optional[Any], subtrees: list[Tree]) -> None:
        """Initialize a new Tree with the given root value and subtrees.

        If root is None, the tree is empty.

        Preconditions:
            - root is not None or subtrees == []
        """
        self._root = root
        self._subtrees = subtrees

    def is_empty(self) -> bool:
        """Return whether this tree is empty.

        >>> t1 = Tree(None, [])
        >>> t1.is_empty()
        True
        >>> t2 = Tree(3, [])
        >>> t2.is_empty()
        False
        """
        return self._root is None

Exercise 1: Designing Recursive Tree Functions

Consider the following definition.1

Let \(T\) be a tree, and \(x\) be a value in the tree. The depth of \(x\) in \(T\) is the distance (counting links) between the item and the root of the tree, inclusive.

Tree diagram

For example, in the above tree:

Your task is to implement the following Tree method.

class Tree:
    def first_at_depth(self, d: int) -> Optional[Any]:
        """Return the leftmost value at depth d in this tree.

        Return None if there are NO items at depth d in this tree.

        Preconditions:
            - d >= 0  # depth 0 is the root of the tree
        """
  1. (base cases) First, let’s consider some base cases. Complete each of the following doctests. If None would be returned, write down None, even though nothing would actually be printed in the Python console (this is just to help you remember when reviewing!).

    >>> empty = Tree(None, [])
    >>> empty.first_at_depth(0)
    
    
    
    >>> empty.first_at_depth(10)
    
    
    
    >>> single = Tree(111, [])
    >>> single.first_at_depth(0)
    
    
    
    >>> single.first_at_depth(3)
    
    
    
  2. (base cases) Implement the base cases below (don’t worry if the size-one case might end up being redundant—you can double-check this after implementing the recursive step).

    class Tree:
        def first_at_depth(self, d: int) -> Optional[Any]:
            """Return the leftmost value at depth d in this tree.
    
            Return None if there are NO items at depth d in this tree.
    
            Preconditions:
                - d >= 0
            """
            if self.is_empty():
    
    
    
    
    
    
    
            elif self._subtrees == []:
    
    
    
    
    
    
            else:
                ...
  3. (recursive step) Now suppose we have a variable tree that refers to the following tree:

    Tree diagram

    1. What should tree.first_at_depth(0) return?

    2. What should tree.first_at_depth(1) return?

    3. Now let’s investigate tree.first_at_depth(1) recursively.

      Fill in the recursive call table below, choosing the right “depth” argument so that the return values of the recursive calls are actually useful for computing tree.first_at_depth(1). (Write None if that’s what is returned.)

      subtree subtree.first_at_depth( )
       
       
       
    4. What should tree.first_at_depth(3) return?

    5. Once again, fill in the recursive call table below, this time to compute tree.first_at_depth(3).

      subtree subtree.first_at_depth( )
       
       
       
    6. Finally, implement the recursive step below.

      class Tree:
          def first_at_depth(self, d: int) -> Optional[Any]:
              """Return the leftmost value at depth d in this tree.
      
              Return None if there are NO items at depth d in this tree.
      
              Preconditions:
                  - d >= 0
              """
              if self.is_empty():
                  ...
              elif self._subtrees == []:
                  ...
              else:
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
      
    7. Is the size-one base case you implemented above redundant? If so, modify your code to remove it. If not, leave a comment explaining why not.

Exercise 2: Tree Deletion

We’ve seen that when deleting an item from a tree, the bulk of the work comes when you’ve already found the item, that is, you are “at” a subtree where the item is in the root, and you need to delete it. This is the code we developed in lecture:

class Tree:
    def remove(self, item: Any) -> bool:
        """Delete *one* occurrence of the given item from this tree.

        Do nothing if the item is not in this tree.
        Return whether the given item was deleted.
        """
        if self.is_empty():
            return False
        elif self._root == item:
            self._delete_root()
            return True
        else:
            for subtree in self._subtrees:
                deleted = subtree.remove(item)
                if deleted:
                    # One occurrence of the item was deleted, so we're done.
                    return True

            # If the loop doesn't return early, the item was not deleted from
            # any of the subtrees. In this case, the item does not appear
            # in this tree.
            return False

Our goal is to complete this function by implementing the helper Tree._delete_root:

class Tree:
    def _delete_root(self) -> None:
        """Remove the root item of this tree.

        Preconditions:
            - not self.is_empty()
        """
  1. We can’t always set the self._root attribute to None. When can we, and when must we do something else?

Next, we’ll look at two strategies for replacing self._root with a new value from somewhere else in the tree.

Strategy 1: “Promote” a subtree

Idea: to delete the root, take the rightmost subtree \(t_1\), and make the root of \(t_1\) the new root of the full tree, and make the subtrees of \(t_1\) become subtrees of the full tree.2

Implement Tree._delete_root using this approach.

class Tree:
    def _delete_root(self) -> None:
        """Remove the root item of this tree.

        Preconditions:
            - not self.is_empty()
        """

















Strategy 2: Replace the root with a leaf

Idea: to delete the root, find the leftmost leaf of the tree, and move that leaf so that it becomes the new root value. No other values in the tree should move.3

Implement Tree._delete_root using this approach. We recommend using an additional helper method to recursive remove and return the leftmost leaf in a tree.

class Tree:
    def _delete_root(self) -> None:
        """Remove the root item of this tree.

        Preconditions:
            - not self.is_empty()
        """




















Additional exercises

  1. Implement a method Tree.items_at_depth, which returns all items at a given depth in the tree.

  2. Consider the following definition. The branching factor of an item in a tree is its number of children (or equivalently, its number of subtrees). In Artificial Intelligence, one of the most important properties of a tree is the average branching factor of its internal values (i.e., its non-leaf values).

    Implement a method Tree.branching_factor that computes the average branching factor of the internal values in a tree, returning 0.0 if the tree has no internal values.

  3. Write a new method Tree.remove_all that deletes every occurrence of the given item from a tree. As with linked lists, you’ll need to be careful here about the order in which you check the items and mutate the tree so that you don’t accidentally skip some occurrences of the item.


  1. This is analogous to a definition we gave for nested lists last week!↩︎

  2. We could have also chosen to “promote” the leftmost subtree, or some other subtree.↩︎

  3. We could have also chosen to use any other leaf to replace the root.↩︎