Big-O and Reading Constraints

Big-O & the Python Toolkit, lesson 1 of 3

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 cheatsheet

Getting 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)
Example

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 + 1 still 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).
Quiz

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 − 1 with O(1) work: n calls, O(n).
  • One call on n / 2 with O(1) work per call: O(log n).
  • Two calls on n − 1 with 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.

Example

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 set of 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.

Example

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.

Quiz

For 1 ≤ nums.length ≤ 2·10⁵, which is the most practical target among these choices?

Quiz

What does this print?

Exercise

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.