Recursion in Data Structures
Introduction to Recursion
Definition of Recursion: Recursion is a technique sometimes used to solve a problem by first solving a smaller version of the exact same problem. It involves solving a problem by solving a smaller problem of the same type.
Recursive Functions: When this technique is implemented in software, it results in functions that call themselves.
Example Recursive Function Structure:
Recursively Defined Problems: Factorials
Recursive Solution for (n factorial):
(n-1)! \times n \text{ if } n > 0
Closed-Form Solution for :
1 \times 2 \times 3 \times \dots \times (n-1) \times n \text{ if } n > 0
Coding and Tracing the Factorial Function
Recursive Implementation in C++:
Tracing :
Values are returned up the chain:
is returned.
is returned.
is returned.
is returned.
is returned.
Final Value:
Iterative Implementation in C++:
Combinations (n choose k)
Problem Definition: Given things, how many different sets of size can be chosen?
Recursive Solution:
for 1 < k < n
Base Cases:
Closed-Form Solution:
for 1 < k < n
Recursive Implementation of Combinations
C++ Code:
Example Trace: Combinations(4, 3):
Final Result:
Recursion vs. Iteration
Conceptual Differences:
Iteration uses a looping construct.
Recursion uses a branching structure.
Efficiency Concerns:
Recursive solutions are often less efficient than iterative solutions in terms of both time and space.
Recursion involves overhead from multiple function calls.
Simplification: Recursion can simplify the solution of a problem, frequently resulting in shorter and more easily understood source code.
How to Write a Recursive Function
Essential Steps:
Determine the size factor: Identify what makes the problem smaller.
Determine the base case(s): Identify the version for which you already know the answer.
Determine the general case(s): Identify where the problem is expressed as a smaller version of itself.
Verify the algorithm: Use the "Three-Question-Method."
The Three-Question Verification Method
The Base-Case Question: Is there a non-recursive way out of the function, and does the routine work correctly for this base case?
The Smaller-Caller Question: Does each recursive call to the function involve a smaller case of the original problem, leading inescapably to the base case?
The General-Case Question: Assuming that the recursive call(s) work correctly, does the whole function work correctly as a whole?
Recursive Binary Search
Iteration vs. Recursion:
The non-recursive implementation uses a
whileloop:while((first <= last) && !found).
Recursive Attributes:
Size Factor: The number of elements in the range
(info[first] ... info[last]).Base Case 1: If
first > last, returnfalse(item not found).Base Case 2: If
item == info[midPoint], returntrue(item found).General Case:
If
item < info[midPoint], search the first half.If
item > info[midPoint], search the second half.
Recursive Implementation in C++:
if(first > last) \, \text{// base case 1} if(item < info[midPoint])
Wrapper Function:
Implementation of Recursion Mechanics
Function Call Process:
Executing the current function is paused.
An activation record is stored on the run-time stack. This record contains variables, parameters, and the return address (the place to start executing upon return).
Actual parameters are bound to formal parameters.
Control is transferred to the called function.
Function Return Process:
After execution, the activation record is popped off the run-time stack.
Old values of parameters and variables are restored.
The return value replaces the function call in the assignment statement/expression.
Recursive Call Mechanics: There is no fundamental difference between recursive and non-recursive calls, except the calling and called functions share the same name.
Trace Example for :
f(3)callsf(2). Stack pushesf(3)details.f(2)callsf(1). Stack pushesf(2)details.f(1)callsf(0). Stack pushesf(1)details.f(0)returns .f(1)pops,y = 2 * 1 = 2, returnsy + 1 = 3.f(2)pops,y = 2 * 3 = 6, returnsy + 1 = 7.f(3)pops,y = 2 * 7 = 14, returnsy + 1 = 15.Value Returned: .
Recursive Item Insertion in a Sorted List
Comparison to Iterative Implementation: Previous iterative implementations required a
predLoc(predecessor location) pointer to insert a new node. Recursive implementation simplifies this.Recursive Attributes:
Size Factor: Number of elements in the current sub-list.
Base Case 1: List is empty.
Base Case 2:
item < location->info. The item should be inserted at the beginning of the current sub-list.General Case:
Insert(location->next, item).
Recursive Implementation in C++:
if((location == NULL) || (item < location->info)) \{ \, \text{// base cases} location->info = item; location->next = tempPtr; Insert(location->next, item); \, \text{// general case}
Note on Pointers: By using a reference to a pointer (
NodeType<ItemType>* &location), the function can modify thenextmember of the previous node directly, eliminating the need forpredLoctrackers.
Recursive Item Deletion in a Sorted List
Recursive Attributes:
Size Factor: Number of elements in the list.
Base Case:
item == location->info. Delete the node pointed to bylocation.General Case:
Delete(location->next, item).
Recursive Implementation in C++:
if(item == location->info) \{ location = location->next; Delete(location->next, item);
Efficiency and Decision Criteria
The Problem of Redundancy: Recursion can be very inefficient in some cases. For instance, in
Comb(6, 4), many identical sub-calls likeComb(3, 2)andComb(2, 1)are re-calculated multiple times across the recursion tree.When to Use Recursion:
The depth of recursive calls is relatively shallow.
The recursive version performs approximately the same amount of work as the non-recursive version.
The recursive version is significantly shorter and simpler than the non-recursive solution.