9618 AS/A Level Computer Science - Python Programming
For this chapter, all examples will be shown in both CIE Pseudocode and Python to help you understand the concepts in both formats used in exams.
Recursion is a highly effective programming technique where a function calls itself to solve a problem or execute a task. Unlike iterative loops, recursion doesn't rely on loop constructs. Instead, it uses the idea of self-reference to break down complicated problems into more manageable subproblems.
Recursion is a process using a function or procedure that is defined in terms of itself and calls itself. The process is defined using a base case (a terminating solution that is not recursive) and a general case (a solution that is recursively defined).
A recursive algorithm must have these three features:
Remember: Every recursive function MUST have a base case! Without it, the function would call itself indefinitely, causing a stack overflow error.
Let's look at a classic example: calculating the factorial of a number. The factorial of n (written as n!) is the product of all positive integers from 1 to n.
Let's trace Factorial(3) step by step to understand the winding and unwinding process.
| Phase | Call | Action | Stack |
|---|---|---|---|
| WINDING | Factorial(3) | 3 ≠ 0,1 → calls Factorial(2) | [Factorial(3)] |
| Factorial(2) | 2 ≠ 0,1 → calls Factorial(1) | [Factorial(3), Factorial(2)] | |
| Factorial(1) | 1 = 1 → returns 1 (BASE) | [Factorial(3), Factorial(2), Factorial(1)] | |
| UNWINDING | Factorial(1) | returns 1 | [Factorial(3), Factorial(2)] |
| Factorial(2) | returns 2 × 1 = 2 | [Factorial(3)] | |
| Factorial(3) | returns 3 × 2 = 6 | [] |
| Call | n | Condition | Return Value |
|---|---|---|---|
| 1 | 3 | 3 ≠ 0,1 | 3 × Factorial(2) |
| 2 | 2 | 2 ≠ 0,1 | 2 × Factorial(1) |
| 3 | 1 | 1 = 1 ✓ | 1 (BASE CASE) |
| 2 | 2 | — | 2 × 1 = 2 |
| 1 | 3 | — | 3 × 2 = 6 |
Think of recursion like climbing a ladder:
A simple countdown program that prints numbers from n down to 0.
Notice the order: we print FIRST, then make the recursive call. This means each value is printed during the winding phase. If we printed after the recursive call, values would print during unwinding (in reverse: 0, 1, 2, 3, 4, 5).
Calculate the sum of all integers from 1 to n.
Sum(5) = 5 + Sum(4) = 5 + 4 + Sum(3) = 5 + 4 + 3 + Sum(2) = 5 + 4 + 3 + 2 + Sum(1) = 5 + 4 + 3 + 2 + 1 = 15
Calculate x raised to the power n (x^n) using recursion.
The Fibonacci sequence: 0, 1, 1, 2, 3, 5, 8, 13, 21... Each number is the sum of the two preceding ones.
This naive Fibonacci implementation has TWO recursive calls, making it very inefficient (exponential time complexity O(2^n)). For large n, this can cause severe performance issues. The iterative approach is much more efficient.
The same problem can often be solved using either recursion or iteration. Let's compare both approaches.
| Recursion | Iteration | |
|---|---|---|
| Benefits |
• More concise code • Elegant for complex problems • Natural for trees/fractals • Easier to read (sometimes) |
• More efficient (less memory) • Faster execution • Easier to debug • No stack overflow risk |
| Drawbacks |
• Higher memory usage • Slower (function call overhead) • Harder to debug • Stack overflow risk |
• Can require more code • Less elegant for some problems • More complex for trees/fractals |
In exams, always mention that iteration uses less memory because it doesn't build up a call stack. Recursion uses more memory because each call adds a frame to the stack.
When a recursive function is called, the compiler/interpreter uses a call stack to keep track of each function call. Each call creates a stack frame containing important information.
Each stack frame contains:
If no base case is present, or if the base case is never reached, the stack never stops growing. This leads to a stack overflow, causing the program to crash.
Without a base case, this function will call itself infinitely. Each call adds a frame to the stack until memory is exhausted → Stack Overflow Error!
To implement recursive procedures and functions, a compiler must produce object code that:
Recursively reverse a string by taking the last character and concatenating it with the reverse of the remaining string.
reverse_string("HELLO"):
| Concept | Description |
|---|---|
| Recursion | A function/procedure that calls itself |
| Base Case | Stopping condition; returns without further recursion |
| General Case | Recursive part that calls itself with modified parameters |
| Winding | Phase where calls build up on the stack |
| Unwinding | Phase where calls return values from base case |
| Call Stack | Structure storing active function calls |
| Stack Frame | Single call snapshot (params, locals, return address) |
| Stack Overflow | Error from infinite recursion exhausting memory |
| Example | Base Case | Recursive Case |
|---|---|---|
| Factorial(n) | n = 0 or 1 → return 1 | return n × factorial(n-1) |
| Countdown(n) | n = 0 → return | countdown(n-1) |
| Sum(n) | n = 1 → return 1 | return n + sum(n-1) |
| Power(x, n) | n = 0 → return 1 | return x × power(x, n-1) |
| Fibonacci(n) | n = 0 → 0, n = 1 → 1 | fib(n-1) + fib(n-2) |
| Reverse(s) | length ≤ 1 → return s | last_char + reverse(rest) |
Answer:
Recursion is a programming technique where a function or procedure calls itself to solve a problem by breaking it into smaller subproblems.
Base Case:
General Case:
Answer (Pseudocode):
Answer (Python):
Example: SumEven(6) = 6 + SumEven(4) = 6 + 4 + SumEven(2) = 6 + 4 + 2 = 12
Winding Phase (Stack grows):
Call Stack at Deepest Point:
Unwinding Phase:
Final Result: 24
Recursive Version (Given):
Iterative Version (Pseudocode):
Advantage: The iterative approach uses less memory because it doesn't build up a call stack. It's also faster due to no function call overhead and easier to debug since the flow is linear.
def bad_factorial(n): return n * bad_factorial(n-1) [6 marks]
Why it causes stack overflow:
Corrected Pseudocode:
Corrected Python:
Pseudocode:
Python:
| Aspect | Recursion | Iteration |
|---|---|---|
| Memory | Higher usage - each call adds a stack frame | Lower usage - no call stack buildup |
| Speed | Slower - function call overhead | Faster - direct loop execution |
| Debugging | Harder - multiple stack frames to trace | Easier - linear flow, single context |
| Code Size | Often more concise | Can require more lines |
| Use Cases | Trees, fractals, divide-and-conquer | Simple repetition, linear tasks |
Stack Frame Contents:
Why it's important for debugging:
Pseudocode:
Python:
Trace for Power(2, 4):
| Call | x | n | Action | Return |
|---|---|---|---|---|
| 1 | 2 | 4 | n ≠ 0 | 2 × Power(2,3) |
| 2 | 2 | 3 | n ≠ 0 | 2 × Power(2,2) |
| 3 | 2 | 2 | n ≠ 0 | 2 × Power(2,1) |
| 4 | 2 | 1 | n ≠ 0 | 2 × Power(2,0) |
| 5 | 2 | 0 | n = 0 ✓ | 1 (BASE) |
| 4 | 2 | 1 | Unwind | 2 × 1 = 2 |
| 3 | 2 | 2 | Unwind | 2 × 2 = 4 |
| 2 | 2 | 3 | Unwind | 2 × 4 = 8 |
| 1 | 2 | 4 | Unwind | 2 × 8 = 16 |
Winding:
Unwinding:
Example - factorial(3):
Answer:
Better approaches: Use iteration, or memoization to store previously calculated values.
Pseudocode:
Python:
Example Output for CountAndDone(5): 5, 4, 3, 2, 1, Done!
Recursion → A programming technique where a function or procedure calls itself to solve a problem by breaking it into smaller subproblems.
Base Case → A terminating condition in a recursive function that returns a value without making further recursive calls. Essential to prevent infinite recursion.
General Case → The recursive part of a function that calls itself with modified parameters, typically operating on a smaller instance of the problem.
Stopping Condition → A condition that must be reachable after a finite number of recursive calls; ensures the recursion terminates properly.
Winding → The phase of recursion where function calls are being added to the call stack. Statements after the recursive call are not executed during this phase.
Unwinding → The phase of recursion where function calls return values, starting from the base case and working back up through the call stack.
Call Stack → A data structure used by the compiler/interpreter to keep track of active function calls. Stores return addresses, parameters, and local variables.
Stack Frame → A single entry on the call stack containing information about one function call: function name, parameters, local variables, and return address.
Stack Overflow → An error that occurs when the call stack exceeds its allocated memory, typically caused by infinite or very deep recursion without a proper base case.
Iteration → A programming technique using loops (FOR, WHILE, REPEAT) to repeat a block of code. An alternative approach to recursion.
Divide and Conquer → A problem-solving strategy where a problem is broken into smaller subproblems, solved independently, and combined. Often implemented using recursion.
Self-Reference → The concept of a function, procedure, or definition referring to itself within its own definition.
Before writing any recursive function, ask: "What is the simplest case where I know the answer?" This is your base case. For factorial, it's n=0 or n=1. For countdown, it's n=0.
WINDING: Calls build up → No return values yet → Statements AFTER recursive call NOT executed
UNWINDING: Calls return values → Stack shrinks → Statements AFTER recursive call ARE executed
When asked to trace a recursive function:
| Function | Base Case | Recursive Case |
|---|---|---|
| Factorial(n) | n ≤ 1 → return 1 | n × factorial(n-1) |
| Countdown(n) | n = 0 → return | countdown(n-1) |
| Sum(n) | n = 1 → return 1 | n + sum(n-1) |
| Power(x, n) | n = 0 → return 1 | x × power(x, n-1) |
| Fibonacci(n) | n ≤ 1 → return n | fib(n-1) + fib(n-2) |
| Reverse(s) | len ≤ 1 → return s | last + reverse(rest) |
| IsPalindrome(s) | len ≤ 1 → TRUE | first=last AND middle |