Recursion¶
Definition¶
Defining a problem or an entity in terms of itself. E.g. An ancestor is either parents or parents’ ancestor.
Recursive Algorithm¶
An algorithm that breaks down the problem to smaller pieces and reuses itself to solve sub-problems.
Many algorithms have both recursive and iterative versions. E.G. binary search
FYI: In functional programming, recursion will replace loop/iteration totally.
Key components¶
Base case - the case that the problem can be solved without reusing itself
Recursive rule - the rule to reduce the problem toward the base case
Function Call Mechanism¶
Each function call creates a new stack frame in the stack memory.
The stack frame contains:
Function arguments.
Local variables.
Return address.
The stack frame is popped out of the stack memory when the function returns.
Stack overflow: When the stack memory is full. Usually caused by infinite recursion.
Recursive function in C++¶
A function/method calls itself (directly or indirectly).
Forward declaration not mandatory except for mutual recursion (A calls B and B calls A)
Record keeping (parameters, return values, global/dynamic variable)
Helper functions (extra parameters for record keeping)
if statement
int factorial(int n) {
if (n <= 1) // base case
return 1;
return n * factorial(n - 1); // recursive rule that calls itself
}
Complexity analysis¶
number of recurve calls \(\times\) total resource in each iteration
recursion tree to visually assist the analysis
Recursive Data Structure¶
Recursive Data Structure in C++¶
A class holds instance or pointer/reference to instance of its own type
E.G. Linked-list
E.G. Tree, directory structure
Example: Recursive Linear Search¶
Linear search can be implemented recursively in multiple ways. Here are three common approaches:
Method 1: Using a Helper Function¶
This approach uses a helper function with an additional parameter to track the current position. The main function provides a clean interface while the helper function handles the recursion.
// Main function with clean interface
int searchFirst(int *array, int size, int toSearch) {
return searchFirstHelper(array, size, toSearch, 0);
}
// Helper function that does the actual recursive work
int searchFirstHelper(int *array, int size, int toSearch, int start) {
if (start >= size) // base case: hit the end
return -1;
if (array[start] == toSearch) // base case: found the element
return start;
return searchFirstHelper(array, size, toSearch, start + 1); // recursive rule
}
Method 2: Using Default Parameters¶
This approach eliminates the need for a helper function by using a default parameter. This provides a cleaner solution with a single function.
int searchFirst1(int *array, int size, int toSearch, int start = 0) {
if (start >= size) // base case: hit the end
return -1;
if (array[start] == toSearch) // base case: found the element
return start;
return searchFirst1(array, size, toSearch, start + 1); // recursive rule
}
Method 3: Comparing from the End¶
This approach compares the tail element in each recursive call and reduces the array size instead of using an index. This eliminates the need for an extra parameter.
int searchLast(int *array, int size, int toSearch) {
if (size == 0) // base case: empty array
return -1;
if (array[size - 1] == toSearch) // base case: found the element
return size - 1;
else
return searchLast(array, size - 1, toSearch); // recursive rule
}
Key Differences¶
Method 1: Most structured; separates interface from implementation.
Method 2: Most concise; single function with default parameter.
Method 3: Searches from the end; reduces size instead of incrementing index.
All three methods have the same time complexity of \(\Theta(n)\) and space complexity of \(\Theta(n)\) due to the recursion stack.
Pitfall¶
missing base case
recursive rule does not reduce toward the base case
skipped base case