22: More Recursion

DRAFT

Announcements

Organizing your War code

Do what you like, of course, but I suggest something like the following pseudocode:

findWinner():
    instantiate a War object

War: 
constructor:
    initialize decks, other values

simulateGame():
    while !gameOver:
      battle()
    return outcome

battle():
  if game over (winner or draw or technical draw):
      set outcome
        return
    increment battle count
    take card from each player, add to spoils
    if a player wins:
      allocate spoils
    else war()

war():
  if game over (not enough cards for war):
      set outcome
        return
    remove three cards from each player, add to to spoils

Notice that we did not use recursion in War, even though we’re learning about it in class! You can certainly implement aspects of it recursively, I don’t recommend it, nor will I help you debug a crazy three-way recursion among different methods.

Q+A session on War

(No notes, just whatever comes up in class.)

Accumulators

In all of the examples we’ve seen so far, the result has been implicitly accumulated – it’s been compute from and returned as the result of a chain of recursive calls. Sometimes doing so is awkward. For example, suppose we want to make a copy of a linked list. Iteratively what do we do? Something like:

static <E> Node<E> copy(Node<E> head) {
    if (head == null) {
        return null;
    }       
    Node<E> newHead = new Node<>();
    newHead.data = head.data;

    Node<E> oldRef = head.next;
    Node<E> newRef = newHead;
    while(oldRef != null) {
        Node<E> n = new Node<>();
        n.data = oldRef.data;
        newRef.next = n;
        newRef = newRef.next;
        oldRef = oldRef.next;
    }
    return newHead;
}

What about recursively?

copy(node) = null if the list is empty, otherwise it’s a copy of the current node, whose next pointer points to a copy of the rest of the list:

    static <E> Node<E> copy(Node<E> head) {
        if (head == null) {
            return null;
        }
        else {
            Node<E> n = new Node<>();
            n.data = head.data;
            n.next = copy(head.next);
            return n;
        }
    }

How’s it look? Base case seems fine. What about for a single-node list (on board). That seems OK. What about a two-node list? OK, now we’re cooking. I actually find this a little easier to read than the iterative case, but maybe that’s just me.

What about if we want to return a reversed copy of the list? Hmm.

reversed(node) = null if the list is empty, otherwise it’s a reversed copy of the rest of the list, with the current node at its end.

But how do we append to the end? We’d have to traverse the list (recursively?) each time. That seems (and is) crap (quadratic). Instead, we’ll build up the reversed list as we go, passing it in as a parameter.

    static <E> Node<E> reversed(Node<E> head) {
        return reversed(head, null);
    }

    static <E> Node<E> reversed(Node<E> head, Node<E> rev) {
        if (head == null) {
            return rev;
        }
        else {
            Node<E> n = new Node<>();
            n.data = head.data;
            n.next = rev;
            return reversed(head.next, n);
        }
    }

Binary trees

Remember earlier in the semester when we talked about binary trees? They’re like lists: structures composed of linked nodes. But in a binary tree, each node has a left and right “child” rather than a single next node:

class Node<E> {
    public E data;
    public Node<E> left;
    public Node<E> right;
}

Some types of binary trees have additional constraints. For example, binary search trees might require that the values stored in the left subtree always be less than or equal to the current node’s value, and the values stored in the right subtree always be greater.

As you’ll see in 187, you can operate on these trees either recursively or iteratively. Some operations are about as hard to write iteratively or recursively (not unlike writing contains in a linked list). Others are more clear one way or the other. Let’s do a few examples. The basic tree looks like this:

class BinarySearchTree<E> { // note the E extends Comparable<E> is necessary 
                            // for the ordering to be enforced
    private Node<E> root;
}

Suppose we have an existing tree. How do we find an element in the tree? Let’s write a contains() method both ways. This bears a strong resemblance to the contains method of a linked list, but has to check the correct child of a node (notably, not both children!).

(1) We start at the root. then, we (2) repeat the following: If the node contains the data, (3) return true; otherwise, (4) descend to the appropriate child. If we reach the end without finding the value, (5) return false.

public boolean contains(E e) {
    // 1
    Node<E> node = root;

    // 2
    while (node != null) {

        // 3
        if (e.equals(node.data)) {
            return true;
        }

        // 4
        if (e.compare(node.data) <= 0) {
            node = node.left;
        } else {
            node = node.right;
        }
    }

    // 5
    return false;
}

What about recursively? We’ll need a helper method to be able to pass the “current” node in, but otherwise it’s much the same.

Our textual description is something like:

In-class exercise

What is the true base case in contains?

In-class exercise

What is the false base case in contains?

contains is false if we’ve reached the end of a branch. It’s true if the current node’s value is the value we’re looking for. Otherwise, it’s equal to contains(left child) if the value is less than the current node, or contains(right child) otherwise.

public boolean contains(E e) {
    return contains(root, e)
}

private boolean contains(Node<E> node, E e) {
    if (node == null) {
        return false;
    }

    if (e.equals(node.data)) {
        return true;
    }

    if (e.compare(node.data) <= 0) {
        return contains(left, e);
    }
    else {
        return contains(right, e);
    }
}

How do we insert data? Just like a list, we need to “traverse” the tree to the “end”, but unlike a list, we can’t just follow the next reference; instead, we need to find the right place in the tree. What’s the right place?

(1) We start at the root. If it’s null, the tree is empty and we can put the value in a new node there.

If not, we need to decide to go left or right. How do we decide? (2) If the value we’re inserting is less than or equal to the current node’s value, we should go left. Otherwise, right. (3) Then keep going, until we get to a null value? Not all the way to a null, otherwise we don’t know what node to “staple” our new node too. Just like a linked list, we want to stop before we go “past” the end, then (4) add the node to the correct side:

public void insert(E e) {
    Node<E> node = new Node<>();
    node.data = e;

    // 1
    if (root == null) {
        root = node;
        return;
    }

    // 2
    Node<E> cur = root;
    Node<E> next;
    if (e.compare(cur.data) <= 0 ) {
        next = cur.left;
    }
    else {
        next = cur.right;
    }

    // 3
    while (next != null) {
        cur = next;
        if (e.compare(cur.data) <= 0 ) {
            next = cur.left;
        }
        else {
            next = cur.right;
        }        
    }

    // 4
    if (e.compare(cur.data) <= 0 ) {
        cur.left = node;
    }
    else {
        cur.right = node;;
    }
}

Note you could shorten this somewhat with the dreaded ternary operator. Instead of:

    if (e.compare(cur.data) <= 0 ) {
        next = cur.left;
    }
    else {
        next = cur.right;
    }

you could write:

    next = (e.compare(cur.data)) <= 0 ? cur.left : cur.right;

What about the recursive version? Again, we need to find the node before the place we want to insert the the node. So we’ll need a helper method, which we’ll use to find the node to insert at:

public void insert(E e) {
    Node<E> node = new Node<>();
    node.data = e;

    // 1
    if (root == null) {
        root = node;
        return;
    }

    Node<E> cur = findInsertionPoint(root, e);
    if (e.compare(cur.data) <= 0 ) {
        cur.left = node;
    }
    else {
        cur.right = node;;
    }
}

public Node<E> findInsertionPoint(Node<E> node, E e) {
    Node<E> next;
    if (e.compare(node.data)) <= 0) { // again, could use ternary op instead
        next = node.left;
    }
    else {
        next = node.right;
    }
    if (next == null) {
        return node;
    }
    else {
        return findInsertionPoint(next, e);
    }
}

Is one more clear than the other? I dunno.

Two more quick binary tree examples, this time of methods that recursion makes easier. Suppose your binary tree contains Integers. How would you find the sum of the values? If this were a list, you would traverse the list and sum them up as you went. But if you “iterate” through a tree, you must go either left or right. What about if you recurse? Then you can define sum() as follows:

The sum of the values of the tree rooted at a given node is equal to:

  • zero, if the node is null
  • otherwise, it’s equal to the current node’s value, plus sum(left child) + sum(right child).

(on board)

That’s a clear recursion, the code would look something like:

int sum(Node<Integer> node) {
    if (node == null) {
        return 0;
    }
    else {
        return node.data + sum(node.left) + sum(node.right);
    }
}

Suppose we wanted to just count the number of nodes in the tree?

In-class exercise

What is the base case in count?

int count(Node<Integer> node) {
    if (node == null) {
        return 0;
    }
    else {
        return 1 + sum(node.left) + sum(node.right);
    }
}

Here’s another example. What if we want to calculate the height of a tree? Remember, the height is the distance from the root to the node furthest from the root (on board).

Again, we don’t know in advance which child of a given node leads toward the “deeper” subtree, so we have to explore both. We can define the height of the subtree rooted at a node as:

  • zero, if the current node has no children
  • one plus the larger of the heights of the left and right subtrees

Again, in Java, though this one’s slightly messier due to the null check:

int height(Node<E> n) {
    if (n.left == 0 && n.right == 0) {
        return 0;
    }
    if (n.left == 0) {
        return 1 + height(n.right);
    }
    if (n.right == 0) {
        return 1 + height(n.left);
    }
    return 1 + Math.max(height(n.left) + height(n.right));
}

Can you write these (sum, count, and height) iteratively? Sure, but not without (at least) one of two things:

  • You could modify the tree to include parent references, which would allow backtracking.
  • Or, you could use auxiliary storage to keep track of the “pending” parts of the tree. This approach exactly parallels how search works: you need to keep track of the “frontier” of nodes, removing one at a time from it, and adding all children of a current node to it. (You don’t need to track the visited set, since there are by definition no loops in a tree.)

Recursion is equivalent to the latter – the program’s call stack is your auxiliary storage, and if you use a stack to track the frontier, your iterative program can be written to visit the nodes in exactly the same order as the recursive version.

Hanoi

Tower of Hanoi, a simple puzzle where you move disks from one peg to another, the goal being to move all the disks from the first peg to the last. The constraints are that you may only move one disk at a time, and you must place a disk on either an empty peg, or atop a disk larger than itself.

A recursive procedure to solve the puzzle is as follows. Label the three pegs the source (that is, the starting) peg, the target (the place we want the disks all to go), and the spare (the other peg). To move m disks from the source to the target, use the following solve(m, source, target, spare) procedure:

  1. If we are being asked to move zero pegs, do nothing (base case).
  2. Move m − 1 disks from the source to the spare peg using solve(m - 1, source, spare, target) This leaves the disk m as a top disk on the source peg and all the other disks on the spare peg.
  3. Move the disk m from the source to the target (which is empty).
  4. Move the m - 1 disks from the spare to the target, using the now-empty source peg as the spare: solve(m - 1, spare, target, source)

What does this look like in code? Let’s declare a Hanoi class to handle this:

public class Hanoi {
    int n;
    private List<Character> a;
    private List<Character> b;
    private List<Character> c;

    public Hanoi(int n) {
        this.n = n;
        a = new ArrayList<>();
        b = new ArrayList<>();
        c = new ArrayList<>();

        for (char i = (char)('1' + n - 1); i >= '1'; i--) {
            a.add(i);
        }
    }

    public String toString() {
        return a.toString() + "\n" + b.toString() + "\n" + c.toString();
    }

        public static void main(String[] args) {
        int n = 3;
        Hanoi h = new Hanoi(n);
        System.out.println(h);
    }

And translate our recursive solver into code, including printouts each time it moves a disk:

    private void solve(int m, List<Character> source, List<Character> target, List<Character> spare) {
        if (m == 0) {
            return;
        }
        solve(m - 1, source, spare, target);
        target.add(source.remove(source.size() - 1));
        System.out.println("----");
        System.out.println(this);
        solve(m - 1, spare, target, source);
    }

And finally, set up to call it:

    public void solve() {
        solve(n, a, c, b);
    }

Et voilà:

    public static void main(String[] args) {
        int n = 3;
        Hanoi h = new Hanoi(n);
        System.out.println(h);
        h.solve();
    }

How does it work? It works well. 😎

You’ll get the tools you need to prove it’s correct in COMPSCI 250.