Time vs Space Complexity: The Trade-Off
Time complexity measures how the number of operations grows with input size; space complexity measures how the extra memory grows. Both are written in Big-O, and the central skill of DSA is choosing the right balance between them — because you can very often spend more memory to save time, or the reverse.
This page explains what each measures, then shows the time-space trade-off with a concrete example you can run in any language.
What time complexity measures
Time complexity counts the operations an algorithm performs as a function of input size n. We ignore constants and keep the dominant term: a loop over the array is O(n), a loop inside a loop is O(n²). It’s a model of growth, not a stopwatch reading — it predicts how the runtime scales, not its exact milliseconds.
What space complexity measures
Space complexity counts the extra memory an algorithm uses beyond its input — usually called auxiliary space. A loop that uses a couple of counters is O(1) space. Building a new array of size n is O(n) space. The recursion call stack counts too: a recursion n levels deep is O(n) space even if you allocate nothing yourself.
#include <vector>
using namespace std;
// O(n) time, O(1) auxiliary space: just a running total.
long long sum(const vector<int>& a) {
long long total = 0;
for (int x : a) total += x;
return total;
}
// O(n) time, O(1) auxiliary space: just a running total.
long sum(int[] a) {
long total = 0;
for (int x : a) total += x;
return total;
}
// O(n) time, O(1) auxiliary space: just a running total.
function sum(a) {
let total = 0;
for (const x of a) total += x;
return total;
}
# O(n) time, O(1) auxiliary space: just a running total.
def sum_all(a):
total = 0
for x in a:
total += x
return total
The trade-off in action
Consider: does an array contain any duplicate value? The memory-free approach compares every pair — O(n²) time, O(1) space:
#include <vector>
using namespace std;
bool hasDuplicate(const vector<int>& a) {
for (size_t i = 0; i < a.size(); i++)
for (size_t j = i + 1; j < a.size(); j++)
if (a[i] == a[j]) return true;
return false;
}
boolean hasDuplicate(int[] a) {
for (int i = 0; i < a.length; i++)
for (int j = i + 1; j < a.length; j++)
if (a[i] == a[j]) return true;
return false;
}
function hasDuplicate(a) {
for (let i = 0; i < a.length; i++)
for (let j = i + 1; j < a.length; j++)
if (a[i] === a[j]) return true;
return false;
}
def has_duplicate(a):
n = len(a)
for i in range(n):
for j in range(i + 1, n):
if a[i] == a[j]:
return True
return False
Now spend memory to save time: a hash set remembers what we’ve seen, turning each lookup into O(1). The result is O(n) time but O(n) space:
#include <vector>
#include <unordered_set>
using namespace std;
bool hasDuplicate(const vector<int>& a) {
unordered_set<int> seen;
for (int x : a) {
if (seen.count(x)) return true;
seen.insert(x);
}
return false;
}
import java.util.*;
boolean hasDuplicate(int[] a) {
Set<Integer> seen = new HashSet<>();
for (int x : a) {
if (!seen.add(x)) return true; // add returns false if already present
}
return false;
}
function hasDuplicate(a) {
const seen = new Set();
for (const x of a) {
if (seen.has(x)) return true;
seen.add(x);
}
return false;
}
def has_duplicate(a):
seen = set()
for x in a:
if x in seen:
return True
seen.add(x)
return False
Same problem, two valid answers. We paid O(n) memory to cut the time from O(n²) to O(n) — usually a great deal when n is large.
How to choose
- Optimize time when inputs are large and memory is plentiful — the common interview default. Most “make it faster” answers add a hash map or precomputed table.
- Optimize space when memory is the bottleneck: huge datasets, embedded systems, or when an in-place algorithm is required. Reversing an array in place is O(1) space; copying to a new one is O(n).
- Both matter. State both complexities for every solution. Interviewers expect “O(n) time, O(n) space” — not just the time half.
For beginners: “Can I trade memory for speed?” is the question to ask whenever you see nested loops. A set or map that records what you’ve already computed is the most common way to delete an inner loop.
Going deeper: Recursive solutions hide space in the call stack. A naive recursion may be O(2ⁿ) time and O(n) stack space; adding memoization trades O(n) memory to collapse the time. Always account for stack depth — see analyzing recursive code.
Next steps
Now learn to derive these numbers yourself: analyzing loops for iterative code and analyzing recursion for recursive code. Then keep the complexity cheat sheet handy and test yourself with the complexity exercises.