Trees (Nested Lists and Tree Recursion)

Practice tree-shaped data using a simple Tree ADT. You will (1) predict results of tree recursion, (2) trace a recursive computation that builds a tree of calls, and (3) implement + extend classic tree-recursive functions (fringe, then a pipeline computation).

0. Predict: First Match in Tree Search + Leaf Count

**Predict the output (4 lines)** before running. Write your prediction as comments first.

```python
# Tree ADT + example tree

def tree(lbl, branches=None):
    if branches is None:
        branches = []
    return [lbl] + list(branches)

def label(t):
    return t[0]

def branches(t):
    return t[1:]

def is_leaf(t):
    return len(branches(t)) == 0

t = tree(3, [
    tree(1),
    tree(2, [tree(1), tree(1)])
])

# Two classic tree recursions

def count_leaves(t):
    if is_leaf(t):
        return 1
    return sum(count_leaves(b) for b in branches(t))

def find_path(t, target):
    if label(t) == target:
        return [label(t)]
    for b in branches(t):
        subpath = find_path(b, target)
        if subpath is not None:
            return [label(t)] + subpath
    return None

print(find_path(t, 2))
print(find_path(t, 1))
print(find_path(t, 99))
print(count_leaves(t))
```

Questions:
1) What does each `print` line output?
2) Why does `find_path(t, 1)` return that particular path (hint: branch order)?

  

1. Trace: Fibonacci Builds a Tree of Calls

In this exercise, the **shape of the returned tree mirrors the recursion**.

1) Trace the recursive calls for `fib_tree(4)`.
   - List the calls in the order they are *initiated*.
   - Then show the return value (the node label) for each call as it unwinds.

2) Predict (then verify):
   - `label(fib_tree(4))`
   - `count_leaves(fib_tree(4))`

Code:
```python
def tree(lbl, branches=None):
    if branches is None:
        branches = []
    return [lbl] + list(branches)

def label(t):
    return t[0]

def branches(t):
    return t[1:]

def is_leaf(t):
    return len(branches(t)) == 0

def count_leaves(t):
    if is_leaf(t):
        return 1
    return sum(count_leaves(b) for b in branches(t))

def fib_tree(n):
    if n <= 1:
        return tree(n)
    left = fib_tree(n - 1)
    right = fib_tree(n - 2)
    return tree(label(left) + label(right), [left, right])

ft4 = fib_tree(4)
print(label(ft4))
print(count_leaves(ft4))
```

Your trace should explain where the **base case** happens and how results are **combined**.

  

2. Implement + Extend: Fringe (SICP Ex 2.28) → Sum of Odd Squares

This exercise has two parts.

**Part A (Implement)**: Implement `fringe(t)`, which returns a Python list of the **leaf labels** of a tree from left to right.

**Part B (Extend)**: Using `fringe(t)` as a *conventional interface* (a flat list of leaves), implement `sum_odd_squares(t)`:
- take the leaf labels
- keep only the odd ones
- square them
- sum the squares

Example goal:
- If the leaf labels are `[1, 2, 3, 4]`, then `sum_odd_squares` should return `1^2 + 3^2 = 10`.

Starter setup (Tree ADT + example trees):
```python
def tree(lbl, branches=None):
    if branches is None:
        branches = []
    return [lbl] + list(branches)

def label(t):
    return t[0]

def branches(t):
    return t[1:]

def is_leaf(t):
    return len(branches(t)) == 0

x = tree(0, [
    tree(1),
    tree(2, [tree(3), tree(4)]),
    tree(5)
])

xx = tree(99, [x, x])
```

Keep your solutions short and respect the abstraction barrier (use `label`, `branches`, `is_leaf`).