Hello! Welcome back to our course.
In the last lesson, we developed a systematic way to analyze the time complexity of iterative algorithms. We learned to identify patterns like , , and by counting operations in loops. Time efficiency, however, is only one side of the coin. An algorithm's performance is also judged by how much memory it consumes.
Today, we'll explore this second pillar of efficiency: space complexity. Our goal is to explain what space complexity is, why it's a critical factor in algorithm design—especially for data engineers working with large datasets—and how to analyze it using the same Big O notation you're now familiar with.
1. What is Space Complexity?
While time complexity measures how many operations an algorithm performs, space complexity measures the amount of memory an algorithm needs to run as a function of its input size.
In an interview context, it's crucial to distinguish between two components of an algorithm's memory usage.

- Input Space: The memory used to store the input data. For example, if your function receives a list of integers, the input space is proportional to .
- Auxiliary Space: This is the extra or temporary memory that the algorithm allocates while it runs. This includes new data structures it creates or space used by function calls (which we'll explore more with recursion).
In most technical interviews, when asked for "space complexity," the interviewer is interested in the auxiliary space complexity. They want to know how much additional memory your algorithm uses, beyond the space taken by the inputs.
Space Complexity: How Algorithms Use Memory
To start, let's read a brief section that defines space complexity and explains why it's so important in modern computing.
Read the first two subsections, 'What is space complexity?' and 'Why space complexity matters'. Pay close attention to the distinction between total space and auxiliary space, and the real-world examples related to cloud computing and data streaming.
As the article highlights, for a Data Engineer, optimizing for space can directly translate to cost savings in the cloud and enable the processing of massive datasets on hardware with finite memory.
2. The Components of Space Complexity
When calculating auxiliary space, we follow a similar logic to time complexity: we count what matters and ignore what doesn't. The video below provides a very clear, structured breakdown.
Understanding the Space Complexity of an Algorithm
This video from Neso Academy formally defines the different parts that contribute to an algorithm's memory usage and clarifies which ones we typically include in our analysis.
Watch the segment from 03:49 to 05:58. The key takeaway is that we ignore constant-sized memory (like simple variables) and focus on memory that grows with the input size, such as data structures or the recursion stack.
To summarize, when analyzing the auxiliary space of an iterative algorithm, we primarily focus on:
- Data Structures: Any new arrays, lists, sets, dictionaries, etc., that the algorithm creates.
- Variables: We generally ignore simple variables (integers, pointers) as they take up constant, , space.
Let's see a practical comparison of two approaches to the same problem, one that uses extra space and one that doesn't.
Time & Space Complexity - Big O Notation - DSA Course in Python Lecture 1
This quick example from Greg Hogg perfectly illustrates the difference between an algorithm that requires extra space and one that works 'in-place'.
Watch from 08:30 to 10:28. Notice how creating a new array for the squared numbers results in O(n) space, while modifying the original array uses O(1) auxiliary space.
This concept of in-place algorithms (those with auxiliary space) is extremely important. They are highly memory-efficient, which is a desirable quality for production code.
3. Common Space Complexity Classes
Just like with time, we use Big O notation to classify how an algorithm's memory usage scales. The growth rates are the same, but now they refer to memory instead of operations.

Let's look at the most common space complexity classes you'll encounter.
Space Complexity: How Algorithms Use Memory
The following article provides excellent, clear examples for each of the main space complexity classes. We will use it as a reference.
Read the sections titled 'Constant space complexity: O(1)', 'Linear space complexity: O(n)', 'Logarithmic and sublinear space complexity', and 'Quadratic space complexity: O(n²)'. Focus on the code examples to understand what kind of operations lead to each complexity class.
Here's a summary of the key classes from the article:
-
— Constant Space: The algorithm uses a fixed amount of memory regardless of the input size. This is the ideal for memory efficiency. Swapping two variables or iterating through an array with a few index variables are space operations.
# O(1) auxiliary space def get_max_element(numbers): max_val = numbers[0] # 'max_val' and 'num' are O(1) space for num in numbers: if num > max_val: max_val = num return max_val -
— Linear Space: The extra memory used grows in direct proportion to the input size . This often happens when you create a copy of the input or use a helper data structure that stores one element for each input element.
# O(n) auxiliary space def reverse_list(numbers): # 'reversed_arr' grows with the size of 'numbers' reversed_arr = [] for i in range(len(numbers) - 1, -1, -1): reversed_arr.append(numbers[i]) return reversed_arr -
— Logarithmic Space: Memory usage grows logarithmically with the input size. This is less common in iterative algorithms but is a key feature of the space complexity for certain recursive algorithms (like recursive binary search), where the number of nested function calls on the stack is . We will revisit this in the recursion module.
-
— Quadratic Space: Memory usage is proportional to the square of the input size. A common example is creating an adjacency matrix to represent a graph with vertices, which requires an grid.
4. The Time vs. Space Trade-off
Often, you can make an algorithm faster by using more memory, or make it more memory-efficient by accepting a slower runtime. This is known as the time-space trade-off, and it's a fundamental concept in algorithm design.
A classic interview example is finding a duplicate number in a list.
-
Approach 1 (More Space, Less Time): Iterate through the list and store each number in a hash set. If you encounter a number that's already in the set, you've found a duplicate.
- Time Complexity: , because you visit each element once.
- Auxiliary Space Complexity: , because in the worst case, you store all elements in the hash set.
-
Approach 2 (Less Space, More Time): For each element, iterate through the rest of the list to see if a duplicate exists.
- Time Complexity: , due to the nested loop.
- Auxiliary Space Complexity: , as you only use a few variables to keep track of indices.
Which approach is better? It depends on the constraints. If the input list is small, the time might be acceptable. If the list is massive and you have plenty of RAM, the time / space solution is far superior. Recognizing and discussing these trade-offs is a sign of a mature problem-solver.
5. Practice: Your Turn to Analyze
Now, let's analyze the auxiliary space complexity of the following Python functions.
Problem 1:
A function that calculates the sum of a list of numbers.
def calculate_sum(numbers):
total = 0 # One variable
for num in numbers:
total += num
return total
Show Answer
The auxiliary space complexity is .
The function only allocates memory for a single variable, total, and the loop variable num. The amount of memory used does not change no matter how large the input numbers list is. Therefore, it uses constant extra space.
Problem 2:
A function that finds all unique elements from a list.
def find_unique_elements(numbers):
unique_set = set() # A new data structure
for num in numbers:
unique_set.add(num)
# Convert set back to a list before returning
return list(unique_set)
Show Answer
The auxiliary space complexity is .
The function creates a new data structure, unique_set. In the worst-case scenario (where all elements in the input numbers list are unique), the unique_set will grow to hold all elements. Since the memory required for the set is directly proportional to the input size , the space complexity is linear. The final conversion to a list also takes space.
Problem 3:
A function that generates a list of all pairs from an input list.
def generate_all_pairs(items):
n = len(items)
pairs = [] # A new list to store pairs
for i in range(n):
for j in range(n):
pairs.append((items[i], items[j]))
return pairs
Show Answer
The auxiliary space complexity is .
The function creates a list called pairs. The nested loops generate pairs. The pairs list will store all of these pairs. Since the size of the memory allocated grows with the square of the input size, the space complexity is quadratic.
Conclusion
Fantastic work today. You have now learned about the second critical dimension of algorithm efficiency: space complexity.
Here are the main takeaways:
- Space Complexity measures an algorithm's memory usage as a function of its input size.
- Auxiliary Space is the extra memory used by an algorithm, and it's typically the focus of interview questions.
- We use Big O notation to classify space complexity, with common classes being (constant), (linear), and (logarithmic).
- An in-place algorithm has an auxiliary space complexity of .
- There is often a time-space trade-off, where you can improve runtime at the cost of using more memory, or vice versa.
So far, we've discussed how to analyze the time and space complexity of algorithms in a general sense. But how efficient are the fundamental tools you use every day in Python? In our next lesson, we will analyze the time and space efficiency of Python's built-in list and dictionary operations, a crucial piece of knowledge for writing high-performance code.