leetcode / Foundations / Complexity analysis intro

Complexity analysis

Big-O is the language every other page speaks. It answers one question: when the input gets ten times bigger, how much slower does my code get? This page builds the idea from counting steps, shows how to read a problem's limits to guess the complexity you are meant to hit, and explains the one subtle case, amortized cost, with a growing array traced append by append.

On this page
  1. Problem
  2. Intuition
  3. Mechanics
  4. What O(f(n)) means
  5. Counting rules
  6. Best, worst and average case
  7. Space complexity
  8. Amortized cost
  9. Worked example
  10. Implementation
  11. Variations and edge cases
  12. Complexity and tradeoffs
  13. Problems
  14. Connections
  15. Read first
  16. Read next
  17. Check yourself
  18. References

Problem

Two correct programs can differ by a factor of a million in running time. An interview problem is almost never "can you get the right answer" and almost always "can you get it fast enough, on an input that is too large for the obvious approach". To choose between approaches before you write them, you need a way to compare them that ignores the speed of the machine and the quality of the compiler.

The questions that need this vocabulary look like: find a pair, a window or a path among up to a hundred thousand items; count something over a huge range; or "can you do better than O(n²)?" said out loud by the interviewer. Each constraint on the input size is a hint about which complexity the intended solution has.

SignalPatternWhy
Input length up to about 10⁵ or 10⁶O(n) or O(n log n)A quadratic loop needs around 10¹⁰ steps, far beyond what one second allows.
Input length up to a few thousandO(n²) is acceptableTwo nested loops over 3,000 items are about 9 million steps.
Input length 20 or smallerExponential search: O(2ⁿ) or O(n!)The limit is small exactly so that trying every subset or order is feasible.
Input value up to 10⁹ or 10¹⁸O(log n) or O(1)You cannot even loop over the value; the answer must come from halving or arithmetic.
"Sorted" in the statementO(log n) lookups or O(n) two-pointer scansOrder lets you discard half the data per question instead of looking at all of it.
"Many queries" on one arrayPreprocess once, answer each in O(1) or O(log n)Paying O(n) per query multiplies into O(n·q).

Intuition

Count the steps your program takes as a function of the input size n, then keep only the part that dominates once n is large. A loop that visits each item once takes about n steps. Two nested loops take about n × n. A loop that throws away half the remaining items each round takes about log₂ n rounds, because that is how many times you can halve n before nothing is left.

The constants don't matter, because they don't change how the cost grows: doubling n doubles a linear cost, quadruples a quadratic cost, and adds just one step to a logarithmic cost. Those growth rates, not the stopwatch time on your laptop, are what separate an approach that scales from one that never finishes.

How five cost functions grow for n from 1 to 16 A line chart with input size n on the horizontal axis from 1 to 16 and the number of steps on the vertical axis up to 64. The logarithmic curve reaches only 4 at n = 16. The linear curve reaches 16 at n = 16. The n log n curve reaches 64 at n = 16. The quadratic curve leaves the chart at n = 9 and the exponential curve leaves it at n = 7. 1 4 8 12 16 16 32 48 64 input size n log n n n log n n² 2ⁿ

Every curve starts near the origin. By n = 16 the quadratic and exponential curves have left the chart while log n has barely moved.

Mechanics

What O(f(n)) means

Say a program takes T(n) steps. We write T(n) = O(f(n)) when, past some input size, T(n) is at most a constant multiple of f(n). Formally: there are constants c and n₀ such that T(n) ≤ c · f(n) for every n ≥ n₀. That is an upper bound. The matching lower bound is Ω, and when both hold the growth is Θ. Interviewers say "big-O" and usually mean the tight Θ for the worst case.

Two consequences give the rules you use daily. Drop constants: 5n + 40 is O(n), since 5n + 40 ≤ 45n for n ≥ 1. Drop lower-order terms: n² + 1,000n is O(n²), because for n ≥ 1,000 the second term is no bigger than the first.

Counting rules

  • Sequence: two blocks one after the other add, so keep the larger one.
  • Nesting: a loop inside a loop multiplies, but only when the inner count really doesn't depend on the outer variable. A loop over j from i + 1 to n runs 10 times for n = 5, which is n(n−1)/2 and still O(n²).
  • Halving: if each round discards a constant fraction of what remains, the round count is logarithmic. Halving 1,024 takes 10 rounds.
  • Recursion: multiply the work per call by the number of calls. A function that calls itself twice, each time on n − 1, makes about 2ⁿ calls.
  • Library calls have a cost too: x in some_list, s[a:b], sorted(...) and string concatenation inside a loop are each loops in disguise. The Python toolkit page tabulates them.

Best, worst and average case

Linear search finds its target in one step if it is first and n steps if it is last. Unless told otherwise, quote the worst case: it is the guarantee. Average case needs a probability model for the input; quicksort is O(n log n) on average but O(n²) in its worst case, and being able to say both is part of a good answer.

Space complexity

Space counts the extra memory your algorithm allocates beyond the input, including the recursion stack. Making a set of every element is O(n) space; a few integer variables are O(1). A recursive function that goes n levels deep uses O(n) stack space even if it allocates nothing else.

Amortized cost

Some operations are usually cheap and occasionally expensive. Appending to a Python list normally writes one slot, but when the underlying array is full it allocates a bigger one and copies everything. Amortized analysis averages the cost over a whole sequence of operations, not over random inputs: any n appends, starting from an empty list, cost O(n) in total, so each append is O(1) amortized.

Why it works: if the capacity doubles each time it fills, the copies cost 1 + 2 + 4 + … which sums to less than twice the final size. Charge every append a flat 3 units: one pays for its own write and the rest is saved to pay for the next copy. If the array grew by a constant amount instead, the copies would add up to O(n²), and the guarantee would be lost. The same argument explains why a sliding window or monotonic stack that looks like a loop inside a loop is still O(n): each element enters and leaves only once.

Worked example

Trace nine appends to a growable array that starts with room for one element and doubles when full. Each row shows what the append cost (one write plus any copying), the running total, and the average cost per append so far.

Append #Capacity afterElements copiedCostTotal so farAverage so far
1 1 0 1 1 1.00
2 2 1 2 3 1.50
3 4 2 3 6 2.00
4 4 0 1 7 1.75
5 8 4 5 12 2.40
6 8 0 1 13 2.17
7 8 0 1 14 2.00
8 8 0 1 15 1.88
9 16 8 9 24 2.67
Cost of each of nine appends to a doubling array A bar chart with one bar per append. Most bars have height 1. The appends that trigger a resize, numbers 2, 3, 5 and 9, are taller and drawn in amber because they include the copying cost; the tallest, append 9, costs 9. The tall bars get rarer as the array grows, which is why the average stays below 3. 1 #1 2 #2 3 #3 1 #4 5 #5 1 #6 1 #7 1 #8 9 #9 amber = this append triggered a resize and copied the old elements

Nine appends cost 24 in total, an average of 2.67. The next resize isn't until append 17, so the expensive bars get further apart every time.

Now put the constraint table to work. Suppose a machine does roughly 10⁸ simple steps in the time limit. The largest n each growth rate can handle:

ComplexityLargest n within 10⁸ steps
O(n)100,000,000
O(n log n)4,523,071
O(n²)10,000
O(n³)464
O(2ⁿ)26

This is why "n up to 10⁵" tells you to aim for O(n log n) or better, and "n up to 20" tells you an exponential search is expected.

Implementation

Complexity is analysed, not implemented, but a habit helps: annotate each line of a solution with what it costs, then multiply or add. Here is the same question, "does any pair sum to the target", at three complexities.

def has_pair_quadratic(nums, target):          # O(n^2) time, O(1) space
    for i in range(len(nums)):
        for j in range(i + 1, len(nums)):
            if nums[i] + nums[j] == target:
                return True
    return False


def has_pair_hash(nums, target):               # O(n) time, O(n) space
    seen = set()
    for x in nums:
        if target - x in seen:                 # set lookup is O(1) on average
            return True
        seen.add(x)
    return False


def has_pair_sorted(nums, target):             # O(n log n) time, O(1) extra space
    nums = sorted(nums)                        # note: sorted() copies, so O(n) space
    left, right = 0, len(nums) - 1
    while left < right:
        total = nums[left] + nums[right]
        if total == target:
            return True
        if total < target:
            left += 1
        else:
            right -= 1
    return False

Variations and edge cases

  • Two inputs, two variables. Looping over a list of length m inside a loop over a list of length n is O(m·n), not O(n²). Name each size separately.
  • Hidden loops. result += piece on a string inside a loop can be O(n²) in the worst case; build a list and "".join it once. list.pop(0) and list.insert(0, x) shift every element: use a deque.
  • Numbers as input. For an integer n, the "size" of the input is its digit count. A loop to n is exponential in the number of digits, which is why value limits like 10⁹ forbid it.
  • Recursion depth. Python's default limit is about a thousand frames. A depth-n recursion on n = 10⁵ crashes, so check the stack space as well as the time.
  • Pitfall: the log base. log₂ n and log₁₀ n differ by a constant factor, so the base never appears in big-O.

Complexity and tradeoffs

  • Brute force is the baseline. Try everything; it is nearly always correct and usually O(n²), O(n³) or O(2ⁿ). State it first, aloud, and then say which repeated work it does. Every pattern in this reference removes a specific kind of repeated work.
  • Time against space. The usual trade is memory for speed: a hash set turns an O(n) membership scan into O(1). The reverse trade exists too, in sorting in place instead of copying, but is rarer in interviews.
  • Big-O hides constants. An O(n log n) algorithm with a heavy constant can lose to an O(n²) one for small n. Interview inputs are chosen so that the exponent matters, and the constant rarely does.
  • It is an upper bound on growth, not a prediction. Two O(n) solutions can differ by 50× in practice. Use it to rule out approaches, not to rank close ones.
  • Where it fails you. It cannot tell you about cache behaviour, input distributions or amortized spikes. Real systems with latency limits care about the worst single operation, not the amortized average.

Problems

Every list problem is taught under one of the pattern pages, so this page has none of its own. Use it as the reading skill behind all of them: for any problem, find the limits, compute the budget, and name the complexity you are aiming for before you code.

No list problem has this page as its home: it teaches the vocabulary every other page uses.

Connections

Read first

Nothing: this page is a starting point.

Read next

Complexity analysis is the starting point for every page in this reference. Pages on recursion and the Python toolkit lean on it directly: recursion trees are how you count calls, and the toolkit page gives the cost of each built-in operation. The two-pointer and sliding-window pages are where amortized reasoning pays off.

Check yourself

A function has a loop of n steps followed by a loop of n² steps. What is its complexity, and why?

O(n²). Blocks in sequence add, and the larger term dominates once n is large: n + n² ≤ 2n².

The input has up to 200,000 elements. Is a double loop over all pairs acceptable?

No. Pairs number about 2 × 10¹⁰, and a budget of roughly 10⁸ steps supports n of about 10,000 for O(n²). Aim for O(n log n) or O(n).

Why is appending to a Python list O(1) amortized even though one append can copy the whole array?

Resizes double the capacity, so copies cost 1 + 2 + 4 + … which is less than twice the final size. Spread over all the appends, that is a constant per append. Growing by a fixed amount instead would give O(n²) total.

A recursive function reaches depth n but allocates no data. What is its space complexity?

O(n): each active call occupies a stack frame, and n frames exist at the deepest point.

References

  • Cormen, Leiserson, Rivest and Stein, Introduction to Algorithms (CLRS), chapter 3 (growth of functions) and chapter 16 (amortized analysis).
  • Steven Skiena, The Algorithm Design Manual, chapter 2 (algorithm analysis).
  • Sedgewick and Wayne, Algorithms, section 1.4 (analysis of algorithms).
  • Robert Tarjan, "Amortized Computational Complexity", SIAM Journal on Algebraic and Discrete Methods, 1985.
  • The NeetCode roadmap and the Blind 75, Grind 169, NeetCode 150 and NeetCode 250 lists, used as the index of topics and problems.