Lecture 23: Even More Recursion

Welcome

Announcements

The last Programming Assignment is up; the last quiz (Quiz 7) will be Monday. Only three classes left, assuming it doesn’t, you know, snow…

On the importance of SRTIs.

Writing out recursion

It’s a good idea to get into the habit of writing out your recursion in text before coding it. Remember last class?

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

translates to

The sum of a list :

Or think of factorial, for example. The factorial of a number is:

And so on.

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.

Passing values through the recursion

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.

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

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). Hmm, hold that thought. First remember the in-class exercise at the end of last class?

	int sum(int[] a) {
		return sum(a, 0);
	}
	
	int sum(int[] a, int i) {
		if (i == a.length) {
			return 0;
		} else {
			return a[i] + sum(a, i + 1);
		}

Here, we had to pass the value i into the “helper method” in order to “keep track” of where we were in the array – it was an extra parameter. If we want to be tricky, we can also pass around a partial result into the recursion. Each step of the recursion builds a little more of the answer, and when we reach the “end” (base case), we return the built-up result.

reversed(node, rev_so_far) = rev_so_far if the list is empty, otherwise put the head of the list at the head of rev_so_far, and return reversed on the rest of the list and the new rev_so_far

In other words, we’ll build up the reversed list as we go, passing it in as a parameter, and returning it when we’re done:

	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);
    }
}

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:

(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 + count(node.left) + count(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:

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

int height(Node<E> n) {
    if (n.left == null && n.right == null) {
        return 0;
    }
    if (n.left == null) {
        return 1 + height(n.right);
    }
    if (n.right == null) {
        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:

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.