DSA Handbook

#The cheat sheet

Every template here was executed against test cases before publishing, including 300 randomised binary-search cases checked against Python's bisect. Cheat sheets are usually where off-by-ones go to hide.


#1 · Constraints → algorithm

The fastest read in the room. The constraint tells you the intended complexity, and the complexity tells you the algorithm.

n up toAllowedMeans
10–12O(n!)Permutations, brute force
≤ 20O(2ⁿ)Subsets, bitmask DP, backtracking
100O(n⁴)3–4 nested loops, interval DP
500O(n³)Floyd-Warshall, matrix chain
5,000O(n²)Two-loop DP, all-pairs
10⁵–10⁶O(n log n)Sort, heap, binary search — the sweet spot
10⁷–10⁸O(n)One pass, counting, hashing
10⁹+O(log n) or O(1)Binary search, maths, closed form

If n ≤ 20 and the question says "return all", the answer is exponential and that is intended. Do not look for a clever polynomial solution.


Interactive simulation — needs JavaScript.


#2 · Cue → pattern

The problem saysReach for
"sorted array"Binary search or two pointers
"two numbers that sum to"Hash map, or two pointers if sorted
"contiguous subarray / substring"Sliding window or prefix sums
"subarray sums to k" with negativesPrefix sums + hash map — window fails
"top k" / "k largest" / "median of a stream"Heap
"next greater / smaller element"Monotonic stack
"return all …", n ≤ 20Backtracking
"how many ways", "min/max cost"DP
"shortest path", unweightedBFS
"shortest path", weightedDijkstra
"connected components", edges arrive over timeUnion-Find
"prefix", "autocomplete", "dictionary of words"Trie
"intervals", "meetings", "merge"Sort by start or end
"cycle in a linked list"Fast/slow pointers
"minimise the maximum" / "maximise the minimum"Binary search on the answer
"in O(1) space" with values 1..nIndex-as-hash, or cycle detection

#3 · The templates

Type these from memory. All verified.

#Binary search — exact

def search(a, target):
    lo, hi = 0, len(a) - 1          # CLOSED interval
    while lo <= hi:                 # <= because hi is inclusive
        mid = lo + (hi - lo) // 2   # avoids overflow in Java/C++
        if a[mid] == target: return mid
        if a[mid] < target: lo = mid + 1
        else:                hi = mid - 1
    return -1

#Binary search — leftmost / rightmost

The two you actually need. Half-open interval; the loop shape differs from the exact version and mixing them is the classic bug.

def left_bound(a, t):               # == bisect_left
    lo, hi = 0, len(a)              # hi = n, HALF-OPEN
    while lo < hi:                  # < not <=
        mid = lo + (hi - lo) // 2
        if a[mid] < t: lo = mid + 1
        else:          hi = mid     # NOT mid-1
    return lo

def right_bound(a, t):              # == bisect_right
    lo, hi = 0, len(a)
    while lo < hi:
        mid = lo + (hi - lo) // 2
        if a[mid] <= t: lo = mid + 1    # the ONLY change: <= not <
        else:           hi = mid
    return lo

left and right differ by one character — < versus <=. Learn the pair together; learning one alone guarantees you derive the other wrongly under pressure.

#Binary search on the answer

The pattern that looks like nothing else. Use when the question is "minimise the maximum" or "maximise the minimum".

def min_capacity(weights, days):
    def feasible(cap):              # monotonic: true for all larger caps
        d, cur = 1, 0
        for w in weights:
            if cur + w > cap:
                d += 1; cur = 0
            cur += w
        return d <= days

    lo, hi = max(weights), sum(weights)   # bounds must be VALID answers
    while lo < hi:
        mid = lo + (hi - lo) // 2
        if feasible(mid): hi = mid        # keep searching lower
        else:             lo = mid + 1
    return lo

Three questions to ask: what am I searching over (not the array — the answer)? Is feasible monotonic? Are my bounds valid answers?

#Sliding window — variable size

def longest_unique(s):
    seen = {}                        # char -> last index
    best = left = 0
    for right, ch in enumerate(s):
        if ch in seen and seen[ch] >= left:
            left = seen[ch] + 1      # >= left: never move `left` BACKWARDS
        seen[ch] = right
        best = max(best, right - left + 1)
    return best

seen[ch] >= left is the whole difficulty. On "abba", when the second a arrives its stored index is 0, which is behind left. Without the guard you jump left backwards and get 3 instead of 2.

#Two pointers — opposite ends

def two_sum_sorted(a, target):
    lo, hi = 0, len(a) - 1
    while lo < hi:
        s = a[lo] + a[hi]
        if s == target: return [lo, hi]
        if s < target:  lo += 1      # need bigger
        else:           hi -= 1      # need smaller
    return []

#Monotonic stack — next greater

def next_greater(nums):
    res, st = [-1] * len(nums), []   # st holds INDICES
    for i, n in enumerate(nums):
        while st and nums[st[-1]] < n:
            res[st.pop()] = n        # n is the answer for everything popped
        st.append(i)
    return res

Store indices, not values — you almost always need the position, and can get the value from it. For distance problems (LC 739) the answer is i - j.

#BFS on a grid

from collections import deque

def shortest_path(grid):
    R, C = len(grid), len(grid[0])
    q = deque([(0, 0, 1)])
    seen = {(0, 0)}                  # mark visited ON PUSH, not on pop
    while q:
        r, c, d = q.popleft()
        if (r, c) == (R - 1, C - 1): return d
        for dr, dc in ((1,0), (-1,0), (0,1), (0,-1)):
            nr, nc = r + dr, c + dc
            if 0 <= nr < R and 0 <= nc < C and grid[nr][nc] == 0 \
               and (nr, nc) not in seen:
                seen.add((nr, nc))
                q.append((nr, nc, d + 1))
    return -1

Mark visited when you push, not when you pop. Popping lets the same cell enter the queue many times before it is first processed, which turns O(V+E) into something much worse.

#Backtracking

def backtrack(path, choices):
    if is_complete(path):
        results.append(path[:])      # COPY -- path is mutated after this
        return
    for choice in choices:
        if not valid(choice, path): continue   # prune
        path.append(choice)          # choose
        backtrack(path, next_choices)# explore
        path.pop()                   # UNDO

#DP — 1D, rolling variables

def rob(nums):
    prev = cur = 0
    for n in nums:
        prev, cur = cur, max(cur, prev + n)
    return cur

Write the O(n)-space table first, then compress. Compressing before the recurrence is correct is how you produce something that is fast and wrong.

#Union-Find

class DSU:
    def __init__(self, n):
        self.p = list(range(n)); self.r = [0]*n; self.count = n
    def find(self, x):
        if self.p[x] != x: self.p[x] = self.find(self.p[x])   # compress
        return self.p[x]
    def union(self, a, b):
        ra, rb = self.find(a), self.find(b)
        if ra == rb: return False        # already joined -> a CYCLE edge
        if self.r[ra] < self.r[rb]: ra, rb = rb, ra
        self.p[rb] = ra
        if self.r[ra] == self.r[rb]: self.r[ra] += 1
        self.count -= 1
        return True

#4 · Complexity

StructureAccessSearchInsertDelete
ArrayO(1)O(n)O(n)O(n)
Sorted arrayO(1)O(log n)O(n)O(n)
Hash map—O(1)O(1)O(1)
Balanced BST / TreeMap—O(log n)O(log n)O(log n)
HeapO(1) peekO(n)O(log n)O(log n)
Linked listO(n)O(n)O(1) at a known nodeO(1)
Trie—O(L)O(L)O(L)
Union-Find—~O(1)——

Sorting is O(n log n). If your solution is already O(n log n), sorting is free — say so.

Recursion space is the call depth, and it is separate from time. The subsets tree is O(2ⁿ) time but only O(n) stack.


#5 · Python ↔ Java

TaskPythonJava
Min-heapheapq (min by default)PriorityQueue<>()
Max-heapnegate valuesPriorityQueue<>(Collections.reverseOrder())
Push+pop in oneheappushpopoffer then poll
Sort by keyxs.sort(key=lambda x: x[1])Arrays.sort(a, (x,y) -> Integer.compare(x[1], y[1]))
Countercollections.Countermap.merge(k, 1, Integer::sum)
Default dictdefaultdict(list)map.computeIfAbsent(k, x -> new ArrayList<>())
Dequecollections.dequeArrayDeque
Binary searchbisect_left/rightArrays.binarySearch (undefined on duplicates)
Build a string"".join(parts)StringBuilder
Integer division// (floors toward −∞)/ (truncates toward 0)
-7 % 32-1
Max intunboundedInteger.MAX_VALUE, overflows

Four that cause real bugs: a - b in a Java comparator overflows — use Integer.compare. Java % keeps the dividend's sign — normalise with ((x % k) + k) % k. -7 // 2 is -4 in Python, -3 in Java. Python tuple comparison falls through to the next element — add a tiebreaker when heaping non-comparable objects.


#6 · Bugs that actually cost you

BugWhere it bites
while lo <= hi with half-open boundsBinary search — infinite loop
hi = mid - 1 in the leftmost templateSkips the answer
Moving left backwardsSliding window on repeated chars
path instead of path[:]Backtracking — all results identical
Forgetting path.pop()Backtracking — results contaminated
Marking visited on popBFS/DFS — exponential blowup
i > 0 instead of i > startDuplicate handling in subsets
s += ch in a loopStrings — silent O(n²)
Slicing inside a loopSame, hidden
No counts[0] = 1Prefix-sum counting — off by exactly the prefixes
Comparing parent[a] == parent[b]Union-Find — compare find(), not parents
Integer overflow on (lo+hi)/2Java/C++ — use lo + (hi-lo)/2
Not handling empty inputEverywhere

#7 · Before you type

[ ] Restate the problem in one sentence
[ ] Ask: duplicates? negatives? empty? sorted? size?
[ ] State the brute force AND its complexity
[ ] Name the pattern out loud, and why
[ ] Say the target complexity before coding
[ ] Walk one small example by hand
[ ] Code it
[ ] Trace the example through your code, out loud
[ ] Check: empty, single element, all-same, largest input

Stating the brute force first is free marks. It shows you understand the problem before optimising it, and it gives you a correctness baseline to compare against.


#8 · The 10 problems that teach the most

If you have one evening:

#ProblemTeaches
1Two Sum (LC 1)Hash map as memory
2Valid Parentheses (LC 20)The stack template
3Longest Substring Without Repeating (LC 3)Sliding window
4Merge Intervals (LC 56)Sort-then-sweep
5Number of Islands (LC 200)Grid DFS/BFS
6Course Schedule (LC 207)Topological sort / cycle detection
7Coin Change (LC 322)DP from a recurrence
8Subsets (LC 78)The backtracking template
9Kth Largest (LC 215)Heap vs quickselect
10Binary Search (LC 704)The bounds, properly

These ten cover eight patterns. If you can re-derive all ten cold, you can attempt most of an interview loop.