Data Structures & Algorithms · cheat sheet

Data Structures & Algorithms

Big-O, data structure and sorting complexity tables, problem-solving patterns and their signals, Python templates, graph algorithms and a DP recipe.

The complexity tables, patterns and tested Python templates behind most coding rounds, on one page.

Big-O essentials

Big-O Example At n = 10^6
O(1) array index, hash lookup, stack push 1
O(log n) binary search, balanced BST, heap push ~20
O(n) one scan, two pointers, BFS/DFS in O(V+E) 10^6
O(n log n) merge sort, heap sort, n heap operations ~2·10^7
O(n^2) all pairs, nested loops, 2D DP 10^12
O(2^n) / O(n!) all subsets / all permutations hopeless
  • Drop constants and lower-order terms: O(3n + n^2) is O(n^2). Sequential steps add, nested loops multiply; separate inputs keep both: O(m + n), O(m·n).
  • Recursion costs roughly branches^depth. T(n) = 2T(n/2) + O(n) is O(n log n) (merge sort); T(n) = T(n/2) + O(1) is O(log n) (binary search).
  • Amortized O(1): a dynamic array doubles when full, so n appends cost O(n) in total.
  • Space includes the recursion stack: DFS down a path-shaped tree is O(n) deep.
  • O is an upper bound, Ω lower, Θ tight. Say whether you mean average or worst case (hash tables, quicksort).

Input size → target complexity

n up to Aim for Typical approach
~10 O(n!) permutations, brute force
~20 O(2^n), O(n·2^n) subsets, backtracking, bitmask DP
~500 O(n^3) Floyd-Warshall, interval DP
~5,000 O(n^2) pairwise DP (LCS, edit distance)
~10^6 O(n log n), O(n) sorting, heaps, hashing, two pointers, windows
values ~10^9+ O(log V) binary search on the answer, math
  • Rule of thumb: roughly 10^8 simple operations per second in C++ or Java, 10^7 in CPython; in Python, aim a notch lower and lean on built-ins.
  • 2^10 ≈ 10^3, 2^20 ≈ 10^6, 2^30 ≈ 10^9; log2(10^6) ≈ 20.

Data structure operations

Structure Access Search Insert Delete Note
Array O(1) O(n); O(log n) sorted O(n) O(n) contiguous, cache friendly
Dynamic array O(1) O(n) O(1) amortized at end, O(n) middle O(1) end, O(n) middle geometric resizing
Linked list O(n) O(n) O(1) at head or known node O(1) at known node singly linked needs the predecessor
Stack / queue O(1) top/front O(n) O(1) O(1) LIFO / FIFO
Hash table n/a O(1) avg, O(n) worst O(1) avg O(1) avg chaining or open addressing
Binary heap O(1) min O(n) O(log n) O(log n) pop build O(n)
BST, balanced O(log n) O(log n) O(log n) O(log n) AVL, red-black; ordered
BST, unbalanced O(n) worst O(n) worst O(n) worst O(n) worst O(log n) on random input
Trie O(L) O(L) O(L) O(L) L = key length; prefixes
Union-find n/a find ~O(1) union ~O(1) n/a α(n) amortized
Graph as Space Edge u→v? Neighbors of u Best for
Adjacency list O(V+E) O(deg u) O(deg u) sparse graphs (most interviews)
Adjacency matrix O(V^2) O(1) O(V) dense graphs, Floyd-Warshall
Edge list O(E) O(E) O(E) Kruskal, Bellman-Ford

Sorting algorithms

Algorithm Best Average Worst Space Stable
Bubble (early exit) O(n) O(n^2) O(n^2) O(1) yes
Selection O(n^2) O(n^2) O(n^2) O(1) no
Insertion O(n) O(n^2) O(n^2) O(1) yes
Merge O(n log n) O(n log n) O(n log n) O(n) yes
Quick O(n log n) O(n log n) O(n^2) O(log n) avg no
Heap O(n log n) O(n log n) O(n log n) O(1) no
Counting (range k) O(n+k) O(n+k) O(n+k) O(n+k) yes
Radix (d digits) O(d(n+k)) O(d(n+k)) O(d(n+k)) O(n+k) yes
Timsort O(n) O(n log n) O(n log n) O(n) yes
  • Comparison sorts can’t beat Ω(n log n): the decision tree has n! leaves, and log2(n!) is Θ(n log n).
  • Python’s sorted and Java’s object sorts use Timsort; Java’s primitive arrays use dual-pivot quicksort. A random pivot makes quicksort’s worst case unlikely.
def lower_bound(a, target):              # first index with a[i] >= target
    lo, hi = 0, len(a)                   # search space is [lo, hi)
    while lo < hi:
        mid = (lo + hi) // 2
        if a[mid] < target:
            lo = mid + 1                 # answer is right of mid
        else:
            hi = mid                     # mid might be the answer
    return lo                            # len(a) if every item is smaller
python
  • Found? i < len(a) and a[i] == target. First index > target: test <= instead (bisect_right). Last index ≤ t: bisect_right(a, t) - 1. Count of t: bisect_right - bisect_left.
  • Rotated sorted array: compare a[mid] with a[hi] to find the sorted half, then check if the target lies in it.
  • Binary search on the answer when feasibility is monotone (“smallest speed or capacity that works”):
def first_true(lo, hi, ok):              # smallest x in [lo, hi] with ok(x)
    while lo < hi:                       # ok must be monotone: F..F T..T
        mid = (lo + hi) // 2
        if ok(mid): hi = mid
        else: lo = mid + 1
    return lo                            # hi if ok is never true
python
  • Built in: bisect.bisect_left, bisect_right, insort (key= since 3.10).

Problem-solving patterns

Pattern Use when you see Classic problems
Two pointers sorted array, pairs, in-place partition, palindromes two sum II, 3Sum
Sliding window contiguous subarray/substring, “longest with at most k” longest substring without repeats
Prefix sums range sums, “subarray sums to k” (+ hash map) subarray sum equals k
Fast / slow pointers cycles, middle of a list, O(1) space linked list cycle
Monotonic stack next greater/smaller element, spans daily temperatures, histogram
Intervals overlapping ranges: sort by start, merge merge intervals, meeting rooms
Top-K with a heap k largest / most frequent, streams kth largest, merge k lists
BFS shortest path unweighted, levels, grids rotting oranges, word ladder
DFS connectivity, islands, paths, trees number of islands, clone graph
Topological sort prerequisites, build order course schedule
Union-find dynamic connectivity, grouping redundant connection
Backtracking “all” subsets / permutations, constraints N-Queens, word search
Dynamic programming count ways, min/max cost, overlapping subproblems coin change, LIS, LCS
Greedy a local choice provably stays optimal interval scheduling, jump game
Binary search on answer “minimize the maximum”, monotone check Koko bananas, ship in D days
Bit manipulation XOR cancels pairs, masks, powers of two single number, counting bits
  • Bits: x & (x - 1) clears the lowest set bit; x & -x isolates it; x > 0 and x & (x - 1) == 0 tests a power of two; (x >> k) & 1 reads bit k; a ^ a == 0; x.bit_count() (3.10); for mask in range(1 << n) enumerates subsets.
  • Top-K: a min-heap capped at size k leaves the kth largest at heap[0], in O(n log k); or heapq.nlargest(k, nums).

Templates: windows & backtracking

def min_window_sum(nums, target):        # shortest subarray with sum >= target
    left = total = 0                     # (all nums positive)
    best = float("inf")
    for right, x in enumerate(nums):
        total += x                       # grow the window
        while total >= target:           # valid: shrink from the left
            best = min(best, right - left + 1)
            total -= nums[left]
            left += 1
    return 0 if best == float("inf") else best
python
def subsets(nums):
    res, path = [], []
    def backtrack(start):
        res.append(path[:])              # record a COPY of the current choice
        for i in range(start, len(nums)):
            path.append(nums[i])         # choose
            backtrack(i + 1)             # explore
            path.pop()                   # un-choose
    backtrack(0)
    return res                           # 2^n subsets
python
  • Each index enters and leaves the window once: O(n). Backtracking prunes as soon as a partial choice can’t succeed.

Templates: BFS, DFS & topo sort

from collections import deque
def bfs(graph, start):                   # graph: {node: [neighbors]}
    dist = {start: 0}
    queue = deque([start])
    while queue:
        node = queue.popleft()
        for nxt in graph[node]:
            if nxt not in dist:          # mark on enqueue, not on dequeue
                dist[nxt] = dist[node] + 1
                queue.append(nxt)
    return dist                          # fewest edges to each reachable node
python
def num_islands(grid):                   # grid of "1"/"0"; mutated in place
    if not grid: return 0
    rows, cols = len(grid), len(grid[0])
    def sink(r, c):                      # DFS flood fill
        if 0 <= r < rows and 0 <= c < cols and grid[r][c] == "1":
            grid[r][c] = "0"             # mark visited
            for dr, dc in ((1, 0), (-1, 0), (0, 1), (0, -1)):
                sink(r + dr, c + dc)
            return 1
        return 0
    return sum(sink(r, c) for r in range(rows) for c in range(cols))
python
def topo_sort(n, edges):                 # Kahn's algorithm; (u, v): u before v
    graph, indeg = [[] for _ in range(n)], [0] * n
    for u, v in edges:
        graph[u].append(v); indeg[v] += 1
    queue = deque(i for i in range(n) if indeg[i] == 0)
    order = []
    while queue:
        u = queue.popleft(); order.append(u)
        for v in graph[u]:
            indeg[v] -= 1
            if indeg[v] == 0: queue.append(v)
    return order if len(order) == n else []   # [] means a cycle
python
  • Multi-source BFS (rotting oranges): seed the queue with every source at distance 0.

Graph algorithms

Algorithm Solves Time Notes
BFS shortest path, unweighted O(V+E) queue
DFS reachability, components, cycles O(V+E) stack or recursion
Topological sort ordering a DAG O(V+E) Kahn or DFS post-order
Dijkstra single source, weights ≥ 0 O((V+E) log V), binary heap wrong with negative edges
Bellman-Ford single source, negative weights O(V·E) V-1 rounds; a Vth improvement means a negative cycle
Floyd-Warshall all pairs O(V^3) time, O(V^2) space small V
Kruskal minimum spanning tree O(E log E) sorted edges + union-find
Prim minimum spanning tree O(E log V), binary heap grows one tree
import heapq
def dijkstra(graph, src):                # graph: {u: [(v, w), ...]}, w >= 0
    dist, heap = {src: 0}, [(0, src)]
    while heap:
        d, u = heapq.heappop(heap)
        if d > dist[u]: continue         # stale entry, skip
        for v, w in graph[u]:
            nd = d + w
            if nd < dist.get(v, float("inf")):
                dist[v] = nd
                heapq.heappush(heap, (nd, v))
    return dist
python
parent, size = list(range(n)), [1] * n
def find(x):
    while parent[x] != x:
        parent[x] = parent[parent[x]]         # path halving
        x = parent[x]
    return x
def union(a, b):                              # False if already connected
    ra, rb = find(a), find(b)
    if ra == rb: return False
    if size[ra] < size[rb]: ra, rb = rb, ra   # union by size
    parent[rb] = ra; size[ra] += size[rb]
    return True
python

Dynamic programming

  1. State: what identifies a subproblem, e.g. dp[i] for the first i items, dp[i][j] for prefixes a[:i] and b[:j].
  2. Transition: combine smaller states, one term per choice (take or skip, which coin).
  3. Base cases: the smallest states (empty prefix, amount 0).
  4. Order: dependencies first; memoization does it for you, tabulation loops upward.
  5. Answer & space: locate the answer; keep one row if that’s all the transition reads.
  • Time = states × work per state. DP needs overlapping subproblems and optimal substructure.
from functools import cache
def coin_change(coins, amount):          # fewest coins, -1 if impossible
    @cache
    def best(rem):                       # state: amount still to pay
        if rem == 0: return 0            # base case
        if rem < 0: return float("inf")
        return 1 + min(best(rem - c) for c in coins)   # transition
    ans = best(amount)
    return -1 if ans == float("inf") else ans
python
def coin_change(coins, amount):          # bottom-up: no recursion depth limit
    dp = [0] + [float("inf")] * amount   # dp[a] = fewest coins that make a
    for a in range(1, amount + 1):       # order: smaller amounts first
        for c in coins:
            if c <= a: dp[a] = min(dp[a], dp[a - c] + 1)
    return -1 if dp[amount] == float("inf") else dp[amount]
python
Problem State Transition Time
Climbing stairs dp[i] ways to step i dp[i-1] + dp[i-2] O(n)
House robber dp[i] best of first i houses max(dp[i-1], dp[i-2] + a[i-1]) O(n)
0/1 knapsack dp[i][w] max(skip, take + value) O(n·W)
LIS dp[i] longest ending at i max(dp[j] + 1), j < i, a[j] < a[i] O(n^2); O(n log n) with bisect
LCS / edit distance dp[i][j] two prefixes diagonal on match, else best neighbor O(m·n)
Interval DP dp[i][j] a range best split point k O(n^3)

Python toolkit for DSA

Need Use Watch out
Stack list.append / pop() pop(0) is O(n)
Queue deque.append / popleft() middle indexing is O(n)
Min-heap heapq.heappush, heappop, heapify max-heap: push -x (3.14 adds heappush_max etc.)
Counting Counter, defaultdict(list) most_common(k) for top-k
Sorted search bisect_left, bisect_right, insort insort is O(n)
Memoization @functools.cache hashable arguments (tuples, not lists)
Ordered map none in the stdlib sorted list + bisect, or third-party sortedcontainers

How to approach the interview

  1. Clarify: input size, value ranges, duplicates, negatives, empty input, sortedness, output format.
  2. Examples: work a normal and an edge case by hand.
  3. Brute force: state it with its complexity.
  4. Optimize: check the input-size table, find the bottleneck, match a pattern, trade memory for time.
  5. Plan out loud and get a nod before coding.
  6. Code cleanly: clear names, small helpers.
  7. Test: trace the example, then empty, one element, duplicates, negatives, maximum size.
  8. Analyze: final time and space, then trade-offs and follow-ups.

Interview tip

Keep talking. A clearly explained brute force, then improved, beats a silent optimal attempt that stalls.

Quick answers

  • Array vs linked list? O(1) indexing and cache locality vs O(1) insert/delete at a known node.
  • Hash collisions? Chaining or open addressing; resizing keeps the load factor low: O(1) average, O(n) worst.
  • Why is quicksort fast in practice? In place and cache friendly; random pivots make O(n^2) unlikely.
  • Stable sort? Equal keys keep input order, so multi-key sorts compose.
  • BFS vs DFS? Shortest unweighted paths but stores whole levels vs less memory, suits paths, cycles, topological order.
  • Dijkstra vs Bellman-Ford? Faster, non-negative weights only vs slower, handles negative weights and detects negative cycles.
  • Heap vs balanced BST? O(1) min, no ordered iteration vs O(log n) everything plus sorted order and range queries.
  • Memoization vs tabulation? Top-down recursion with a cache vs bottom-up loops (no recursion limit, easy space savings).
  • DP vs greedy? DP tries every choice via subproblems; greedy commits to one and needs a proof (exchange argument).
  • Cycle in a linked list? Floyd’s fast and slow pointers meet inside it; O(1) space.
  • Cycle in a graph? Undirected: union-find or DFS ignoring the parent edge. Directed: three-color DFS, or Kahn’s leaving nodes unprocessed.
  • Why is comparison sorting Ω(n log n)? log2(n!) comparisons are needed to tell n! orderings apart.

Gotchas & traps

  • Off-by-one: choose [lo, hi) with lo < hi or [lo, hi] with lo <= hi and keep the invariant. With lo = mid, use (lo + hi + 1) // 2 or the loop never ends.
  • Integer overflow (Java, C++; Python ints don’t overflow): (lo + hi) / 2 can overflow, so write lo + (hi - lo) / 2; keep sums and products in long.
  • Recursion limit: CPython defaults to 1000 frames, so deep DFS or memoization raises RecursionError. Use an explicit stack or bottom-up DP; sys.setrecursionlimit can still crash the process.
  • Mutable defaults: def dfs(node, path=[]) shares one list across calls; [[0] * m] * n shares one row.
  • Backtracking must record path[:], not path.
  • Mark BFS nodes visited on enqueue, or they’re queued repeatedly.
  • heapq compares whole tuples: a priority tie falls through to items that may not support <. Push (priority, counter, item).
  • Always test empty input, one element, all duplicates and the maximum size.

Practice next: DSA questions and the DSA quiz.

esc