Lecture 2 : Recursion and Time & Space Complexity Analysis | DSA in Python

Learn Python Recursion by understanding how a function calls itself to solve a problem in smaller steps. You will also learn how to measure algorithm performance using space and time complexity, which are important concepts in Data Structures and Algorithms (DSA).
authorImageVarun Saharawat16 Jun, 2026
Recursion in Python

Writing efficient programs requires a good understanding of how code runs and uses memory. Many beginners find Recursion in Python difficult because recursive functions call themselves repeatedly. Without proper understanding, this can lead to errors or high memory usage. Learning recursion helps you solve complex problems by breaking them into smaller and easier parts. It also helps you understand important DSA concepts and write scalable programs.

What Is Recursion in Python?

Python Recursion is a programming technique where a function calls itself during execution. This allows a large problem to be divided into smaller problems of the same type until the final solution is reached. Instead of using loops such as for or while, a recursive function keeps calling itself until a stopping condition is met.

Python Recursion Example

# A simple recursion example

def countdown(number):

    if number <= 0:

        print("Done!")

    else:

        print(number)

        countdown(number - 1)

In this example, the function keeps calling itself with a smaller value until the number becomes 0.

How Python Recursion Works

When a recursive function executes, Python produces another function call in memory. Each call has its own variable set so the different executions of the function do not interfere with each other.

The Python interpreter maintains track of all function calls in sequence. When the stopping condition is met, the functions unwind one after another, returning control back through the earlier calls. This means that Python Recursion can be used to solve big problems by breaking them down into smaller, more manageable steps.

Components of Recursion in Python

Every recursive function needs two important parts to work correctly. Without these parts, the function may run forever and cause errors.

Base Case in Python Recursion

The base case is the stopping condition of a recursive function. When this condition is met, the function returns a result and stops making more recursive calls.

The base case solves the smallest version of the problem and helps the program end safely. If a recursive function does not have a proper base case, Python will continue making function calls until it reaches the maximum recursion limit and raises a RecursionError.

Recursive Case in Python Recursion

The recursive case is the part of the function where it calls itself.

Each recursive call should move closer to the base case by reducing the problem size. This ensures that the function eventually reaches the stopping condition and finishes execution.

Why Both Components Are Important

The base case tells the function when to stop, while the recursive case tells it how to continue solving the problem. Together, these two components make Python Recursion work correctly and help solve complex problems in smaller and simpler steps.

Types of Recursion in Python

Understanding the structural design of your self-referential functions helps you select the right algorithm for specific DSA concepts.

Direct Recursion

This structure occurs when a function directly explicitly calls itself within its own body definition. It is the most common form found in software engineering.

Indirect Recursion

In this setting, a reciprocal relation is created where a function calls a second function, and the second function calls the first. This forms a circular execution chain that will be stopped by a validity check.

Python

# Example of mutual indirect recursion
def function_a(n):
    if n > 0:
        print(n)
        function_b(n - 1)

def function_b(n):
    if n > 0:
        print(n)
        function_a(n - 1)

Tail Recursion

A function uses tail recursion when the recursive call is the absolute final statement executed within the function. No operations, mathematical calculations, or modifications happen after the call returns.

Non-Tail Recursion

In this structure, further mathematical computations or actions are performed after the recursive call returns its result. To finish these pending tasks the system needs to remember the past states . This makes optimisation more challenging.

Recursion in Python Implementation

Reviewing concrete mathematical and search examples shows how Python Recursion functions under real-world conditions.

1. Factorial Calculation

Finding the factorial of an integer involves multiplying that number by every positive integer below it. The recursive case naturally matches the formula n! = n * (n - 1)!.

Python

def recursion_factorial(a):
    if a == 1 or a == 0:
        return 1
    else:
        return a * recursion_factorial(a - 1)
print(recursion_factorial(5)) # Output: 120

2. Fibonacci Sequence Generation

The Fibonacci sequence builds each number by summing the two values that came before it. This process requires a non-tail approach because the system must add two separate internal recursive executions together.

Python

def recursive_fibonacci(a):
    if a <= 0:
        return 0
    elif a == 1:
        return 1
    else:
        return recursive_fibonacci(a - 1) + recursive_fibonacci(a - 2)

3. Binary Search Algorithm

A binary search locates an item inside a sorted list by repeatedly dividing the search space in half. This process demonstrates how a recursive case can dramatically reduce a problem's size with each step.

Python

def binary_search(arr, target, left, right):
    if left > right:
        return -1
   
    mid = (left + right) // 2
    if arr[mid] == target:
        return mid
    elif arr[mid] < target:
        return binary_search(arr, target, mid + 1, right)
    else:
        return binary_search(arr, target, left, mid - 1)

Time Complexity Analysis of Recursion in Python

To understand how fast a recursive function runs, you need to analyze how the number of operations grows as the input size (n) increases. In recursion, this is often done by studying the pattern of recursive calls.

Linear Time Complexity O(n)

Some recursive functions make only one recursive call at each step. A good example is the factorial function. As the value of n decreases by 1 in every call, the number of operations grows at the same rate as the input size. This results in a time complexity of O(n).

Exponential Time Complexity O(2ⁿ)

Some recursive functions create two recursive calls from each call. The classic Fibonacci sequence is a common example. Because every call creates more calls, the total number of operations grows very quickly. This creates a tree-like structure and results in a time complexity of O(2ⁿ).

Logarithmic Time Complexity O(log n)

Some recursive algorithms reduce the input size by half during each step. Binary Search is a popular example. Since the input becomes much smaller after every call, the number of operations stays low. This gives the algorithm an efficient time complexity of O(log n).

Importance of Time Complexity Analysis 

Understanding time complexity helps you choose the best recursive solution for a problem. It allows you to compare different algorithms and write code that performs well even when the input size becomes large.

Space Complexity in Recursion in Python

Unlike standard loops, recursive steps depend heavily on the system's memory allocation behaviors. The total complexity of space of an algorithm is determined by the peak depth of the runtime execution stack.

Factor

Iterative Loops

Recursive Functions

Memory Allocation

Reuses a single activation record frame.

Creates a new activation frame for every call.

Typical Space Scale

Highly efficient O(1) constant space.

Scales to O(n) based on max stack depth.

Overflow Risks

Zero risk of stack exhaustion.

High risk of triggering a RecursionError.

When a function executes in Python, the interpreter reserves a block of memory called an activation record or stack frame. This frame stores local variables, parameters, and the exact code line to return to. In a deep recursion loop, these frames pile up on the stack until the base case triggers a unwinding process. If the input size exceeds the system's limits, the program crashes with a stack overflow error.

Recursion in Python Advanced Applications

Many advanced DSA ideas rely on self-referential functions because shapes like trees and graphs are inherently recursive.

Tree Traversal and Manipulation

A binary tree consists of a root node where each left and right branch connects to another sub-tree. Finding data within these shapes requires algorithms like Depth-First Search (DFS) that step through nodes recursively.

Python

class TreeNode:
    def __init__(self, value):
        self.value = value
        self.left = None
        self.right = None

def tree_height(node):
    if node is None:
        return 0
    else:
        left_height = tree_height(node.left)
        right_height = tree_height(node.right)
        return max(left_height, right_height) + 1

Calculating the total node count or checking for tree symmetry follows the same design pattern. The algorithm processes the current node, then passes the child nodes to the same function to analyze the deeper branches.

Advanced Sorting with Quicksort

The Quicksort algorithm selects a single pivot item from a list, divides the remaining elements into smaller or larger sub-lists, and then sorts those sub-lists recursively. This approach demonstrates how dividing a problem into smaller parts can solve complex sorting tasks efficiently.

Optimization Techniques for Recursion

To build production-ready applications, you must optimize your recursive code to minimize runtime overhead and prevent memory issues.

1. Caching with Memoization

Repeated calculations are a major efficiency bottleneck in algorithms like the Fibonacci sequence. Memoization eliminates this waste by storing the results of expensive function calls in a cache, ensuring the application only computes each unique input once.

Python

from functools import lru_cache
# Optimizing a recursive function using built-in Python caching tools
@lru_cache(maxsize=None)
def fibonacci_memo(n):
    if n <= 0:
        return 0
    elif n == 1:
        return 1
    return fibonacci_memo(n - 1) + fibonacci_memo(n - 2)

2. Adjusting the System Recursion Limit

Python includes a safety guard to prevent infinite loops from exhausting system memory. You can inspect and change this threshold using the sys module if your specific data structures require deeper traversal.

Python

import sys
# Checking the current environment stack boundary limit
print(sys.getrecursionlimit())
# Increasing the stack limit for deep data structures
sys.setrecursionlimit(2000)

Note: While raising this limit allows for deeper recursion, doing so prematurely without optimizing your code can lead to unexpected segmentation faults or system crashes if the underlying operating system stack memory runs out.

Recursion in Python vs Iteration

Choosing between recursion and iteration depends on your needs. You may need cleaner code or better performance, depending on the problem you are solving.

Code Simplicity

Recursion often makes code shorter, cleaner, and easier to read. It works especially well for hierarchical structures such as trees, nested folders, and JSON data because these problems naturally follow a recursive pattern.

Better Memory Usage

Iteration uses loops such as for and while loops. These solutions usually run faster and use less memory because they do not create multiple function calls.

Performance and Optimization

Python does not undertake automatic tail call optimisation. This means that a recursive function, even a nicely written one, creates a new stack frame at each function call. For this reason iterative solutions are generally preferred to direct solutions when working with huge data sets or situations that require many repeated actions.

Which One Should You Choose?

Use recursion when it makes the code easier to understand, especially for trees, graphs, and divide-and-conquer algorithms. Use iteration when speed and memory efficiency are more important, especially for large linear datasets.

FAQs

What causes a RecursionError in Python?

A RecursionError is raised when a recursive function reaches the maximum recursion depth in Python. This generally happens when the base case is absent or wrong or the size of input is too great.

How is time complexity different for loops and recursion?

For loops , time complexity is the number of times the loop executes . In the case of recursion, you need to analyse how many recursive calls are made and how the execution tree expands as the input size rises.

Why does complexity of space increase in recursion?

Whenever a recursive function calls itself, python produces a new stack frame in memory. It stores local variables and function information in these stack frames. Memory use grows with an increasing number of recursive calls.

Can Python Recursion be replaced with loops?

Yes, every recursive solution can be rewritten using loops and a stack data structure. Loop-based solutions are often preferred when reducing memory usage is important.

When should I use recursion in DSA?

Recursion is helpful when working with tree structures, graphs and divide and conquer algorithms like Quicksort and Merge Sort. This typically makes the code easier to read and write, especially for problems that map naturally to a recursive solution.
Popup Close ImagePopup Open Image
Talk to a counsellorHave doubts? Our support team will be happy to assist you!
Popup Image
avatar

Get Free Counselling Today

and Clear up all your Doubts

Talk to Our Counsellor just by filling out the form.
Student Name
Phone Number
IN
+91
OTP
Email Id
Join 15 Million students on the app today!
Point IconLive & recorded classes available at ease
Point IconDashboard for progress tracking
Point IconLakhs of practice questions
Download ButtonDownload Button
Banner Image
Banner Image