Skip to content

Recursive Wonders - Navigating the Depths of Programming Magic

Published: at 04:23 PM

In the world of computational mathematics, there exists a fascinating technique that dances on the edge of complexity and elegance — recursion. In this comprehensive guide, we’ll unravel the mysteries of recursion, exploring its fundamentals, advantages, challenges, types, real-world applications, and best practices.

Table of contents

Open Table of contents

The Basics of Recursion

Recursion is a problem-solving approach that can be used to generate simple solutions to certain kinds of problems that are difficult to solve by other means. Recursion reduces a problem into one or more simpler versions of itself.

For recursion, I’ll take the examples in Python for a better understanding, but you can implement this recursion in any language of your choice; it is not limited to a single programming language.

def countdown(n):
    if n <= 0:
        print("Blastoff!")
    else:
        print(n)
        countdown(n - 1)

The job this function does is it first checks if the value of n is less than or equal to 0. If it is, then it prints Blastoff, otherwise it goes into the else block. The magic lies in the else block. The else block first prints the value of n, and then calls the function inside of itself.

countdown(n - 1)

This block of code calls the function, but this time n - 1 is given in as the parameters. So, if, say, 3 is given in as the parameter in the function call in the main block, the function will first print the value 3, and then execute the block countdown(2). Then, this will then print 2, and then call countdown(1). Then, this will print 1 and call countdown(0). The magic happens here.

The value of n has now reached 0, so this block of code will be executed,

if n <= 0:
    print("Blastoff!")

This is where the function stops calling itself, and reaches a final point. So, the output will look something like this.

3           # print 3 and call countdown(2)
2           # print 2 and call countdown(1)
1           # print 1 and call countdown(0)
Blastoff!   # print Blastoff, as n <= 0 becomes True

So, when the program called countdown(0) in its print and decrement sequence, it printed Blastoff, and exited out of the function.

The problem can solved with an iterative approach as well. You might devise yourself a correlation between the two!

n = 3

while n > 0:
    # Resembles the function for a general case when n > 0
    print(n)
    n = n - 1
else:
    # Resembles the function for the case when n <= 0
    print("Blastoff")

This program gives the same output.

Now, if, say, the countdown() function was defined like this,

def countdown(n):
    print(n)
    countdown(n - 1)

then this print and decrement sequence would have continued on forever, and the recursion stack (you’ll get to know about this in a moment) would have grown indefinitely, and you would have encountered this error,

RecursionError: maximum recursion depth exceeded

This is what happens if the recursion does not stop at any point. To avoid this, you put a condition which stops the recursive call at your desired point of time. That desired point of time depends upon the problem you’re trying to solve.

The beauty of the Factorial

I know recursion is a hard concept to grasp, but you can understand it with slight help from mathematics.

You know the factorial of an integer n is the product of the integer times the factorial of the integer just behind it, so the factorial can be defined as n! = n * (n - 1)!. This is what is called recursion. Now you might wonder, what will happen if the value of n is, say, 2. According to our equation, the mathematical expression for 2! would look something like this.

2!  = 2 * 1!
    = 2 * 1 * 0!
    = 2 * 1 * 0 * -1!
    = 2 * 1 * 0 * -1! * -2!
    ...

This would happen indefinitely and the function would never stop and we will never get a value. To stop this and make the factorial useful, we define the factorials of 0 and 1 ourselves as 1. So by definition,

0! = 1
1! = 1

So when 2! is needed, this is what happens,

2!  = 2 * 1!
    = 2 * 1
    = 1

And for 3!,

3!  = 3 * 2!
    = 3 * 2 * 1!
    = 3 * 2 * 1
    = 3 * 2
    = 6

This is recursion in its very roots.

Creating the logic for a Recursive Function

Whenever you want to declare a recursive function, you should remember this line,

Do what you know; of what you don’t know, do the part of it which you know.

I know this might seem complex at first, but I’ll explain this while implementing a recursive solution to the Factorial.

For the factorial,

Do what you know

For the factorial, we know that if n is 0 or 1, the factorial equals 1. So on implementing this part, the factorial function looks like this.

def factorial(n):
    if (n == 0) or (n == 1):
        return 1

Of what you don’t know, do the part of it which you know

In the case of factorials, actually you don’t know the value for the factorial of any number, you just go by the definition and calculate the values. The definition of the factorial, my friends, is the part of the unknown you know. So, when n is something other than 0 or 1, the factorial calls itself in the manner, n! = n * (n - 1)!. So, for any general case, the function call factorial(n) should give,

n * factorial(n - 1)

So, upon incorporating this part into our original factorial definition, it will finally look something like this,

def factorial(n):
  if (n == 0) or (n == 1):
      return 1
  else:
      return n * factorial(n - 1)

This is how we solve the Factorial in programming.

Steps to solve a problem recursively

For a problem to have a recursive solution, it should satisfy the below two conditions

Then to solve the problem recursively, you should follow the following steps.

We can understand this approach by solving yet another problem with recursion: Finding the length of the string.

We can directly tell the length of the string when we get an empty string. Voila! We found the base case! Keep a note of this.

Now, for a general case, in real life as well, we count the letters one by one. So to implement this, we have to find a way to have the computer do it one by one. We can use slicing here! What we can do is we can ask the computer to count one character from the beginning and call the function again to find the rest of the characters. Now the computer will again count one character from the beginning of the remaining charaters and call the function again with the other remaining characters. Voila! We found the recursive case!

Now you might wonder what will happen if the string goes empty in this count one and send the remaining sequence. We already got that covered in the base case!

So the recursive algorithm for this program will look something like this,

if the string is empty (has no characters)
    the length is 0
else
    the length is 1 plus the length of the string
    that excludes the first character

So, the program for this in Python will look something like this,

def length(string):
    # If the string is empty, we return 0
    # Note that we cannot use the len() function
    # as that is the problem we are trying to solve XD
    if string == "":
        return 0
    # For the other cases, we return the sum of 1 and the
    # length of the remaining characters
    else:
      return 1 + length(string[1:])

For the sequence “ace”, we can trace the recursive calls.

length("ace")           ->  3               # The first call returns 3 at the end
1 + length("ce")        ->  1 + 2           # Unwinding the first call
    1 + length("e")     ->      1 + 1       # Unwinding the second call
        1 + length("")  ->          1 + 0   # Unwinding the third call
            0           # The base case returned 0

This is a simplified Call Stack or the Recursion Stack of the recursive program we tried to solve. The function, on being called, calls another function in place of itself, and the program remembers where to return the value that you got!

>>  l = length("ace")

    '''
    When you do the above, you can now illustrate what happens inside!

    l = length("ace")
      = 1 + length("ce")
      = 1 + 1 + length("e")
      = 1 + 1 + 1 + length("")
      = 1 + 1 + 1 + 0
      = 3

    '''

>>  print(l)
3

Advantages and Challenges of Recursion

Advantages

Recursion, when used cleverly, is like a magic trick in coding. It helps us solve problems in a way that’s short, sweet, and mirrors how problems naturally work.

Challenges

As with any magic, there are some challenges.

Best Practices and Tips

Mastering the art of recursion needs you to adhere to some established best practices. A few are given below.

These practices, when followed, allows you to harness the maximum strength of recursion whilst helping you avoid potential challenges.

Conclusion

You must have got some idea about the concept of recursion by now. I recommend you to create programs to (1) print string characters in reverse, and (2) get the n-th number in the Fibonacci Sequence with the help of recursion. After that, as a challenge, implement both the linear search and the binary search with recursion. I’ll try to upload the solutions of the programs I mentioned here on my GitHub as soon as ass possible; for the time being, if you can’t solve any of the problems mentioned, you can explore and understand the solutions present on the internet or simply ask ChatGPT!