Reference · Algorithms

Complexity Cheat Sheet

What each operation costs, in one place: Python's built-in containers, the classic data structures, sorting, graph algorithms, dynamic programming, and vectorized NumPy and pandas. Use it to choose a structure before you write code, and to explain why slow code is slow.

⏱ Keep open while you workTheory: M19 Complexity & Big-OSource for built-ins: Python wiki: TimeComplexity

Growth rates

Big-O describes how cost grows with input size n, ignoring constant factors. Below, n is the container size, k is the size of the argument or result, V/E are vertices and edges. "Amortized O(1)" means an occasional expensive step averages out to constant per operation. "Average" for hash tables assumes a decent hash function; the worst case (all keys colliding) is O(n).

ClassNamen = 1,000n = 1,000,000Typical example
O(1)constant11dict lookup, list index
O(log n)logarithmic~10~20binary search, heap push
O(n)linear1,0001,000,000scan a list, x in list
O(n log n)linearithmic~10,000~20,000,000sorted(), merge sort
O(n²)quadratic1,000,0001012 (hours)nested loops over the same data
O(2n)exponential10301-all subsets, naive recursion
O(n!)factorial--all permutations (brute-force TSP)

Python built-in operation costs (CPython)

OperationAverageNotes
xs[i], xs[i] = v, len(xs)O(1)Contiguous array of pointers
xs.append(v)O(1) amortizedOccasional resize copies everything (M20)
xs.pop()O(1)From the end
xs.pop(0), xs.insert(0, v)O(n)Shifts every element; use deque
xs.pop(i), xs.insert(i, v), del xs[i]O(n - i)Shifts the tail
v in xs, xs.index(v), xs.count(v), xs.remove(v)O(n)Linear scan
xs[a:b]O(k)k = b - a; creates a copy
xs.extend(ys), xs + ysO(k) / O(n + k)+ builds a new list
xs.sort(), sorted(xs)O(n log n)O(n) if already sorted (Timsort/Powersort)
min, max, sum, xs.reverse(), xs.copy()O(n)
xs * kO(nk)
OperationAverageWorstNotes
d[k], d[k] = v, del d[k], k in d, d.get(k)O(1)O(n)Hash table with open addressing (M23)
d.pop(k), d.setdefault(k, v)O(1)O(n)
d.popitem()O(1)O(1)Removes the last inserted
Iterate keys/values/itemsO(n)O(n)Insertion order
d.copy(), d | otherO(n)O(n)
len(d)O(1)O(1)

Counter and defaultdict are dicts, with the same costs. Counter.most_common(k) is O(n log k).

OperationAverageNotes
x in s, s.add(x), s.discard(x)O(1)Worst O(n)
s | tO(len(s) + len(t))
s & tO(min(len(s), len(t)))Iterates the smaller set
s - tO(len(s))
s ^ tO(len(s) + len(t))
s <= t (subset)O(len(s))
set(iterable)O(n)Pays off after a few membership tests
OperationCostNotes
append, appendleft, pop, popleftO(1)Doubly linked list of fixed-size blocks (M22)
dq[0], dq[-1]O(1)
dq[i] in the middleO(n)Use a list for random access
dq.rotate(k)O(k)
x in dq, dq.remove(x)O(n)
deque(maxlen=k).appendO(1)Drops from the other end: a ring buffer
OperationCostNotes
heapify(xs), heapify_max(xs) (3.14)O(n)Bottom-up build (M25)
heappush, heappop, heappushpop, heapreplace (and the _max versions)O(log n)Sift up / sift down
heap[0]O(1)Peek at the smallest (or largest for a max-heap)
nlargest(k, xs), nsmallest(k, xs)O(n log k)For k close to n, just sort
merge(*sorted_iterables)O(N log k)k inputs, N items total, lazy
Remove an arbitrary item / x in heapO(n)Use lazy deletion instead
OperationCostNotes
s[i], len(s)O(1)Fixed-width internal storage (PEP 393)
s[a:b]O(k)Copies
s + tO(n + m)Builds a new string
s += t in a loopO(n²) total (worst)Collect parts and use "".join(parts): O(total)
sub in s, s.find(sub), s.count(sub)O(n) typicalCPython uses fast search (Two-Way for long needles)
s.split(), s.replace(), s.lower(), s.strip()O(n)
hash(s)O(n) onceCached on the object afterwards
s == tO(n)Short-circuits on length or first difference
OperationCostNotes
bisect_left(xs, x), bisect_right(xs, x)O(log n)xs must be sorted (M30)
insort(xs, x)O(n)O(log n) search + O(n) list insert
bisect_left(xs, x, key=f)O(log n)key since 3.10

Data structure operations

StructureAccessSearchInsertDeleteSpacePython
Dynamic arrayO(1)O(n)O(1)* end, O(n) middleO(1) end, O(n) middleO(n)list
Sorted arrayO(1)O(log n)O(n)O(n)O(n)list + bisect
Singly linked listO(n)O(n)O(1) at head / known nodeO(1) at head, O(n) otherwiseO(n)hand-rolled (M21)
Doubly linked listO(n)O(n)O(1) at a known nodeO(1) at a known nodeO(n)hand-rolled; deque for ends
StackO(1) topO(n)O(1) pushO(1) popO(n)list
Queue / dequeO(1) endsO(n)O(1)O(1)O(n)collections.deque
Hash table-O(1) avg, O(n) worstO(1) avgO(1) avgO(n)dict, set
Binary heapO(1) minO(n)O(log n)O(log n) minO(n)heapq
BST (unbalanced)-O(log n) avg, O(n) worstsamesameO(n)hand-rolled (M24)
Balanced BST (AVL, red-black)-O(log n)O(log n)O(log n)O(n)third-party sortedcontainers
Trie-O(L)O(L)O(L)O(total chars)dict of dicts (M27)
Union-find-O(α(n)) findO(α(n)) union-O(n)two lists
Fenwick / segment treeO(log n) query-O(log n) update-O(n)a list
Bloom filter-O(k), false positivesO(k)not supportedO(m) bitsbytearray / int
LRU cacheO(1) get-O(1) putO(1) evictO(capacity)OrderedDict, functools.lru_cache

* amortized. L = key length; α = inverse Ackermann (at most 4 for any practical n); k = number of hash functions.

Sorting algorithms

AlgorithmBestAverageWorstExtra spaceStable?When to use
Bubble sortO(n)O(n²)O(n²)O(1)YesTeaching only
Selection sortO(n²)O(n²)O(n²)O(1)NoMinimizes writes
Insertion sortO(n)O(n²)O(n²)O(1)YesTiny or nearly sorted inputs
Shell sortO(n log n)gap-dependent (~n1.3)O(n1.5) or betterO(1)NoEmbedded, no recursion
Merge sortO(n log n)O(n log n)O(n log n)O(n)YesLinked lists, external sorting
QuicksortO(n log n)O(n log n)O(n²)O(log n) stackNoIn-place general purpose (random pivot)
HeapsortO(n log n)O(n log n)O(n log n)O(1)NoGuaranteed bound, constant space
Counting sortO(n + k)O(n + k)O(n + k)O(n + k)YesSmall integer range k
Radix sort (LSD)O(d(n + b))O(d(n + b))O(d(n + b))O(n + b)YesFixed-width keys (d digits, base b)
Bucket sortO(n + k)O(n + k)O(n²)O(n + k)Yes*Uniformly distributed floats
Python sorted / list.sort (Timsort with Powersort merging)O(n)O(n log n)O(n log n)O(n)YesAlways, in real Python code

* if the per-bucket sort is stable. A sort is stable when equal keys keep their original order, which lets you sort by several keys in passes. Comparison sorts cannot beat O(n log n) in the worst case. Watch them race in M29 Sorting:

Graph algorithms

AlgorithmSolvesTimeSpaceRequirements
BFSShortest path in edges, levels, componentsO(V + E)O(V)Unweighted
DFSReachability, cycles, components, orderingO(V + E)O(V)Iterative to avoid recursion limits
Topological sort (Kahn / DFS)Order tasks with dependenciesO(V + E)O(V)DAG; graphlib.TopologicalSorter
Dijkstra (binary heap)Single-source shortest pathsO((V + E) log V)O(V)Non-negative weights
Bellman-FordShortest paths, detects negative cyclesO(VE)O(V)Any weights
Floyd-WarshallAll-pairs shortest pathsO(V³)O(V²)Small dense graphs
A*Shortest path to one goalO(E log V) worst; often far lessO(V)Admissible heuristic
Prim (binary heap)Minimum spanning treeO(E log V)O(V)Connected, undirected
KruskalMinimum spanning treeO(E log E)O(V)Union-find
Tarjan / KosarajuStrongly connected componentsO(V + E)O(V)Directed
RepresentationSpaceEdge checkIterate neighborsBest for
Adjacency list (dict[node, list])O(V + E)O(deg)O(deg)Sparse graphs (most real ones)
Adjacency set (dict[node, set])O(V + E)O(1) avgO(deg)Sparse graphs with edge queries
Adjacency matrixO(V²)O(1)O(V)Dense graphs, NumPy math

Depth: M26 Graphs, M31 Graph Algorithms.

Dynamic programming problems

ProblemStateTimeSpace (optimized)
Fibonacci / climbing stairsdp[i]O(n)O(1)
Coin change (min coins / ways)dp[amount]O(n · amount)O(amount)
0/1 knapsackdp[i][w]O(n · W) pseudo-polynomialO(W)
Longest common subsequencedp[i][j]O(m · n)O(min(m, n))
Edit (Levenshtein) distancedp[i][j]O(m · n)O(min(m, n))
Longest increasing subsequencedp[i] / tails + bisectO(n²) / O(n log n)O(n)
Grid pathsdp[r][c]O(r · c)O(c)
Subset sum / partitiondp[s]O(n · S)O(S)
Matrix-chain multiplicationdp[i][j] intervalsO(n³)O(n²)
Traveling salesman (Held-Karp)dp[mask][i]O(n² · 2n)O(n · 2n)

The cost of a DP is (number of states) × (work per state). Depth: M32 Dynamic Programming.

NumPy and pandas vectorization

NumPy and pandas operations have the same Big-O as the equivalent Python loop, often O(n), but the loop runs in compiled code over contiguous memory, so the constant is 10–100× smaller. The rule: push loops into the library.

OperationCostNotes
a + b, a * 2, np.sqrt(a), comparisonsO(n), vectorizedBroadcasting avoids copies of the smaller operand
a.sum(), a.mean(), a.max()O(n)Pairwise summation for floats
a[i], basic slice a[2:10]O(1)A slice is a view: no copy
Boolean mask a[a > 0], fancy index a[[1, 5]]O(n) / O(k)Always a copy
np.append, np.concatenate in a loopO(n²) totalPreallocate, or collect in a list then concatenate once
np.sort(a)O(n log n)Introsort by default; kind="stable" for stable
A @ B (n × n)O(n³)BLAS-optimized and multithreaded
np.linalg.solve (n × n)O(n³)Never compute inv(A) @ b
np.searchsortedO(log n) per queryVectorized bisect
pandas df.groupby(k).agg(...)O(n) hashing (+ sort if sort=True)
pandas merge / joinO(n + m) hash join typicalCan explode for many-to-many keys
pandas df.apply(f, axis=1), iterrows()O(n) Python callsSlow; prefer column expressions
pandas df.loc[label] on a unique indexO(1) avgHash-based index lookup

Depth: M48 NumPy, M51 pandas, M52 Polars & Arrow.

Which structure should I use?

flowchart TD
  Q{"What do you mostly do?"} --> A["Look up by key"]
  Q --> B["Keep things in order"]
  Q --> C["Take the smallest or largest next"]
  Q --> D["Add and remove at the ends"]
  Q --> E["Test membership or de-duplicate"]
  Q --> F["Heavy numeric math"]
  A --> A1{"Need the keys sorted?"}
  A1 -- no --> A2["dict  O(1)"]
  A1 -- yes --> A3["sorted list + bisect, or a balanced tree"]
  B --> B1{"Mostly append and index?"}
  B1 -- yes --> B2["list"]
  B1 -- "insert in the middle often" --> B3["bisect.insort for small n, else a balanced tree"]
  C --> C1["heapq  O(log n)"]
  D --> D1{"Only one end?"}
  D1 -- "yes, LIFO" --> D2["list as a stack"]
  D1 -- "both ends or FIFO" --> D3["collections.deque"]
  E --> E1{"Need counts?"}
  E1 -- no --> E2["set  O(1)"]
  E1 -- yes --> E3["Counter"]
  F --> F1["NumPy array or pandas / Polars"]
    

Check yourself

Going further