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 NoneConsider 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.

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
"""(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)
(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:
...(recursive step) Now suppose we have a variable
tree that refers to the following tree:

What should tree.first_at_depth(0) return?
What should tree.first_at_depth(1) return?
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( ) |
|---|---|
![]() |
|
![]() |
|
![]() |
What should tree.first_at_depth(3) return?
Once again, fill in the recursive call table below, this time to
compute tree.first_at_depth(3).
subtree |
subtree.first_at_depth( ) |
|---|---|
![]() |
|
![]() |
|
![]() |
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:
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.
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 FalseOur 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()
"""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.
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()
"""
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()
"""
Implement a method Tree.items_at_depth, which
returns all items at a given depth in the tree.
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.
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.