Unit 11: Recursion
Topics covered in this unit
By the time you are done with this unit you will:
- be able to give examples of "real-life" (non-Computer Science) recursion, including visual examples.
- be able to identify the two requirements of a recursive function: a base case and a recursive function call with a modified parameter.
- be able to write a simple counting loop using a recursive function.
- be able to demonstrate recursive summing and factorial recursion.
- be able to follow recursive function calls by observing a stack trace.
11.0. Overview

Visual recursion on a box of hot chocolate mix
Recursion is an important topic in computer science, probably the most important topic that doesn't get covered well in most introductory computer science courses. That's partly because it can be a little confusing at first, and maybe because the concept of recursion—drilling down deeper and deeper into the memory of the computer, with the possibility of never getting out alive—is a little scary, at least existentially.
Using recursion and "thinking recursively" is an important skill in computer science, and we're going to spend a good part of our time in this course doing just that: using recursion and thinking recursively.
Let's get started!
11.1. Recursion
Definition: Recursion
In computer programming, recursion occurs when a function is defined in terms of itself: a function will call itself as part of its execution.
In addition to an internal, recursing call of the function, the function must include non-recursively defined values—values that the function can use to cease the recursion.
Ummmm.... yeah. So what does that mean?
Let's take a look at a specific example.
11.1.1. Simple recursion
Take a moment to analyze this Blastoff.java program.
/** * This program uses a static method to demonstrate * recursive calls. */ public class Blastoff { /** * This recursive method follows the 2 rules of recursion: * 1. Have a "base case" for which you STOP recursing. * 2. Have the method call itself with a parameter that has changed */ public static void countdown(int n) { if (n == 0) // BASE CASE { System.out.println("Blastoff!"); return; } else { System.out.println(n); countdown(n - 1); // RECURSIVE CALL return; } } public static void main(String[] args) { countdown(5); } }Then do a stack trace of the program using the online tool.
Here's another tool that you can use to visualize Java code execution: Java Visualizer.
11.1.2. Simple looping with recursion
Here's a simple loop that we can write recursively. Given an initial value start
and an end value stop
, this recursive loop counts from start
to stop - 1
:
public class RecursiveLoop { public static void loop(int start, int stop) { if (start == stop) { return; // BASE CASE } else { System.out.println(start); loop(start + 1, stop); // RECURSIVE CALL } return; } public static void main(String[] args) { loop(0, 10); } }
11.1.3. Summing values in a loop, versus recursively
If we want to write a function to add the values from 1 through n using a loop, it's not too difficult:
public static int sumValues(int n) { sum = 0; for (int i = 0; i <= n; i++) { sum += i; } return sum }
If you System.out.println(sumValues(10));
this code runs just fine, and is what you'd ordinarily write to accomplish something like this.
We can also write a recursive version of this function, however, a version in which the function calls itself. One of the keys to doing this is think about what's happening when we add those numbers. If I want to add the numbers from 1 to 10, I can think about that sum as being 10 + the sum of all the lower numbers 1 through 9.
I might write it like this: The sum of numbers 1-10 = 10 + the sum of numbers 1-9.
What is the sum of the numbers 1-9, though? Isn't that just 9 + the sum of numbers 1-8?
Likewise, The sum of numbers 1-8 = 8 + the sum of numbers 1-7. There's a pattern emerging here.
Take a look at this function:
public static int sumValues(int n) { return n + sumValues(n - 1); }
That single line of code in this function returns a value that includes n
+ the sum of the numbers lower than n
. When the sum()
function calls itself, that's the recursive part of this function.
It's a clever way of looping that is actually much simpler and cleaner than the loop_sum
written above. It might even be easier to conceptualize, at least if you're comfortable with the concept of recursion.
I ran into a problem when I actually tried to run this code, however:
java.lang.StackOverflowError at RecursiveSum.sumValues(RecursiveSum.java:19) at RecursiveSum.sumValues(RecursiveSum.java:19) at RecursiveSum.sumValues(RecursiveSum.java:19) at RecursiveSum.sumValues(RecursiveSum.java:19) at RecursiveSum.sumValues(RecursiveSum.java:19)
The reason this function ran into trouble is that it never identified a way for it to stop calling itself. At some point, we need the function to be able to tell itself that it shouldn't call itself anymore.
How do I know when I'm done adding the numbers from n
on down? Once n
gets down to the value 1
, I shouldn't call the function recursively again. I just want to send back the value of 1
.
public static int sumValues(int n) { if (n == 0) return 0; else return n + sumValues(n - 1); }
This program works correctly.
Make sure you understand exactly what's happening here.
- We define a function which, as part of its description, calls itself in a slightly modified form.
- We call the function (
sumValues(10)
for example) with a specific value, which will be identified asn
during the that call. - When the function calls itself again with the value
n - 1
, all of the values for that first function call are kept in memory, and the new function call creates a new "state" in memory with the new values (n - 1
forn
, etc.). - This process continues, which each new function call generating a new instance of itself in memory until the limiting condition is met.
- The
return
instruction exits from the current state, and the program returns to processing the previous state of the function. - This process of calling functions and returning from their states continues until the very first function call returns a value. At that point the recursive process has completed.
11.1.4. Factorial recursion
Summing the values from 1 to n is a nice simple introduction to recursion. Now try this.
Calculating factorials recursively
Write a recursive function fact(n)
that returns the value of n!
.
public static long fact(long n) { if (n == 1) return 1; else return n * fact(n - 1); } public static void main(String[] args) { System.out.println(fact(20)); }
11.1.5. Accidental recursion
Based on what you know about recursion to this point, take a look at the problem below.
Where'd my memory go?
A student writing a game wanted to include a "play again" option in the program:
public static void playGame() { // code to play game of blackjack // more code System.out.println("Play again (y/n)? "); String continue = in.next(); if continue.equals("y") { playGame() } }
The student tested the program a few times and it appeared to work fine.
So why is this way of writing the program incorrect?
Every time the playGame()
function is called, the current state of the game is saved in memory and set aside so we can create a new playGame()
state. Although the computer's memory will probably be able to handle a few games like this—maybe even a lot of games—each time the user plays, memory is being taken in the computer's memory: a memory leak. The student is inadvertently using recursion—the playGame()
function is calling itself—when a loop is what should be used for this program.
The Two Rules of Recursion
- All recursive programs need to call themselves with a modified parameter (
n - 1
instead ofn
, for example). - All recursive programs need to define a base case which will result in a non-recursive
return
from the function.
11.1.6. Fibonacci recursion
You almost certainly know the Fibonacci sequence, which begins with the values 0 and 1. (There are other ways of defining the Fibonacci sequence, but this is the one we'll use here.) Each successive value in the series is based on the sum of the previous two values.
The Fibonacci series is a great example to think about recursively, and it's easily coded.
RecursiveFibonacci.java
Write a program RecursiveFibonacci.java that includes two static functions—fib(int n)
which calculates and returns the nth fibonacci value, and a main()
—that demonstrates calling the fib
function.
For the purposes of this project, we'll use the modern definition of the Fibonacci sequence, which begins at 0. And because we're in a computer science course, we'll refer to that first time by its index, 0. So the 0th term in the Fibonacci sequence will be 0, and the sixth term in the sequence is at index 6, which is the value 8.
BONUS: Give the the fib()
method a variety of Fibonacci values to calculate, and display the running time for each call of the function.
11.2. Visual Recursion
There are obvious "visual recursions" like the hot chocolate can above, or a series of nested Russian dolls, with each doll of size n
containing a smaller doll of size n - 1
, down to some final size.
There are other visual recursions as well. Here's one.
It's possible to draw our own graphical recursions, of course.
11.2.1. The Processing application and the Turtle
class
The Processing application you can download at at processing.org. For our study here, we're first going to write a Turtle
class that we can use in our Processing application.
The idea of a "Turtle" that can draw graphics was developed by Seymour Papert and others in the 1960s. Papert envisioned technology as a means of inspiring whole new ways of learning, and his book Mindstorms, published in 1980, outlined many of those ideas. (The book's title was subsequently used as the name of a robotics system developed by Lego.)


Rather than using a mechanical turtle as developed by Papert, we're going to use a virtual turtle that moves across the computer screen. This computer has a "tail" that can be "down," in which case it will leave a trace across the computer screen, or "up," in which case it will move without leaving a trail.
A summary of this virtual Turtle's methods is given here.
The Turtle
class
A Turtle
object has four instance variables:
-
x
= the x-coordinate of the turtle's current position -
y
= the y-coordinate... -
angle
= the direction the turtle is pointing in degrees -
penDown
=true
when the pen is "down" and leaving a trail
There's a constructor for the Turtle class that initializes it to a starting x- and y-coordinate, with an initial direction of 90 (pointing to the right relative to 0 degrees north), and the tail initially down.
-
Turtle(float x, float y)
The methods for the Turtle include:
-
forward(float distance)
- displaces the turtle by the given distance relative to its current position -
back(float distance)
- displaces the turtle backwards -
moveTo(float newX, float newY)
- moves the turtle to the new location specified -
left(float angle)
- rotates the turtle to the left by the specified number of degrees -
right(float angle)
- rotates to the right -
penUp()
- sets the pen up -
penDown()
- sets the pen down
Writing a class in Processing is slightly different from writing it in straight Java. Do the following assignment to make sure you've got a working Turtle
class before proceeding.
A Processing program and the PTurtle
class
Using the Processing library, write a main program called TurtleTester
that creates a Turtle
object and has it draw a square.
The Turtle class is called PTurtle
, for Processing Turtle.
The PTurtle
class itself is written in a separate class (a separate tab in the Processing environment). Remember that the main() part of a Processing package with its setup()
and draw()
methods has to be in the first tab, with other classes as needed in additional tabs to the right.

/** * Defines a Turtle class that can be used in the * Processing environment. Based on code from Jamie Matthews, * http://j4mie.org/blog/simple-turtle-for-processing/ */ class PTurtle { // instance variables float x, y; // Current position of the turtle float angle; // Current heading of the turtle // 0 = positive x-axis boolean penDown; // The state of the pen /** * Constructs a Turtle object * @param x the initial x-coordinate of the Turtle * @param y the initial y-coordinate of the Turtle */ PTurtle (float x, float y) { this.x = x; this.y = y; angle = 0; // ie, pointing along the positive x-axis penDown = true; } /** * Moves the turtle forward by the specified distance * @param distance the distance to move forward */ void forward (float distance) { float newX = x + cos(radians(angle)) * distance; float newY = y + sin(radians(angle)) * distance; // If the pen is down, draw a line to the new position if (penDown) line(x, y, newX, newY); // Update position x = newX; y = newY; } /** * Moves the turtle back by the specified distance * @param distance the distance to move backward */ void back (float distance) { forward(-distance); } /** * Moves the turtle directly to a new x,y location * @param newX the new x-coordinate * @param newY the new y-coordinate */ void moveTo(float newX, float newY) { x = newX; y = newY; } /** * Turns the turtle to the left by the given angle. * @param angle the number of degrees to rotate left */ void left (float angle) { this.angle -= angle; } /** * Turns the turtle to the right by the given angle. * @param angle the number of degrees to rotate right */ void right (float angle) { this.angle += angle; } /** * Lifts the pen up so that the tail won't draw */ void penUp() { penDown = false; } /** * Sets the pen up so that the tail will draw */ void penDown() { penDown = true; } }
11.2.2. RecursiveSpiral
Take a look at the this RecursiveSpiral.pde
and see if you can anticipate what will be drawn before the code executes.
// declares instance variables for the main() // Just a Turtle object PTurtle t; // Sets up the graphical window's size, background color, // and initializes the turtle to a position in the // lower-left corner of the screen. void setup() // initializes the screen and turtle { size(700, 700); background(#ffffff); t = new PTurtle(0, 0); t.penUp(); t.moveTo(50, height - 50); t.penDown(); } // A recursive function that will be called by the main // (draw) loop. void drawAndTurn(PTurtle t, float lineLength) { if (lineLength <= 0) return; else { t.forward(lineLength); t.left(90); drawAndTurn(t, lineLength - 3); } } // The main draw() loop. Ordinarily, this would be run // over and over, but here, with a noLoop at the end, it // only runs once. void draw() { drawAndTurn(t, 600); noLoop(); }
11.2.3. Recursive Trees
The recursive tree is based on a branch, which produces branches, each of which produce branches...
Each branch is a little shorter than the one before it.
RecursiveTree
A tree consists of a series of recursive branches. Each recursive branch of a given thickness and length has two branches sticking out of it, shorter, until we get down to a branch length of some base value.
With your PTurtle
class to help, write a Processing class RecursiveTree
with a static method branch
defined in it.
The branch
method has parameters t
(for the turtle), and length
. Each branch
should make recursive branch
es coming from it.
11.2.3.1. Improvements to the RecursiveTree
The Turtle class that we've written so far has some capabilities, but the trees that we can create don't look very tree-like. There are two modifications that we can make to PTurtle
that will give us a lot more power.
Improving PTurtle
We'd like to be able to vary the width of a line that the Turtle can draw—what Processing calls the "strokeWidth"— and we'd like to be able to specify the color of a line using Red, Green, and Blue values. Let's modify PTurtle
:
- Add instance variables for the
int
variableweight
(line thickness) andint
valuesred
,green
, andblue
. - Modify the PTurtle constructor so that these new variables are initialized.
- Add methods to the PTurtle class to
setWidth(int newWidth)
,setRed(int newRed)
,setGreen(int newGreen)
, andsetBlue(int newBlue)
. After these methods change any of the instance variables, they also need to call the appropriate Processing instruction as well.
Write a short tester program to demonstrate the capabilities of your new and improved PTurtle
.
Now that we've got expanded capabilities in PTurtle
, we can expand RecursiveTree
.
Improving RecursiveTree
In the recursive calls to the branch
method, include parameters for thickness, with farther branches having a smaller thickness.
And/or, shift the red, green, or blue colors of each branch by a given amount. Note that you'll want to make sure you don't accidentally give a value that is too high (> 255) or too low (< 0). You can do this by using Math.max(value1, value2)
and Math.min(value1, value2)
.
11.3. More Difficult Recursion
We'll have the chance to explore some more complex recursion a little later on in this course. For now, let's take a look at two standard applications of recursion: solving the "Towers of Hanoi" puzzle and navigating a maze.
11.3.1. The Towers of Hanoi
The Towers of Hanoi is a classic problem which we'll demonstrate in class.
This problem lends itself quite nicely to a recursive solution: each move, you're moving a disk from a source tower to another tower.
Consider what happens if we have Tower A with two disks on it and we're trying to get them all over to Tower C. The general strategy: move all the top disks to B, then move A to C, then move all the disks from B to C.
We'd use the exact same strategy with three disks...
But how do we code that?
11.3.2. Navigating a Maze
Coming soon!