Chapter 2
20 min read
Section 9 of 261

Space Complexity Analysis

Complexity Analysis

Learning Objectives

By the end of this section, you will be able to:

  1. Understand the distinction between input space and auxiliary space
  2. Analyze space complexity by identifying memory allocations in code
  3. Calculate stack space for recursive algorithms and understand its impact
  4. Recognize space-time tradeoffs and make informed design decisions
  5. Implement in-place algorithms that minimize memory usage
  6. Apply space analysis to real-world scenarios with memory constraints
Why This Matters: In embedded systems, mobile devices, and large-scale data processing, memory is often the limiting factor. An algorithm that runs in O(n) time but requires O(n²) space may be unusable when processing gigabytes of data. Understanding space complexity is essential for building systems that scale.

Why Space Complexity Matters

Time complexity tells us how long an algorithm takes. Space complexity tells us how much memory it uses. Both matter, but space is often overlooked until it becomes a critical bottleneck.

The Memory Hierarchy

Modern computers have a hierarchy of storage, each level trading capacity for speed:

LevelSizeSpeedCost per GB
CPU Registers~1 KB~1 nsPriceless
L1 Cache32-64 KB~4 ns~$1000
L2 Cache256 KB - 1 MB~10 ns~$100
L3 Cache4-32 MB~40 ns~$10
RAM8-128 GB~100 ns~$5
SSD256 GB - 4 TB~100 µs~$0.10
HDD1-16 TB~10 ms~$0.02

When your algorithm exceeds available RAM, data must be swapped to disk. This can make your "O(n)" algorithm effectively 1000x slower due to disk I/O latency.

When Space Becomes Critical

  • Embedded Systems: IoT devices, microcontrollers, and sensors often have kilobytes, not gigabytes, of RAM
  • Mobile Applications: High memory usage drains battery and triggers OS memory warnings
  • Big Data Processing: When your dataset exceeds RAM, space-efficient algorithms become essential
  • Concurrent Systems: Each thread or process has its own stack; high space complexity limits parallelism
  • Real-Time Systems: Memory allocation can introduce unpredictable delays

Memory Pressure

When an application uses too much memory, the operating system starts "swapping" - moving data between RAM and disk. This causes severe performance degradation. A space-efficient algorithm prevents this scenario.

Types of Memory Usage

When analyzing space complexity, we distinguish between two types of memory:

1. Input Space

This is the memory required to store the input itself. For example, if we pass an array of n integers to a function, the input takes O(n) space.

Counting Convention

Input space is typically not counted in space complexity analysis. We focus on auxiliary space - the extra memory the algorithm needs beyond the input.

2. Auxiliary Space

This is the extra space used by the algorithm, excluding the input. It includes:

  • Variables: Integers, pointers, counters
  • Data Structures: New arrays, hash tables, trees created during execution
  • Call Stack: Stack frames for recursive function calls
  • Temporary Storage: Buffers, intermediate results
Total Space=Input Space+Auxiliary Space\text{Total Space} = \text{Input Space} + \text{Auxiliary Space}

When we say "space complexity is O(1)", we mean the auxiliary space is constant - the algorithm uses the same amount of extra memory regardless of input size.

Examples of Each Type

AlgorithmInput SpaceAuxiliary SpaceTotal
Find max in arrayO(n)O(1)O(n)
Copy arrayO(n)O(n)O(2n) = O(n)
Merge sortO(n)O(n)O(n)
Quick sort (in-place)O(n)O(log n) stackO(n)
Create adjacency matrixO(n)O(n²)O(n²)

Quick Check

An algorithm receives an n\u00D7n matrix as input and creates a temporary array of size n during processing. What is its auxiliary space complexity?


Interactive: Memory Layout

Use this interactive visualization to see how different algorithms allocate memory. Toggle between O(1), O(n), O(n²), and O(log n) space algorithms to understand the distinction between input and auxiliary space.

Memory Layout VisualizerUnderstanding Auxiliary vs. Input Space
Input Size (n)8

Sum Array (In-Place)

O(1)

Uses only a fixed number of variables regardless of input size

Input Space8 units
Input Array
Auxiliary Space2 units
Input (not counted in space complexity)
Auxiliary Space
Call Stack
def sum_array(arr):
    total = 0         # O(1) space
    for x in arr:     # O(1) space
        total += x
    return total

Analysis

With n = 8:
• Input space: 8 units
• Auxiliary space: 2 units
Space complexity: O(1) (ignoring input)


Analyzing Space in Code

To determine space complexity, we identify all memory allocations and determine how they scale with input size.

Example 1: O(n) Auxiliary Space

This function creates a new array proportional to the input size:

O(n) Space - Creating a New Array
🐍reverse_array.py
1Function Definition

Takes an array and returns a new reversed array. The input array is not modified.

2Get Array Length - O(1)

Storing the length is a single integer, constant space regardless of array size.

3Allocate Result Array - O(n)

This is the key line! We create a NEW array of size n. This requires O(n) auxiliary space because the space grows linearly with input size.

EXAMPLE
For n = 1000, we allocate 1000 new slots
5Loop Variable - O(1)

The loop counter i is just a single integer. Loop variables contribute O(1) space.

6Copy Elements

We copy elements from the original array to the result in reverse order. The space is already allocated; this doesn't add more.

8Return Result

Total auxiliary space: O(n) for the result array, plus O(1) for variables n and i. Final: O(n).

2 lines without explanation
1def reverse_array(arr):
2    n = len(arr)
3    result = [0] * n      # O(n) auxiliary space
4
5    for i in range(n):
6        result[n - 1 - i] = arr[i]
7
8    return result

Example 2: O(1) Auxiliary Space (In-Place)

The same operation can be done with constant extra space by modifying the input directly:

O(1) Space - In-Place Reversal
🐍reverse_inplace.py
1Function Definition

Takes an array and reverses it IN PLACE, modifying the original array directly.

2Left Pointer - O(1)

A single integer tracking the left index. Constant space regardless of array size.

3Right Pointer - O(1)

A single integer tracking the right index. Together with left, we use 2 integers = O(1) space.

5Loop Until Pointers Meet

The loop runs n/2 times, but loop iterations don't add to space complexity - only the variables used do.

7Swap In Place

This is the magic! We swap elements directly in the original array. No new array is created. Python's tuple unpacking uses O(1) temporary space.

EXAMPLE
[1,2,3,4,5] → after swap: [5,2,3,4,1]
11Return Modified Array

Total auxiliary space: O(1) - just two integer pointers! We modified the input instead of creating a copy.

5 lines without explanation
1def reverse_array_inplace(arr):
2    left = 0               # O(1) space
3    right = len(arr) - 1   # O(1) space
4
5    while left < right:
6        # Swap elements
7        arr[left], arr[right] = arr[right], arr[left]
8        left += 1
9        right -= 1
10
11    return arr  # Modified in place

Key insight: The in-place version uses only 2 integer variables regardless of array size. If n = 1,000,000, the first version needs 8 MB extra (assuming 8-byte integers), while the second needs just 16 bytes.

Counting Space Systematically

When analyzing code for space complexity, ask these questions:

  1. What variables are declared? Each primitive variable (int, float, pointer) is O(1) space.
  2. Are any arrays or collections created? A new array of size n is O(n) space.
  3. Is recursion used? Each recursive call adds a stack frame; maximum depth determines stack space.
  4. Are there nested data structures? A 2D array n×m is O(n×m) space.

Quick Check

Which operation uses O(n) auxiliary space?


Recursion and Stack Space

Recursion is a powerful technique, but it has a hidden space cost: the call stack. Every recursive call adds a frame to the stack, and these frames accumulate until base cases are reached.

How the Call Stack Works

When a function is called, the system:

  1. Pushes a stack frame containing parameters, local variables, and return address
  2. Executes the function body
  3. Pops the frame when the function returns

For recursive functions, frames accumulate until the base case is reached, then unwind as functions return.

Example: Fibonacci Recursion

Recursive Fibonacci - O(n) Stack Space
🐍fibonacci_recursive.py
1Recursive Function

Each call to fibonacci creates a new stack frame containing n, the return address, and local state.

2Base Case Check

When n ≤ 1, we return immediately without making more calls. This is where recursion stops.

5Two Recursive Calls

We call fibonacci(n-1) first. This call must complete BEFORE we can call fibonacci(n-2). The calls aren&apos;t simultaneous - the stack grows one call at a time.

EXAMPLE
fib(5) waits for fib(4), which waits for fib(3), etc.
7Stack Frame

Each recursive call pushes a frame onto the call stack. A frame typically contains: parameters (n), return address, and local variables.

8Maximum Depth

The deepest the stack gets is n frames (when computing fib(n-1) → fib(n-2) → ... → fib(1)). After reaching the base case, frames start popping.

9Space Complexity

Even though fib(5) makes ~15 total calls, the MAXIMUM simultaneous frames is 5. Space complexity is about maximum concurrent usage, not total allocations.

3 lines without explanation
1def fibonacci(n):
2    if n <= 1:          # Base case
3        return n
4
5    return fibonacci(n - 1) + fibonacci(n - 2)
6
7# Each call adds a stack frame
8# Maximum depth: n calls deep
9# Stack space: O(n)

Stack Overflow

If recursion is too deep, the call stack exceeds available memory, causing a stack overflow. Most systems have stack limits (typically 1-8 MB). Deep recursion on large inputs can crash your program!

Recursion Space Complexity by Type

Recursion PatternStack DepthSpace ComplexityExample
Linear (f calls f once)nO(n)Factorial, linked list traversal
Binary (halves each call)log nO(log n)Binary search, BST operations
Branching (f calls f k times)nO(n)Fibonacci (max depth, not total calls)
Tail recursion (optimized)1O(1)Tail-call optimized languages

Tail Recursion Optimization

Some languages (Scheme, Haskell, some compilers for C) can optimize tail recursion - where the recursive call is the last operation - to use O(1) stack space. Python and most JavaScript engines do NOT support this optimization.

Interactive: Recursion Stack

Watch how different recursive algorithms build up the call stack. Pay attention to the maximum depth - that determines the space complexity, not the total number of calls.

Recursion Stack VisualizerStack Space Grows with Recursion Depth
Input Size (n)6
Speed:1x

Factorial

Stack Depth: O(n)

Each call waits for the next, creating a linear stack depth

Call Stack(1 frame)

factorial(n=6)
Bottom of Stack

Code

def factorial(n):
    if n <= 1:
        return 1
    return n * factorial(n - 1)
Building Stack
Unwinding (Returning)

Space Complexity Analysis

• Maximum stack depth: 6 frames
• Each frame stores: function arguments, local variables, return address
• Space complexity: O(n) auxiliary space
For n = 6, the algorithm makes 6 nested calls

Quick Check

A recursive function makes 2 recursive calls per invocation (like tree traversal). If the maximum depth is d, what is the stack space complexity?


Space-Time Tradeoffs

One of the most important concepts in algorithm design is the space-time tradeoff: we can often speed up algorithms by using more memory, or reduce memory by accepting slower execution.

Classic Example: Memoization

The naive recursive Fibonacci has O(2⊃n) time but only O(n) stack space. By adding O(n) space for a memo table, we achieve O(n) time:

Memoization - Trading Space for Time
🐍fibonacci_memoized.py
1Memoization with Dictionary

The memo dictionary stores previously computed results. Default argument creates a shared dict across all calls.

2Check Cache First

Before computing, check if we already know the answer. Dictionary lookup is O(1) average time.

9Store Result

After computing a value, store it in the memo table. This takes O(n) space total (one entry per value from 0 to n).

12Time-Space Tradeoff

We traded O(n) space for dramatic time improvement: O(2^n) → O(n). Each subproblem is computed exactly once.

13Total Space

O(n) for the memo table + O(n) for the call stack = O(n) total. The stack doesn&apos;t disappear; memoization just prevents exponential recomputation.

8 lines without explanation
1def fibonacci_memo(n, memo={}):
2    if n in memo:           # O(1) lookup
3        return memo[n]
4
5    if n <= 1:
6        return n
7
8    # Store result before returning
9    memo[n] = fibonacci_memo(n-1, memo) + fibonacci_memo(n-2, memo)
10    return memo[n]
11
12# Time: O(n) - each value computed once
13# Space: O(n) - memo table + O(n) stack = O(n)

Tradeoff Spectrum

More SpaceLess TimeTechnique
Hash tableO(n) → O(1) lookupCaching, indexing
Memo tableExponential → polynomialDynamic programming
Precomputed tableO(1) vs recomputationLookup tables (sin, log)
Graph adjacency matrixO(1) edge checkDense graph operations
Suffix arrayO(1) pattern matchingString algorithms
Less SpaceMore TimeTechnique
In-place sortSame time, O(1) spaceHeap sort vs merge sort
Streaming algorithmsO(1) spaceApproximate counting
Bit manipulationO(1) vs O(n)Bitsets for sets
RecomputationNo storageOn-demand calculation
The Golden Rule: Use space to avoid redundant computation. If you are computing the same thing multiple times, store the result. If you are only using each result once, don't store it.

Interactive: Trading Space for Time

Explore different approaches to the same problem and see how they trade off between time and space complexity. Notice how additional memory can dramatically reduce computation time.

Space-Time TradeoffTrading Memory for Speed (and vice versa)
Input Size (n)20

Fibonacci Numbers

Compute the nth Fibonacci number

Compare Approaches

Naive Recursion
O(2ⁿ)O(n)
Time1,048,576 ops
Space20 units
Memoization
O(n)O(n)
Time20 ops
Space20 units
Tabulation
O(n)O(n)
Time20 ops
Space20 units
Space-Optimized
O(n)O(1)
Time20 ops
Space1 units

Naive Recursion

Pure recursion, recomputes same values many times

Time: O(2ⁿ)
Space: O(n)
def fib(n):
    if n <= 1:
        return n
    return fib(n-1) + fib(n-2)

Key Tradeoff

The naive recursive approach has exponential time but only linear stack space. By using O(n) extra space for memoization, we reduce time to O(n). With careful design, we can even achieve O(1) space while keeping O(n) time.


Common Space Complexity Patterns

Recognizing these patterns will help you quickly estimate space complexity:

O(1) - Constant Space

  • Fixed number of variables
  • In-place algorithms
  • Iterative algorithms with no dynamic allocation
🐍python
1def find_max(arr):
2    max_val = arr[0]  # O(1)
3    for x in arr:
4        if x > max_val:
5            max_val = x
6    return max_val

O(log n) - Logarithmic Space

  • Recursive binary search
  • Balanced tree operations (recursion depth)
  • Divide-and-conquer with O(log n) recursion depth
🐍python
1def binary_search(arr, target, lo, hi):
2    if lo > hi:
3        return -1
4    mid = (lo + hi) // 2
5    if arr[mid] == target:
6        return mid
7    elif arr[mid] < target:
8        return binary_search(arr, target, mid+1, hi)  # O(log n) stack
9    else:
10        return binary_search(arr, target, lo, mid-1)

O(n) - Linear Space

  • Creating a copy of input
  • Hash tables for lookup
  • Recursion with O(n) depth
  • Storing intermediate results
🐍python
1def two_sum(nums, target):
2    seen = {}  # O(n) worst case
3    for i, num in enumerate(nums):
4        complement = target - num
5        if complement in seen:
6            return [seen[complement], i]
7        seen[num] = i
8    return []

O(n²) - Quadratic Space

  • 2D matrices for n elements
  • Adjacency matrix for n vertices
  • Some dynamic programming tables
🐍python
1def floyd_warshall(n, edges):
2    # O(n²) space for distance matrix
3    dist = [[float('inf')] * n for _ in range(n)]
4    # ... fill matrix ...
5    return dist

In-Place Algorithms

An in-place algorithm uses O(1) auxiliary space by modifying the input directly rather than creating copies. This is valuable when memory is constrained or when the input is too large to duplicate.

Examples of In-Place Operations

OperationIn-Place VersionNot In-Place Version
Reverse arrayTwo-pointer swapCreate new reversed array
SortHeap sort, Quick sortMerge sort (standard)
Remove duplicatesOverwrite with two pointersHash set then rebuild
Rotate arrayReverse subarraysCreate rotated copy
PartitionDutch National FlagCreate separate arrays

In-Place Sorting: Quick Sort

Quick sort achieves O(n log n) average time with O(log n) auxiliary space (for recursion), making it preferred over merge sort in many scenarios:

🐍python
1def quicksort(arr, lo, hi):
2    if lo < hi:
3        pivot_idx = partition(arr, lo, hi)  # In-place partitioning
4        quicksort(arr, lo, pivot_idx - 1)   # O(log n) stack depth (average)
5        quicksort(arr, pivot_idx + 1, hi)
6
7def partition(arr, lo, hi):
8    pivot = arr[hi]
9    i = lo - 1
10    for j in range(lo, hi):
11        if arr[j] <= pivot:
12            i += 1
13            arr[i], arr[j] = arr[j], arr[i]  # Swap in place
14    arr[i + 1], arr[hi] = arr[hi], arr[i + 1]
15    return i + 1

Trade-offs of In-Place Algorithms

In-place algorithms modify the input, which can be problematic if:
  • The original data needs to be preserved
  • Multiple threads access the data concurrently
  • The data is read-only or immutable
Always consider whether modifying the input is acceptable in your context.

Real-World Impact

Space complexity has profound implications in real systems:

Example 1: Mobile App Memory

iOS and Android kill apps that use too much memory. A photo editing app that loads all images into RAM will crash on large galleries. Solution: stream images, loading only what's visible (O(viewport) space instead of O(gallery)).

Example 2: Database Sorting

When sorting 100 GB of data on a machine with 16 GB RAM, in-memory merge sort is impossible. Databases use external merge sort, which uses O(memory) space at a time by processing chunks sequentially.

Example 3: Embedded Systems

A microcontroller with 2 KB of RAM cannot use algorithms that allocate large arrays. Embedded developers choose algorithms like:

  • Heap sort (O(1) space) over merge sort (O(n) space)
  • Streaming algorithms for data processing
  • Fixed-size ring buffers instead of dynamic arrays

Example 4: Graph Algorithms

For a social network with 1 billion users:

  • Adjacency matrix: 10&sup18; entries = impossible
  • Adjacency list: O(V + E), feasible for sparse graphs

The space complexity choice determines whether the algorithm is even possible to run.


Summary

Space complexity analysis is essential for building scalable systems:

Key Concepts

ConceptKey Insight
Auxiliary SpaceExtra memory beyond input - what we measure
Stack SpaceRecursion depth determines stack space usage
In-Place AlgorithmsO(1) auxiliary space by modifying input
Space-Time TradeoffUse memory to avoid recomputation, or vice versa
Memory HierarchyExceeding RAM causes catastrophic slowdowns

Space Complexity Cheat Sheet

ComplexityDescriptionExample
O(1)Fixed number of variablesTwo-pointer swap, in-place max
O(log n)Recursive halvingBinary search recursion stack
O(n)Linear data structuresHash table, array copy, memo table
O(n log n)Divide-and-conquer with copyingMerge sort with merging buffer
O(n²)2D structuresAdjacency matrix, DP tables
Key Takeaway: When analyzing algorithms, always ask: "What memory does this allocate, and how does it grow with input?" For recursive code, trace the maximum stack depth. Space-efficient algorithms enable processing of data that exceeds available memory.

Exercises

Analysis Questions

  1. Analyze the space complexity of merge sort. Why is it O(n) even though the recursion depth is O(log n)?
  2. A function calls itself n times, but only keeps the last k results in memory. What is its space complexity?
  3. Compare the space complexity of BFS (using a queue) vs DFS (using recursion) for traversing a tree of height h with n nodes.
  4. An algorithm creates a hash set of unique elements from an array. What are the best-case and worst-case auxiliary space complexities?

Coding Challenges

  1. Implement an in-place algorithm to remove all duplicates from a sorted array, returning the new length.
  2. Given a linked list, reverse it using O(1) extra space.
  3. Implement iterative (O(log n) space) versions of tree traversal (inorder, preorder, postorder).
  4. Design a streaming algorithm to find the median of a data stream using O(n) space where n is the stream length so far.

Design Questions

  1. You need to process a 10 GB file on a machine with 1 GB RAM. What techniques would you use?
  2. A mobile app needs to display a list of 1 million items. How would you design the data structures to minimize memory usage while maintaining scroll performance?

Solution Hints

  • Q1: Merge sort needs O(n) space for the merge step, regardless of recursion depth
  • Q3: BFS uses O(w) queue space where w is max width; DFS uses O(h) stack space where h is height
  • Coding 1: Use two pointers - one for reading, one for writing

In the next section, we'll explore Amortized Analysis - a technique for analyzing algorithms where occasional expensive operations are balanced by many cheap ones, giving a more accurate picture of average-case performance.