Data Structures and Algorithms Week4: Recursion and Call Stack
recursion 递归
recursive 递归的
self-referential 自引用
traverse 遍历
Recursion
Content
- Recursion
- What is recursion
- What it is good for
- When not to use recursion
- Backtracking
Basic concepts about Recursion
-
Self-referential (defined in terms of itself)
-
Natural numbers:
- a) 1 is a natural number
- b) The successor of a natural number is a natural number
-
Recursion is a programming technique much used in declarative (functional) programming -a different way of getting repetition.
![]() | ![]() |
|---|
Recursive entertainment
The movie Inception described one scenario:
Dream in dream

Recursive pattern: Fractals
| Koch snowflake | the Barnsley fern |
|---|---|
![]() | ![]() |
| Shapes made of smaller versions of self | Not a photo, It is generated by a mathematical formula |
Recursive description: Ancestor
An ancestor is:
- a) a parent, or
- b) a parent’s ancestor

Factorial function
- Factorial 5, written 5!, is:
- and 6! is
- , so
⬇
Factorial function, for non-negative integers is:
- a)
- b) if , then
Factorial function in Java
int factorial (int n) {/* pre: n >= 0 post: factorial(n) = n! */ if (n == 0) then return 1; else return n * factorial(n - 1);} /* factorial */This function has problem:
- inefficient and
- factorial(n) > maximum integer probably
Infinite recursion (silly)
void TellStory() { System.out.println("It was a dark and stormy night"); System.out.println("and the captain said to the mate"); System.out.println(" 'Tell us a story mate' "); System.out.println("and this is the story he told … "); TellStory();} /* TellStory */This function has problem:
- infinite and
- lead to stack overflow
Useful recursion
- To be useful the recursion must terminate, so there must be
- at least one non-recursive, base case (in factorial, it is 0!)
- as well as recursive cases.
Other recursive examples
-
A linked list is:
- a) empty, or
- b) has a head (first element)
and a tail, which is a linked list
-
A tree is:
- a) empty, or
- b) has a root,
and left (sub-) tree and right (sub-) tree
A Recursive Class-Linked lists
public class Node { public String data; public Node next; // Here’s the recursive bit! One of the fields of Node is another Node public Node(String data) { this.data = data; }}
public static void main(String[] args) { Node n1 = new Node("Have"); Node n2 = new Node("a"); n1.next = n2; Node n3 = new Node("banana"); n2.next = n3; Node node = n1; while (node != null) { System.out.print(node.data + " "); node = node.next; }}Length of a list
- How to calculate the length of a list :
- a) the length of an empty list is 0
- b) the length of a (non-empty) list is:
1 + the length of the tail of the list
int length (Node ls) {/* pre: ls is a well formed linked listpost: length(ls) is the number of nodes in the list starting at ls */ if (ls == null) return 0; // empty list else return 1 + length(ls.next); // 1+ length of tail of list} /* length */Practice in class
- using f(i, j) represents the 𝑗𝑡ℎ number in the 𝑖𝑡ℎ line.
Pascal’s Triangle(1654)
Yanghui’s Triangle(1261)
public class YanghuiTriangle { public YanghuiTriangle() {} public int calculateYanghuiTriangle(int i, int j) { if (j==1 || i==j) { return 1; } else { return calculateYanghuiTriangle(i-1, j-1)+calculateYanghuiTriangle(i-1,j); } } public static void main(String[] args) { YanghuiTriangle yh = new YanghuiTriangle(); System.out.println(yh.calculateYanghuiTriangle(5,3)); }}- The Tower of Hanoi is a classic mathematical puzzle that consists of three rods (or pillars) and several disks of different sizes, which can slide onto any rod. The puzzle starts with the disks in a neat stack in ascending order of size on one rod, the smallest at the top, thus making a conical shape.
- The objective of the puzzle is to move the entire stack to another rod, obeying the following simple rules:
- Only one disk can be moved at a time.
- Each move consists of taking the upper disk from one of the stacks and placing it on top of another stack or on an empty rod.
- No disk may be placed on top of a smaller disk.

- Input
- an integer n, which represents the number of disks on trod A
- for example:
3- Output
- displaying the steps of moving the disks;
- for example:
A->CA->BC->BA->CB->AB->CA->CA->B means moving the top disk from trod A to the top of trod B

In contrast to recursive: iterative
- The term iterative comes from the Latin iter meaning a journey, as in itinerary, itinerant, iteration, iterator, …
- In programming it refers to making repetitions by using ‘looping’ statements, such as while, for, …
Iterative example - isPresent
static boolean isPresent (int[] a, int x) { // pre: true // post: isPresent(a, x) <=> “there is an x in a” int i = 0; while(i != a.length && a[i] != x) { i++; } // i == a.length || a[i] == x return i != a.length;}Recursive example - isPresent()
static boolean isPresentAux(int[] a, int x, int start) {/* pre: truepost: isPresent(a, x, start) <=>"there is an x in a", starting from start */ boolean result; if (start == a.length) result = false; // empty sequence else if (a[start] == x) result = true; // found x else result = isPresentAux(a, x, start+1); return result;}static boolean isPresent (int[] a, int x) {/* pre: truepost: isPresent(a, x) <=> “there is an x in a” */ return isPresentAux(a, x, 0);}Traversing a list: recursive, forward
- Traversing a (singly) linked list recursively in the forward direction is easy:
void traverse (Node ls) { if (ls != null) { Process(ls.data); // process head traverse(ls.next); // traverse tail of list }} /* traverse */Traversing a list: recursive, backward
- Traversing a (singly) linked list recursively in the backward direction is easy too:
void reverseTraverse (Node ls) { if (ls != null) { reverseTraverse(ls.next); Process(ls.data); }} /* reverseTraverse */How recursion works
- When a method is called its parameters, local variables and return address are stacked on the method-call stack.
- Nested calls lead to deeper stacking.
- A call of a method to itself is just another nested call.
When not to use recursion
- It is best to use a recursive algorithm when the data structure is itself recursively defined
- Don’t use a recursive approach when a simple iterative approach is available
- Examples: searching, traversing, and inserting in a list is easy to do iteratively
➢ But, traversing a list backwards (‘backtracking’) is easy to do recursively but hard to do iteratively.
When not to use recursion - example
Fibonacci numbers:
fib0 = 0 fib1 = 1 fibn = fibn-1 + fibn-2, for n > 0
int Fib (int n) { /* doubly recursive *//* pre: n >= 0post: Fib(n) is the nth Fibonacci number */ if (n == 0) return 0; else if (n == 1) return 1; else return Fib(n - 1) + Fib (n - 2);} /* fib */Very inefficient: values repeatedly calculated, then ‘forgotten’
- How many times program called
Fib()recursively when calculatingFib(5)?- 15 times

- 15 times
- Iterative version
int Fib (int n) { /* iterative *//* pre: n >= 0post: Fib(n) is the nth Fibonacci number */ int i, x, y, z; i= 1; x= 1; y= 0; while (i != n) { z = x; i = i + 1; x = x + y; y = z; } return x;} /* Fib */Tracing recursion - example
- Need to take care to know which call is which.
int sum(int arr[], int n) { if (n == 0) return 0; else { int smallResult = sum(arr, n - 1); return smallResult + arr[n - 1]; }}- Assume the array contains:
{2, 4, 6}, and that the call to the sum is:sum(arr, 3);which will sum the first three elements of the array.

When is recursion really useful
- Really useful if backtracking is involved.
- Call-stack keeps stack of values to return to.
reverseTraverse: iterative version
- What if we want to traverse the list in reverse order?
- Needs backtracking.
- We need to traverse, stacking pointers to nodes, and then return by unstacking them.
public static void reverseTraverseIterative(Node first) { Node p = first; NodeStack ns = new NodeStack(); // assume such a class available while (p != null) { ns.push(p); p = p.next; } while (!ns.empty()) { System.out.println(ns.pop().data); }}Not so simple iteratively.
Questions
- Which of these operations needs backtracking?
- Find length of a list
- Insert in an ordered list
- Find average value in a list
- Display above-average values in a list
- What’s the time complexity of the method below that displays all the elements in a LinkedList?
static void displayList(LinkedList<String> list) { for (int i = 0; i < list.size(); i++) { System.out.println(list.get(i)); }}Answer:
- get has to traverse through the list to find the ith element, so that is n, but the loop calls get n times,
- so O(n2) - a slow process.
Summary
- Iteration often more efficient than recursion (but Fibonacci is a very extreme case).
- Sometimes there is a choice of iterative versus recursive.
- Recursive typically closer to definition of data structure.
- Recursion is very useful when backtracking involved.
- Backtracking used in optimal-fitting algorithms - beyond scope of this module.
- After studying the material of this week and attempting the exercises, you should be able to:
- understand the concept of recursion
- apply a recursive approach to defining an algorithm
- know when not to use recursion
- know how to trace a recursion
- We will use recursion a great deal with trees.
Call stack
Content
- The call stack
- Recursion and the call stack
- Exceptions and the call stack
- Finally blocks and the try with resources construct
What information MUST be maintained when a method is called?
public static void main(String[] args) { double answer = getSum(2,3);// Need to remember which line to return to in the calling method.// This information is no longer needed when the called method returns. System.out.println(answer);}public static double getSum(double x, double y) { double sum = x+y;// Need to keep track of the values of local variables and formal parameters.// This information is no longer needed when the called method returns. return sum;}public static void main(String[] args) { double answer = getAverage(2,3); System.out.println(answer);}public static double getAverage(double x, double y){ double average = getSum(x,y)/2; return average;}public static double getSum(double x, double y) { double sum = x+y; return sum;}getAverageis called. Need to keep track of its local variables, etc.getSumis called. Need to keep track of its local variables etc.getSumreturns. No longer need information stored in step 2.getAveragereturns. No longer need information stored in step 1.
The data most recently added is first to be removed.
What would be a suitable data structure?
The call stack (Schematic)
public static void main(String[] args) { double answer = getAverage(2,3); System.out.println(answer);}public static double getAverage(double x, double y){ double average = getSum(x,y)/2; return average;}public static double getSum(double x, double y) { double sum = x+y; return sum;}| getSum x=2 y=3 |
A value for sum is calculated. This then becomes the return value for the method. |
| getAverage x=2 y=3 average=2.5 |
Stack frame for getSum is “popped”. Return value used to calculate a value for variable average. This is then returned. |
| main args=[] answer = 2.5 |
Control returned to main method |
Information about each running method is kept in a stack frame, which is pushed onto the stack when the method is called, and popped when it returns.
Visualising the call stack
A useful tool:
https://cscircles.cemc.uwaterloo.ca/java_visualize/
allows you to visualize the call stack as you run a program
Understanding Recursion
- There are two ways to convince yourself that a recursive method works:
- Reason about it inductively. This is probably the best thing to do first.
- Think about what happens to the call stack as it runs. Try this if option 1 is not working for you.
Inductive reasoning
- Let us use induction to convince ourselves that the method below works. We need to do two things:
- Convince ourselves that it works for the base case.
- Convince ourselves that it works for the recursive case, if we assume that any recursive calls do what they are supposed to do.
/**Reverse the order of the elements in an ArrayList of Strings**/public static void reverse(ArrayList<String> a) { if (a.size() > 1) { String s = a.remove(0); reverse(a); a.add(s); }}Inductive reasoning:Base case
- If
a.size() <= 1then the reverse method is not called recursively, and the ArrayList a is unchanged. This is exactly what should happen.

Inductive reasoning: recursive case
a→[“A”, “B”, “C”, “D”]
/*** Reverse the order of the elements in an ArrayList of Strings*/public static void reverse(ArrayList<String> a) { if (a.size() > 1) { String s = a.remove(0); reverse(a); a.add(s); }}s→“A”
a→[“B”, “C”, “D”]
/*** Reverse the order of the elements in an ArrayList of Strings*/public static void reverse(ArrayList<String> a) { if (a.size() > 1) { String s = a.remove(0); reverse(a); a.add(s); }}s→“A”
a→[“D”, “C”, “B”]
/*** Reverse the order of the elements in an ArrayList of Strings*/public static void reverse(ArrayList<String> a) { if (a.size() > 1) { String s = a.remove(0); reverse(a); a.add(s); }}Inductive reasoning
- We have proved that:
- Our method works when
a.size() == 1ora.size() == 0. - For any n, if the method works for
a.size() == n-1, then it also works fora.size() == n. - So we can see that…
- The method works for
a.size() == 2(because of A and B). - The method works for
a.size() == 3(because of B and C). - The method works for
a.size() == 4(because of B and D) - The method works for
a.size() == 5(because of B and E) - And so on…
- Our method works when
- However if that’s hard to understand, we can also think about what happens on the call stack…
Recursion and the call stack
public static void reverse(ArrayList<String> a) { if (a.size() > 1) { String s = a.remove(0); reverse(a); a.add(s); }}
public static void reverse(ArrayList<String> a) { if (a.size() > 1) { String s = a.remove(0); reverse(a); a.add(s); }}
public static void reverse(ArrayList<String> a) { if (a.size() > 1) { String s = a.remove(0); reverse(a); a.add(s); }}Each stack frame has its own separate copy of the variables a and s

public static void reverse(ArrayList<String> a) { if (a.size() > 1) { String s = a.remove(0); reverse(a); a.add(s); }}
public static void reverse(ArrayList<String> a) { if (a.size() > 1) { String s = a.remove(0); reverse(a); a.add(s); }}
public static void reverse(ArrayList<String> a) { if (a.size() > 1) { String s = a.remove(0); reverse(a); a.add(s); }}
Base case. Topmost invocation of the method returns.
public static void reverse(ArrayList<String> a) { if (a.size() > 1) { String s = a.remove(0); reverse(a); a.add(s); }}
public static void reverse(ArrayList<String> a) { if (a.size() > 1) { String s = a.remove(0); reverse(a); a.add(s); }}


CHECKED EXCEPTIONS (RECAP)
public static void writeArray(ArrayList<Integer> nums) { PrintWriter out = new PrintWriter(new FileWriter("OutFile.txt")); for (int num : nums) { out.println(num); } out.close();}- This code will not compile because the
FileWriterconstructor can throw anIOException, which is checked. - We must either
catchthis exception, or explicitly declare that the methodthrowsit.
Solution 1: Catch the exception
public static void writeArray(ArrayList<Integer> nums) { try { PrintWriter out = new PrintWriter(new FileWriter("OutFile.txt")); for (int num : nums) { out.println(num); } out.close(); } catch (IOException ex) { System.out.println("Error opening file"); }}Solution 2: add a “throws” clause
public static void writeArray(ArrayList<Integer> nums) throws IOException { PrintWriter out = new PrintWriter(new FileWriter("OutFile.txt")); for (int num : nums) { out.println(num); } out.close();}- The compiler will be happy with this, because we have acknowledged that the exception can be thrown. If there is an exception handler further down the call stack then it can be caught.
- Question: Which solution is better?
Solution 1: Catch the exception
public static void writeArray(ArrayList<Integer> nums) { try { PrintWriter out = new PrintWriter(new FileWriter("OutFile.txt")); for (int num : nums) { out.println(num); } out.close(); } catch (IOException ex) { System.out.println("Error opening file"); }}- This solution assumes that the method is being called in a context in which we can write a message to standard output and be confident that someone will see it.
- We can’t guarantee that this is true, so it is probably better to throw the exception and allow it to be caught by a handler that can deal with it more sensibly.
Better solution: Throw early, catch late
- In general, a method should not catch an exception unless you can guarantee that it will be able to handle the exception sensibly.
- If you cannot do this, then it is better to allow the exception to be thrown.
- But what actually happens when you throw it?
Exceptions
- When an exception is thrown the call stack is “unwound”. That is to say entries are popped from the stack until a suitable catch block is found. The code in the catch block is then executed.
- The catch block might be in the same method as the one that generated the exception, in which case no unwinding is necessary
- If no suitable catch block is found then the program terminates with some sort of error message.
Method E throws an exception that can be handled by Method B’s catch block

Method B has a suitable catch block.
Stack unwinds until we find the catch block

Method B has a suitable catch block.
Stack unwinds until we find the catch block

Method B has a suitable catch block.
Stack unwinds until we find the catch block
Exception is “caught”. Code in catch block is executed.

Method B has a suitable catch block.
- Programming tip:
- If your code does this, ask yourself if you could have achieved the same effect with an ‘if’ statement.

Finally blocks
- The code below is intended to read a set of text files and write out the first line of each to an output file.
- However if a problem if an exception is thrown while reading one of the input files, the output file will not be closed. We could fix this using a ‘finally’ block.
public static void firstLines(ArrayList<String> fileNames, String outFileName) throws FileNotFoundException { PrintStream outStream = new PrintStream(outFileName); for (String file : fileNames) { FileInputStream inStream = new FileInputStream(file); Scanner scan = new Scanner(inStream); String line = scan.nextLine(); outStream.println(line); } outStream.close();}public static void firstLines(ArrayList<String> fileNames, String outFileName) throws FileNotFoundException { PrintStream outStream = new PrintStream(outFileName); try { for (String file : fileNames) { FileInputStream inStream = new FileInputStream(file); Scanner scan = new Scanner(inStream); String line = scan.nextLine(); outStream.println(line); } } finally { outStream.close(); }}- If an exception occurs during the try block, the finally block is executed, then the call stack is unwound until a handler for the exception is found.
- Note that the role of the finally block is not to handle the exception.
- If the try block completes normally then the finally block is still executed.
Try with resources
- It is common for finally blocks to close resources that have been opened immediately before the try block. If the resource(s) in question implement the interface
java.lang.Autocloseablethen the try … finally structure can be replaced by a “try with resources” construct, as illustrated below.
public static void firstLines(ArrayList<String> fileNames, String outFileName) throws FileNotFoundException { try (PrintStream outStream = new PrintStream(outFileName)) { for (String file : fileNames) { FileInputStream inStream = new FileInputStream(file); Scanner scan = new Scanner(inStream); String line = scan.nextLine(); outStream.println(line); } }}Summary
- When a method is invoked, a stack frame is pushed onto the call stack. When the method returns the frame is popped.
- Each recursive call to a method pushes a separate frame onto the call stack. So you end up with multiple frames, representing multiple invocations of the method.
- When an exception is thrown, the call stack is unwound until a method with a suitable try block is found.
- If a try block is associated with a finally block, the code in this block is always executed. If an exception occurs, the finally block is executed and then the call stack is unwound until a catch block is found.
- A “try with resources” construct can be used as an alternative to a finally block in cases where resources need to be closed.
支持与分享
如果这篇文章对你有帮助,欢迎分享给更多人或赞助支持!



