Guides Guide 2 Reference

Constraints and Complexity

The constraint list is not fine print. It is the interviewer telling you which complexity they expect, and often which pattern to use. Read it first.

Candidates skip straight to the examples. The constraints are more useful. “n ≤ 20” means exponential is fine and they want backtracking. “n ≤ 10⁵” means anything quadratic will fail. Reading the bound converts a guess into a decision.

Contents

  1. The operations budget
  2. Input size to expected complexity
  3. Reading a constraint as a hint
  4. Counting the operations in your own code
  5. What the growth rates really mean
  6. The space budget
  7. How to state a complexity

The operations budget

A judge or an interviewer is usually thinking about one second of runtime. In C++ that buys roughly 10⁸ simple operations. Python is interpreted and runs about ten times slower, so budget 10⁷, or a few million if each step does real work such as building tuples or hashing strings.

So the working rule is: estimate the operation count as a function of n, plug in the largest n in the constraints, and check it lands under about 10⁷. That is the entire method.

LanguageRough ops per secondImplication
C++ or Rust10⁸The number most problem setters design against.
Java or Go10⁸ to 5 × 10⁷Close enough to C++ for most bounds.
Python10⁷One order of magnitude tighter. Assume this.
Python with numpy or built-ins10⁸Work pushed into C, such as sum, sort, join.
This matters in a real interview more than people expect. If n = 10⁵ and your solution is O(n log n), that is about 1.7 million operations, comfortably fine. If it is O(n²), that is 10¹⁰, which is hours. You can tell the interviewer your approach will not work before writing it, which is exactly the judgement they are looking for.

Input size to expected complexity

This is the table to memorise. Find the largest n in the constraints, read across.

n up toExpected complexityWhat that usually means
10 – 12O(n!)Permutations. Backtracking, travelling salesman by brute force.
15 – 20O(2ⁿ) or O(2ⁿ · n)Subsets, bitmask DP. The clearest signal in the whole table.
50 – 100O(n⁴)Four nested loops, or interval DP over pairs.
500O(n³)Floyd-Warshall, matrix chain, some 2D DP.
5,000O(n²)Two nested loops, DP over pairs of indices, edit distance.
10⁵ (100,000)O(n log n)Sorting, heaps, binary search per element, interval sweeps.
10⁶ (1,000,000)O(n)One pass. Windows, prefix sums, monotonic stacks, counting.
10⁸ and aboveO(log n) or O(1)Binary search on the answer, or a closed-form formula.
The two most informative rows. n ≤ 20 almost always means subsets or bitmask, because 2²⁰ is about a million and 2³⁰ is a billion. And n = 10⁵ almost always means sort it or use a hash map, because n² is 10¹⁰ and n log n is under 2 million. Recognising these two on sight saves five minutes of thinking.

Reading a constraint as a hint

Constraints tell you more than the complexity. Several phrasings map almost one-to-one onto a pattern.

Constraint textWhat it is telling you
“Solve in O(1) extra space”No hash map, no copy. Think two pointers, in-place swaps, or Floyd.
“Do not modify the input”Rules out sorting and in-place tricks. Often points at cycle detection or binary search on the value range.
“Must be O(log n)”Binary search. Not a hint, an instruction.
“The array is sorted”Two pointers or binary search. Sortedness is never mentioned by accident.
“Values are in the range 1 to n”Cyclic sort, or use the array itself as a hash table.
“Values may be negative”Kills the sliding window. Think prefix sums.
“All values are positive”Enables the window, and enables greedy shrinking.
“Return any valid answer”No tie-break needed. Often topological sort.
“Answers fit in a 32-bit integer”Irrelevant in Python. Say so, since it is aimed at other languages.
“Queries arrive one at a time”A dynamic structure. Union-Find, a heap, or a prefix structure.
“1 ≤ k ≤ n” with n large and k smallA size-k heap, giving O(n log k) not O(n log n).
“The string contains only lowercase letters”The alphabet is 26, so a fixed-size array works and the space is O(1).
Ask about the constraints that are missing. If the statement never says whether values can be negative, whether the input is sorted, or how large n gets, ask. Every one of those changes the answer, and asking is scored as a positive, not as ignorance.

Counting the operations in your own code

Three rules cover almost every case.

RuleMeaning
Sequence addsTwo loops one after another is O(n) + O(n) = O(n). Not O(n²).
Nesting multipliesA loop inside a loop is O(n) × O(n) = O(n²).
The biggest term winsO(n² + n log n + n) = O(n²). Drop constants and lower terms.

The nested loop that is not quadratic

def not_quadratic(nums: list[int]) -> int:
    """Two pointers, or a monotonic stack: nested syntax, linear cost."""
    left = 0
    total = 0

    for right in range(len(nums)):
        while left < right and nums[left] < nums[right]:
            left += 1               # left only ever moves FORWARD
            total += 1

    return total
Count pointer moves, not loop nesting. Here left starts at 0, only increases, and never passes n. So the inner while runs at most n times across the entire outer loop, not n times per iteration. Total: O(n). This amortised argument is the same one behind the sliding window, monotonic stacks, and cyclic sort. Interviewers ask about it directly, so have the sentence ready.

The single loop that is quadratic

def secretly_quadratic(items: list[int]) -> list[int]:
    """One loop, but each body statement is O(n)."""
    out: list[int] = []

    for item in items:
        if item in out:             # O(len(out)) list scan
            continue
        out.insert(0, item)         # O(n) shift

    return out

No nesting in sight, and it is O(n²). The lesson: look inside the loop body, not just at the loop headers. in on a list, insert(0, ...), slicing, min, max, sum and string concatenation are all linear.

Recursive cost

RecurrenceSolves toExample
T(n) = T(n/2) + O(1)O(log n)Binary search
T(n) = T(n/2) + O(n)O(n)Quickselect, average case
T(n) = 2T(n/2) + O(1)O(n)Tree traversal
T(n) = 2T(n/2) + O(n)O(n log n)Merge sort, quicksort
T(n) = 2T(n-1) + O(1)O(2ⁿ)Naive Fibonacci, subsets
T(n) = n · T(n-1)O(n!)Permutations

The difference between the second and fourth rows is the whole reason quickselect beats quicksort for finding one element: recursing into one half instead of both.

Dynamic programming has a shortcut

For any DP, the time is number of states × work per state. Two nested indices with an O(1) transition is O(n²). Two indices with an O(n) inner scan is O(n³). You can read the complexity straight off the state definition, before writing a line. See the five questions.

What the growth rates really mean

Concrete numbers, so the abstraction has weight. Each cell is the operation count.

nlog nnn log n2ⁿ
10310331001,024
100710066410⁴10³⁰
1,0001010³10⁴10⁶beyond counting
10⁵1710⁵1.7 × 10⁶10¹⁰
10⁶2010⁶2 × 10⁷10¹²

Three readings worth internalising.

The space budget

Memory limits are usually 256 MB. A Python int in a list costs roughly 28 bytes for the object plus 8 for the pointer, so a list of 10⁶ integers is around 40 MB, not 8 MB. A list of 10⁷ is uncomfortable. This surprises people coming from C++.
StructureRough bytes per element10⁶ elements
list of small ints~36~36 MB
set or dict of ints~100~100 MB
list of tuples of two ints~130~130 MB
array.array("i")44 MB
bytearray11 MB

Conventions for what counts as extra space:

How to state a complexity

Say four things, in this order. It takes fifteen seconds and it closes the question cleanly.

SayExample
1. The time, with the reason“O(n log n). The sort dominates, the sweep after it is linear.”
2. The space, and what is in it“O(n) for the hash map. O(1) extra if you do not count the output.”
3. What the variables mean“n is the number of intervals, not the length of the timeline.”
4. Whether it is worst or average case“Average O(n). Worst case O(n²) if the hash degenerates.”
Do not quote a bound with a bare letter. “O(k)” means nothing until you say what k is. Problems with two inputs need both: string matching is O(n + m), not O(n). Being sloppy here is one of the easiest ways to look less careful than you are.
Amortised is not average. Amortised O(1) means any sequence of operations averages out, guaranteed, as with list.append. Average case O(1) means it usually holds, as with a dict lookup, and an adversary could break it. Using the right word is a small, visible signal.

The five things to carry forward


Guide 1 — The Python Toolkit Guide 3 — The Interview Script