Recursion can be a bit of mind bender.
But only because it gets a lot of power out of very simple pieces that you already understand.
We’re used to calling methods to solve sub-problems
public int hypotenuse(int a, int b) {
// Call Math.pow to do exponentation
int a_2 = Math.pow(a, 2);
int b_2 = Math.pow(b, 2);
// Call Math.sqrt to do compute square root
return Math.sqrt(a_2 + b_2);
}
Each human handles one method call.
As a human method call your job is to know how to execute your code for a given argument and return a value to the human who asked you.
If your code requires you to call a method you need to find an appropriate human method and ask them for the answer you need.
hypotenuse(3, 4)
Cast: hypotenuse
, Math.pow
, and
Math.sqrt
.
public int hypotenuse(int a, int b) {
// Call Math.pow to do exponentation
int a_2 = Math.pow(a, 2);
int b_2 = Math.pow(b, 2);
// Call Math.sqrt to do compute square root
int sum = a_2 + b_2;
return Math.sqrt(sum);
}
n! = n × n - 1 × n - 2 … × 1
Or in math class:
factorial(0) = 1
factorial(n) = n × factorial(n - 1)
We can easily translate the mathematical definition into Java code:
public int factorial(int n) {
if (n == 0) {
return 1;
} else {
return n * factorial(n - 1);
}
}
factorial(4)
Cast: Five factorial
s
public int factorial(int n) {
if (n == 0) {
return 1;
} else {
return n * factorial(n - 1);
}
}
public int factorial(int n) {
int f = 1;
for (int i = 1; i <= n; i++) {
f *= i;
}
return f;
}
In general loops can easily be translated into recursion but not the other way around.
Mathematical definition:
fibonacci(0) = 0
fibonacci(1) = 1
fibonacci(n) = fibonacci(n - 1) + fibonacci(n - 2)
In Java:
public int fib(int n) {
if (n < 2) {
return n;
} else {
return fib(n - 1) + fib(n - 2);
}
}
fibonacci(4)
Cast: Four fibonacci
s.
public int fib(int n) {
if (n < 2) {
return n;
} else {
return fib(n - 1) + fib(n - 2);
}
}
Yes, but it’s not quite as obvious how because of the tree structure.
public int fib(int n) {
int a = 0;
int b = 1;
for (int i = 0; i < n; i++) {
int tmp = a;
a = b;
b += tmp;
}
return a;
}
Let’s define a class that can represent a file on our hard drive.
There are two kinds of files.
Single files are things like a document or a video that have a direct size.
Folders contain other files and have a size determined by the sum of sizes of the files they contain.
File
public abstract class File {
private String name;
public File(String name) {
this.name = name;
}
public abstract int getSize();
}
SingleFile
public class SingleFile extends File {
private int size;
public SingleFile(String name, int size) {
super(name);
this.size = size;
}
public int getSize() { return size; }
}
Folder
public class Folder extends File {
private File[] contents;
public Folder(String name, File[] contents) {
super(name);
this.contents = contents;
}
public int getSize() {
// implementation on next slide
}
}
Folder.getSize
public int getSize() {
int size = 0;
for (File f: contents) {
size += f.getSize();
}
return size;
}
We’re using a loop to iterate over contents
but we’re
using recursion by calling getSize()
on each of the
files directly contained in this folder.
Any elements of contents
that are also
Folder
s will take care of computing their own size from
their own contents.
getSize
without recursion?Sure. Anything’s possible.
But unlike fibonacci
where we’re essentially moving
through a linear sequence this is really a tree and if we don’t want
to use recursion we need some other data structure to keep track of
where we are in the tree.
You know how to write a for
loop to loop over a linear
structure like an array, String
, or
ArrayList
.
You know how to write nested for
loops to iterate over
a 2d structure like a String[][]
.
To iterate over a tree structure the natural way to do it is with recursion.
Every non-infinite recursion has two parts:
One or more base cases
One or more recursive cases
In a recursive method, the base cases are the problems we can solve without any further recursive method calls.
You can think of the base cases as the smallest problems the method or recursive procedure knows how to solve.
Also the ones where they can’t be solved recursively.
factorial
?factorial(0)
fibonacci
?Two base cases:
fib(0)
and fib(1)
.
getSize()
?
The getSize
method in SingleFile
.
Recursive cases are the rest—whenever the method invokes itself.
The recursive calls have to be used to to solve a smaller problem than the current problem so that eventually we will end up at a base case.
factorial
recursive casefactorial(n - 1)
Clearly, n - 1
is clearly smaller than
n
so this meets our criteria.
fibonacci
recursive casesTwo recursive cases:
fibonacci(n - 1)
and fibonacci(n - 2)
And again, both n - 1
and n - 2
are
smaller than n
so they are both moving toward the base
cases.
getSize
recursive cases
The getSize
method in Folder
is recursive
since it calls getSize
on each File
in
contents
.
We can think of the “size” of the problem as the number of layers of
sub-folders in a File
, with
SingleFiles
having zero.
Then each recursive call reduces this size by one, so solving a smaller problem.
The call stack keeps track of where we are in the recursion.
Each method call has its own set of arguments.