Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 5

Analyzing Space Complexity: Auxiliary Space and Recursion

Recommended resources

Hello! Welcome to your fifth lesson.

In our last session, we delved into time complexity and Big O notation, establishing a framework for analyzing how an algorithm's runtime scales with input size. You learned to analyze both iterative and recursive code, a foundational skill for your interview preparation.

However, time is only one dimension of efficiency. An algorithm also consumes memory. A solution that is blazingly fast but requires an enormous amount of RAM might be impractical. This lesson focuses on the second pillar of algorithm analysis: Space Complexity.

By the end of this lesson, you will be able to analyze an algorithm's memory usage, distinguishing between the space taken by the input and the extra space it requires. Crucially, we will also explore how recursion impacts memory through the call stack. Mastering both time and space analysis will give you the complete vocabulary to discuss an algorithm's performance in any technical interview.

1. What is Space Complexity?

Just as time complexity measures how runtime grows, space complexity measures how the amount of memory an algorithm needs grows as the input size (n) increases. Like time complexity, we use Big O notation to describe this growth.

An algorithm's total memory usage can be broken down into two parts:

  • Input Space: The memory required to store the input data itself (e.g., an array of size n).
  • Auxiliary Space: The extra or temporary memory that an algorithm uses during its execution. This includes variables, data structures created to aid the computation, and the space used by function calls.

Space Complexity = Input Space + Auxiliary Space

In many interview contexts and competitive programming, when someone asks for "space complexity," they are often most interested in the auxiliary space complexity. Why? Because the input space is a given requirement of the problem, while the auxiliary space is a direct consequence of your algorithmic choices. A solution with O(1) auxiliary space is generally better than one with O(n) auxiliary space, even if both have the same total space complexity of O(n).

Time and Space Complexity - Strivers A2Z DSA Course

Let's start with a clear definition of space complexity and the distinction between auxiliary and input space from the 'Time and Space Complexity' video by take U forward.

Watch the segment from 25:54 to 27:26. Focus on the core definition: Space Complexity = Auxiliary Space + Input Space.


2. Analyzing Space in Iterative Algorithms

Let's analyze some familiar iterative code to see how this works.

Example 1: Constant Auxiliary Space, O(1)

Consider a function that sums the elements of an array.

// arr has n elements
long sumArray(int[] arr) {
    long sum = 0; // One variable (e.g., 8 bytes)
    // The loop counter 'i' is also one variable (e.g., 4 bytes)
    for (int i = 0; i < arr.length; i++) {
        sum += arr[i];
    }
    return sum;
}
  • Input Space: The array arr takes O(n) space.
  • Auxiliary Space: We use two variables, sum and i. The memory they occupy does not change, regardless of whether the array has 10 elements or 10 million. This is constant space, which we denote as O(1).
  • Total Space Complexity: O(n) (input) + O(1) (auxiliary) = O(n).

Example 2: Linear Auxiliary Space, O(n)

Now, let's look at a function that creates a new data structure. Imagine we want to count the frequency of each number in an array. A HashMap is a good tool for this.

Time and Space Complexity

The article 'Time and Space Complexity' on GeeksforGeeks provides a perfect Java example for this scenario. It also clearly defines auxiliary space.

First, look at the Java code in the section titled 'Example : Frequency of array elements'. Notice the creation of a HashMap. Then, read the paragraph that starts with 'There is also auxiliary space...'. This will crystallize the difference between the total space and the extra space used by the freq map.

Let's analyze the code from the article:

static void countFreq(int arr[], int n) {
    // Auxiliary space starts here
    HashMap<Integer, Integer> freq = new HashMap<>();

    for (int i = 0; i < n; i++) {
        // The map grows as we add elements
        freq.put(arr[i], freq.getOrDefault(arr[i], 0) + 1);
    }
    // ...
}
  • Input Space: The array arr takes O(n) space.
  • Auxiliary Space: We create a HashMap called freq. In the worst-case scenario (where all n elements in arr are unique), the map will store n key-value pairs. Therefore, the space required by the map grows linearly with the input size. The auxiliary space is O(n).
  • Total Space Complexity: O(n) (input) + O(n) (auxiliary) = O(n).

Notice that while both examples have O(n) total space complexity, their auxiliary space requirements are vastly different. This is the kind of trade-off you'll constantly evaluate.

Test your understanding!

What is the auxiliary space complexity of the following Java function, which creates a new array containing the squares of the input array's elements?

int[] getSquares(int[] nums) {
    int n = nums.length;
    int[] squares = new int[n]; // A new array is created
    for (int i = 0; i < n; i++) {
        squares[i] = nums[i] * nums[i];
    }
    return squares;
}
Show answer

The function creates a new array, squares, which has the same size as the input array, n. The space required for this new array scales linearly with the input size. Therefore, the auxiliary space complexity is O(n). The variables n and i contribute O(1) space, which is non-dominant and can be ignored.

An Important Rule for Interviews

It can be tempting to save space by modifying the input data directly. For example, in the getSquares problem above, you could overwrite nums[i] with its square. While this reduces auxiliary space to O(1), it's generally considered bad practice unless you're explicitly told it's acceptable.

Time and Space Complexity - Strivers A2Z DSA Course

The 'take U forward' video gives a crucial piece of advice about modifying input data that is highly relevant for your interview preparation goals.

Watch the segment from 29:36 to 31:56. This explains the software engineering principle behind not tampering with input data and how to communicate this trade-off to an interviewer.


3. Space Complexity of Recursive Algorithms: The Call Stack

Analyzing space in recursive algorithms requires understanding the call stack. When a function is called, a block of memory called a stack frame is allocated to store its local variables, parameters, and return address. When functionA calls functionB, a new stack frame for functionB is pushed on top of functionA's frame.

The space complexity of a recursive algorithm is determined by the maximum depth of the call stack at any point during execution.

Example 1: Linear Space Recursion, O(n)

Let's revisit the factorial function from the last lesson.

int factorial(int n) {
    if (n <= 1) {
        return 1;
    }
    return n * factorial(n - 1);
}

When you call factorial(4), the call stack looks like this before the base case is hit:

  1. factorial(4) is called.
  2. factorial(4) calls factorial(3). Stack: [factorial(4), factorial(3)]
  3. factorial(3) calls factorial(2). Stack: [factorial(4), factorial(3), factorial(2)]
  4. factorial(2) calls factorial(1). Stack: [factorial(4), factorial(3), factorial(2), factorial(1)]

The stack reaches its maximum depth of n (or n+1 depending on how you count). Since each stack frame takes O(1) space, the total space complexity due to recursion is O(n).

Big O Notation Series #8: Space Complexity and Big O Gotchas

The video 'Space Complexity and Big O Gotchas' from Kantan Coding provides an excellent visual explanation of how recursive calls build up on the call stack, leading to linear space complexity.

Watch the entire segment from 00:01 to 03:54. The countdown function is analogous to our factorial example. Pay close attention to the statement that 'each one of these calls exists on the call stack simultaneously.'

Example 2: Time vs. Space in Branching Recursion

In the last lesson, we saw that the fibonacci function has a time complexity of O(2^n) because of its branching recursion tree. What about its space complexity?

int fibonacci(int n) {
    if (n <= 1) {
        return n;
    }
    return fibonacci(n - 1) + fibonacci(n - 2); 
}
Fibonacci Recursion Tree and Call Stack
This image illustrates the call stack for `fib(4)`. Notice how the stack depth never exceeds `n+1`. When `fib(2)` returns, its frame is popped before `fib(1)` is called.

Although the total number of calls is exponential, they don't all exist on the call stack at the same time. The program explores one branch of the recursion tree down to its base case, and only then does it backtrack to explore another. The maximum number of stack frames on the call stack at any single moment is determined by the height of the recursion tree, which is n.

Therefore, the space complexity of this fibonacci function is O(n), not O(2^n). This is a critical distinction and a common interview question.

Example 3: Logarithmic Space Recursion, O(log n)

What happens if a recursive call halves the problem size, like in a recursive binary search?

// A simplified recursive function that divides its input
void function(int n) {
    if (n <= 1) {
        return; // Base case
    }
    function(n / 2); // Recurse on half the problem
}

The sequence of calls would be n -> n/2 -> n/4 -> ... -> 1. The number of times you can divide n by 2 until you reach 1 is logâ‚‚(n). The maximum depth of the call stack is log n. Therefore, the space complexity is O(log n).


Visualizing Space Complexity

Just like with time complexity, we can visualize how different space complexities scale. Efficient algorithms aim for lower space requirements, especially as the input size n grows large.

Space Complexity Growth Rates
A graph showing the growth of common space complexity classes. The goal is to stay on the flatter curves (O(1), O(log n)) whenever possible.
Test your understanding!

Consider this recursive function that sums the elements of an array. What is its auxiliary space complexity?

int recursiveSum(int[] arr, int index) {
    // Base case
    if (index == arr.length) {
        return 0;
    }
    // Recursive step
    return arr[index] + recursiveSum(arr, index + 1);
}

Hint: Think about the maximum depth of the call stack.

Show answer

The function starts at index = 0 and calls itself with index + 1 until index reaches arr.length. If the array has n elements, the recursion will go n levels deep before hitting the base case (recursiveSum(arr, 0) calls recursiveSum(arr, 1), and so on, up to recursiveSum(arr, n)).

The maximum depth of the call stack is proportional to n. Therefore, the auxiliary space complexity is O(n).


Conclusion

You have now completed your tour of the fundamentals of algorithm analysis. You can analyze an algorithm's efficiency in terms of both time and space, for both iterative and recursive solutions. This two-dimensional perspective is essential for making informed decisions as a software engineer and for succeeding in technical interviews.

Key Takeaways:

  • Space Complexity measures an algorithm's memory usage as a function of input size n.
  • Auxiliary Space is the extra memory used by an algorithm, distinct from the input space. This is often the primary focus of analysis.
  • Iterative Algorithms: Space is determined by the variables and data structures you declare. This can be O(1), O(n), or other complexities.
  • Recursive Algorithms: Space is primarily determined by the maximum depth of the call stack. Linear recursion is typically O(n), while "divide and conquer" recursion is often O(log n).
  • Time vs. Space: A branching recursion like Fibonacci can have exponential time (O(2^n)) but linear space (O(n)).

Preview of the next lesson:
Now that you have the tools to analyze algorithms, we will begin learning specific techniques to design efficient ones. In the next lesson, we will cover the Two-Pointer Technique, a simple yet powerful pattern for solving a wide range of problems on arrays and strings in optimal time and space.