On This Page

What Is Big-O?

Big-O notation describes how an algorithm's runtime or space usage grows as the input size grows. It strips away constants and lower-order terms to focus on the shape of the growth.

A linear work model doubles when n doubles; a quadratic model grows fourfold. Big-O gives an asymptotic upper bound, rather than an exact runtime prediction. O(1) bounds work independently of n under the stated operation-cost model.

There are actually three notations that people conflate:

Big-O alone does not specify worst, average, or best case. State the case separately. Use Θ when you have a matching upper and lower bound.

💡 Quick Mental Model For n = 1,000,000, representative function values are log₂ n ≈ 20, √n = 1,000, n log₂ n ≈ 20 million, and n² = one trillion. These compare growth, not measured runtimes; constants, hardware, language, and input shape still matter.

The Hierarchy (Memorize This)

These representative functions are ordered by asymptotic growth. A feasible algorithm depends on input size, constants, output size, and the actual time and memory budget.

NotationNamen = 10n = 100n = 1,000n = 1,000,000Growth
O(1)Constant1111Constant
O(log n)Logarithmic371020Logarithmic
O(√n)Square root310321,000Sublinear
O(n)Linear101001,0001,000,000Linear
O(n log n)Linearithmic3366410,00020,000,000Linearithmic
O(n²)Quadratic10010,0001,000,00010¹²Quadratic
O(n³)Cubic1,0001,000,00010⁹10¹⁸Cubic
O(2ⁿ)Exponential1,02410³⁰——Exponential
O(n!)Factorial3,628,800———Factorial

Growth Rate Comparison

Toggle functions with the legend buttons. Change the range, then enter n or move across the chart to inspect values. Curves above the vertical scale are clipped; the text readout still shows their values. These are representative functions, not benchmark timings.

O(1)
O(log n)
O(√n)
O(n)
O(n log n)
O(n²)
O(n³)
O(2ⁿ)

How to Determine Complexity

Count the work in loops and recursive calls, including library operations and the cost of each comparison or copy.

The Rules

  1. Sequential blocks → add. Do A then B? O(A + B). Keep both terms when A and B are independent input sizes.
  2. Nested blocks → multiply. For each element, do something? O(outer) × O(inner).
  3. Drop constants. O(3n) = O(n). O(n/2) = O(n). Constants don't change the shape.
  4. Drop lower-order terms. O(n² + n) = O(n²). The bigger term dominates.
  5. Recursive calls → draw the recursion tree. Count total nodes × work per node.

Worked Examples

Example 1: Simple Loop

Python
def find_max(arr):
    maximum = arr[0]        # O(1)
    for x in arr:           # runs n times
        if x > maximum:     # O(1) per iteration
            maximum = x     # O(1) per iteration
    return maximum

One loop over n elements, constant work each time. Total: n × O(1).

→ O(n)

Example 2: Nested Loops

Python
def has_duplicate(arr):
    for i in range(len(arr)):           # n times
        for j in range(i + 1, len(arr)):# n-1, n-2, ... 1 times
            if arr[i] == arr[j]:
                return True
    return False

Inner loop runs n-1 + n-2 + ... + 1 = n(n-1)/2 times total. That's n²/2 - n/2. Drop constants and lower terms.

→ O(n²)

Example 3: Loop That Halves

Python
def binary_search(arr, target):
    lo, hi = 0, len(arr) - 1
    while lo <= hi:              # how many times?
        mid = (lo + hi) // 2
        if arr[mid] == target:
            return mid
        elif arr[mid] < target:
            lo = mid + 1         # eliminate half
        else:
            hi = mid - 1         # eliminate half
    return -1

Each iteration cuts the search space in half. Starting from n, you halve until you reach 1: n → n/2 → n/4 → ... → 1. That's log₂(n) steps.

→ O(log n)

Example 4: Loop Inside a Loop... But Not O(n²)

Python
def two_pointer_sum(arr, target):
    # arr is sorted
    left, right = 0, len(arr) - 1
    while left < right:          # looks like one loop...
        s = arr[left] + arr[right]
        if s == target:
            return (left, right)
        elif s < target:
            left += 1           # left moves right
        else:
            right -= 1          # right moves left
    return None

Two pointers moving toward each other. Each step, one pointer moves. Total steps: at most n. Don't be tricked by the while loop — count how many times the body executes, not the nesting depth.

→ O(n)

Example 5: Recursion (Fibonacci — Naive)

Python
def fib(n):
    if n <= 1:
        return n
    return fib(n - 1) + fib(n - 2)  # two recursive calls

Each call branches into two. The recursion tree has depth n. Total nodes ≈ 2ⁿ. Each node does O(1) work. This is why naive Fibonacci is unusably slow past n ≈ 40.

→ O(2ⁿ)

Example 6: Sorting Then Scanning

Python
def find_closest_pair(arr):
    arr.sort()                        # O(n log n)
    min_diff = float('inf')
    for i in range(1, len(arr)):      # O(n)
        diff = arr[i] - arr[i-1]
        min_diff = min(min_diff, diff)
    return min_diff

Sort costs O(n log n). Linear scan costs O(n). Sequential → add → O(n log n + n). Drop the smaller term.

→ O(n log n)

Example 7: Recursion With Memoization

Python
def fib_memo(n, cache=None):
    if cache is None:
        cache = {}
    if n in cache:
        return cache[n]
    if n <= 1:
        return n
    cache[n] = fib_memo(n - 1, cache) + fib_memo(n - 2, cache)
    return cache[n]

Each subproblem is computed once, giving O(n) arithmetic operations and O(n) cached values and recursion depth. This uses the usual unit-cost arithmetic model; large Fibonacci integers make individual additions more expensive. The fresh cache avoids sharing state between independent calls.

→ O(n)

Example 8: String Building Trap

Python
# Repeated copying can require O(n²) total work
result = ""
for char in string_of_length_n:
    result += char  # do not rely on an in-place optimization

If each concatenation copies the existing prefix, the total copied length is 1 + 2 + ... + n = n(n+1)/2, or Θ(n²). Interpreter optimizations may change a particular result. Use "".join(parts) in Python or a mutable builder when constructing many pieces.

→ O(n²) copying in the general case

Data Structure Operations

Average Case

Data StructureAccessSearchInsertionDeletionSpace
ArrayO(1)O(n)O(n)O(n)O(n)
Dynamic ArrayO(1)O(n)O(1)*O(n)O(n)
Singly Linked ListO(n)O(n)O(1)†O(1)†O(n)
Doubly Linked ListO(n)O(n)O(1)†O(1)O(n)
StackO(n)O(n)O(1)O(1)O(n)
QueueO(n)O(n)O(1)O(1)O(n)
DequeO(n)O(n)O(1)O(1)O(n)
Hash MapN/AO(1)*O(1)*O(1)*O(n)
Hash SetN/AO(1)*O(1)*O(1)*O(n)
BST (balanced)O(log n)O(log n)O(log n)O(log n)O(n)
BST (unbalanced)O(n)O(n)O(n)O(n)O(n)
AVL / Red-Black TreeO(log n)O(log n)O(log n)O(log n)O(n)
Binary HeapO(1)‡O(n)O(log n)O(log n)O(n)
TrieN/AO(m)O(m)O(m)O(total characters)
Union-FindN/AO(α(n))N/AN/AO(n)
Segment TreeN/AO(log n)O(log n)N/AO(n)
Fenwick Tree (BIT)N/AO(log n)O(log n)N/AO(n)
Skip ListO(log n)O(log n)O(log n)O(log n)O(n)
Bloom FilterN/AO(k)O(k)N/AO(m)
LRU CacheO(1)O(1)O(1)O(1)O(n)
B-TreeO(log n)O(log n)O(log n)O(log n)O(n)

Dynamic-array insertion means append and is amortized. Hash bounds are expected averages under suitable hashing; resize costs are separately amortized. † Linked-list insert/delete assumes the node or required predecessor is known. ‡ Heap access means the root and deletion means extract-root. Trie m = key length; Bloom-filter m = allocated bits and k = hash functions. Union-Find bounds assume both path compression and union by rank/size. Range-tree search means range query; insertion means point update. All bounds assume constant-cost keys/hashes unless noted.

Worst Case (When It Matters)

⚠️ Hash Maps Aren't Always O(1) A conventional hash table can take O(n) per operation when collisions accumulate. Expected O(1) lookup assumes suitable hashing and a bounded load factor; amortization accounts for occasional resizing. Some implementations use other collision structures, so check the actual implementation and key cost.
Data StructureAccessSearchInsertionDeletion
Hash MapN/AO(n)O(n)O(n)
BST (no balancing)O(n)O(n)O(n)O(n)
Skip ListO(n)O(n)O(n)O(n)
Quick Sort (partition)O(n²) — when pivot is always min/max

Sorting Algorithms

AlgorithmBestAverageWorstSpaceStable?When to Use
Bubble SortO(n)O(n²)O(n²)O(1)✅Never, except teaching
Selection SortO(n²)O(n²)O(n²)O(1)❌Minimizing swaps
Insertion SortO(n)O(n²)O(n²)O(1)✅Small or nearly sorted
Merge SortO(n log n)O(n log n)O(n log n)O(n)✅Stability needed, linked lists
Quick SortO(n log n)O(n log n)O(n²)O(log n) average; O(n) worst❌General purpose, cache-friendly
Heap SortO(n log n)O(n log n)O(n log n)O(1)❌O(1) space guarantee
Counting SortO(n+k)O(n+k)O(n+k)O(n+k)✅Small range integers; stable output array
Radix SortO(d·(n+k))O(d·(n+k))O(d·(n+k))O(n+k)✅Fixed-length integers/strings
Bucket SortO(n+k)O(n+k)O(n²)O(n+k)Depends on bucket sortExpected uniform distribution
Tim SortO(n)O(n log n)O(n log n)O(n)✅Python and Java object-array sorting

k = counting range or radix · d = digits. Space is auxiliary space for the conventional array implementations. Quicksort can bound its stack to O(log n) by recursing only on the smaller partition. Radix stability requires a stable per-digit sort.

💡 Interview Shortcut Check the language contract: Python sorting is stable; Java object sorting is stable, while primitive sorting uses a different algorithm. C++ std::sort does not promise stability; use std::stable_sort when needed. Counting and radix sorts exploit bounded key representations, so the comparison-sort lower bound does not apply to them.

Graph Algorithms

AlgorithmTimeSpaceUse Case
BFSO(V+E)O(V)Shortest path (unweighted), level-order
DFSO(V+E)O(V)Connectivity, cycle detection, topo sort
DijkstraO((V+E) log V)O(V) indexed; O(V+E) lazyShortest path (nonnegative weights)
Bellman-FordO(V·E)O(V)Shortest path (negative weights)
Floyd-WarshallO(V³)O(V²)All-pairs shortest path
A* SearchO((V+E) log V)§O(V) indexed; O(V+E) lazyShortest path with heuristic (games, maps)
Topological SortO(V+E)O(V)Dependency ordering (DAGs)
Kruskal's MSTO(E log E)O(V)Minimum spanning tree (sparse)
Prim's MSTO((V+E) log V)O(V) indexed; O(V+E) lazyMinimum spanning tree; heap implementation
Tarjan's SCCO(V+E)O(V)Strongly connected components
Edmonds-KarpO(V·E²)O(V+E)Maximum flow
Kosaraju's SCCO(V+E)O(V+E)Strongly connected components; includes reversed graph

V = vertices · E = edges. Adjacency-list input storage is excluded unless the algorithm constructs a copy. Heap-based bounds depend on the queue implementation; a lazy queue can hold O(E) entries. § A* bound shown for a finite graph with a binary heap, nonnegative weights and a consistent heuristic; inconsistent heuristics may require reopening vertices. Heuristic quality affects explored work, not a universal O(E) guarantee. Kosaraju also needs O(V+E) if it builds a reversed graph.

Space Complexity

Time gets all the attention, but space complexity trips people up in interviews just as often. Same Big-O notation, but counting memory instead of operations.

What Counts as "Space"?

Auxiliary space measures extra live memory beyond the input. Include the recursion stack and any copies or output buffers, and state whether output storage is excluded. Total space also includes the original input. Modifying an input in place does not itself allocate a copy.

Variables
O(1)
Hash Set of input
O(n)
2D DP table
O(n²)
Recursion stack
O(depth)
Adjacency matrix
O(V²)
Adjacency list
O(V+E)

Common Space Gotchas

Space-Time Tradeoffs

TechniqueTradesTimeSpace
Hash map lookupSpace → TimeO(1) lookupO(n)
MemoizationSpace → TimePoly instead of expO(subproblems)
Two pointersTime → SpaceO(n)O(1)
Bit manipulationSpace → SpaceSameO(1) vs O(n)
Rolling DPCleverness → SpaceSameO(n) → O(1)
💡 Interview Move When you present an O(n) space solution, always mention: "We could optimize space to O(1) by [technique] if needed." Even if you don't implement it, showing awareness impresses.

Amortized Analysis

Some operations are usually fast but occasionally slow. Amortized analysis averages the cost over a sequence of operations. It's not the same as average case — it's a guarantee over any sequence, not a probabilistic argument.

The Classic Example: Dynamic Array append()

When you append() to a Python list or push_back() in C++, most of the time it's O(1) — just write to the next slot. But when the array is full, it allocates a new array (by an implementation-dependent growth factor) and copies everything over. That single resize is O(n).

So is append O(1) or O(n)? It's O(1) amortized. Here's why:

Text
Start empty with capacity 1; double when full:

Insert #1: no resize                       → write 1
Insert #2: capacity 1→2                    → copy 1 + write 1
Insert #3: capacity 2→4                    → copy 2 + write 1
Insert #4: no resize                       → write 1
Insert #5: capacity 4→8                    → copy 4 + write 1
Insert #6-8: no resize                     → write 1 each

Across n appends: copies = 1 + 2 + 4 + ... < 2n
Writes = n; total work < 3n, or O(1) amortized per append.

The expensive resizes happen so rarely that they get "absorbed" by all the cheap inserts. This is the banker's method — each cheap insert "pays" a little extra into a bank, and when the expensive operation hits, there's enough saved up to cover it.

Other Amortized Bounds

🎯 Interview Phrasing When you say "O(1) amortized," be prepared to explain what triggers the expensive operation and why the average stays constant. Just saying "amortized" without understanding it looks worse than not saying it at all.

Common Complexity Mistakes

These trip up everyone — from beginners to people who should know better. Don't be embarrassed if you've made them; just stop making them.

❌ "This has two loops, so it's O(n²)"

Two full n-iteration loops in sequence take O(n); nesting them takes O(n²). Nested loops with shared pointers or changing bounds may do less work, so count their total iterations.

✅ Check if loops are nested or sequential.

❌ "HashMap is always O(1)"

Expected O(1) lookup depends on hashing and load-factor assumptions. An insertion sequence also includes occasional resize work, which can be O(1) amortized per insertion. These are different claims; neither removes worst-case collision costs.

✅ State the hashing assumptions and whether the bound is expected, amortized, or worst-case.

❌ "Recursion depth = time complexity"

A recursive function with depth n doesn't necessarily have O(n) time. If it branches at each level (like naive fibonacci), the time is O(branches^depth). Depth is about stack space, not time.

✅ Count total recursive calls, not just depth.

❌ "I used sorting so it's O(n log n) total"

Only if the work after sorting is ≤ O(n log n). If you sort in O(n log n) then run an O(n²) algorithm on the result, the total is O(n²). The later quadratic phase dominates.

✅ Total = max of sequential steps, not the first one.

❌ "The inner loop runs n times too, so it's O(n²)"

Not always. If the inner loop's bound depends on the outer variable (like for j in range(i)), the total is 0+1+2+...+(n-1) = n(n-1)/2 — still O(n²) in this case. But if the inner loop runs a shrinking amount (like binary search), it might be O(n log n).

✅ Sum the actual iterations — don't assume.

❌ Confusing O(n) space with O(1) space when using recursion

Recursive solutions look clean but use O(depth) stack space. A "constant space" solution means no extra allocations including the call stack. If they ask for O(1) space, they usually mean iterative.

✅ Recursion = O(depth) space. Always mention it.

❌ "O(log n) because I'm dividing something"

Only if you're dividing the problem size by a constant factor each step. Subtracting a constant gives O(n), not O(log n). n → n-1 → n-2 is linear. n → n/2 → n/4 is logarithmic.

✅ Dividing → O(log n). Subtracting → O(n).

Rules of Thumb

💡 Input Size → Expected Complexity Use these as rough starting points for a small-operation workload. They are not time-limit guarantees: measure the actual implementation, memory use, and output size.
n (input size)Candidate complexityCommon Approaches
n ≤ 10O(n!) or O(2ⁿ)Brute force, backtracking, permutations
n ≤ 20O(2ⁿ)Bitmask DP, backtracking with pruning
n ≤ 100O(n³)Floyd-Warshall, triple-nested loops
n ≤ 500O(n³)Cubic DP, all-pairs
n ≤ 5,000O(n²)2D DP, brute-force pairs
n ≤ 100,000O(n log n)Sorting, divide & conquer, segment tree
n ≤ 1,000,000O(n)Two pointers, sliding window, hash map
n ≤ 10,000,000O(n)Linear scan, counting, streaming
n > 10,000,000O(log n) or O(1)Binary search, math, hash lookup
⚠️ Common Gotcha O(n log n) ≠ O(n). For n = 1,000,000, that's 20 million vs 1 million operations. Usually doesn't matter in practice, but an interviewer might ask you to optimize from O(n log n) to O(n) — which typically means switching from sorting to hashing.

Quick Constraint → Complexity Decoder

Constraints suggest approaches to investigate; they do not determine a unique solution:

Implementation references

Language guarantees differ from textbook algorithm names. Check the Python sorting guide, Python string documentation, Java Arrays contract, C++ sorting specification, and Boost A* analysis.