Big-O and Reading Constraints
Estimate how code scales and turn a problem's constraints into a target complexity.
13 min, 0 of 4 activities solved
View cheatsheetGetting Python ready… examples can run in a moment.
In an interview (and on every judge) your code is graded on two things: is it correct, and is it fast enough for the biggest input allowed?
Big-O describes how the number of steps grows
as the input size n grows. You keep the fastest
growing term and drop constants: 3n² + 5n + 7
is just O(n²).
The classes you'll meet, fastest to slowest:
- O(1): constant (
nums[i], average dict lookup for fixed-size keys) - O(log n): halve the problem each step (binary search)
- O(n): one pass over the input
- O(n log n): sorting, or n heap operations
- O(n²): every pair (nested loops)
- O(2ⁿ): every subset (backtracking)
Count the steps
Each row makes n 10× bigger. The log column
grows by about 3, the linear one by 10×, the
quadratic one by 100×. That gap is why an
O(n²) answer dies on n = 100,000.
Reading Big-O off code:
- Steps one after another add: O(n) + O(n) is still O(n).
- Independent nested loops multiply: a loop of m inside a loop of n is O(nm). If both sizes are n, that's O(n²).
- An inner loop that starts at
i + 1still counts: (n−1) + (n−2) + … + 1 = n(n−1)/2, which is O(n²). - Halving a value until it reaches 1, or doubling from 1 until it reaches n, takes O(log n) steps.
- Nested syntax alone doesn't prove O(n²). If an inner pointer only moves forward across the whole input, count its total moves: at most n.
- A built-in call counts as its own cost:
max(nums)inside a loop is a hidden inner loop (next lesson).
What's the time complexity of this duplicate check?
Recursion: add the work across all calls. If every call does the same amount of work, multiply the number of calls by that amount. Draw the call tree and count it.
- One recursive call on
n − 1with O(1) work: n calls, O(n). - One call on
n / 2with O(1) work per call: O(log n). - Two calls on
n − 1with O(1) work per call: the tree doubles each level, O(2ⁿ). - Two calls on half the input plus a linear merge: O(n) work per level and O(log n) levels, so O(n log n), as in merge sort.
- With memoization, each distinct argument is computed once: (number of states) × (work per state).
Recursion also uses space: every waiting call sits on the call stack, so depth d costs O(d) memory.
Counting calls: naive vs memoized
Same answer, 21,891 calls versus 21. The naive
version branches twice per call (exponential);
the cached one computes each n once (linear).
Try fib(25) on both.
Time vs space. Auxiliary space counts peak extra memory beyond the input and required output. State that convention; output still takes real memory. Returning n answers needs O(n) output space even with O(1) working space.
- Two index variables: O(1) space.
- A
setof everything seen: O(n) space. - Recursion depth n: O(n) stack space.
The most common trade in interviews is spending O(n) memory (a set or dict) to turn O(n²) time into O(n) time.
Amortized O(1). list.append is O(1)
amortized: the list keeps spare capacity, and
when it runs out it grows by a proportion of its
size and copies everything. Those copies are rare
enough that n appends cost O(n) in total.
Watch a list over-allocate
The size jumps only now and then; in between,
append just fills a spare slot. Each jump is
a capacity change; a resize may copy O(n) items.
A sequence of n appends still costs O(n), so
each append costs O(1) amortized. Exact byte
counts depend on the Python build.
Constraints are a hint. Before thinking about an algorithm, read the limit on n. These are rough starting targets, not timing promises. Python loop bodies, built-ins, hardware and time limits differ; Pyodide also has browser overhead. Work backwards, then test a large input:
| n up to | Target | Typical tools |
|---|---|---|
| about 8 | factorial search may fit | permutations |
| about 20 | exponential search may fit | subsets, bitmasks |
| about 100 | O(n³) may fit | triple loops |
| about 1,000 | O(n²) may fit | pairs, 2D DP |
| 10⁵ | aim for O(n log n) or O(n) | sorting, hashing |
| 10⁶ | prefer O(n); check O(n log n) | scans, efficient sorts |
| 10⁹ as a numeric bound | O(log n) or O(1) | binary search, math |
So "1 ≤ n ≤ 10⁵" is the problem setter telling you: avoid comparing every pair; look for O(n log n) or better. A value bound of 10⁹ is different from having 10⁹ input elements. Read every dimension: an r × c grid has rc cells.
For 1 ≤ nums.length ≤ 2·10⁵, which is the most practical target among these choices?
What does this print?
From constraint to target
Complete estimate(n, cls) so it returns roughly
how many steps an algorithm of complexity class
cls takes on input size n. The classes are
"1", "log n", "n", "n log n",
"n^2", "n^3" and "2^n".
Use math.log2 for the logarithms. For example
estimate(1000, "n^2") is 1_000_000 and
estimate(8, "n log n") is 24.
Assume n ≥ 1. Only try "2^n" for small n:
even constructing that exact integer costs
memory. These formulas compare growth; they do
not predict a judge's time limit.