In this part we will discuss the concept of a call stack and recursion. Recursion is when a method calls itself (directly or indirectly). Recursion is specially well suited for implementing computation that are itself recursively defined (as we will see below by means of the example of Fibonacci numbers).
Tail recursion is when the recursive call (the place inside the method body where the method calls itself) is the last statement in the method body.
As a simple example of tail recursion consider printing all integers up to n
in decreasing order.
package lecture;
public class UpToNReverse {
public static void printUpToN (int n) {
System.out.println("" + n);
if (n > 1)
printUpToN(n-1);
}
}
package lecture;
public class UpToN {
public static void printUpToN (int cur, int n) {
System.out.println("" + cur);
if (cur < n)
printUpToN(cur+1, n);
}
}
import lecture.UpToN;
UpToN.printUpToN(1,6);
import lecture.UpToNReverse;
UpToNReverse.printUpToN(6);
What if want to print the numbers in increasing order instead. Can recursion still be used? Turns out yes, the only thing that we have to change is to place the recursive call before the print statement.
package lecture;
public class UpToN {
public static void printUpToN (int n) {
System.out.println("starting call for " + n);
if (n > 1)
printUpToN(n-1);
System.out.println("finished recursion for " + n);
System.out.println("" + n);
}
}
import lecture.UpToN;
UpToN.printUpToN(6);
As another example of tail recursion let's consider searching through a singly linked list.
package lecture;
public class StringList {
protected class StringListElement {
public String data;
public StringListElement next;
public StringListElement(String data) {
this.data = data;
this.next = null;
}
}
private StringListElement head;
public StringList() {
this.head = null;
}
public StringList(String ... e) {
this.head = null;
for(String el: e) {
add(el);
}
}
public void add(String e) {
StringListElement n = new StringListElement(e);
if (head == null) {
head = n;
}
else {
StringListElement last = findLast(head);
last.next = n;
}
}
public String get(int position) throws Exception {
StringListElement cur = head;
for(int i = 0; i < position; i++) {
if (cur == null) {
throw new Exception(String.format("no element at position %d", position));
}
cur = cur.next;
}
return cur.data;
}
// really not a smart way to implement this, but illustrates recursion
public StringListElement findLast(StringListElement e) {
if (e.next == null)
return e;
else
return findLast(e.next);
}
public String toString() {
StringListElement cur = head;
StringBuilder b = new StringBuilder();
b.append("[");
while(cur != null) {
b.append(cur.data);
if (cur.next != null)
b.append(", ");
cur = cur.next;
}
b.append("]");
return b.toString();
}
}
import lecture.StringList;
StringList x = new StringList("Peter", "Bob", "Alice");
x.add("Jane");
return ((StringList) x).toString();
The Fibonacci numbers are recursively defined as: f(0) = 0
, f(1) = 1
, for n > 1
: f(n) = f(n-1) + f(n-2)
package lecture;
public class Fibonacci {
public static int fib(int n) {
if (n == 0)
return 0;
if (n == 1)
return 1;
else
return fib(n - 1) + fib(n - 2);
}
}
import lecture.Fibonacci;
return Fibonacci.fib(10);
Now let's try to see what is the sequence of calls in this implementation.
package lecture;
import java.util.List;
import java.util.ArrayList;
public class Fibonacci {
private static List<Integer> f = new ArrayList<Integer> ();
public static int doFib(int n) {
for(int i = 0; i < 20; i++)
f.add(-1);
return fib(n);
}
public static int fib(int n) {
if (f.get(n) != -1)
return f.get(n);
System.out.printf("fib(%d)\n", n);
if (n == 0) {
f.set(n, 0);
return 0;
}
if (n == 1) {
f.set(n, 1);
return 1;
}
else {
int res = fib(n - 1) + fib(n - 2);
f.set(n, res);
return res;
}
}
}
import lecture.Fibonacci;
return Fibonacci.doFib(5);
Problems like computing Fibonacci numbers where a solution is computed from multiple subsolutions allow for an optimization that called dynamic programming which enables repeated computed of partial solutions to be avoided by storing them. Thus, dynamic programming trades memory for computational efficiency.
package lecture;
import java.util.Vector;
public class FibonacciDP {
public static Vector<Integer> f = new Vector<Integer>();
public static int fib(int n) {
System.out.printf("fib(%d) - ", n);
if (f.size() > n) {
System.out.print("use cached value\n");
return f.get(n);
}
System.out.print("compute value\n");
if (n == 0) {
f.add(0);
return 0;
}
if (n == 1) {
f.add(1);
return 1;
}
else {
int fn = fib(n - 1) + fib(n - 2);
f.add(fn);
return fn;
}
}
}
import lecture.FibonacciDP;
return FibonacciDP.fib(5);
As an eample of a tree data structure, let's consider an organigram of a company which records who is reporting to whom. We can model this information as a tree, where the node corresponding to a person X
is a child of the node corresponding to a person Y
is X
reports to Y
. Once we have created a data structure to store such information and used it to instantiate an organigram, we may want to visualize it. One option for is to print one node in the tree per line and indendent node by a number of tabs that equals the depth of the element in the tree.
package lecture;
public class TreeNode {
public String person;
public TreeNode[] children;
public TreeNode(String person, TreeNode ... children) {
this.person = person;
this.children = children;
}
}
Now how can we create a print all nodes in the tree with the right indentation. First, we observe that the problem of printing a tree like this can be broken down into subtasks:
i
i+1
.This recursive description immeditialy translates into code:
package lecture;
public class TreePrinter {
public static void printTree(TreeNode root) {
printTree(root, 0);
}
public static void printTree(TreeNode t, int indent) {
StringBuilder ind = new StringBuilder();
for(int i = 0; i < indent; i++)
ind.append('\t');
System.out.printf("%s%s\n", ind, t.person);
if (t.children != null)
for(TreeNode child: t.children)
printTree(child, indent + 1);
}
}
import lecture.TreeNode;
import lecture.TreePrinter;
// in this example Bob reports to Peter who reports to Alice. Jane also reports to Alice.
// Alice
// Peter
// Bob
// Jane
TreeNode root = new TreeNode("Alice",
new TreeNode("Peter",
new TreeNode("Bob", null)
),
new TreeNode("Jane", null)
);
TreePrinter.printTree(root);
To better highlight the call structure, consider an even simpler example: counting the number of nodes in the tree. Again we can define this number recursively. The number $C(x)$ of nodes in a tree rooted at a node $x$ is
$$1 + \sum_{c\,\,\text{is child of x}} C(c)$$package lecture;
public class TreeCounter {
public static int countNodes(TreeNode t) {
int i = 1;
if(t.children != null)
for(TreeNode child: t.children)
i += countNodes(child);
return i;
}
}
import lecture.TreeNode;
import lecture.TreeCounter;
// in this example Bob reports to Peter who reports to Alice. Jane also reports to Alice.
// Alice
// Peter
// Bob
// Jane
TreeNode root = new TreeNode("Alice",
new TreeNode("Peter",
new TreeNode("Bob", null)
),
new TreeNode("Jane", null)
);
return TreeCounter.countNodes(root);
Say we have a tree like the one above, but the tree is limited to be binary. That is, no node has more than two children. Every node stores some payload, for now a String
. Assume we want to search for a node with a particular payload in the tree. We can again break this down recursively:
Observe that the correctness of the algorithm above is not affected by the order in which we execute steps 1 to 3. Thus, we have 3 different ways how to traverse the tree in which nodes will be visited in different orders:
package lecture;
public class BinaryTreeNode {
public String person;
public BinaryTreeNode leftChild = null;
public BinaryTreeNode rightChild = null;
public BinaryTreeNode(String person, BinaryTreeNode leftChild, BinaryTreeNode rightChild) {
this.person = person;
this.leftChild = leftChild;
this.rightChild = rightChild;
}
public BinaryTreeNode (String person, BinaryTreeNode leftChild) {
this(person, leftChild, null);
}
public BinaryTreeNode (String person) {
this(person, null, null);
}
}
package lecture;
public class TreeTraversal {
public static boolean PreOrder (BinaryTreeNode t, String key) {
if (t == null)
return false;
System.out.printf("check self (%s)\n", t.person);
if (t.person.equals(key)) {
return true;
}
if (t.leftChild != null) {
System.out.printf("visit left of (%s)\n", t.person);
if (PreOrder(t.leftChild, key))
return true;
}
if (t.rightChild != null) {
System.out.printf("visit right of (%s)\n", t.person);
if (PreOrder(t.rightChild, key))
return true;
}
return false;
}
public static boolean PostOrder (BinaryTreeNode t, String key) {
if (t == null)
return false;
if (t.leftChild != null) {
System.out.printf("visit left of (%s)\n", t.person);
if (PostOrder(t.leftChild, key))
return true;
}
if (t.rightChild != null) {
System.out.printf("visit right of (%s)\n", t.person);
if (PostOrder(t.rightChild, key))
return true;
}
System.out.printf("check self (%s)\n", t.person);
if (t.person.equals(key)) {
return true;
}
return false;
}
public static boolean InOrder (BinaryTreeNode t, String key) {
if (t == null)
return false;
if (t.leftChild != null) {
System.out.printf("visit left of (%s)\n", t.person);
if (InOrder(t.leftChild, key))
return true;
}
if (t.person.equals(key)) {
System.out.printf("check self (%s)\n", t.person);
return true;
}
if (t.rightChild != null) {
System.out.printf("visit right of (%s)\n", t.person);
if (InOrder(t.rightChild, key))
return true;
}
return false;
}
}
import lecture.BinaryTreeNode;
import lecture.TreeTraversal;
// in this example Bob reports to Peter who reports to Alice. Jane also reports to Alice.
// Alice
// Peter
// Bob
// Jane
BinaryTreeNode root = new BinaryTreeNode("Alice",
new BinaryTreeNode("Peter",
new BinaryTreeNode("Bob")
),
new BinaryTreeNode("Jane")
);
boolean hasJane;
System.out.println("********** PREORDER **********");
hasJane = TreeTraversal.PreOrder(root, "Jane");
System.out.println("********** POSTORDER **********");
hasJane = TreeTraversal.PostOrder(root, "Jane");
System.out.println("********** INORDER **********");
hasJane = TreeTraversal.InOrder(root, "Jane");
return hasJane;
Let us consider the problem of the towers of Hanoi to illustrate the concept of divide and conquer which solves a complex problem by recursively dividing it into simpler subproblems whose solutions can be combined into a solution of the complex problem. The towers of Hanoi is a good example where a problem whose solution is non-obvious can be solved relatively easily using divide and conquer.
In the towers of Hanoi problem we have three stakes (left, middle, right) and a set of n discs of sizes n, n-1, ...,1. These discs are stacked in decreasing size on the left stake. To solve the towers of Hanoi one has to move the discs to the right stake such that they are ordered by increasing size. A valid move is taking the top disc from one of the stakes and moving it to a different stake such that it is not placed on top of a smaller disc. For instance, here is a solution for 3 discs:
Start with all 3 disc on the left stake
1
2
3
| | |
Move disc 1 to the right stake
2
3 1
| | |
Move disc 2 to the middle stake
3 2 1
| | |
Move disc 1 to the middle stake
1
3 2
| | |
Move disc 3 to the right stake
1
2 3
| | |
Move disc 1 to the left stake
1 2 3
| | |
Move disc 2 to the right stake
2
1 3
| | |
Move disc 1 to the right stake
1
2
3
| | |
Moving 3 elements is relatively straight-forward. However, for a large number of elements it is not even clear immediatly whether it is even possible to solve the puzzle. Looking at the solution above we can make the following observation:
Note that this breaks down the problem of moving n discs into a subproblem of moving n-1 discs and one of moving 1 disc. Now, of course we have to move n-1 discs while there still exists a larger disc n on one of the stacks. However, since this disc is on the bottom of some stack and all n-1 discs are smaller this does not prevent us in any movement of the n-1 discs. That is, we are truely creating an independent subproblem of a smaller size. That is, the towers of Hanoi puzzle has a relatively easy recursive solution.
/**
*
*/
package lecture;
import java.util.Stack;
import java.util.List;
import java.util.ArrayList;
/**
* @author lord_pretzel
*
*/
public class TowersOfHanoi {
private List<Stack<Integer>> stacks;
private int numDiscs;
int moves = 0;
public TowersOfHanoi (int n) {
this.numDiscs = n;
stacks = List.of(new Stack<Integer>(), new Stack<Integer>(), new Stack<Integer>());
Stack<Integer> left = stacks.get(0);
for(int i = n; i > 0; i--)
left.push(i);
}
public String toString() {
StringBuilder result = new StringBuilder();
int maxLen = Math.max(Math.max(stacks.get(0).size(), stacks.get(1).size()), stacks.get(2).size());
for(int i = maxLen - 1; i >= 0; i--) {
String line = String.format("%s %s %s\n",
(stacks.get(0).size() > i) ? stacks.get(0).get(i) : " ",
(stacks.get(1).size() > i) ? stacks.get(1).get(i) : " ",
(stacks.get(2).size() > i) ? stacks.get(2).get(i) : " "
);
result.append(line);
}
result.append("| | |");
return result.toString();
}
public void solve() {
System.out.println("initial configuration:\n\n" + toString());
move(0, 2, numDiscs);
}
public void move(int from, int to, int n) {
if (n == 0)
return;
System.out.printf("solve moving %d discs from %d to %d\n", n, from, to);
int buf = getBuffer(from,to);
move(from, buf, n-1); // move n-1 discs: from -> buf
moveSingleDisc(from, to); // move nth disc: from -> to
move(buf, to, n-1); // move n-1 discs: buf -> to
}
public void moveSingleDisc(int from, int to) {
moves++;
int disc = stacks.get(from).pop();
stacks.get(to).push(disc);
System.out.printf("move disc %d from %d to %d:\n\n%s\n", disc, from, to, toString());
}
private int getBuffer(int from, int to) {
if (from == 0 && to == 2)
return 1;
if (from == 0 && to == 1)
return 2;
if (from == 1 && to == 0)
return 2;
if (from == 1 && to == 2)
return 0;
if (from == 2 && to == 0)
return 1;
else // (from == 2 && to == 1)
return 0;
}
public static void main(String[] args) {
TowersOfHanoi h = new TowersOfHanoi(7);
h.solve();
System.out.printf("\n\n----------------------\nNumber of Moves: %d",
h.moves);
}
}
import lecture.TowersOfHanoi;
TowersOfHanoi.main(null);
The recursive definition enables us to express the running time of the algorithm as a recusive equation:
$$ T(n) = T(n-1) + 1 + T(n-1) = 2 \cdot T(n-1) + 1 $$To solve the problem for $n$ discs we have to solve the problem for $n-1$ discs twice and then move the largest disc (of size $n$) once. For a single disc we can solve the problem in one move:
$$ T(1) = 1 $$To arrive at a closed form (a non-recursive version of this equation), we can compute the first few values to see whether a pattern emerges:
$$T(1) = 1$$$$T(2) = 3$$$$T(3) = 7$$$$T(4) = 15$$$$T(5) = 31$$These numbers are all powers of $2$ minus $1$.
$$T(1) = 1 = 2^1 - 1$$$$T(2) = 3 = 2^2 - 1$$$$T(3) = 7 = 2^3 - 1$$$$T(4) = 15 = 2^4 - 1$$$$T(5) = 31 = 2^5 - 1$$So we may hypothetize that $T(n) = 2^n-1$. To prove this hypothesis correct, we can use induction.
Base Case:
$$T(1) = 1 = 2^1 -1$$Inductive Step:
$$T(n) = 2 \cdot T(n-1) + 1$$$$= 2 \cdot (2^{n-1} - 1) + 1$$ $$= 2^n - 2 + 1\\ = 2^n - 1 $$