Space complexity describes how much memory an algorithm uses as the input size grows. Just like time complexity focuses on running time, space complexity focuses on memory usage.
This topic is important because an algorithm may be fast but still consume too much memory. In practical systems, both time and space must be considered before choosing a solution.
Space complexity usually has two parts:
| Component | Meaning | Example |
|---|---|---|
| Input space | Memory used to store the input itself | An array of n integers needs O(n) input space |
| Auxiliary space | Extra memory used by the algorithm apart from input | Temporary arrays, recursion stack, hash maps |
In most algorithm analysis discussions, we focus more on auxiliary space, because input space is usually given as part of the problem.
This difference is very important:
i, sum, and max, then the input space is O(n) but the auxiliary space is O(1).
| Complexity | Name | Typical Meaning | Example |
|---|---|---|---|
| O(1) | Constant space | Uses fixed extra memory | In-place swap, finding maximum |
| O(log n) | Logarithmic space | Usually recursion depth is logarithmic | Recursive Binary Search |
| O(n) | Linear space | Extra memory grows directly with input | Merge Sort auxiliary array, hash table |
| O(n^2) | Quadratic space | Uses a 2D tl-table or matrix | Adjacency matrix, 2D DP table |
An algorithm has constant space complexity when it uses only a fixed number of variables, regardless of input size.
def find_max(arr):
max_val = arr[0]
for num in arr:
if num > max_val:
max_val = num
return max_val
def reverse_in_place(arr):
left, right = 0, len(arr) - 1
while left < right:
arr[left], arr[right] = arr[right], arr[left]
left += 1
right -= 1
These algorithms work directly on the input and use only a few extra variables, so their auxiliary space is O(1).
Logarithmic space usually appears in recursive algorithms where the recursion depth grows like log n.
def binary_search(arr, target, left, right):
if left > right:
return -1
mid = (left + right) // 2
if arr[mid] == target:
return mid
if arr[mid] < target:
return binary_search(arr, target, mid + 1, right)
return binary_search(arr, target, left, mid - 1)
# Each call cuts the problem roughly in half.
# Recursion depth is O(log n), so stack space is O(log n).
Linear space means the algorithm creates extra memory proportional to the input size.
def count_frequencies(arr):
freq = {}
for num in arr:
freq[num] = freq.get(num, 0) + 1
return freq
def factorial(n):
if n <= 1:
return 1
return n * factorial(n - 1)
# The hash map can grow to O(n).
# Recursive factorial uses O(n) call stack space.
Quadratic space appears when the algorithm stores a 2D structure such as a matrix or table.
def create_matrix(n):
matrix = [[0] * n for _ in range(n)]
return matrix
# Matrix has n rows and n columns.
# Total extra space is O(n^2).
Every recursive call adds a new frame to the call stack. That frame stores local variables, parameters, and the return address.
| Recursive Algorithm | Recursion Depth | Stack Space |
|---|---|---|
| Factorial | n | O(n) |
| Recursive Binary Search | log n | O(log n) |
| Merge Sort | log n | O(log n) stack, plus O(n) auxiliary arrays |
| Naive Fibonacci | n | O(n) stack |
An in-place algorithm modifies the input directly and uses only a constant amount of extra memory. Such algorithms are very useful when memory is limited.
| Algorithm | Auxiliary Space | In-Place? |
|---|---|---|
| Bubble Sort | O(1) | Yes |
| Selection Sort | O(1) | Yes |
| Heap Sort | O(1) | Yes |
| Merge Sort | O(n) | No |
A very common idea in DAA is the time-space trade-off. Sometimes we use more memory to make an algorithm faster. Sometimes we save memory but accept slower execution.
| Problem | Memory-Saving Approach | Faster Approach | Trade-off |
|---|---|---|---|
| Fibonacci | Iterative with 2 variables: O(1) space | DP array: O(n) space | Same time, but iterative saves space |
| Duplicate detection | Sort first: low extra space | Hash set: O(n) space | Hashing is faster but uses more memory |
| Sorting | Heap Sort: O(1) space | Merge Sort: O(n) space | Merge Sort needs more memory |
# O(n) space
def fib_array(n):
if n <= 1:
return n
dp = [0] * (n + 1)
dp[1] = 1
for i in range(2, n + 1):
dp[i] = dp[i - 1] + dp[i - 2]
return dp[n]
# O(1) space
def fib_optimized(n):
if n <= 1:
return n
prev2, prev1 = 0, 1
for i in range(2, n + 1):
curr = prev1 + prev2
prev2 = prev1
prev1 = curr
return prev1
The following tl-table gives a rough intuition about how memory grows for different complexity classes:
| Space Complexity | n = 100 | n = 1,000 | n = 10,000 | Main Observation |
|---|---|---|---|---|
| O(1) | Constant | Constant | Constant | Almost no growth |
| O(log n) | Very small | Still very small | Small | Grows very slowly |
| O(n) | Moderate | Larger | Much larger | Direct proportional growth |
| O(n^2) | Large | Very large | Often impractical | Becomes expensive quickly |
Explore 500+ free tutorials across 20+ languages and frameworks.