Recursion
By Notes Vandar
5.1 Principle of recursion
Recursion is a programming technique where a function calls itself directly or indirectly to solve a problem. It is based on the principle of dividing a problem into smaller subproblems of the same type. The recursive approach consists of two main components:
- Base Case: This is the condition under which the recursion stops. It prevents infinite recursion by providing a simple case that can be solved without further recursion.
- Recursive Case: This is where the function calls itself with a smaller or simpler input, moving towards the base case. The recursive case breaks the problem into smaller subproblems.
How Recursion Works
- Function Call Stack: Each recursive call creates a new layer in the call stack. When the base case is reached, the function returns a value to the previous call, unwinding the stack.
- Stack Overflow: If there are too many recursive calls without reaching the base case, it can lead to a stack overflow error due to limited stack space.
Example of Recursion
A classic example of recursion is calculating the factorial of a number nn:
Factorial Definition
- Base Case: factorial(0)=1\text{factorial}(0) = 1
- Recursive Case: factorial(n)=n×factorial(n−1)\text{factorial}(n) = n \times \text{factorial}(n – 1)
C Implementation
Here’s a simple C program that demonstrates recursion through the factorial function:
#include <stdio.h>
// Function to calculate factorial recursively
int factorial(int n) {
// Base case
if (n == 0) {
return 1;
}
// Recursive case
return n * factorial(n – 1);
}
int main() {
int number;
printf(“Enter a positive integer: “);
scanf(“%d”, &number);
if (number < 0) {
printf(“Factorial is not defined for negative numbers.\n”);
} else {
printf(“Factorial of %d = %d\n”, number, factorial(number));
}
return 0;
}
5.2 Comparison between recursion and iteration
Recursion and iteration are both fundamental techniques used to solve problems and perform repeated tasks in programming. Each approach has its advantages and disadvantages. Here’s a detailed comparison:
Aspect | Recursion | Iteration |
---|---|---|
Definition | A function calls itself to solve smaller subproblems. | A loop repeats a block of code until a condition is met. |
Syntax | Typically involves function calls and base/recursive cases. | Uses looping constructs like for , while , and do-while . |
State Management | Each recursive call maintains its own state. | A single state is maintained in the loop. |
Memory Usage | Uses more memory due to the call stack. | Generally uses less memory as it operates in a single frame. |
Performance | May be slower due to overhead from multiple function calls. | Usually faster as there’s no overhead from function calls. |
Termination | Requires a base case to terminate recursion. | Terminates when the loop condition becomes false. |
Readability | Can be more readable and easier to understand for problems that fit the recursive model. | May become complex for problems that have a natural recursive structure. |
Debugging | Harder to debug due to multiple function calls. | Easier to debug since all logic is contained in a single loop. |
Use Cases | Suitable for problems involving trees, graphs, or problems naturally defined recursively (e.g., factorial, Fibonacci). | Preferred for straightforward repetitive tasks and when performance is critical. |
Example Comparison
Let’s consider the example of calculating the factorial of a number using both recursion and iteration.
Recursive Implementation
#include <stdio.h>
// Recursive function to calculate factorial
int factorial_recursive(int n) {
if (n == 0) {
return 1; // Base case
}
return n * factorial_recursive(n – 1); // Recursive case
}
Iterative Implementation
#include <stdio.h>
// Iterative function to calculate factorial
int factorial_iterative(int n) {
int result = 1;
for (int i = 1; i <= n; i++) {
result *= i; // Multiply result by i
}
return result;
}
5.3 Common Recursive Problems: Factorial, Fibonacci Sequence, GCD, and Tower of Hanoi
Here’s an overview of four classic recursive problems, along with their definitions, recursive algorithms, and explanations.
1. Factorial
Definition: The factorial of a non-negative integer nn is the product of all positive integers less than or equal to nn. It is denoted by n!n!.
Recursive Formula:
- Base Case: 0!=10! = 1
- Recursive Case: n!=n×(n−1)!n! = n \times (n-1)!
C Implementation:
#include <stdio.h>
// Recursive function to calculate factorial
int factorial(int n) {
if (n == 0) {
return 1; // Base case
}
return n * factorial(n – 1); // Recursive case
}
int main() {
int number;
printf(“Enter a non-negative integer: “);
scanf(“%d”, &number);
printf(“Factorial of %d = %d\n”, number, factorial(number));
return 0;
}
2. Fibonacci Sequence
Definition: The Fibonacci sequence is a series of numbers where each number is the sum of the two preceding ones, usually starting with 0 and 1. The sequence is: 0, 1, 1, 2, 3, 5, 8, 13, …
Recursive Formula:
- Base Cases: F(0)=0F(0) = 0, F(1)=1F(1) = 1
- Recursive Case: F(n)=F(n−1)+F(n−2)F(n) = F(n-1) + F(n-2)
C Implementation:
#include <stdio.h>
// Recursive function to calculate Fibonacci
int fibonacci(int n) {
if (n == 0) {
return 0; // Base case
} else if (n == 1) {
return 1; // Base case
}
return fibonacci(n – 1) + fibonacci(n – 2); // Recursive case
}
int main() {
int n;
printf(“Enter a non-negative integer: “);
scanf(“%d”, &n);
printf(“Fibonacci of %d = %d\n”, n, fibonacci(n));
return 0;
}
3. Greatest Common Divisor (GCD)
Definition: The GCD of two integers is the largest positive integer that divides both numbers without leaving a remainder.
Euclidean Algorithm:
- Base Case: If b=0b = 0, then GCD(a,b)=a\text{GCD}(a, b) = a
- Recursive Case: GCD(a,b)=GCD(b,amod b)\text{GCD}(a, b) = \text{GCD}(b, a \mod b)
C Implementation:
#include <stdio.h>
// Recursive function to calculate GCD
int gcd(int a, int b) {
if (b == 0) {
return a; // Base case
}
return gcd(b, a % b); // Recursive case
}
int main() {
int a, b;
printf(“Enter two non-negative integers: “);
scanf(“%d %d”, &a, &b);
printf(“GCD of %d and %d = %d\n”, a, b, gcd(a, b));
return 0;
}
4. Tower of Hanoi
Definition: The Tower of Hanoi is a classic puzzle that involves moving a stack of disks from one peg to another, using a third peg as an auxiliary. The rules are:
- Only one disk can be moved at a time.
- A disk can only be placed on top of a larger disk or an empty peg.
Recursive Algorithm:
- Move n−1n-1 disks from source peg to auxiliary peg.
- Move the nth disk from source peg to destination peg.
- Move n−1n-1 disks from auxiliary peg to destination peg.
C Implementation:
#include <stdio.h>
// Recursive function to solve Tower of Hanoi
void towerOfHanoi(int n, char source, char destination, char auxiliary) {
if (n == 1) {
printf(“Move disk 1 from %c to %c\n”, source, destination); // Base case
return;
}
towerOfHanoi(n – 1, source, auxiliary, destination); // Step 1
printf(“Move disk %d from %c to %c\n”, n, source, destination); // Step 2
towerOfHanoi(n – 1, auxiliary, destination, source); // Step 3
}
int main() {
int n;
printf(“Enter the number of disks: “);
scanf(“%d”, &n);
printf(“Solution for Tower of Hanoi with %d disks:\n”, n);
towerOfHanoi(n, ‘A’, ‘C’, ‘B’); // A, B, and C are the names of rods
return 0;
}
5.4 Applications and Efficiency of recursion
Recursion is a powerful programming technique that has a wide range of applications in computer science and software development. Below are some common applications of recursion, along with an analysis of its efficiency.
Applications of Recursion
- Mathematical Computations:
- Factorial Calculation: Used to calculate the factorial of a number.
- Fibonacci Sequence: Generates Fibonacci numbers recursively.
- Greatest Common Divisor (GCD): Efficiently finds GCD using the Euclidean algorithm.
- Sorting Algorithms:
- Quicksort: A recursive sorting algorithm that divides the array into smaller subarrays based on a pivot element.
- Mergesort: A divide-and-conquer algorithm that recursively sorts and merges subarrays.
- Tree and Graph Traversal:
- Depth-First Search (DFS): Used in trees and graphs to explore nodes recursively.
- Binary Tree Traversal: Inorder, Preorder, and Postorder traversals can be implemented using recursion.
- Backtracking Algorithms:
- N-Queens Problem: Places N queens on a chessboard such that no two queens threaten each other.
- Sudoku Solver: Solves Sudoku puzzles using recursive backtracking.
- Dynamic Programming:
- Many dynamic programming problems can be solved using a recursive approach with memoization to optimize overlapping subproblems, such as the Knapsack problem and matrix chain multiplication.
- Fractal Generation:
- Recursion is used in algorithms to generate fractals like the Mandelbrot set and Sierpinski triangle.
- Game Theory:
- Used in algorithms for solving games like Tic-Tac-Toe and Chess, where recursion helps evaluate possible moves.
Efficiency of Recursion
Time Complexity
- The time complexity of a recursive algorithm depends on the number of recursive calls and the amount of work done in each call.
- For example, the naive recursive approach for calculating Fibonacci numbers has exponential time complexity O(2n)O(2^n) due to overlapping subproblems, while a dynamic programming approach reduces it to O(n)O(n).
Space Complexity
- Recursive algorithms can consume more memory due to the call stack:
- Each recursive call adds a new frame to the call stack, consuming stack memory.
- In the worst case, the space complexity can be O(n)O(n), where nn is the depth of the recursion.
Stack Overflow
- Recursive algorithms are subject to stack overflow errors if the recursion depth exceeds the maximum stack size, especially for deep recursive calls (e.g., calculating Fibonacci numbers without memoization).
Optimization Techniques
- Tail Recursion:
- A recursive function is tail-recursive if the recursive call is the last operation in the function. Some languages optimize tail recursion to reduce stack usage, effectively transforming it into iteration.
- Memoization:
- Caches results of expensive function calls and returns the cached result when the same inputs occur again. This is particularly useful for overlapping subproblems (e.g., in Fibonacci calculations).
- Iterative Solutions:
- In cases where recursion can lead to inefficiency or stack overflow, converting a recursive solution to an iterative one can improve performance and reduce memory usage.