The Power of Recursion: A Technique for Solving Complex Problems

The Power of Recursion: A Technique for Solving Complex Problems

10 mins read136 Views Comment
Updated on Oct 3, 2023 11:36 IST

This article will explore the fundamentals of recursion, including how it works, its most useful applications, and some typical examples of recursive algorithms.

2023_09_Copy-of-What-is-9.jpg

Recursion is a technique used to solve complicated problems in computer science and mathematics by breaking them down into simpler problems that can be solved recursively. The basic concept behind recursion is to call a function within itself, with each recursive call solving a smaller sub-problem until a base case is reached. The advantage of recursion lies in its ability to solve complex problems that might be difficult to solve using traditional methods. It also allows for more elegant and efficient solutions and can improve code readability and maintainability.

History of Recursion

Recursion is a fundamental concept in computer science and mathematics that refers to a function or procedure that calls itself directly or indirectly. It is a powerful tool for solving problems that involve repetitive or hierarchical structures, such as trees or graphs. The idea of recursion has been around for centuries and can be traced back to ancient Greek and Indian mathematics. The Greek mathematician Euclid used recursive definition to construct his famous algorithm for finding the greatest common divisor of two numbers. 

In India, the mathematician Pingala used recursion in his work on Sanskrit poetry, where he developed a system for describing the meter of poetic verse using recursive rules. In the 19th century, the German mathematician Georg Cantor used recursion to develop his theory of infinite sets, which became the foundation of modern set theory. 

The logician and philosopher Gottlob Frege also explored the concept of recursion who used recursive functions to define the natural numbers. In the mid-20th century, recursion became a key concept in computer science with the development of high-level programming languages. The first programming language to support recursion was Lisp, developed in the late 1950s. Recursion quickly became an important tool for solving problems in computer science, and it is now a standard feature in most programming languages. Today, recursion is used in various fields, from artificial intelligence and machine learning to bioinformatics and data science. It is a powerful and versatile tool that has revolutionized how we approach complex problems in science and technology.

Recommended online courses

Best-suited Programming courses for you

Learn Programming with these high-rated online courses

3.02 L
36 months
3.15 L
4 years
Free
6 weeks
7.1 K
6 weeks
Free
10 hours
Free
8 weeks
1.24 L
48 months
Free
15 weeks

How Recursion Works

Recursion is a technique that relies on the function calling itself repeatedly until a base case is reached. The base case is a special case that can be solved without recursion, and it is the case that stops the recursion.

The general structure of a recursive function is as follows:

def recursive_function(input):
    # Base case(s)
    if <input meets some condition>:
        return <some value>


    # Recursive case
    else:
        # Break down the problem into smaller sub-problems
        subproblem = <break down input into smaller sub-problems>
        # Solve each sub-problem recursively
        result = recursive_function(subproblem)
        # Combine the results of the sub-problems
        output = <combine the results>
        return output

funcalls

Source : DEV Community

This method involves checking if the current problem is the base case in a function. If it is, the solution for the base case is returned. If it is not, the parameters are modified and the function calls itself recursively with the updated parameters. The function then combines the solution for the current problem with the result of the recursive call to solve the original problem. As an example, let’s consider computing the factorial of a non-negative integer n. The factorial of n is the product of all positive integers less than or equal to n. For instance, the factorial of 5 is 5 * 4 * 3 * 2 * 1 = 120. To solve this problem recursively, we would need to create a function that checks if the current problem is the base case. If not, it modifies the parameters and calls itself recursively until it reaches the base case. Finally, the function combines the solution for the current problem with the result of the recursive call to solve the original problem.

 
def factorial(n):
if n==0:
return 1
else:
return n*factorial(n-1)
print(f"Factorial of 5 is {factorial(5)}")
# output
# Factorial of 5 is 120
Copy code

This recursive function calculates the factorial of a non-negative integer. The base case occurs when n is equal to 0, and in this case, the function returns 1. Otherwise, it returns n multiplied by the factorial of (n – 1). This recursive call is repeated until the base case is reached. 

Master the Concept of Recursion Java
Master the Concept of Recursion Java
This blog will make you understand the concept of Recursion in Java with the help of several problems.
Types of Recursion in C 
Types of Recursion in C 
Recursion is a process in which a particular procedure is repeated in a similar manner until a specific condition is met. Recursion can be a powerful tool for solving certain...read more
Introduction to Recursion Functions in Python
Introduction to Recursion Functions in Python
When programming, in many places you would have implemented a function that invokes or calls another function. However, when a function calls itself we call that recursion.
Recursion in C++
Recursion in C++
Recursion in C++ is a method that calls itself repeatedly until a predetermined condition is satisfied. A recursive function has a base case and a recursive condition, and it repeatedly...read more
Solving Tower of Hanoi Problem
Solving Tower of Hanoi Problem
The Tower of Hanoi, a classic mathematical puzzle, challenges the mind with its simple rules yet complex solutions. Originating from an ancient legend, this brainteaser involves moving a stack of...read more
Solving Tower of Hanoi Program in C
Solving Tower of Hanoi Program in C
Tower of Hanoi Puzzle is a brainteaser that involves moving a stack of discs between three pegs, adhering to specific constraints. Tower of Hanoi Program in C is used to...read more

When to use Recursion

Recursion can be an elegant and efficient solution to complex problems, but it is not always the best approach. It’s crucial to consider several factors when deciding whether to use recursion. One of them is whether the problem can be divided into smaller sub-problems that can be solved recursively. It’s also important to define a well-defined base case, as a missing base case can cause infinite recursion and a stack overflow error. Additionally, it’s necessary to determine whether recursion is efficient for the given problem, and if the problem can be solved using a divide-and-conquer approach, recursion may be a viable solution.

Common Examples of Recursive Algorithms

Recursive algorithms are widely used in computer science and mathematics to solve complex problems. Here are some examples of well-known recursive algorithms: Fibonacci Sequence: The Fibonacci sequence is a sequence of numbers in which each number is the sum of the two preceding numbers. The sequence starts with 0 and 1, and the first few numbers are 0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, and so on. 

A recursive solution to compute the nth number in the Fibonacci sequence is:

 
def fibonacci(n):
if n == 0:
return 0
elif n == 1:
return 1
else:
return fibonacci(n - 1) + fibonacci(n - 2)
print(f"8th term in Fibonacci Sequence is {fibonacci(8)}")
# output -> 8th term in Fibonacci Sequence is 21
Copy code

This function computes the nth Fibonacci number by recursively computing the (n-1)th and (n-2)th Fibonacci numbers until the base case of n=0 or n=1 is reached.

Binary Search

A Binary search algorithm is often used to search for a specific element in a sorted array. The algorithm works by repeatedly dividing the array in half until the element is found or it is determined that the element is not in the array. 

Here’s an example of a recursive implementation of binary search in Python:

 
def binary_search(arr, start, end, x):
if end >= start:
mid = start + (end - start) // 2
if arr[mid] == x:
return mid
elif arr[mid] > x:
return binary_search(arr, start, mid - 1, x)
else:
return binary_search(arr, mid + 1, end, x)
else:
return -1
arr = [1,2,3,4,5]
print(f"Element found at index {binary_search(arr,0,len(arr)-1,5)}")
#output = Element found at index 4
Copy code

This function searches the sorted array arr for the element x by recursively dividing the array in half and searching the appropriate sub-array until the element is found or it is determined that the element is not in Factorial of a Number:

 
def fact(n):
if n==0 or n==1:
return 1
else:
return n*fact(n-1)
n = int(input("Enter a number to find the factorial of it"))
print(fact(n))
Copy code

rec

Source : Stack Overflow

Tower of Hanoi

The Tower of Hanoi puzzle involves moving a set of disks of different sizes from one peg to another, following certain rules. A recursive solution can be used to solve this puzzle by breaking it down into smaller sub-problems. In each recursive call, the function moves a smaller sub-stack of disks to the target peg using the spare peg. This process continues until the base case is reached, which is when there is only one disk to move. Then, the function moves that disk to the target peg. The solution for the original problem is obtained by combining the solutions to the sub-problems.

 
def tower_of_hanoi(n, source, target, auxiliary):
if n == 1:
print(f"Move disk 1 from {source} to {target}")
return
tower_of_hanoi(n-1, source, auxiliary, target)
print(f"Move disk {n} from {source} to {target}")
tower_of_hanoi(n-1, auxiliary, target, source)
tower_of_hanoi(3, "A", "C", "B")
#output ->
# Move disk 1 from A to C
# Move disk 2 from A to B
# Move disk 1 from C to B
# Move disk 3 from A to C
# Move disk 1 from B to A
# Move disk 2 from B to C
# Move disk 1 from A to C
Copy code

Towers-Of-Hanoi

Source : Geeky Codes

This function recursively solves the Tower of Hanoi problem by moving n-1 disks from the source peg to the auxiliary peg, then moving the nth disk from the source peg to the destination peg, and finally moving the n-1 disks from the auxiliary peg to the destination peg.

Here are some examples of recursive algorithms for common data structures:

Binary Search Tree Traversal

in-order-pre-order-post-order-traversal-binary-tree

Source: Teachics

Binary search trees (STs) are data structures that are commonly used for efficient storage and searching of data. One common operation on BSTs is traversal, which involves visiting all nodes in the tree in a specific order. Recursive algorithms can be used to traverse BSTs in the following orders: Inorder traversal: visits the left subtree, then the current node, then the right subtree. Preorder traversal: visits the current node, then the left subtree, then the right subtree. Postorder traversal: visits the left subtree, then the right subtree, then the current node. Here are some examples of recursive algorithms for each of these traversal orders:

 
class Node:
def __init__(self, val):
self.val = val
self.left = None
self.right = None
def inorder(root):
if root is not None:
inorder(root.left)
print(root.val, end=" ")
inorder(root.right)
def postorder(root):
if root is not None:
postorder(root.left)
postorder(root.right)
print(root.val, end=" ")
def preorder(root):
if root is not None:
print(root.val, end=" ")
preorder(root.left)
preorder(root.right)
# Example usage:
root = Node(1)
root.left = Node(2)
root.right = Node(3)
root.left.left = Node(4)
root.left.right = Node(5)
print("Inorder traversal:")
inorder(root)
print("\nPostorder traversal:")
postorder(root)
print("\nPreorder traversal:")
preorder(root)
#output
# Inorder traversal:
# 4 2 5 1 3
# Postorder traversal:
# 4 5 2 3 1
# Preorder traversal:
# 1 2 4 5 3
Copy code

Linked List Reversal

Linked lists are a type of data structure consisting of nodes connected to each other by pointers. One common operation on linked lists is reversal, which involves reversing the order of the nodes in the list. A recursive algorithm for linked list reversal is:

 
class Node:
def __init__(self, data):
self.data = data
self.next = None
class LinkedList:
def __init__(self):
self.head = None
def append(self, data):
new_node = Node(data)
if self.head is None:
self.head = new_node
return
current_node = self.head
while current_node.next is not None:
current_node = current_node.next
current_node.next = new_node
def print_list(self):
current_node = self.head
while current_node is not None:
print(current_node.data, end=" ")
current_node = current_node.next
def reverse(self):
prev_node = None
current_node = self.head
while current_node is not None:
next_node = current_node.next
current_node.next = prev_node
prev_node = current_node
current_node = next_node
self.head = prev_node
# Example usage:
linked_list = LinkedList()
linked_list.append(1)
linked_list.append(2)
linked_list.append(3)
linked_list.append(4)
linked_list.append(5)
print("Original list:")
linked_list.print_list()
linked_list.reverse()
print("\nReversed list:")
linked_list.print_list()
#output
# Original list:
# 1 2 3 4 5
# Reversed list:
# 5 4 3 2 1
Copy code

This function uses recursion to reverse a linked list by recursively calling itself with the next node as the argument until the end of the list is reached, and then reversing the pointers between the nodes.

Graph Traversal

DFS: A recursive implementation of DFS involves starting at a given node and recursively visiting all adjacent nodes that have not been visited yet. The algorithm can be implemented using a boolean array to keep track of visited nodes.

 BFS: A recursive implementation of BFS is less common than an iterative implementation, but it can be done using a queue data structure. The recursive algorithm involves visiting all nodes at a given depth before recursively calling the algorithm on the next depth. The algorithm can also use a boolean array to keep track of visited nodes.

BFS-DFS

Source: Medium

 
class Graph:
def __init__(self, n):
self.adj_list = [[] for _ in range(n)]
self.visited = [False] * n
def add_edge(self, u, v):
self.adj_list[u].append(v)
self.adj_list[v].append(u)
def dfs(self, start):
self.visited[start] = True
print(start)
for neighbor in self.adj_list[start]:
if not self.visited[neighbor]:
self.dfs(neighbor)
def bfs(self, start):
queue = [start]
self.visited[start] = True
while queue:
node = queue.pop(0)
print(node)
for neighbor in self.adj_list[node]:
if not self.visited[neighbor]:
queue.append(neighbor)
self.visited[neighbor] = True
def main():
# Create a graph with 6 vertices and 7 edges
graph = Graph(6)
graph.add_edge(0, 1)
graph.add_edge(0, 2)
graph.add_edge(1, 3)
graph.add_edge(2, 3)
graph.add_edge(2, 4)
graph.add_edge(3, 4)
graph.add_edge(3, 5)
# Perform DFS starting from vertex 0
print("DFS traversal:")
graph.dfs(0)
# Reset visited array for BFS
graph.visited = [False] * 6
# Perform BFS starting from vertex 0
print("BFS traversal:")
graph.bfs(0)
if __name__ == "__main__":
main()
#output
# DFS traversal:
# 0
# 1
# 3
# 2
# 4
# 5
# BFS traversal:
# 0
# 1
# 2
# 3
# 4
# 5
Copy code

Recursion vs Iteration:

Let’s understand Recursion vs Iteration

Working with tree structures allows recursive algorithms to mimic the data’s structure directly, making them more succinct and understandable in many situations. A binary tree’s nodes can be added up using the code below, for instance:

 
int sum(Node* root) {
if (root == NULL) {
return 0;
} else {
return root->val + sum(root->left) + sum(root->right);
}
}
Copy code

This recursive approach is easy to read and understand and mirrors the tree’s structure. On the other hand, an iterative solution would require the use of a stack or queue to keep track of nodes, making it more complex. Another example is the famous Fibonacci sequence. The iterative approach requires a loop to calculate the sequence, whereas the recursive approach is more concise and easier to understand:

 
int fibonacci(int n) {
if (n <= 1) {
return n;
} else {
return fibonacci(n-1) + fibonacci(n-2);
}
}
Copy code

Recursion can generally be more efficient than iteration when dealing with recursive data structures or when the problem can be naturally expressed recursively. However, it is important to remember that recursive algorithms can be less efficient than iterative ones in some cases, particularly when dealing with large input sizes or deeply nested recursive calls.

Common pitfalls of Recursion:

Infinite Recursion:

 One of the most frequent hazards of recursion is infinite recursion, in which a function repeatedly invokes itself without ever reaching the base case. This can cause a stack overflow and cause your software to fail. Always identify a base case and make sure that each recursive call moves you closer to that base case in order to prevent infinite recursion. 

Overlapping subproblems: Recursion can also fall victim to overlapping subproblems, when the same subproblem is handled more than once and more calculations are required. This can be particularly troublesome for methods for dynamic programming. You can use memoization or tabulation to store the outcomes of earlier computations in order to prevent overlapping subproblems.

Performance Issues: Recursion may be slower than iterative methods due to the overhead of making function calls and manipulating the stack. This can be particularly challenging for complex recursive calls or huge inputs. You can try to improve the base case and make sure that each recursive call shrinks the size of the problem in order to improve performance.

Some questions for you to solve!

  1. Write a recursive function to calculate the factorial of a given number.
  2. Write a recursive function to calculate the nth number in the Fibonacci sequence.
  3. Write a recursive function to count the number of occurrences of a given character in a string.
  4. Write a recursive function to check if a given string is a palindrome. 
  5. Write a recursive function to find the greatest common divisor of two numbers.
  6. Write a recursive function to reverse a given linked list. 
  7. Write a recursive function to find the sum of all elements in a binary tree. 
  8. Write a recursive function to print all possible permutations of a given string. 
  9. Write a recursive function to sort a given array using the quicksort algorithm. 
  10. Write a recursive function to find the maximum element in a given binary tree.

Author – Papireddy Eppala

About the Author

This is a collection of insightful articles from domain experts in the fields of Cloud Computing, DevOps, AWS, Data Science, Machine Learning, AI, and Natural Language Processing. The range of topics caters to upski... Read Full Bio