LearnAbout

Big-O Notation

On this page
Quick reference
O(1) โ€” constantO(1)
O(log n) โ€” logarithmicO(log n)
O(n) โ€” linearO(n)
O(n log n) โ€” linearithmicO(n log n)
O(nยฒ) โ€” quadraticO(nยฒ)
O(2โฟ) โ€” exponentialO(2โฟ)
O(n!) โ€” factorialO(n!)
The Idea

Two programs that solve the same problem can both be "correct" and still be wildly different in practice โ€” one finishes instantly, the other locks up your laptop once the input gets big. Big-O is the language for talking about that difference. It answers one question: as the input size N grows, how does the amount of work grow with it? Not "how many milliseconds did it take on my machine" โ€” that depends on CPU speed, language, even what else is running. Big-O describes the shape of the growth curve, which is true on any machine, in any language, forever.

That's why we drop the machine-specific stuff and talk in terms of N. A loop that touches every element once does roughly N units of work, whether N is 10 or 10 million โ€” we call that O(n). A loop nested inside another loop does roughly N ร— N units of work โ€” O(nยฒ). The exact constants (is it 2N operations or 5N?) get thrown away, because they don't change the shape of the curve, and the shape is what determines whether your program survives contact with a large input.

Within that, three cases matter: best case (the friendliest possible input โ€” rarely useful to plan around), worst case (the input that makes your algorithm work hardest โ€” what you should design for), and average case (what typically happens across realistic inputs โ€” useful when worst case is rare and expensive to avoid, like a hash map's collision handling). When people say "this algorithm is O(n)" with no qualifier, they almost always mean worst case โ€” that's the default you should assume too.

Big-O is about the trend as N โ†’ large, not the exact number of operations. O(n) means "work scales linearly with input size," full stop โ€” it says nothing about which is faster between one O(n) algorithm and another O(n) algorithm on a specific input.
Build It
How to derive it
// The skill: read code, count how the "work" scales with N, name the class.
// Ask one question per loop/structure: "does this range depend on N, and how?"
//   - a fixed number of steps, no matter how big N gets           -> O(1)
//   - one pass over N items                                       -> O(n)
//   - a loop inside a loop, each over N items                     -> O(n^2)
//   - a loop that cuts the remaining work in half each step       -> O(log n)
// Multiply nested/sequential costs, then keep only the fastest-growing term.
constant time โ€” O(1)
function firstElement(arr):
    return arr[0]        // one lookup, no matter if arr has 10 or 10 million items
No loop touches N at all โ€” the cost doesn't change as the input grows, so it's O(1) even if the line itself is "expensive."
single loop โ€” O(n)
function sum(arr):
    total = 0
    for x in arr:          // runs exactly N times
        total += x          // constant work per iteration
    return total
One line of constant work, run once per element: N iterations ร— O(1) each = O(n). This is the baseline pattern โ€” any single pass over the input is O(n).
nested loop โ€” O(nยฒ)
function hasDuplicatePair(arr):
    for i in range(len(arr)):        // N times
        for j in range(len(arr)):    // N times, for EACH i
            if i != j and arr[i] == arr[j]:
                return true
    return false
The outer loop runs N times; for each of those, the inner loop also runs N times. Total work is N ร— N = O(nยฒ) โ€” this is the classic "compare every pair" shape.
halving loop โ€” O(log n)
function binarySearch(sortedArr, target):
    lo, hi = 0, len(sortedArr) - 1
    while lo <= hi:
        mid = (lo + hi) // 2
        if sortedArr[mid] == target:
            return mid
        elif sortedArr[mid] < target:
            lo = mid + 1
        else:
            hi = mid - 1     // each iteration THROWS AWAY half the remaining range
    return -1
The search space is cut in half every iteration, not reduced by a fixed amount. Going from N down to 1 by repeated halving takes logโ‚‚(N) steps โ€” that's what makes it O(log n), dramatically slower-growing than O(n).
Know It

These classes are listed in order from cheapest to most expensive growth. The gut-check column is the part worth memorizing โ€” it's what tells you, mid-interview or mid-code-review, whether an approach is going to survive a realistic input size.

OperationTimeSpaceWhy
O(1) โ€” constantO(1)โ€”Same cost at n = 10 or n = 1,000,000 โ€” a hash lookup, an array index, a fixed arithmetic formula.
O(log n) โ€” logarithmicO(log n)โ€”At n = 1,000,000, logโ‚‚n โ‰ˆ 20. Halving the problem each step gets you to the answer absurdly fast โ€” binary search is the canonical example.
O(n) โ€” linearO(n)โ€”One pass over the input. At n = 1,000,000, that's 1,000,000 operations โ€” fast, and the best you can usually do if you must look at every element.
O(n log n) โ€” linearithmicO(n log n)โ€”At n = 1,000,000, โ‰ˆ 20,000,000 operations โ€” still fast. The signature of good sorting algorithms (merge sort, heap sort).
O(nยฒ) โ€” quadraticO(nยฒ)โ€”At n = 1,000,000, that's a trillion operations โ€” don't. Fine at n = 100, dangerous once input size isn't guaranteed small. Every-pair comparisons land here.
O(2โฟ) โ€” exponentialO(2โฟ)โ€”At n = 30 this is already over a billion; at n = 1,000,000 it's incomprehensibly large. Shows up in brute-force "try every subset" approaches.
O(n!) โ€” factorialO(n!)โ€”At n = 20, larger than the number of atoms you can reasonably enumerate. "Try every ordering/permutation" territory โ€” only tractable for tiny N.
Break It

Constants aren't always noise

N is small and bounded

Big-O drops constants because they stop mattering as N grows large โ€” but if N is capped at, say, 20 (a fixed-size board, a small config list), an O(nยฒ) algorithm with tiny constants can beat an O(n log n) one with heavier per-step overhead in practice. Big-O describes the trend at scale, not "which code is faster on my actual input."

A shrinking loop is still O(nยฒ)

range shrinks by 1 each outer iteration

A loop like "for i in range(n): for j in range(i, n)" looks smaller each pass โ€” the inner loop runs n, then n-1, then n-2 times. But the total is n + (n-1) + (n-2) + ... + 1 = n(n+1)/2, and that's still a quadratic expression. Dropping constants and lower-order terms, n(n+1)/2 is O(nยฒ), not O(n) โ€” total work, not per-iteration feel, is what Big-O measures.

Worst case vs. average case

an operation's cost depends on the data, not just N

A hash map lookup is O(1) on average โ€” but if every key collides into the same bucket (bad hash function, adversarial input), it degrades to O(n) worst case, since you'd walk a full bucket chain. Quoting "O(1)" without the qualifier hides that risk; know which case you're claiming.

A hidden O(n) inside a loop

the 'simple' operation inside your loop isn't O(1)

`for i in range(n): result += arr[:i]` looks like a single O(n) loop, but slicing/concatenating a string or list is itself O(k) in the size being built โ€” so the real cost is 1 + 2 + ... + n โ‰ˆ O(nยฒ), not O(n). Always ask whether the line inside your loop is actually constant time before counting the loop itself.

Use It
Two Sum
Brute force checks every pair โ€” O(nยฒ). A hash map storing "value seen โ†’ index" turns the second loop into a lookup: O(n).
Easy
Contains Duplicate
Pairwise comparison is O(nยฒ). Dropping every element into a hash set as you go and checking membership first is O(n).
Easy
Best Time to Buy and Sell Stock
Checking every pair of buy/sell days is O(nยฒ). Tracking the running minimum price seen so far turns it into one O(n) pass.
Easy
Valid Anagram
Sorting both strings and comparing is O(n log n). Counting character frequencies in one pass each is O(n).
Easy
Majority Element
Sorting to find the middle element is O(n log n). Counting occurrences with a hash map (or Boyer-Moore voting) gets there in O(n).
Easy
Missing Number
Sorting and scanning for the gap is O(n log n). Comparing the sum of 0..n against the array's actual sum is O(n).
Easy
Move Zeroes
A space improvement, not time: building a new filtered array is O(n) extra space. Shifting non-zero elements left in place gets O(1) space.
Easy
Intersection of Two Arrays
Checking every element of one array against every element of the other is O(nยทm). Dropping one array into a hash set first makes lookups O(1), for O(n+m) total.
Easy
Single Number
A hash set tracking counts is O(n) time and O(n) space. XOR-ing every element together cancels all paired values, giving O(n) time and O(1) space โ€” same time class, better space.
Easy
Squares of a Sorted Array
Squaring then sorting is O(n log n) vs O(n) with two indices starting at both ends and closing inward toward the middle.
Easy