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.

O(n) means "scales linearly" — double the input, double the time. O(n²) means "scales quadratically" — double the input, quadruple the time. O(1) means "constant" — input size doesn't matter.

There are actually three notations that people conflate:

In interviews, nobody cares about the distinction. Just use Big-O and describe the worst case unless asked otherwise.

💡 Quick Mental Model For n = 1,000,000: O(1) = instant, O(log n) ≈ 20 ops, O(√n) ≈ 1000 ops, O(n) = 1M ops (~1ms), O(n log n) = 20M ops (~20ms), O(n²) = 1 trillion ops (~15 min), O(2ⁿ) = heat death of the universe.

The Hierarchy (Memorize This)

From fastest to slowest growing. Everything above the line is "good" for interviews; everything below gets you a raised eyebrow.

NotationNamen = 10n = 100n = 1,000n = 1,000,000Verdict
O(1)Constant1111Instant
O(log n)Logarithmic371020Beautiful
O(√n)Square root310321,000Great
O(n)Linear101001,0001,000,000Good
O(n log n)Linearithmic3366410,00020,000,000Fine
O(n²)Quadratic10010,0001,000,00010¹²Slow
O(n³)Cubic1,0001,000,00010⁹10¹⁸Painful
O(2ⁿ)Exponential1,02410³⁰No
O(n!)Factorial3,628,800Why

Growth Rate Comparison

Click a function in the legend to toggle it. Use the scale buttons to zoom into different ranges.

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

How to Determine Complexity

Forget the formulas. Here's the practical approach: look at the loops, look at the recursion, and count the work.

The Rules

  1. Sequential blocks → add. Do A then B? O(A) + O(B) → drop the smaller one.
  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={}):
    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]

Same recursion, but each subproblem is computed only once. There are n unique subproblems, each doing O(1) work. The memoization collapses the exponential tree into a linear chain.

→ O(n)

Example 8: String Building Trap

Python
# Looks O(n), is actually O(n²) in many languages
result = ""
for char in string_of_length_n:
    result += char  # creates a NEW string each time!

String concatenation in Python/Java creates a new string each time. Iteration i copies i characters. Total: 1 + 2 + ... + n = n(n-1)/2. Use "".join(list) or StringBuilder instead.

→ O(n²) — not O(n)!

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(n·m)
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)

* amortized average · † given a pointer to the node · ‡ min/max only · m = key length · k = hash functions · α = inverse Ackermann (effectively constant)

Worst Case (When It Matters)

⚠️ Hash Maps Aren't Always O(1) A badly chosen hash function can degrade a hash map to O(n) for every operation. All elements collide into one bucket → it's just a linked list. In practice, language built-ins use good hashes, so you get O(1) amortized. But know the worst case exists.
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)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(k)Small range integers
Radix SortO(d·n)O(d·n)O(d·n)O(n+k)Fixed-length integers/strings
Bucket SortO(n+k)O(n+k)O(n²)O(n+k)Uniform distribution
Tim SortO(n)O(n log n)O(n log n)O(n)Python/Java default — real-world data

k = range of values · d = number of digits

💡 Interview Shortcut Unless asked to implement a specific sort, just use your language's built-in sort (Tim Sort in Python/Java, Introsort in C++). It's O(n log n) and stable. If they ask you to sort without a comparison-based algorithm, reach for counting sort or radix sort — they break the O(n log n) barrier.

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)Shortest path (positive weights)
Bellman-FordO(V·E)O(V)Shortest path (negative weights)
Floyd-WarshallO(V³)O(V²)All-pairs shortest path
A* SearchO(E)§O(V)Shortest 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)Minimum spanning tree (dense)
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)Strongly connected components (two-pass)

V = vertices · E = edges · § depends on heuristic quality

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"?

You're measuring auxiliary space — extra memory your algorithm uses beyond the input. The input itself doesn't count (unless you're modifying it in place, which might matter for the discussion).

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 (typically 2x the size) 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
Starting with capacity 1, doubling each time:

Insert #1: capacity 1→2, copy 1 element    → 1 work
Insert #2: no resize                        → 1 work
Insert #3: capacity 2→4, copy 2 elements    → 2 work
Insert #4: no resize                        → 1 work
Insert #5: capacity 4→8, copy 4 elements    → 4 work
Insert #6-8: no resize                      → 1 work each

After n inserts: total copies = 1+2+4+...+n ≈ 2n
Average per insert: 2n/n = 2 = O(1)

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 O(1) Operations

🎯 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²)"

Nested loops → O(n²). Sequential loops → O(n + n) = O(n). Two loops doesn't mean quadratic unless one is inside the other.

✅ Check if loops are nested or sequential.

❌ "HashMap is always O(1)"

Average case with a good hash function: yes. Worst case with collisions: O(n). In interviews, say "O(1) amortized" or "O(1) average." If the interviewer is picky, they'll appreciate the distinction.

✅ Say "O(1) average" or "O(1) amortized" — never just "O(1)."

❌ "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 sort didn't help.

✅ 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 Interview problems usually have time limits. Use the input constraints to guess the expected complexity:
n (input size)Max Acceptable 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

When you read a problem on LeetCode and see the constraints section, here's your instant filter: