Big O — time & space
How runtime and memory scale as input n grows. Drop the constants, keep the dominant term.
How runtime and memory scale as input n grows. Drop the constants, keep the dominant term. Colour tells you how much it hurts.
The shape of it
Same axes, wildly different fates. Past a certain n, the curve you picked is the whole story.
Complexity classes — best to worst
Data structure operations (average time)
| Structure | Access | Search | Insert | Delete |
|---|---|---|---|---|
| Array | O(1) | O(n) | O(n) | O(n) |
| Dynamic array | O(1) | O(n) | O(n) | O(n) |
| Stack / Queue | O(n) | O(n) | O(1) | O(1) |
| Linked list | O(n) | O(n) | O(1) | O(1) |
| Hash table | n/a | O(1) | O(1) | O(1) |
| Binary search tree | O(log n) | O(log n) | O(log n) | O(log n) |
| Balanced BST (AVL, R-B) | O(log n) | O(log n) | O(log n) | O(log n) |
| Binary heap | peek O(1) | O(n) | O(log n) | O(log n) |
Hash table and BST degrade to O(n) worst case (bad hashing / unbalanced tree). Dynamic-array append is amortized O(1) — the O(n) above is insertion at an arbitrary index, and resizing is what makes the append amortized rather than flat. Linked-list insert/delete is O(1) only once you hold the node — finding it is O(n).
Sorting algorithms
| Algorithm | Best | Average | Worst | Space |
|---|---|---|---|---|
| Quicksort | O(n log n) | O(n log n) | O(n²) | O(log n) |
| Merge sort | O(n log n) | O(n log n) | O(n log n) | O(n) |
| Heapsort | O(n log n) | O(n log n) | O(n log n) | O(1) |
| Timsort (Python, Java) | O(n) | O(n log n) | O(n log n) | O(n) |
| Insertion sort | O(n) | O(n²) | O(n²) | O(1) |
| Bubble sort | O(n) | O(n²) | O(n²) | O(1) |
| Selection sort | O(n²) | O(n²) | O(n²) | O(1) |
| Counting sort | O(n+k) | O(n+k) | O(n+k) | O(k) |
| Radix sort | O(n·k) | O(n·k) | O(n·k) | O(n+k) |
Comparison sorts can’t beat O(n log n) on average. Counting/radix dodge that by not comparing — but need bounded keys (k = key range / digit count).
How to read the code
dropDrop constants & lower terms. O(2n + 5) → O(n), O(n² + n) → O(n²).1 loopOne loop over n → O(n).nestedLoops inside loops multiply: two nested → O(n²), three → O(n³).side by sideSequential loops add, and adding collapses to the biggest: O(n)+O(n) → O(n).÷2Input halved each step → O(log n) (binary search, tree descent).sort+loopLoop doing log-n work per item, or a scan after a sort → O(n log n).recurseRecursion ≈ branches ^ depth. Two branches, depth n → O(2ⁿ). Memoize to cut it down.spaceSpace = extra memory you allocate, not the input. Recursion counts the call stack.
Common patterns at a glance
Worked examples — the same idea in two languages
Direct access
No loop. The work is one indexed read — same cost for ten items or ten million.
function first<T>(arr: T[]): T {
// one step, any length
return arr[0];
}def first(arr):
# one step, any length
return arr[0]Binary search
Each pass discards half the remaining range, so a million elements resolve in ~20 steps. Input must be sorted.
function search(a: number[], target: number): number {
let lo = 0, hi = a.length - 1;
while (lo <= hi) {
const mid = (lo + hi) >> 1;
if (a[mid] === target) return mid;
if (a[mid] < target) lo = mid + 1;
else hi = mid - 1;
}
return -1;
}def search(a, target):
lo, hi = 0, len(a) - 1
while lo <= hi:
mid = (lo + hi) // 2
if a[mid] == target:
return mid
if a[mid] < target:
lo = mid + 1
else:
hi = mid - 1
return -1Linear scan
One pass, one touch each. Double the input, double the work — a straight line.
function total(nums: number[]): number {
let sum = 0;
for (const n of nums) sum += n; // touches each once
return sum;
}def total(nums):
running = 0
for n in nums: # touches each once
running += n
return runningSort, then scan
The sort dominates. O(n log n) followed by an O(n) scan collapses to O(n log n) — the bigger term wins.
function hasDup(nums: number[]): boolean {
const s = [...nums].sort((a, b) => a - b); // O(n log n)
for (let i = 1; i < s.length; i++) // O(n)
if (s[i] === s[i - 1]) return true;
return false;
}def has_dup(nums):
s = sorted(nums) # O(n log n)
for i in range(1, len(s)): # O(n)
if s[i] == s[i - 1]:
return True
return FalseEvery pair
A loop inside a loop → n × n comparisons. A hash set of seen values trades space to bring this down to O(n).
function hasPairSum(nums: number[], t: number): boolean {
for (let i = 0; i < nums.length; i++)
for (let j = i + 1; j < nums.length; j++) // every pair
if (nums[i] + nums[j] === t) return true;
return false;
}def has_pair_sum(nums, t):
for i in range(len(nums)):
for j in range(i + 1, len(nums)): # every pair
if nums[i] + nums[j] == t:
return True
return FalseNaive recursion
Each call spawns two more, so the call tree roughly doubles per step. Memoizing the results collapses it to O(n).
function fib(n: number): number {
if (n < 2) return n;
return fib(n - 1) + fib(n - 2); // two branches
}def fib(n):
if n < 2:
return n
return fib(n - 1) + fib(n - 2) # two branchesRule of thumb: Big O is the worst-case ceiling as n → ∞. It ignores constants,
so an O(n) routine can lose to O(n²) on tiny inputs — but pick for scale and
you rarely regret it.