Recursion in Python Tutorial

ryanthelin

Ryan Thelin

Posted on February 5, 2021

Recursion in Python Tutorial

Recursion is one of the fundamental concepts in computer science and is essential for programmers and data scientists alike. Not only are many sort and search algorithms recursive, but every Python interview will include some recursion-based questions. This marks recursion as a key concept to revise before any coding interview.

Today, we'll help you brush up on your recursive programming skills in Python and walk through 6 practice problems to get you hands-on experience.

Here’s what we’ll cover today:

Transition to Python in half the time

Brush up on the skills interviewers are looking for with dozens of hands-on practice problems.

Ace the Python Coding Interview

What is recursion?

Recursion is a concept in computer science when a function calls itself and loops until it reaches the desired end condition. It is derived from the mathematical concept of recursive definitions, which defines elements in a set in terms of other elements in the set.

Each recursive implementation has a base case, which is when the desired state has been reached, and a recursive case where the desired state has not been reached and the function enters another recursive step.

Alt Text

The behavior in the recursive case before the recursive function call, the internal self-call, is repeated on each step. Recursive structures are therefore useful when you can achieve a greater problem (the base case) by completing repeating subproblems (the recursive case) that incrementally move the program to the base case.

It results in similar behavior to for or while loops, except recursion progresses closer to a target condition, while for loops run a set number of times, and while loops run until a condition is no longer met.

In other words, recursion is declarative because you set the state you want to reach and for/while loops are iterative because you have to set the number of repetitions.

Take a look at the difference in syntax between iterative and recursive approaches:

Recursive

def RecursiveFunction() :
  # Base Case
  if <baseCaseCondition> : #sets target state
    <return some base case value>

  # Recursive Case
  else :
    <return(recursive call and any other task)>
Enter fullscreen mode Exit fullscreen mode

for loop

arr = ["A", "B", "C"]
for x in arr: #sets the number of iterations
  print(x)
Enter fullscreen mode Exit fullscreen mode

while loop

i = 1
while i < 6: #sets number of iterations
  print(i)
  i += 1
Enter fullscreen mode Exit fullscreen mode

Recursive solutions are best when a problem has clear subproblems that must be repeated and if you're unsure how many times you'd need to loop with an iterative solution.

For example, if you wanted to create a factorial function program that finds the factorial of an unknown number.

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

num = 5
print(factorial(num))
Enter fullscreen mode Exit fullscreen mode

Direct vs Indirect Recursion

Thus far we've only discussed direct recursion, where the recursive function explicitly calls itself in its recursive step. There is also indirect recursion where the recursive call is removed from the function but still runs as a result of the original recursive step.

For example, functionA calls functionB, which then calls functionA again.

Direct

def function1(p1, p2, ..., pn) :
  # Some code here
  function1(p1, p2, ..., pn)
  # Some code here
Enter fullscreen mode Exit fullscreen mode

Indirect

def function1(p1, p2, ..., pn) :
  # Some code here
  function2(p1, p2, ..., pn)
  # Some code here

def function2(p1, p2, ..., pn) :
  # Some code here
  function1(p1, p2, ..., pn)
  # Some code here
Enter fullscreen mode Exit fullscreen mode

Pros and cons of recursion in Python

All programming languages support recursion, however, not all are equally optimized.

Iteration is often preferred in Python and is regarded as more "pythonic" due to built-in optimizations. Generally, recursive solutions are preferred over iterative solutions on larger computations as recursion often results in less code and faster performance at scale.

Pros:

  • Faster when optimized: If you include recursion optimizations like tail-end recursion and memoization, recursive approaches are faster in Python.
  • Less code: Recursive solutions are more compact, which means you can write recursive solutions faster and have less code to review when debugging.
  • Declarative: Many developers find the logic of declaring the desired state to be more understandable than iteration, which focuses on the steps needed to reach an unstated goal.
  • Efficient Sort and Search: Recursion is especially useful for Python data science as it is the foundation of popular sort algorithms like mergesort.

Cons

  • Maximum recursion depth: Python has a limited call stack that supports only 1000 nested steps. While this may seem like a lot, it becomes an issue when dealing with large structures like lists and arrays. This can be overridden at your own rise withsys.setrecursionlimit(1500).
  • Supported, not suggested: Python allows recursive calls but does not include many built-in optimizations. Unlike iterative optimizations, developers must code all-recursive improvements themselves.
  • Uses more memory: Each call saves the previous step in the call stack until the recursive process is complete. This means recursive solutions use more memory than iterative ones.

I did not add readability to either column, as some developers find recursion easier to understand, while others find the nested behavior confusing. Ultimately, it's case-by-case per problem and developer.

Recursion in Python

Now, let's take a deeper look at recursive functions in Python.

Below is an example program that recursively prints the pattern: 10 5 0 5 10.

def printPattern(targetNumber) :

  # Base Case
  if (targetNumber <= 0) :
    print(targetNumber)
    return

 # Recursive Case
  print(targetNumber)
  printPattern(targetNumber - 5)
  print(targetNumber)

# Driver Program 
n = 10
printPattern(n)
Enter fullscreen mode Exit fullscreen mode

We want to print each number twice, except 0 that is only printed once in the middle. This lets us know that if (targetNumber <= 0) is our base case.

Our recursive case is therefore lines 7-9.

On line 8, you can see the recursive call, printPattern(targetNumber - 5), which moves our program into the next recursive step.

Line 9 prints the final 10 and is only run once as it is after the recursive call.

Remember, only behavior before the recursive call is repeated in the recursive loop.

Take a look at this program flowchart to see how the program steps connect:

Alt Text

Now that we've taken apart this recursive program, let's look at some applications of recursion.

Keep revising Python.

Recursion is a staple of any Python Coding Interview. Educative's hands-on, text-based courses let you transition to Python and land that next job fast.

Ace the Python Coding Interview

Python recursion with numbers

Fibonacci Sequence

First, we'll look at a classic recursion example: the Fibonacci sequence. This program takes a given number and returns its Fibonacci numbers.

def recur_fibo(n):
   # Base Case
   if n <= 1:
       return n
   else:
   # Recursive Case
       return(recur_fibo(n-1) + recur_fibo(n-2))

# Driver Code
num = 10
print (recur_fibo(num))
Enter fullscreen mode Exit fullscreen mode

You can see our base case on line 2, if n >= 1. If this is not met, we move into the recursive case on line 5, which features two recursive calls.

Each time the loop recurses, n is lowered, meaning that our base case will eventually become true.

Sum from 1 to n

Now, we'll see how we can use recursion to sum all numbers from 1 to the given number. This program takes a positive integer as input and returns a single printout of the sum of all numbers between 1 and the integer.

def sumTill(targetNumber) :
  # Base Case
  if targetNumber == 1 :
    return targetNumber

  # Recursive Case
  else :
    return targetNumber + sumTill(targetNumber - 1)

# Driver Code
targetNumber = 5
print(sumTill(targetNumber))
Enter fullscreen mode Exit fullscreen mode

Our program starts with the given number and adds the number one lower on each recursive step until it has reached 1.

The base case is on line 3, if targetNumber ==1. The recursive case adds the current value of targetNumber then calls sumTill() with a value lower by 1.

Python recursion with strings and arrays

Recursion is also helpful with strings or arrays. Many recursion interview questions will ask you to transform strings or arrays until all match certain criteria.

Remove spaces from a string

For this exercise, we'll create a program that takes a given string and returns a new string without any space or tab (/t) characters. For example, Hello World would become HelloWorld.

def remove(string):
  # Base Case
  if not string:
      return ""

  # Recursive Case
  if string[0] == "\t" or string[0] == " ":
      return remove(string[1:])
  else:
      return string[0] + remove(string[1:])

# Driver Code
print(remove("Hello\tWorld"))
Enter fullscreen mode Exit fullscreen mode

Removing tabs from a null string "" will just return the null string "". Therefore, our base case will be when our original string is empty (line 3).

For the recursive case on lines 7 - 10, we check whether or not the current element is "\t" or " ":

  • If the current element is "\t" or " " we discard it and call another instance of the function after removing that element.
  • If the current element is not "\t" or " ", we append it to the output string and call another instance of the function after removing that element.

Invert an Array

Now, we'll create a recursive program that takes a given array and returns the same array with elements in reverse order. For example, 1, 2, 3, 4 would become 4, 3, 2, 1.

def reverse(array):
  # Base case1
  if len(array) == 0: # If we encounter an empty array, simply return an empty array
    return []

  # Base case2
  elif len(array) == 1 : # Inverting an array of size 1 returns the same array
   return array

  # Recursive case
  return [array[len(array) - 1]] + reverse(array[:len(array) - 1])
  # The first part is storing the last element to be appended later
  # The second part is calling another instance of the same function with the last element removed

# Driver Code
array = [1, 2, 3, 4]
print(reverse(array))
Enter fullscreen mode Exit fullscreen mode

For this solution, we'll make a temporary variable that saves the final element from the passed array on each recursive step. In the end, these values will be used to repopulate the array in reverse order because nested recursive functions complete deepest first.

The base cases of this problem are when the array has 0 or 1 element within because inverting these arrays will return the same array.

Python recursion with data structures

Finally, let's see how recursive functions can be used on data structures like linked lists and Binary Trees.

Reverse a Linked List

Reversing a linked list is similar to reversing an array, but is a bit more complicated. This program will accept a linked list and return the same linked list with nodes in the opposite order.

For example, the linked list 3, 4, 7, 11 would have a return value of 11, 7, 4, 3.

# main
import linkedList as l
def helperFunction(myLinkedList, current, previous) : # This function reverses the linked list recursively
    # Base case
    if current.next is None : 
        myLinkedList.head = current  
        current.next = previous 
        return 
    next = current.next
    current.next = previous 

    # Recursive case
    helperFunction(myLinkedList, next, current)  
def reverse(myLinkedList): 
    # Check if the head node of the linked list is null or not
    if myLinkedList.head is None: 
        return 

    # Call the helper function --> main recursive function
    helperFunction(myLinkedList, myLinkedList.head, None) 
# Driver Code
myLinkedList = l.LinkedList() 
myLinkedList.append(3) 
myLinkedList.append(4) 
myLinkedList.append(7) 
myLinkedList.append(11) 
print("Original Linked List")
myLinkedList.printList() 
reverse(myLinkedList) 
print("\nReversed Linked List")
myLinkedList.printList() 
Enter fullscreen mode Exit fullscreen mode
# node.py
class Node: 
    def __init__(self, data): 
        self.data = data 
        self.next = None
Enter fullscreen mode Exit fullscreen mode
# linkedList.py
import node as n
class LinkedList: 
    def __init__(self) :
        self.head = None
    def append(self, new_data): 
        new_node = n.Node(new_data) 

        if self.head is None : # If head node is null 
            self.head = new_node 
            return
        last = self.head 
        while (last.next): 
            last = last.next
        last.next =  new_node # add new node to end of list
    def printList(self):
        temp = self.head
        while(temp):
            print(temp.data)
            temp = temp.next
Enter fullscreen mode Exit fullscreen mode

In the code snippet above, we pass our linked list, myLinkedList, to the function reverse(). This function checks whether or not the head of the linked list is null. If the head node is not null, we call the helperFunction(). This function is recursive and is where the reversal of the linked list takes place.

In the helperFunction(), the nodes are reversed using the following statements:

next = current.next # The original next node of the current node is saved
current.next = previous # The next of the current node is updated
Enter fullscreen mode Exit fullscreen mode

After this change, we recursively call the function again:

# Recursive case
helperFunction(myLinkedList, next, current)
# The current node in the next recursive call will be the "next" node that we saved.
# The previous node will be the parent function's current node
Enter fullscreen mode Exit fullscreen mode

The base case for this problem is where we reach the end of the linked list. This is where the next node of the current node will be None. We will make this last node the head of the linked list, update its position, and return.

Print all leaf nodes of a Binary Tree from left to right

This question is a bit tricky but is exactly the type of question you'll see in Python coding interviews.

We'll create a program that prints all leaf nodes as they appear from left to right on the tree diagram. This is the tree we'll be using.

Alt Text

The correct solution for this tree is 4 6 7 9 10.

Reminder: Leaf nodes are nodes that have no children and therefore end the subtree branch.

# Function to print leaf
# nodes from left to right
def printLeafNodes(root: Node) -> None:

    # If node is null, return
    if (not root):
        return

    # If node is leaf node, 
    # print its data
    if (not root.left and
        not root.right):
        print(root.data, 
              end = " ")
        return

    # If left child exists, 
    # check for leaf recursively
    if root.left:
        printLeafNodes(root.left)

    # If right child exists, 
    # check for leaf recursively
    if root.right:
        printLeafNodes(root.right)

# Driver Code
if __name__ == "__main__":

    # Creates binary tree shown in
    # above diagram
    root = Node(1)
    root.left = Node(2)
    root.right = Node(3)
    root.left.left = Node(4)
    root.right.left = Node(5)
    root.right.right = Node(8)
    root.right.left.left = Node(6)
    root.right.left.right = Node(7)
    root.right.right.left = Node(9)
    root.right.right.right = Node(10)

    # print leaf nodes of the given tree
    printLeafNodes(root)
Enter fullscreen mode Exit fullscreen mode
# Binary tree node
class Node:

    def __init__(self, data):
        self.data = data
        self.left = None
        self.right = None
Enter fullscreen mode Exit fullscreen mode

This program takes the root node and prints all leaf nodes from left to right. Our base case is either 1) there are no remaining nodes, or 2) there are no remaining leaf nodes.

The program follows the connections of each child going left each time until it finds a leaf node. The program prints this value then repeats this subproblem along a different path of nodes.

Eventually, the function returns, and all leaf nodes will be printed from left to right.

What to learn next

Recursion is an essential part of any Python developer's skillset. Recursive questions often take up a large portion of interview questions at coding interviews and are essential for dynamic programming questions. The best way to learn these concepts is with hands-on experience with real interview questions.

Here are some questions to check out if you want to keep improving:

  • 0-1 knapsack problem
  • Balanced parentheses question
  • Convert tree to a doubly linked list
  • Reverse a stack
  • Lowest common ancestor

You can find walkthroughs of these and more recursion interview questions in Educative's Ace the Python Coding Interview Path. This Path includes comprehensive practice and sample questions for top tested concepts like recursion, dynamic programming, concurrency, and OOP.

By the end, you'll be able to confidently walk into your next interview, knowing that you've practiced dozens of the most asked interview questions.

Continue reading about Python and recursion

💖 💪 🙅 🚩
ryanthelin
Ryan Thelin

Posted on February 5, 2021

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related

Recursion in Python Tutorial
tutorial Recursion in Python Tutorial

February 5, 2021