# Lecture 16: Recursion and the Call Stack ## Prove each call makes progress to a base case, and count the frames it keeps **Instructor:** [Dr. Debasish Pattanayak](https://drdebmath.github.io)
## Where Lecture 15 ended · Function calls form a stack - Lecture 15 ended with a hint: function calls form a **call stack**, the last function called returns first. Today uses it to explain recursion. - Lecture 8 introduced recursion with a **base case** and a smaller call (the factorial). We trusted that such a function stops. - **Today's question:** how do we know a recursive function stops, and what do its frames cost? Plan: a worked example, the general form, a termination argument, backtracking, memoization, and the cost of depth.
## Dijkstra and ALGOL 60 · Procedures that call themselves
- The ALGOL 60 language allowed a procedure to call itself. - In 1960 Edsger W. Dijkstra (1930–2002), at the Mathematical Centre in Amsterdam, published "Recursive Programming" in *Numerische Mathematik*. - His aim: to call a subroutine "while one or more previous activations of the same subroutine have not yet come to an end". - In August 1960 he and Jaap Zonneveld completed the first ALGOL 60 compiler, for the Electrologica X1 computer.
Edsger W. Dijkstra, 2002. Photo: Hamilton Richards, Wikimedia Commons,
CC BY-SA 3.0
.
## One stack, a new use · Calls wait their turn - Dijkstra's paper: "The basic concept of the method is the so-called *stack*." A "stack pointer" marks the first free place; the unit removed is always the most recently added one. - That is the last-in, first-out order of Bauer's stack in Lecture 15, now used for calls. Each active call keeps its parameters, local variables, and return place in a **stack frame** (activation record). - Today's `printBackward` shows it: the node printed last is the one whose frame was pushed first. **Lesson:** a recursive call is not magic. Each call gets its own frame, and the stack remembers where to resume.
## Print a list backward · The call stack remembers the nodes ~~~cpp void printForward(const Node* p) { if (p == nullptr) return; // base case: empty list std::cout << p->value << ' '; // print before the call printForward(p->next); } void printBackward(const Node* p) { if (p == nullptr) return; // base case: empty list printBackward(p->next); // recursive case: the rest std::cout << p->value << ' '; // print after the call } // main: head = 1 -> 2 -> 3; printForward(head); printBackward(head); then delete the nodes ~~~ ~~~text 1 2 3 3 2 1 ~~~ The only difference is whether the print comes before or after the call. While `printBackward` waits for the rest of the list, its frame holds the node, so the frames on the stack hold the nodes in reverse.
## printBackward step by step · Frames push, then pop
1 calls
·
2 base case
·
3 returns
1
next
2
next
3
next
nullptr
call stack
printBackward(p → 1)
printed:
printBackward(head);
First call: p points at node 1, which is not nullptr
1 calls
·
2 base case
·
3 returns
1
next
2
next
3
next
nullptr
call stack
printBackward(p → 1)
printBackward(p → 2)
printed:
printBackward(p->next);
Node 1 calls itself with p->next; frame 1 waits
1 calls
·
2 base case
·
3 returns
1
next
2
next
3
next
nullptr
call stack
printBackward(p → 1)
printBackward(p → 2)
printBackward(p → 3)
printed:
printBackward(p->next);
Node 2 calls itself with p->next; frames 1 and 2 wait
1 calls
·
2 base case
·
3 returns
1
next
2
next
3
next
nullptr
call stack
printBackward(p → 1)
printBackward(p → 2)
printBackward(p → 3)
printBackward(p = nullptr)
printed:
if (p == nullptr) return;
Base case: p is nullptr, so this call returns at once
1 calls
·
2 base case
·
3 returns
1
next
2
next
3
next
nullptr
call stack
printBackward(p → 1)
printBackward(p → 2)
printBackward(p → 3)
(p = nullptr) returned
printed: 3
std::cout << p->value << ' ';
The nullptr frame is popped; back in p → 3, print 3
1 calls
·
2 base case
·
3 returns
1
next
2
next
3
next
nullptr
call stack
printBackward(p → 1)
printBackward(p → 2)
(p → 3) returned
printed: 3 2
std::cout << p->value << ' ';
The p → 3 call returns; back in p → 2, print 2
1 calls
·
2 base case
·
3 returns
1
next
2
next
3
next
nullptr
call stack
printBackward(p → 1)
(p → 2) returned
printed: 3 2 1
std::cout << p->value << ' ';
The p → 2 call returns; back in p → 1, print 1
1 calls
·
2 base case
·
3 returns
1
next
2
next
3
next
nullptr
call stack
stack empty
printed: 3 2 1
return;
The first call returns: the stack is empty and the list printed backward
- Each call to `printBackward` pushes a frame; the call with `nullptr` is the base case and returns first. - Each frame then pops in reverse order, and prints the node it kept.
## Recursive function: the general form ~~~cpp ReturnType solve(Problem p) { if (p is a smallest case) return direct answer; // base case ... solve(smaller p) // recursive case: smaller input ... // combine the results } ~~~ | Part | Meaning | | --- | --- | | Base case | A smallest input answered directly, with no recursive call. | | Recursive case | Calls the function on a smaller input of the same kind. | | Progress measure | A non-negative quantity that strictly decreases on every call: `n`, the list length, `hi - lo`. | | Combine step | Uses the result of the smaller call to build the answer: add, attach, print after. | | Pre-condition | What the caller must guarantee, for example `n >= 0`. Check it with `assert` (Lecture 12). | A recursive function stops when every call shrinks the measure and the measure cannot go below the base case.
## Prove termination · A measure that shrinks to the base case ~~~cpp void printBinary(int n) { assert(n >= 0); // pre-condition (Lecture 12) if (n >= 2) printBinary(n / 2); // measure n halves: reaches 0 or 1 std::cout << n % 2; } // main: printBinary(6); printBinary(13); ~~~ ~~~text 110 1101 ~~~ The measure is `n`: each call passes `n / 2`, which is smaller and still non-negative, so the calls reach 0 or 1, the base case. Printing after the call writes the highest bit first. An error to avoid: ~~~cpp int countDown(int n) { if (n == 0) return 0; // base case return countDown(n - 2); // progress skips the base case when n is odd } ~~~ `countDown(5)` calls with 5, 3, 1, -1, -3, and never reaches 0. The calls never stop: in practice the frames fill the stack and the program crashes with a stack overflow.
## Reverse a list recursively · Reverse the rest, then attach the head ~~~cpp Node* reverse(Node* head) { if (head == nullptr || head->next == nullptr) return head; // 0 or 1 node Node* newHead = reverse(head->next); // reverse the rest head->next->next = head; // the old second node now points back head->next = nullptr; // head becomes the last node return newHead; } // main: list 1 2 3 4; print; head = reverse(head); print ~~~ ~~~text 1 2 3 4 4 3 2 1 ~~~
1 calls
·
2 base case
·
3 rewire
1
next
2
next
3
next
4
next
nullptr
head
call stack
reverse(1)
Node* newHead = reverse(head->next);
main calls reverse(1): head points at 1, which has a next node, so recurse
1 calls
·
2 base case
·
3 rewire
1
next
2
next
3
next
4
next
nullptr
head
call stack
reverse(1)
reverse(2)
Node* newHead = reverse(head->next);
reverse(2) is called: head points at 2, and reverse(1) waits on the stack
1 calls
·
2 base case
·
3 rewire
1
next
2
next
3
next
4
next
nullptr
head
call stack
reverse(1)
reverse(2)
reverse(3)
Node* newHead = reverse(head->next);
reverse(3) is called: head points at 3, and three calls now wait
1 calls
·
2 base case
·
3 rewire
1
next
2
next
3
next
4
next
nullptr
head
newHead
call stack
reverse(1)
reverse(2)
reverse(3)
reverse(4)
if (head == nullptr || head->next == nullptr) return head;
Base case: 4->next is nullptr, so reverse(4) returns node 4 and newHead points at 4
1 calls
·
2 base case
·
3 rewire
1
next
2
next
3
next
4
next
head
newHead
call stack
reverse(1)
reverse(2)
reverse(3)
head->next->next = head;
reverse(3): 4's next now points back at 3, while 3 still points at 4 (a 2-cycle)
1 calls
·
2 base case
·
3 rewire
1
next
2
next
3
next
4
next
nullptr
head
newHead
call stack
reverse(1)
reverse(2)
reverse(3)
head->next = nullptr;
3's next becomes nullptr, which breaks the 2-cycle; reverse(3) returns newHead (node 4)
1 calls
·
2 base case
·
3 rewire
1
next
2
next
3
next
4
next
head
newHead
call stack
reverse(1)
reverse(2)
head->next->next = head;
reverse(2): head points at 2, so 3's next now points back at 2
1 calls
·
2 base case
·
3 rewire
1
next
2
next
3
next
4
next
nullptr
head
newHead
call stack
reverse(1)
reverse(2)
head->next = nullptr;
2's next becomes nullptr; reverse(2) returns newHead (node 4)
1 calls
·
2 base case
·
3 rewire
1
next
2
next
3
next
4
next
head
newHead
call stack
reverse(1)
head->next->next = head;
reverse(1): head points at 1, so 2's next now points back at 1
1 calls
·
2 base case
·
3 rewire
1
next
2
next
3
next
4
next
nullptr
head
newHead
call stack
reverse(1)
head->next = nullptr;
1's next becomes nullptr; reverse(1) returns newHead (node 4)
1 calls
·
2 base case
·
3 rewire
1
next
2
next
3
next
4
next
nullptr
head
call stack
(empty)
head = reverse(head);
Back in main: head points at 4 and the list reads 4 -> 3 -> 2 -> 1 -> nullptr
- The measure is the list length: each call handles a list one node shorter. - Write `head = reverse(head);`: the old head is now the last node, and the function returns the new head. - O(n) time, and O(n) stack frames because each node waits for the rest.
## Recursion follows nested structure · Folder sizes ~~~cpp struct Folder { std::string name; int kb; // size of the files directly inside std::vector
sub; // subfolders }; int total(const Folder& f) { int sum = f.kb; for (const Folder& s : f.sub) sum += total(s); // no subfolders: no call return sum; } void show(const Folder& f, int depth) { std::cout << std::string(2 * depth, ' ') << f.name << "/ " << total(f) << " KB\n"; for (const Folder& s : f.sub) show(s, depth + 1); } ~~~ ~~~text cs103/ 169 KB labs/ 62 KB lab1/ 12 KB lab2/ 30 KB notes/ 103 KB slides/ 95 KB ~~~ **Takeaways** - The base case is implicit: a folder with no subfolders makes no recursive call. - The measure is the depth of the tree below the folder; each call goes one level down. - A structure that contains smaller structures of its own kind is a tree; trees come next lecture.
## Backtracking · Choose, explore, undo ~~~cpp void permute(const std::string& letters, std::string& current, std::vector
& used) { if (current.size() == letters.size()) { // base case: every letter placed std::cout << current << '\n'; return; } for (std::size_t i = 0; i < letters.size(); ++i) { if (used[i]) continue; used[i] = true; // choose current.push_back(letters[i]); permute(letters, current, used); // explore current.pop_back(); // undo used[i] = false; } } // main: letters = "abc"; current empty; used all false; permute(letters, current, used); ~~~ ~~~text abc acb bac bca cab cba ~~~ **Backtracking** tries a choice, explores what follows, then undoes the choice to try the next one. The measure is the number of unused letters. There are n! permutations, so the output size grows as n!.
## Memoization · Solve each subproblem once ~~~cpp long long calls = 0; // counts every call long long paths(int r, int c) { // routes using only right and down moves ++calls; if (r == 0 || c == 0) return 1; // base case: one straight route return paths(r - 1, c) + paths(r, c - 1); // arrive from above or from the left } long long pathsMemo(int r, int c, std::vector
>& memo) { ++calls; if (r == 0 || c == 0) return 1; if (memo[r][c] != -1) return memo[r][c]; // already solved return memo[r][c] = pathsMemo(r - 1, c, memo) + pathsMemo(r, c - 1, memo); } // main: n = 12; memo is (n+1) x (n+1), filled with -1; print each result and calls ~~~ ~~~text naive: 2704156 routes, 5408311 calls memo: 2704156 routes, 289 calls ~~~ The naive version solves the same `(r, c)` over and over. **Memoization** stores each answer the first time, so there are at most (r+1)·(c+1) distinct subproblems. The same idea makes the longest common subsequence (LCS) of two strings fast; it is a lab problem.
## Recursion or iteration · Each pending call keeps a frame ~~~cpp int lengthLoop(const Node* p) { int n = 0; for (; p != nullptr; p = p->next) ++n; return n; } int lengthRec(const Node* p) { if (p == nullptr) return 0; return 1 + lengthRec(p->next); // each call waits for the next } // main: build a list of 10,000,000 nodes; print lengthLoop, then lengthRec ~~~ ~~~text loop: 10000000 ~~~ The recursive call then ends the program with a segmentation fault (exit status 139, `g++ -std=c++17 -Wall`, no optimization): 10,000,000 pending frames exhaust the stack, a **stack overflow**. The limit is implementation-specific; here the default stack was 8176 KB. Lecture 14 noted the same risk for a long `std::unique_ptr` chain, which destroys recursively. | Task | Time | Extra space | |---|---|---| | Recursive list reversal | O(n) | O(n) stack frames | | Iterative list reversal | O(n) | O(1) | | All permutations of n letters | at least n! outputs | O(n) depth | | Naive `paths(n, n)` | exponential | O(n) depth | | Memoized `paths(n, n)` | O(n²) | O(n²) table |
## Poll 1: What does f(3) print?
~~~cpp void f(int n) { if (n == 0) return; std::cout << n; f(n - 1); std::cout << n; } ~~~ - **A)** 321123 - **B)** 123321 - **C)** 321321 - **D)** 333
📊 sli.do · #2288951
## Common errors · Recursion that never stops or loses its result | Error | Example | Result | |---|---|---| | No base case | `int f(int n) { return n * f(n - 1); }` | calls never stop: stack overflow | | Progress skips the base case | base case `n == 0`, call `f(n - 2)` with odd `n` | calls never stop: stack overflow | | Call on the same argument | `return 1 + f(n);` | no progress: stack overflow | | Result thrown away | `reverse(head);` without `head = ` | `head` is now the last node: the list appears to have one node | | Forgetting to undo | no `pop_back()` or `used[i] = false` | wrong or missing results | | Very deep recursion | `lengthRec` on 10,000,000 nodes | stack overflow | Without a base case or a shrinking measure the calls never stop, and running out of stack is undefined behavior. A pre-condition such as `assert(n >= 0)` catches some inputs early.
## Poll 2: Which call never reaches its base case?
~~~cpp int halvings(int n) { if (n == 1) return 0; return 1 + halvings(n / 2); } ~~~ - **A)** `halvings(8)` - **B)** `halvings(5)` - **C)** `halvings(1)` - **D)** `halvings(0)`
📊 sli.do · #2288951
## Questions about recursion | Question | Answer | |---|---| | Is recursion slower than a loop? | Usually a little: each call pushes and pops a frame. The larger cost is the memory for the frames. Choose by clarity first, and measure if it matters. | | How deep can recursion go? | Implementation-specific. The stack is often a few megabytes, enough for thousands or tens of thousands of frames, not millions. | | When should I choose recursion? | When the data or the problem is nested (folders, trees) or the choices branch (backtracking). A loop suits a flat sequence. | | Can every recursion become a loop? | Yes, with an explicit stack such as `std::stack` (Lecture 15) that does the work of the call stack. | | What is tail recursion? | A function whose recursive call is the last thing it does. A compiler may reuse the frame, but C++ does not require it, so do not rely on it. | | Does memoization change the answer? | No. It stores answers already computed, so the result is the same and only the number of calls drops. |
## Summary: recursion and the call stack - A recursive function has a base case, a recursive case on a smaller input, and a measure that strictly decreases to the base case. - Every call pushes a stack frame (activation record); the frames pop in reverse order, so work placed after the call runs in reverse. - Backtracking is choose, explore, undo; memoization stores each subproblem's answer so it is solved once. - Deep recursion costs one frame per pending call: O(n) stack space, and a stack overflow if too deep. - Any recursion can be turned into a loop with an explicit stack. **Reading:** Savitch, *Problem Solving with C++*, chapter “Recursion”; Stroustrup, *Programming: Principles and Practice Using C++* (2nd ed.), section “Function call implementation” in chapter “Technicalities: Functions, etc.”.
## Lab practice: recursion **IC151 lab practice · 5 problems** 1. **Count.** Write `int countGreater(const Node* p, int x)` recursively: the number of nodes with a value greater than `x`. Name the base case and the measure. 1. **Binary strings.** Print all binary strings of length `n` with no two adjacent 1s, by backtracking. For `n = 3` there are five. 1. **LCS.** Compute the length of the longest common subsequence of two strings with memoization. Test on `"ABCBDAB"` and `"BDCABA"`: the answer is 4. 1. **Loop version.** Rewrite `printBackward` as a loop with a `std::stack` (Lecture 15). What does the stack hold at each step? 1. **Depth.** Call a recursive list length on lists of growing size until it fails, then compile with `-fsanitize=address` and read the report. Roughly how many frames fit? Reaching this set marks the lecture as studied in this browser. [Open the IC151 lab setup and batch schedule](ic151.html).