Toolmingo
Guides12 min read

Big O Notation Cheat Sheet: Time & Space Complexity Guide

A complete Big O notation guide — all complexity classes explained, data structure operations table, sorting algorithms comparison, and practical code examples. With a quick reference cheat sheet.

Big O notation describes how an algorithm's performance scales with input size. It's the universal language for comparing algorithm efficiency — ignoring constants and low-order terms to focus on the growth rate that matters.

Quick reference

All complexity classes from fastest to slowest, with everyday examples.

Notation Name Example
O(1) Constant Hash map lookup, array index
O(log n) Logarithmic Binary search, balanced BST
O(n) Linear Loop through array, linear search
O(n log n) Linearithmic Merge sort, heap sort, Tim sort
O(n²) Quadratic Bubble sort, nested loops
O(n³) Cubic Matrix multiplication (naive)
O(2ⁿ) Exponential Recursive Fibonacci, subsets
O(n!) Factorial Brute-force traveling salesman

The rule of thumb: for n = 1,000,000, O(1) and O(log n) are instant; O(n) is fast; O(n log n) is acceptable; O(n²) starts to hurt; O(2ⁿ) and O(n!) are infeasible.

Understanding growth rates

Concrete numbers help make this intuitive. Operations for different n values:

n O(1) O(log n) O(n) O(n log n) O(n²) O(2ⁿ)
1 1 0 1 0 1 2
10 1 3 10 33 100 1,024
100 1 7 100 664 10,000 1.27 × 10³⁰
1,000 1 10 1,000 9,966 1,000,000 ∞ (infeasible)
1,000,000 1 20 1,000,000 ~20M 10¹²

O(1) — Constant time

Performance is independent of input size. The fastest possible.

# O(1) examples
def get_first(arr):
    return arr[0]           # Index access — always one step

def lookup(hashmap, key):
    return hashmap[key]     # Hash map lookup — O(1) average

def push(stack, value):
    stack.append(value)     # Push to stack/list end — O(1) amortized

def is_even(n):
    return n % 2 == 0       # Arithmetic — O(1)
// O(1) in JavaScript
const getFirst = arr => arr[0];
const lookup = (map, key) => map.get(key);  // Map.get is O(1)
const peek = stack => stack[stack.length - 1];

O(log n) — Logarithmic time

Input is halved (or divided) each step. Very efficient for large n.

# Binary search — O(log n)
def binary_search(arr, target):
    left, right = 0, len(arr) - 1
    while left <= right:
        mid = (left + right) // 2
        if arr[mid] == target:
            return mid
        elif arr[mid] < target:
            left = mid + 1
        else:
            right = mid - 1
    return -1

# Each iteration halves the search space:
# n=1000 → ~10 iterations (log₂ 1000 ≈ 10)
# n=1000000 → ~20 iterations (log₂ 1000000 ≈ 20)

Other O(log n) operations:

  • Balanced BST lookup, insert, delete
  • Heap push/pop
  • bisect.bisect_left() in Python
  • Exponentiation by squaring

O(n) — Linear time

One pass through the data. The minimum for problems that require reading all input.

# Linear search — O(n)
def find(arr, target):
    for i, val in enumerate(arr):
        if val == target:
            return i
    return -1

# Sum array — O(n)
def total(arr):
    return sum(arr)

# Find max — O(n)
def maximum(arr):
    result = arr[0]
    for val in arr:
        if val > result:
            result = val
    return result

# Count occurrences — O(n)
from collections import Counter
def count_chars(s):
    return Counter(s)   # O(n) where n = len(s)

Key insight: a single loop over n elements is O(n), even with constant-time operations inside.

O(n log n) — Linearithmic time

The lower bound for comparison-based sorting. Typical of divide-and-conquer algorithms.

# Merge sort — O(n log n)
def merge_sort(arr):
    if len(arr) <= 1:
        return arr
    mid = len(arr) // 2
    left = merge_sort(arr[:mid])   # O(log n) levels of recursion
    right = merge_sort(arr[mid:])
    return merge(left, right)       # O(n) merge at each level

def merge(left, right):
    result = []
    i = j = 0
    while i < len(left) and j < len(right):
        if left[i] <= right[j]:
            result.append(left[i])
            i += 1
        else:
            result.append(right[j])
            j += 1
    return result + left[i:] + right[j:]

# Python's built-in sort (Timsort) is O(n log n)
arr.sort()
sorted_arr = sorted(arr)

O(n²) — Quadratic time

Nested loops over the same data. Acceptable for small n (< 1,000), problematic for large n.

# Bubble sort — O(n²)
def bubble_sort(arr):
    n = len(arr)
    for i in range(n):              # outer loop: n times
        for j in range(n - i - 1): # inner loop: n times
            if arr[j] > arr[j + 1]:
                arr[j], arr[j + 1] = arr[j + 1], arr[j]

# Find all pairs — O(n²)
def all_pairs(arr):
    pairs = []
    for i in range(len(arr)):
        for j in range(i + 1, len(arr)):
            pairs.append((arr[i], arr[j]))
    return pairs

# Two-sum naive — O(n²)  [the O(n) version uses a hash map]
def two_sum_naive(arr, target):
    for i in range(len(arr)):
        for j in range(i + 1, len(arr)):
            if arr[i] + arr[j] == target:
                return [i, j]

Spot the pattern: any algorithm with two nested loops that each scale with n is O(n²).

O(2ⁿ) — Exponential time

Doubles with each additional element. Only feasible for very small n (< ~30).

# Naive recursive Fibonacci — O(2ⁿ)
def fib(n):
    if n <= 1:
        return n
    return fib(n - 1) + fib(n - 2)  # Two calls, each branching again

# Fix: memoization brings it to O(n)
from functools import lru_cache
@lru_cache(maxsize=None)
def fib_memo(n):
    if n <= 1:
        return n
    return fib_memo(n - 1) + fib_memo(n - 2)

# Generate all subsets — O(2ⁿ)
def all_subsets(arr):
    if not arr:
        return [[]]
    rest = all_subsets(arr[1:])
    return rest + [[arr[0]] + subset for subset in rest]

O(n!) — Factorial time

Grows faster than exponential. Only viable for n ≤ 10–12.

# Generate all permutations — O(n!)
from itertools import permutations

def all_perms(arr):
    return list(permutations(arr))

# Brute-force traveling salesman — O(n!)
def tsp_brute(cities, dist):
    from itertools import permutations
    best = float('inf')
    for perm in permutations(cities):
        cost = sum(dist[perm[i]][perm[i+1]] for i in range(len(perm)-1))
        best = min(best, cost)
    return best

Data structure operations

Time complexity for common data structure operations (average case):

Structure Access Search Insert Delete Notes
Array O(1) O(n) O(n) O(n) Insert/delete shifts elements
Dynamic array (list) O(1) O(n) O(1)* O(n) *amortized append
Linked list O(n) O(n) O(1) O(1) Insert/delete at known node
Stack O(n) O(n) O(1) O(1) Push/pop only
Queue O(n) O(n) O(1) O(1) Enqueue/dequeue only
Deque O(n) O(n) O(1) O(1) Both ends
Hash map N/A O(1) O(1) O(1) O(n) worst case (collision)
Hash set N/A O(1) O(1) O(1) O(n) worst case
BST (balanced) O(log n) O(log n) O(log n) O(log n) AVL, Red-Black
BST (unbalanced) O(n) O(n) O(n) O(n) Degenerate case
Heap O(1) min/max O(n) O(log n) O(log n) Priority queue
Trie O(k) O(k) O(k) O(k) k = key length

Sorting algorithms

Algorithm Best Average Worst Space Stable
Bubble sort O(n) O(n²) O(n²) O(1) Yes
Selection sort O(n²) O(n²) O(n²) O(1) No
Insertion sort O(n) O(n²) O(n²) O(1) Yes
Merge sort O(n log n) O(n log n) O(n log n) O(n) Yes
Quick sort O(n log n) O(n log n) O(n²) O(log n) No
Heap sort O(n log n) O(n log n) O(n log n) O(1) No
Counting sort O(n+k) O(n+k) O(n+k) O(k) Yes
Radix sort O(nk) O(nk) O(nk) O(n+k) Yes
Timsort (Python/JS) O(n) O(n log n) O(n log n) O(n) Yes

When to use what:

  • Small n (< 20): insertion sort (low overhead)
  • General purpose: merge sort or built-in sort (Timsort)
  • Memory constrained: heap sort
  • Nearly sorted data: insertion sort or Timsort
  • Integer keys in range: counting or radix sort

Space complexity

Big O also measures memory usage. The same notation applies.

# O(1) space — only uses a fixed number of variables
def sum_array(arr):
    total = 0        # O(1) extra space
    for x in arr:
        total += x
    return total

# O(n) space — stores n elements
def copy_array(arr):
    return arr[:]    # New array of size n

# O(n) space — recursion stack depth
def factorial(n):
    if n <= 1:
        return 1
    return n * factorial(n - 1)  # n stack frames

# O(log n) space — binary search recursive
def binary_search_rec(arr, target, left=0, right=None):
    if right is None:
        right = len(arr) - 1
    if left > right:
        return -1
    mid = (left + right) // 2
    if arr[mid] == target:
        return mid
    elif arr[mid] < target:
        return binary_search_rec(arr, target, mid + 1, right)
    else:
        return binary_search_rec(arr, target, left, mid - 1)
# Stack depth = O(log n) since we halve each time

Recognizing complexity from code

Pattern recognition guide:

# O(1): No loops, no recursion relative to n
result = arr[0] + arr[-1]

# O(log n): Input is halved each step
while n > 1:
    n //= 2

# O(n): Single loop through data
for x in arr:
    process(x)

# O(n log n): Sort + linear scan, or divide-and-conquer
arr.sort()           # O(n log n)
for x in arr:        # O(n)
    ...              # total: O(n log n)

# O(n²): Nested loops both over n
for i in range(n):
    for j in range(n):
        ...

# O(n²): Quadratic even with different variables if both scale with n
for i in range(len(arr)):
    for j in range(len(arr)):
        ...

# O(n + m): Two separate loops over different inputs
for x in list_a:    # O(n)
    ...
for y in list_b:    # O(m)
    ...

# O(n * m): Nested loops over different inputs
for x in list_a:
    for y in list_b:
        ...

Amortized complexity

Some operations are occasionally expensive but cheap on average.

# Dynamic array append — O(1) amortized, O(n) worst case
arr = []
for i in range(n):
    arr.append(i)    # Usually O(1), occasionally O(n) when resizing
# Total: O(n) for n appends → O(1) amortized per append

# Hash map operations — O(1) amortized
d = {}
for i in range(n):
    d[i] = i         # O(1) amortized (rare resizing/rehashing)

Common algorithm patterns and their complexity

Pattern Typical complexity Example
Two pointers O(n) Find pair summing to target in sorted array
Sliding window O(n) Longest substring without repeating chars
Binary search O(log n) Search in sorted array
BFS / DFS O(V + E) Graph traversal (V = vertices, E = edges)
Dynamic programming O(n²) typical Longest common subsequence
Divide and conquer O(n log n) Merge sort
Backtracking O(2ⁿ) or O(n!) Permutations, N-Queens
Greedy Varies Activity selection: O(n log n)
Memoization Reduces exponential → polynomial Fibonacci: O(2ⁿ) → O(n)

Optimisation examples

Seeing how to improve complexity:

# BEFORE: O(n²) — check if any two numbers sum to target
def has_pair_naive(arr, target):
    for i in range(len(arr)):
        for j in range(i + 1, len(arr)):
            if arr[i] + arr[j] == target:
                return True
    return False

# AFTER: O(n) — use a hash set
def has_pair_fast(arr, target):
    seen = set()
    for x in arr:
        if target - x in seen:
            return True
        seen.add(x)
    return False


# BEFORE: O(n²) — count duplicates with nested loop
def count_dupes_naive(arr):
    count = 0
    for i in range(len(arr)):
        for j in range(i + 1, len(arr)):
            if arr[i] == arr[j]:
                count += 1
    return count

# AFTER: O(n) — use Counter
from collections import Counter
def count_dupes_fast(arr):
    freq = Counter(arr)
    return sum(c * (c - 1) // 2 for c in freq.values())


# BEFORE: O(n) — check membership in list
items = [1, 2, 3, ..., n]
if x in items:   # O(n) scan

# AFTER: O(1) — use set
items_set = set(items)  # O(n) once
if x in items_set:       # O(1) lookup

Common mistakes

Mistake Why it's wrong Fix
Ignoring constants in small-n code O(1000n) is technically O(n) but 1000× slower than O(n) Consider actual constants for practical perf
Assuming hash maps are always O(1) Worst case is O(n) due to collisions True in practice, but be aware
Forgetting recursion stack space Recursive algorithms use O(depth) stack space Add space complexity to analysis
Counting nested loops as O(n²) blindly Only if both loops scale with n Two loops each over different fixed-size inputs is O(1)
Ignoring input shape arr.sort() is O(n log n) but O(n) on nearly-sorted data Know your data
Measuring wrong n n should be the relevant input size For string algorithms, n = length; for graphs, n = vertices
Premature optimisation O(n²) is fine for n < 100 Profile first; optimise where it matters

Big O, Big Θ, Big Ω

Three related notations that are often confused:

Notation Meaning Example
O (Big O) Upper bound — worst case Quick sort is O(n²) worst case
Ω (Big Omega) Lower bound — best case Any comparison sort is Ω(n log n)
Θ (Big Theta) Tight bound — exact growth Merge sort is Θ(n log n)

In practice, "Big O" is used loosely to mean "the expected growth rate", not strictly the worst case. Context usually makes this clear.

FAQ

Q: Does Big O account for real-world performance?
No. O(n) with a large constant can be slower than O(n²) for small n. Big O describes asymptotic behaviour (as n → ∞). Always profile real code — the "theoretically better" algorithm isn't always faster in practice.

Q: What's the difference between O(n) and O(2n)?
Nothing — constants are dropped. O(2n) = O(n). Big O only captures the growth rate, not the exact count of operations.

Q: Is O(n + m) the same as O(n)?
Not necessarily. If m can grow independently of n (like processing two separate inputs), keep O(n + m). If m is bounded by n, it simplifies. For example, BFS on a graph is O(V + E), not O(V).

Q: How do I calculate complexity for recursive functions?
Use the recurrence relation and solve with the Master Theorem. T(n) = 2T(n/2) + O(n) → O(n log n) (merge sort). Or draw the recursion tree: depth × work per level.

Q: When should I actually care about Big O?
When n is large enough that growth rate matters. For n < 100, almost anything works. For n > 100,000, O(n²) algorithms become painfully slow. For n > 10,000,000, even O(n log n) needs care.

Q: Is O(log n) always base-2?
The base doesn't matter for Big O — log₂n and log₁₀n differ only by a constant factor. By convention, log in CS usually means log₂, but Big O drops the base.

Related tools

Keep reading

All Toolmingotools are free & run in your browser

No sign-up, no upload, no watermark. Your files never leave your device.

Browse all tools