Trees
- A data abstraction used for representing hierarchical relationships.- Two different metaphors of a tree:- Recursive description (wooden trees):- A tree possesses a root label and a list of branches.
- Each branch is a a tree, and also has a root label.
- A tree with zero branches is a leaf- A leaf is also a tree
 
 
- Relative description: (family trees):- Each location in a tree is called a node.
- Each node has a label that can be any value.
- One node can be the parent/child of another.- Ancestors, descendants, siblings, etc.
 
 
 
- Recursive description (wooden trees):
 
- Two different metaphors of a tree:
- People often refer to labels by their locations: “each parent is the sum of its children”
 ![Tree Abstraction]() 
Implementing the Tree Abstraction
- A tree has a root label and a list of branches (this is for the constructor)- Each branch is a tree
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
def tree(label, branches=[]): # The default value of branches has nothing. So by default, we create a leaf of a tree
    for branch in branches:
        assert is_tree(branch) # Make sure that all branches are trees
    return [label] + list(branches) # Ensure that any sequence of trees is converted into a list
def label(tree):
    return tree[0]
def branches(tree):
    return tree[1:]
def is_tree(tree):
    if type(tree) != list or len(tree) < 1:
        return False
    for branch in branches(tree):
        if not is_tree(branch):
            return False
    return True
def is_leaf(tree):
    return not branches(tree)
1
2
from tree import tree
tree(3, [tree(1), tree(2, [tree(1), tree(1)])])
1
[3, [1], [2, [1], [1]]]
Tree Processing
- Any function that takes in a tree as an input or returns a tree is likely tree recursive as well.
- Tree are typically generated programmatically, for instance, a fibonacci tree:
1
2
3
4
5
6
7
def fib_tree(n):
    if n <= 1:
        return tree(n)
    left, right = fib_tree(n-2), fib_tree(n-1)
    return tree(label(left) + label(right), [left, right])
fib_tree(4)
1
[3, [1, [0], [1]], [2, [1], [1, [0], [1]]]]
Tree Processing Uses Recursion
- In most tree processing functions, processing a leaf is the base case.- Recursive case makes a recursive call on each branch, then aggregates the results.
 
1
2
3
4
5
6
7
def count_leaves(t):
    """Count the leaves of a tree."""
    if is_leaf(t):
        return 1
    return sum([count_leaves(branch) for branch in branches(t)])
count_leaves(fib_tree(4))
1
5
Discussion Question 1
- Implement leaves, which returns a list of the leaf labels of a tree
1
2
3
4
5
6
def leaves(tree):
    if is_leaf(tree):
        return [label(tree)]
    return sum([leaves(branch) for branch in branches(tree)], [])
leaves(fib_tree(5))
1
[1, 0, 1, 0, 1, 1, 0, 1]
Creating Trees
- A function that creates a tree from another tree is typically also recursive
1
2
3
4
5
6
7
8
9
10
def increment_leaves(t):
    """Return a tree like t but with leaf labels incremented."""
    def helper(t):
        if is_leaf(t):
            return tree(label(t) + 1)
        return tree(label(t), [helper(branch) for branch in branches(t)])
    return helper(t)
print(fib_tree(4))
print(increment_leaves(fib_tree(4)))
1
2
[3, [1, [0], [1]], [2, [1], [1, [0], [1]]]]
[3, [1, [1], [2]], [2, [2], [1, [1], [2]]]]
1
2
3
4
5
6
def increment(t):
    """Return a tree like t but with leaf labels incremented."""
    return tree(label(t) + 1, [increment(branch) for branch in branches(t)])
print(fib_tree(4))
print(increment(fib_tree(4)))
1
2
[3, [1, [0], [1]], [2, [1], [1, [0], [1]]]]
[4, [2, [1], [2]], [3, [2], [2, [1], [2]]]]
Example: Printing Trees
1
2
3
4
5
6
def print_tree(t, level=0):
    print(level * "   " + "|--" + str(label(t)))
    for b in branches(t):
        print_tree(b, level+1)
print_tree(fib_tree(6))
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|--8
   |--3
      |--1
         |--0
         |--1
      |--2
         |--1
         |--1
            |--0
            |--1
   |--5
      |--2
         |--1
         |--1
            |--0
            |--1
      |--3
         |--1
            |--0
            |--1
         |--2
            |--1
            |--1
               |--0
               |--1
Example: Summing Paths
- Some recursive functions build up their result by manipulating the return values of a recursive call.
- Other recursive functions build up their results by passing information into the recursive call as an argument.
- Ex: Factorial
1
2
3
4
5
6
7
8
9
10
11
12
13
14
def fact(n):
    if n == 0:
        return 1
    else:
        return n * fact(n-1) # manipulating the return value of a recursive call.
    
def fact_times(n, k):
    """Returns k * n * (n-1) * ... * 1"""
    if n == 0:
        return k
    else:
        return fact_times(n-1, k * n) # Result of the recursive call is the result of the current call.
    
- In fact_times, the result is being built up with each recursive call, until we hit the base case, which just returns the solution.- In factall the multiplying occurs after we’ve hit the base case.
 
- In 
- We can apply similar recursive logic to trees
1
2
3
4
5
6
7
8
9
10
11
12
from tree import *
numbers = tree(3, [tree(4), tree(5, [tree(6)])])
haste = tree("h", [tree("a", [tree("s"), tree("t")]), tree("e")])
def print_sums(t, sum_so_far):
    if is_leaf(t):
        print(sum_so_far + label(t))
    for branch in branches(t):
        print_sums(branch, sum_so_far+label(t))
print_sums(haste, "")
1
2
3
has
hat
he
Example: Counting Paths
- Create a function that count the number of paths within a tree that have a total label sum.
1
2
3
4
5
6
7
8
9
10
def count_paths(t, total):
    """Return the number of paths from the root to any node in tree t for which the labels along the path sum to total."""
    if total-label(t) == 0:
        found = 1
    else:
        found = 0
    return found + sum([count_paths(branch, total-label(t)) for branch in branches(t)])
t = tree(3, [tree(-1), tree(1, [tree(2, [tree(1)]), tree(3)]), tree(1, [tree(-1)])])
count_paths(t, 3)
1
2
 
 