Complexity Analysis
Big O Notation
Big O notation is a mathematical way to describe the limiting behavior of a function as its argument grows. In computer science we use it to describe how an algorithm's time or space requirements scale with input size, ignoring constant factors and lower-order terms.
Big O notation provides an upper bound on the growth rate of an algorithm's resource requirements. It helps us understand how an algorithm's performance scales with input size, allowing us to compare different algorithms and make informed decisions about which one to use.
Common Big O Complexities
| Notation | Name | Example |
|---|---|---|
| O(1) | Constant | Array access; hash table lookup (average case — worst case is O(n) when every key collides) |
| O(log n) | Logarithmic | Binary search |
| O(n) | Linear | Linear search, iterating through array |
| O(n log n) | Linearithmic | Merge sort, heap sort |
| O(n²) | Quadratic | Bubble sort, nested loops |
| O(2ⁿ) | Exponential | Recursive Fibonacci (naive) |
| O(n!) | Factorial | Generating all permutations |
Time Complexity
Time complexity describes how the runtime of an algorithm increases as the input size grows. It's one of the most important metrics for evaluating algorithm efficiency.
Analyzing Time Complexity
To analyze time complexity, count the number of basic operations performed:
# Example: Linear Search
def linear_search(arr, target):
for i in range(len(arr)): # O(n) iterations
if arr[i] == target: # O(1) operation
return i # O(1) operation
return -1 # O(1) operation
# Overall: O(n) time complexity
Asymptotic Notation and Case Analysis Are Two Different Things
This is the most commonly confused point in complexity analysis, and it is worth getting straight before anything else. You will often see O described as "worst case", Ω as "best case" and Θ as "average case". That is not what they mean. They are independent ideas that combine freely.
Asymptotic notation bounds a function
Given some function f(n) — whatever function you have chosen to study — the notation describes how it grows:
- O(g(n)) — upper bound. f grows no faster than g. Formally: there exist constants c > 0 and n0 such that f(n) ≤ c·g(n) for all n ≥ n0.
- Ω(g(n)) — lower bound. f grows at least as fast as g.
- Θ(g(n)) — tight bound. Both at once: f is O(g) and Ω(g).
- o(g(n)) and ω(g(n)): strict versions — f grows strictly slower, or strictly faster.
Case analysis chooses which function you bound
- Best case: the input of size n that the algorithm handles fastest.
- Average case: the expectation over some stated input distribution. "Average" is meaningless without saying which distribution.
- Worst case: the input of size n that the algorithm handles slowest. This is usually what you want, because it is the only guarantee.
The two axes combine
Every one of these statements about insertion sort is well-formed and true:
Best case is Θ(n) - already-sorted input, one comparison per element
Worst case is Θ(n²) - reverse-sorted input
Worst case is O(n³) - true, just a loose upper bound
Best case is O(n²) - also true, also loose
Worst case is Ω(n²) - the quadratic behaviour is unavoidable on that input
Notice that "best case is O(n²)" is perfectly valid. O is an upper bound, and an upper bound does not have to be tight. If O really meant "worst case", that sentence would be self-contradictory.
In everyday use people write O when they mean Θ, and it is usually harmless. But when a bound matters — when you are proving something, or comparing two algorithms whose bounds are close — say which case you are analysing and which bound you are asserting.
Space Complexity
Space complexity describes how much memory an algorithm uses relative to the input size.
Auxiliary space is the extra memory the algorithm allocates beyond its input; total space includes the input itself. By convention, when someone says "binary search uses O(1) space" they mean auxiliary space — the input array is O(n) but is not counted, because the caller already had it. Every space figure on this site is auxiliary space unless stated otherwise. It is worth being explicit about which you mean, since the two differ by an O(n) term for any algorithm that takes an array.
Space Complexity Examples
# O(1) space - constant space
def find_max(arr):
max_val = arr[0] # O(1) space
for num in arr: # O(1) space for loop variable
if num > max_val:
max_val = num
return max_val
# O(n) space - linear space
def copy_array(arr):
result = [] # O(n) space
for num in arr:
result.append(num)
return result
# O(log n) space - recursive call stack
def binary_search_recursive(arr, target, left, right):
if left > right:
return -1
mid = (left + right) // 2
if arr[mid] == target:
return mid
elif arr[mid] < target:
return binary_search_recursive(arr, target, mid + 1, right)
else:
return binary_search_recursive(arr, target, left, mid - 1)
Complexity Comparison
Understanding how different complexities compare helps in choosing the right algorithm:
For an input size of n = 1,000,000:
- O(1): 1 operation
- O(log n): ~20 operations
- O(n): 1,000,000 operations
- O(n log n): ~20,000,000 operations (n × log₂(n) ≈ 1,000,000 × 20)
- O(n²): 1,000,000,000,000 operations
As you can see, the difference between O(n) and O(n²) becomes enormous as input size grows!
Practical Tips for Complexity Analysis
- Focus on the dominant term: O(n² + n) = O(n²)
- Ignore constants: O(2n) = O(n). Note this makes O(n/2) and O(n) the same set — they are equal, not approximately equal.
- Consider nested loops: nested loops often indicate O(n²) or higher — but check the bounds. A loop running to n with an inner loop running to log n is O(n log n).
- Recursion depth matters: each recursive call adds a stack frame, so depth is a space cost.
- Data structure choice: different operations have different complexities.
Amortized Analysis
Some operations are occasionally expensive but cheap on average across a sequence, and worst-case analysis of a single operation badly misrepresents them. Appending to a dynamic array is the standard example: usually O(1), but when capacity is exhausted the array is reallocated and copied in O(n). Because capacity doubles, those copies happen exponentially rarely, and the total cost of n appends is O(n) — so each append is O(1) amortized.
Amortized is not the same as average-case. Average-case is a probabilistic claim over inputs; amortized is a worst-case guarantee over a sequence of operations, with no randomness involved. Union-Find's O(α(n)) and the O(1) of hash table insertion under doubling are both amortized bounds.
Where Asymptotic Analysis Stops Helping
- Constants matter at real sizes. An O(n log n) algorithm with a large constant can lose to an O(n²) one for n in the hundreds. This is exactly why practical sorts switch to insertion sort on small subarrays.
- The RAM model ignores caches. It charges the same for every memory access, but a cache miss can cost two orders of magnitude more than a hit. This is why quicksort routinely beats heapsort despite the worse worst case — it has far better locality.
- Pseudo-polynomial is not polynomial. An algorithm running in O(n·W) is polynomial in the value W but exponential in the number of bits needed to write W down. See the knapsack discussion in Dynamic Programming.
What's Next?
Now that you understand complexity analysis, you're ready to explore specific algorithms. Check out:
- Recursion & Divide-and-Conquer - Understand recursive complexity
- Trees - Analyze tree operation complexities
- Sorting Algorithms - See complexity analysis in action
- Searching Algorithms - Compare O(n) vs O(log n) search