Python DSA
Syntax Atlas
Every pattern, method, and gotcha needed to solve ~90% of DSA problems — in one searchable page. Built for fast lookup while grinding problems or prepping for interviews.
Lists & Arrays
Python's dynamic array. The workhorse of DSA — think of it as a row of labeled boxes where you can add, remove, or peek at any box in O(1) at the ends.
Creation Patterns
# Empty / pre-filled
arr = []
arr = [0] * n # n zeros (1D)
arr = [0] * (rows * cols) # flat 2D (rare)
# 2D matrix — ALWAYS use comprehension (avoid shared rows!)
matrix = [[0] * cols for _ in range(rows)]
# From iterables
arr = list(range(n)) # [0, 1, ..., n-1]
arr = list(map(int, input().split()))
arr = list("abc") # ['a', 'b', 'c']
arr = [int(x) for x in input().split()]
[[0]*cols]*rows creates rows aliases of the same list. Mutating one row mutates all. Always use the comprehension form.
Indexing & Slicing
arr = [10, 20, 30, 40, 50] idx: 0 1 2 3 4 (positive) neg: -5 -4 -3 -2 -1 (negative) arr[1:4] -> [20, 30, 40] # start inclusive, end exclusive arr[:3] -> [10, 20, 30] # from beginning arr[2:] -> [30, 40, 50] # to end arr[-2:] -> [40, 50] # last 2 arr[::2] -> [10, 30, 50] # step=2 arr[::-1] -> [50, 40, 30, 20, 10] # reverse! arr[1:4:2] -> [20, 40] # start:stop:step
Methods — In-place vs New
# ---- In-place (mutate original) ----
arr.append(x) # O(1) amortized
arr.extend(iterable) # O(k)
arr.insert(i, x) # O(n) — shifts
arr.pop() # O(1) — remove last
arr.pop(i) # O(n) — shifts
arr.remove(x) # O(n) — first match by value
arr.clear() # O(n)
arr.sort() # O(n log n)
arr.reverse() # O(n)
# ---- Return new (don't mutate) ----
arr + other # concatenation
arr * k # repeat k times
sorted(arr) # new sorted list
arr.copy() # shallow copy (= arr[:])
arr.index(x) # first index of x, ValueError if missing
arr.count(x) # occurrences
len(arr) # length
x in arr # O(n) membership
List Comprehensions
# Basic: [expr for var in iter if cond]
squares = [x*x for x in range(10)]
evens = [x for x in arr if x % 2 == 0]
# With function call
upper = [s.upper() for s in words]
# Nested (2D)
flat = [x for row in matrix for x in row]
# Conditional expression
label = ['even' if x % 2 == 0 else 'odd' for x in arr]
# Multiple vars from pairs
keys = [k for k, v in pairs if v > 0]
for loop.
Built-in Aggregators
| Function | Returns | Example |
|---|---|---|
| len(arr) | O(1) | size |
| sum(arr) | O(n) | total |
| min(arr) / max(arr) | O(n) | extreme |
| any(arr) | O(n) short-circuit | any truthy? |
| all(arr) | O(n) short-circuit | all truthy? |
| sorted(arr) | O(n log n) | new sorted list |
| enumerate(arr) | O(1) | (index, value) pairs |
| zip(a, b) | O(1) | parallel pairs |
Dictionaries & Hash Maps
Think of a dict as a coat-check room: give a name (key), get the coat (value) back instantly — O(1) average lookup. The single most important data structure for frequency counting, memoization, and graph adjacency.
Core Operations
# Creation
d = {}
d = dict()
d = {"a": 1, "b": 2}
d = dict(pairs) # from [(k, v), ...]
d = {k: v for k, v in pairs} # comprehension
# Access
d[key] # KeyError if missing!
d.get(key) # None if missing
d.get(key, default) # default if missing
# Mutate
d[key] = value # set/overwrite
del d[key] # remove (KeyError if missing)
d.pop(key) # remove + return value
d.pop(key, default) # safe remove
d.update(other_dict) # merge in-place
d.setdefault(key, default) # set if missing, return value
# Iteration
for k in d: # keys (default)
for k in d.keys():
for v in d.values():
for k, v in d.items(): # both — most common
# Membership
key in d # O(1) average
d[key] = d.get(key, 0) + 1 — but Counter (below) is cleaner.
defaultdict — Auto-init Missing Keys
from collections import defaultdict
# Each access to a missing key auto-creates the default
counts = defaultdict(int) # default 0
counts["apple"] += 1 # no KeyError
groups = defaultdict(list) # default []
groups["fruit"].append("apple") # no KeyError
graph = defaultdict(set)
graph[0].add(1) # adjacency set
unique_words = defaultdict(set)
# Common: build adjacency list
graph = defaultdict(list)
for u, v in edges:
graph[u].append(v)
graph[v].append(u) # undirected
Counter — Frequency Map on Steroids
from collections import Counter
c = Counter("abracadabra") # {'a': 5, 'b': 2, 'r': 2, 'c': 1, 'd': 1}
c = Counter([1, 2, 2, 3, 3, 3])
c["a"] # 5 (0 if missing, not KeyError)
c.most_common(k) # top-k as [(elem, count), ...]
c.most_common() # sorted by freq desc
c.most_common()[:-k-1:-1] # bottom-k (least common)
# Set-like operations
c1 + c2 # union (sum counts)
c1 - c2 # difference (keep positive)
c1 & c2 # intersection (min counts)
c1 | c2 # union (max counts)
# Update / subtract
c.update(other_counter_or_iterable)
c.subtract(other)
Counter is like a tally sheet — it counts each item you feed it, then tells you which items were most popular.
OrderedDict & Python 3.7+ Guarantee
Since Python 3.7, regular dict preserves insertion order. Use OrderedDict only when you need its extra methods (move_to_end, popitem(last=False)) — perfect for LRU cache implementations.
from collections import OrderedDict
class LRUCache:
def __init__(self, capacity):
self.cap = capacity
self.cache = OrderedDict()
def get(self, key):
if key not in self.cache:
return -1
self.cache.move_to_end(key) # mark as recently used
return self.cache[key]
def put(self, key, value):
if key in self.cache:
self.cache.move_to_end(key)
self.cache[key] = value
if len(self.cache) > self.cap:
self.cache.popitem(last=False) # evict oldest (LRU)
Sets & Tuples
Sets give O(1) "have I seen this?" lookups — the backbone of deduplication and graph visited-tracking. Tuples are immutable, hashable lists — perfect for dict keys and multi-return values.
Sets
A = {1, 2, 3, 4} B = {3, 4, 5, 6}
A | B Union {1, 2, 3, 4, 5, 6} "in either"
A & B Intersection {3, 4} "in both"
A - B Difference {1, 2} "in A, not B"
A ^ B Symmetric {1, 2, 5, 6} "in exactly one"
# Creation
s = set()
s = {1, 2, 3}
s = set([1, 2, 2, 3]) # {1, 2, 3} — deduplication!
# Mutation — all O(1) average
s.add(x)
s.remove(x) # KeyError if missing
s.discard(x) # no error if missing (safer)
s.pop() # remove arbitrary element
s.clear()
s.update(iterable) # add many
# Membership — O(1) average, vs O(n) for lists
x in s
# Set algebra (returns new sets)
s | t s.union(t, ...) # |
s & t s.intersection(t, ...) # &
s - t s.difference(t, ...) # -
s ^ t s.symmetric_difference(t)
s.issubset(t)
s.issuperset(t)
s.isdisjoint(t) # no common elements
# Frozen set — immutable, hashable
fs = frozenset([1, 2, 3]) # can be a dict key
set. Iterate a list checking membership 1000 times? set is ~1000× faster than list for large N.
Tuples
# Creation
t = ()
t = (1, 2, 3)
t = 1, 2, 3 # parens optional
t = (1,) # single element (comma!)
# Unpacking
a, b, c = t
a, *rest = t # rest is a list
first, *middle, last = t
a, b = b, a # swap (Pythonic!)
# Multiple return
def divmod_pair(a, b):
return a // b, a % b
q, r = divmod_pair(17, 5)
# As dict keys (lists can't be!)
memo = {(row, col): value for ...}
# NamedTuple — readable, lightweight class
from collections import namedtuple
Point = namedtuple('Point', ['x', 'y'])
p = Point(3, 4)
p.x, p.y # field access
p[0], p[1] # also index access
Strings
Strings are immutable sequences of characters — every "modification" creates a new string. Slicing works just like lists. The methods here cover ~95% of string-processing problems.
Slicing & Methods
# Slicing — identical to lists
s[::-1] # reverse
s[i:j], s[i:j:k], s[-k:]
# Split / Join
s.split() # split on whitespace
s.split(',') # split on delimiter
s.split(',', maxsplit=2) # limit splits
','.join(list_of_strings) # join (must be all strings)
''.join(chars) # fastest char->string
# Search / Test
s.find(sub) # first index, -1 if missing
s.rfind(sub) # last index
s.index(sub) # like find but raises ValueError
s.count(sub) # occurrences
s.startswith(prefix)
s.endswith(suffix)
# Modify (return new string)
s.replace(old, new)
s.replace(old, new, count) # limit replacements
s.strip() # remove leading/trailing whitespace
s.lstrip(), s.rstrip()
s.strip('xy') # remove specific chars from ends
s.upper(), s.lower()
s.title(), s.capitalize()
s.swapcase()
# Test character class
s.isalpha(), s.isdigit(), s.isalnum()
s.isupper(), s.islower()
s.isspace()
# Char <-> int
ord('a') # 97
chr(97) # 'a'
# Lowercase letters: 97..122, uppercase: 65..90
# Convert letter to 0..25 index:
idx = ord(c) - ord('a')
f-strings & Formatting
f"Hello {name}"
f"{x:.2f}" # 2 decimal places: 3.14
f"{x:0>5d}" # zero-padded: 00123
f"{x:>10}" # right-align width 10
f"{x:<10}" # left-align
f"{x:^10}" # center
f"{x:,}" # thousands sep: 1,234,567
f"{x:.2%}" # percentage: 85.50%
f"{x:#x}" # hex: 0xff
f"{x:#b}" # binary: 0b1010
f"{x:e}" # scientific: 1.23e+04
f"{x!r}" # repr form
f"{d[k]=}" # debug: shows d[k]=value
s += "x" in a tight loop is O(n²). Use ''.join(list) instead, or a list of chars then join once.
Stacks & Queues (deque)
Use list for stacks (push/pop at end = O(1)). For queues (FIFO), deque gives O(1) on both ends — lists are O(n) for pop(0) or insert(0).
STACK (LIFO) QUEUE (FIFO) ┌───┐ ┌───┬───┬───┬───┐ │ C │ ← push/pop │ A │ B │ C │ D │ ├───┤ └─▲─┴───┴───┴─▲─┘ │ B │ │ │ ├───┤ enqueue dequeue │ A │ (rear) (front) └───┘ Use list for stack: Use deque for queue: arr.append(x) # push dq.append(x) # enqueue arr.pop() # pop dq.popleft() # dequeue
deque — Double-Ended Queue
from collections import deque
dq = deque()
dq = deque([1, 2, 3])
dq = deque(maxlen=5) # auto-evicts oldest when full
# All O(1) at both ends
dq.append(x) # add right
dq.appendleft(x) # add left
dq.pop() # remove right
dq.popleft() # remove left
# Peek (no removal)
dq[0] # leftmost
dq[-1] # rightmost
# Bulk
dq.extend(iter) # add many to right
dq.extendleft(iter) # add many to left (reversed!)
# Rotation (in-place)
dq.rotate(k) # rotate right by k
dq.rotate(-k) # rotate left by k
len(dq)
x in dq # O(n)
deque.popleft() as the queue. Monotonic-stack problems use list with append/pop.
Heaps & Priority Queues
Python's heapq is a min-heap. Think of it as a pile where the smallest item is always on top — push anything, pop the minimum in O(log n). For max-heap, negate values.
1 Heap property:
/ \ parent ≤ children
3 2 (min-heap)
/ \
7 4
Stored as array: [1, 3, 2, 7, 4]
index: 0 1 2 3 4
Parent of i: (i - 1) // 2
Left child of i: 2 * i + 1
Right child of i: 2 * i + 2
Core Operations
import heapq
heap = [] # always start with empty list
# All O(log n)
heapq.heappush(heap, x) # push
heapq.heappop(heap) # pop smallest (raises if empty)
heapq.heappushpop(heap, x) # push then pop smallest (atomic)
heapq.heapreplace(heap, x) # pop then push (atomic)
# Peek (O(1))
heap[0] # smallest element
# Heapify existing list — O(n) (not O(n log n)!)
arr = [5, 3, 8, 1, 9]
heapq.heapify(arr) # arr is now a heap in-place
# Top-K patterns (use heap, not full sort, for streaming)
heapq.nlargest(k, arr) # [9, 8, 5] for k=3
heapq.nsmallest(k, arr) # [1, 3, 5]
heapq.nsmallest(k, arr, key=fn)
Max-Heap Trick & Priority Tuples
# --- Max-heap via negation ---
maxheap = []
heapq.heappush(maxheap, -x)
largest = -heapq.heappop(maxheap)
# --- Heap of tuples: sorted by first element, then second, ... ---
# Use for priority queues: (priority, tiebreaker, item)
pq = []
heapq.heappush(pq, (priority, count, item)) # count breaks ties
# WARNING: if item is unorderable (dict, custom obj), tuples
# comparison will throw TypeError. Always include a
# tiebreaker counter before the item.
# --- K largest in stream (heap stays size k) ---
import heapq
def k_largest(nums, k):
heap = nums[:k]
heapq.heapify(heap)
for x in nums[k:]:
if x > heap[0]:
heapq.heapreplace(heap, x)
return sorted(heap, reverse=True)
Trees, Graphs & Traversals
Most tree/graph problems in Python don't need fancy libraries — just custom classes for nodes, a dict for adjacency, and recursion (or stack/queue) for traversal.
Node Class Definitions
# --- Singly Linked List ---
class ListNode:
def __init__(self, val=0, next=None):
self.val = val
self.next = next
# Build: 1 -> 2 -> 3
head = ListNode(1, ListNode(2, ListNode(3)))
# --- Binary Tree ---
class TreeNode:
def __init__(self, val=0, left=None, right=None):
self.val = val
self.left = left
self.right = right
# --- N-ary Tree ---
class Node:
def __init__(self, val=None, children=None):
self.val = val
self.children = children or []
# --- Graph (adjacency list — most common) ---
graph = {0: [1, 2], 1: [0, 3], 2: [0], 3: [1]}
# Or using defaultdict for incremental building
from collections import defaultdict
graph = defaultdict(list)
for u, v in edges:
graph[u].append(v)
graph[v].append(u) # add only if undirected
# Weighted graph
graph = defaultdict(dict)
graph[u][v] = weight
Tree Traversals
1
/ \
2 3
/ \ \
4 5 6
Pre-order (root, L, R): 1 2 4 5 3 6 "visit on the way down"
In-order (L, root, R): 4 2 5 1 3 6 "sorted for BST"
Post-order (L, R, root): 4 5 2 6 3 1 "visit on the way up"
Level-order (BFS): 1 2 3 4 5 6 "by depth"
# Recursive (DFS) — clean & Pythonic
def preorder(root):
if not root: return []
return [root.val] + preorder(root.left) + preorder(root.right)
def inorder(root):
if not root: return []
return inorder(root.left) + [root.val] + inorder(root.right)
def postorder(root):
if not root: return []
return postorder(root.left) + postorder(root.right) + [root.val]
# In-order iterative (the classic)
def inorder_iter(root):
res, stack = [], []
node = root
while node or stack:
while node: # go all the way left
stack.append(node)
node = node.left
node = stack.pop() # visit
res.append(node.val)
node = node.right
return res
# Level-order (BFS) — queue + per-level grouping
from collections import deque
def levelorder(root):
if not root: return []
res = []
q = deque([root])
while q:
level = []
for _ in range(len(q)): # snapshot size = current level width
node = q.popleft()
level.append(node.val)
if node.left: q.append(node.left)
if node.right: q.append(node.right)
res.append(level)
return res
# Iterative DFS with explicit stack
def preorder_iter(root):
if not root: return []
res, stack = [], [root]
while stack:
node = stack.pop()
res.append(node.val)
if node.right: stack.append(node.right) # push right first!
if node.left: stack.append(node.left)
return res
Graph BFS & DFS
from collections import deque, defaultdict
# BFS — shortest path in unweighted graph
def bfs(graph, start):
visited = {start}
q = deque([start])
while q:
node = q.popleft()
for nb in graph[node]:
if nb not in visited:
visited.add(nb)
q.append(nb)
# BFS with distance tracking
def bfs_dist(graph, start):
dist = {start: 0}
q = deque([start])
while q:
node = q.popleft()
for nb in graph[node]:
if nb not in dist:
dist[nb] = dist[node] + 1
q.append(nb)
return dist
# DFS recursive
def dfs(graph, node, visited=None):
if visited is None: visited = set()
visited.add(node)
for nb in graph[node]:
if nb not in visited:
dfs(graph, nb, visited)
# DFS iterative
def dfs_iter(graph, start):
visited = set()
stack = [start]
while stack:
node = stack.pop()
if node in visited: continue
visited.add(node)
for nb in graph[node]:
if nb not in visited:
stack.append(nb)
# Topological sort (Kahn's algorithm, BFS-based)
def topo_sort(num_nodes, edges):
adj = defaultdict(list)
indeg = [0] * num_nodes
for u, v in edges:
adj[u].append(v)
indeg[v] += 1
q = deque([i for i in range(num_nodes) if indeg[i] == 0])
order = []
while q:
u = q.popleft()
order.append(u)
for v in adj[u]:
indeg[v] -= 1
if indeg[v] == 0:
q.append(v)
return order if len(order) == num_nodes else [] # [] = cycle
# Connected components (undirected)
def count_components(n, edges):
graph = defaultdict(list)
for u, v in edges:
graph[u].append(v); graph[v].append(u)
seen = set()
count = 0
for i in range(n):
if i not in seen:
count += 1
stack = [i]
while stack:
u = stack.pop()
if u in seen: continue
seen.add(u)
stack.extend(graph[u])
return count
Dijkstra — Shortest Path with Weights
import heapq
def dijkstra(graph, start, n):
# graph[u] = [(v, weight), ...]
dist = [float('inf')] * n
dist[start] = 0
pq = [(0, start)] # (distance, node)
while pq:
d, u = heapq.heappop(pq)
if d > dist[u]: continue # stale entry, skip
for v, w in graph[u]:
if dist[u] + w < dist[v]:
dist[v] = dist[u] + w
heapq.heappush(pq, (dist[v], v))
return dist
Built-in Essentials
The functions you'll reach for in nearly every problem. Master these and you'll write less code that does more.
Iteration Helpers
# enumerate — index + value pairs
for i, val in enumerate(arr):
...
for i, val in enumerate(arr, start=1): # 1-indexed
...
# zip — pair up iterables (stops at shortest)
for a, b in zip(list1, list2):
...
list(zip([1,2,3], ['a','b','c'])) # [(1,'a'), (2,'b'), (3,'c')]
# zip_longest — pads missing with fillvalue
from itertools import zip_longest
for a, b in zip_longest(A, B, fillvalue=0):
...
# Transpose a matrix
list(zip(*matrix)) # rows become columns
# map / filter (prefer comprehensions, but useful)
list(map(int, "1 2 3".split())) # [1, 2, 3]
list(map(fn, iterable))
list(filter(lambda x: x > 0, arr))
# reversed — returns iterator
list(reversed(arr))
# range
range(n) # 0..n-1
range(a, b) # a..b-1
range(a, b, step)
range(n, 0, -1) # n..1 descending
# sorted — returns new list (see Sorting section)
Math Built-ins
# Aggregations
sum(arr) # total
sum(arr, start=10) # total + 10
sum(arr[i] for i in range(0, n, 2)) # sum of even indices
min(arr), max(arr)
min(arr, key=fn) # element minimizing fn
max(arr, key=lambda x: (x[0], -x[1])) # multi-criteria
# Pair min/max (returns (smaller, larger))
lo, hi = min(a, b), max(a, b)
# Or:
lo, hi = (a, b) if a < b else (b, a)
# Numeric
abs(x)
round(x, ndigits)
pow(a, b) # a ** b
pow(a, b, m) # (a**b) % m — efficient modular exp!
divmod(a, b) # returns (a // b, a % b)
q, r = divmod(17, 5) # (3, 2)
# Booleans
any([False, True, False]) # True (short-circuits)
all([True, True, False]) # False (short-circuits)
any(x > 0 for x in arr) # works with generators
all(0 <= x < n for x in arr)
# Type conversions
int("42"), int("ff", 16) # 42, 255
str(42), float("3.14")
bool(0) # False (0, "", [], {}, None are falsy)
list("abc"), set([1,1,2])
dict([("a",1),("b",2)])
# Type checks
isinstance(x, int)
isinstance(x, (int, float)) # multiple types
Competitive Input Reading
# Fast input
import sys
input = sys.stdin.readline
n = int(input())
arr = list(map(int, input().split()))
matrix = [list(map(int, input().split())) for _ in range(n)]
a, b, c = map(int, input().split())
# Multiple test cases
t = int(input())
for _ in range(t):
n, m = map(int, input().split())
# Increase recursion limit (default 1000)
import sys
sys.setrecursionlimit(10**6)
itertools & functools
Lazy iterators for combinatorics and reduction. Generate permutations, combinations, and Cartesian products in one line — no nested loops.
Combinatorics
from itertools import (
permutations, combinations, combinations_with_replacement,
product, chain, accumulate, groupby, count, cycle, repeat,
islice, takewhile, dropwhile, pairwise
)
# Permutations — all orderings
list(permutations([1,2,3])) # 6 tuples of length 3
list(permutations([1,2,3], 2)) # 6 tuples of length 2
# Combinations — order doesn't matter
list(combinations([1,2,3,4], 2)) # [(1,2),(1,3),(1,4),(2,3),(2,4),(3,4)]
list(combinations_with_replacement([1,2,3], 2))
# Cartesian product
list(product([1,2], ['a','b'])) # [(1,'a'),(1,'b'),(2,'a'),(2,'b')]
list(product([1,2,3], repeat=2)) # 3x3 grid of pairs
list(product(range(n), range(m))) # all (i,j) coords
# Chain — flatten one level
list(chain([1,2], [3,4], [5])) # [1,2,3,4,5]
list(chain.from_iterable([[1,2],[3,4]])) # same, takes iterable of iterables
# Accumulate — prefix sums (and more)
list(accumulate([1,2,3,4])) # [1, 3, 6, 10]
list(accumulate([1,2,3,4], max)) # running max: [1,2,3,4]
list(accumulate([1,2,3,4], lambda a,b: a*b)) # running product
# Pairwise (Python 3.10+) — sliding pairs
list(pairwise([1,2,3,4])) # [(1,2),(2,3),(3,4)]
# Infinite iterators (use with islice!)
islice(count(), 5) # [0,1,2,3,4]
islice(cycle([1,2,3]), 7) # [1,2,3,1,2,3,1]
list(repeat(0, 5)) # [0,0,0,0,0]
# Takewhile / dropwhile
list(takewhile(lambda x: x < 5, [1,4,6,3,8])) # [1,4]
list(dropwhile(lambda x: x < 5, [1,4,6,3,8])) # [6,3,8]
# Groupby — must sort first usually!
data = [('a',1),('a',2),('b',3)]
for key, group in groupby(data, key=lambda x: x[0]):
print(key, list(group))
functools
from functools import lru_cache, reduce, cmp_to_key, partial
# lru_cache — automatic memoization (top-down DP!)
@lru_cache(maxsize=None)
def fib(n):
if n < 2: return n
return fib(n-1) + fib(n-2)
# Note: args must be hashable. Clear with fib.cache_clear()
# reduce — fold left
reduce(lambda a, b: a + b, [1,2,3,4]) # 10
reduce(lambda a, b: a * b, [1,2,3,4]) # 24
reduce(lambda a, b: a if a > b else b, arr) # = max(arr)
# cmp_to_key — convert old-style comparator to key func
def compare(a, b):
if a < b: return -1
if a > b: return 1
return 0
arr.sort(key=cmp_to_key(compare))
# partial — bind some arguments
add_5 = partial(lambda a, b: a + b, 5)
add_5(3) # 8
@lru_cache(maxsize=None) turns any pure recursive function into memoized DP — fastest way to write top-down DP in a contest.
Math & Numbers
Number theory essentials — GCD, modular arithmetic, primes, large integers. Python handles big ints natively (no overflow!), so most of these "just work".
math Module
import math
# Constants
math.inf # +infinity
-math.inf # -infinity
math.pi, math.e
float('inf'), float('-inf')
# Integer math
math.gcd(a, b) # greatest common divisor
math.lcm(a, b) # Python 3.9+
math.isqrt(n) # integer sqrt (floor) — exact, no float
math.comb(n, k) # n choose k (binomial)
math.perm(n, k) # n! / (n-k)!
math.factorial(n)
# Float math
math.ceil(x), math.floor(x)
math.log(x), math.log2(x), math.log10(x)
math.exp(x)
math.pow(x, y) # always float, prefer x ** y for ints
math.sqrt(x)
math.isclose(a, b, rel_tol=1e-9)
# Trig
math.sin, math.cos, math.tan, math.atan, math.atan2
# Useful builtins
divmod(a, b) # (a // b, a % b) — both at once
pow(a, b, m) # (a ** b) % m — fast modular exponentiation
bin(13) # '0b1101'
oct(8) # '0o10'
hex(255) # '0xff'
int('1101', 2) # 13 — parse binary
int('ff', 16) # 255 — parse hex
Modular Arithmetic & Primes
MOD = 10**9 + 7 # typical modulo
# Modular addition / multiplication
(a + b) % MOD
(a * b) % MOD
# Modular inverse (Fermat's little theorem, MOD prime)
pow(a, MOD - 2, MOD) # a^(-1) mod MOD
# Sieve of Eratosthenes
def sieve(n):
is_prime = [True] * (n + 1)
is_prime[0] = is_prime[1] = False
for i in range(2, int(n**0.5) + 1):
if is_prime[i]:
for j in range(i*i, n+1, i):
is_prime[j] = False
return is_prime # or [i for i, p in enumerate(is_prime) if p]
# Prime factorization
def factorize(n):
factors = {}
d = 2
while d * d <= n:
while n % d == 0:
factors[d] = factors.get(d, 0) + 1
n //= d
d += 1
if n > 1:
factors[n] = factors.get(n, 0) + 1
return factors
# Fast exponentiation (when you can't use pow's 3-arg form)
def power(base, exp, mod):
result = 1
base %= mod
while exp > 0:
if exp & 1:
result = result * base % mod
base = base * base % mod
exp >>= 1
return result
pow(a, b, m) uses fast modular exponentiation internally — O(log b). Don't reimplement unless asked.
Bit Manipulation
Bits are the fastest way to represent small sets, encode state, or pull tricks like "find the unique number". Memorize these operators and a handful of patterns.
Operators & Common Tricks
# Operators
a & b # AND — both 1 → 1
a | b # OR — either 1 → 1
a ^ b # XOR — different → 1 (same → 0)
~a # NOT — bitwise complement
a << n # left shift = a * (2**n)
a >> n # right shift = a // (2**n)
# ---- Common tricks ----
# Check odd / even
x & 1 # 1 if odd, 0 if even
# Check i-th bit (0-indexed)
(x >> i) & 1
x & (1 << i) # nonzero if set
# Set i-th bit
x | (1 << i)
# Clear i-th bit
x & ~(1 << i)
# Toggle i-th bit
x ^ (1 << i)
# Lowest set bit (power of 2)
x & -x # e.g. 12 & -12 = 4
# Clear lowest set bit
x & (x - 1) # 12 & 11 = 8
# Count set bits (popcount)
bin(x).count('1') # any Python
x.bit_count() # Python 3.10+ — fast
# Check power of two
x > 0 and (x & (x - 1)) == 0
# Swap two variables (no temp — though Python's a,b=b,a is cleaner)
a ^= b; b ^= a; a ^= b
# Iterate set bits
while x:
lsb = x & -x
# use lsb...
x &= x - 1 # clear lowest set bit
# Iterate all subsets of mask (proper subsets)
sub = mask
while sub:
# use sub
sub = (sub - 1) & mask
# Iterate all subsets of size k
from itertools import combinations
for combo in combinations(range(n), k):
mask = sum(1 << i for i in combo)
a ^ a == 0 and a ^ 0 == a, making it perfect for "find the lone unique element".
Sorting & Searching
Python's sort is Timsort — stable, O(n log n). Master key= functions and the bisect module and you'll handle 95% of search/sort problems.
Sorting
# In-place vs new
arr.sort() # in-place, returns None
new = sorted(arr) # new list, original unchanged
# Reverse
arr.sort(reverse=True)
sorted(arr, reverse=True)
# Single key
arr.sort(key=len) # by length
arr.sort(key=abs) # by absolute value
arr.sort(key=lambda x: x[1]) # by second element
# Multi-key: tuple key
arr.sort(key=lambda x: (x[0], x[1])) # asc both
arr.sort(key=lambda x: (x[0], -x[1])) # asc first, desc second
arr.sort(key=lambda x: (-x[0], -x[1])) # desc both
arr.sort(key=lambda x: (x.age, x.name)) # multiple fields
# Sort by custom logic via cmp_to_key (rare — usually key= is enough)
from functools import cmp_to_key
arr.sort(key=cmp_to_key(lambda a, b: -1 if a < b else 1))
# Stable sort — Python's sort IS stable.
# Equal-key elements keep original relative order.
# Sort strings by length, then alphabetically
words.sort(key=lambda s: (len(s), s))
# Sort indices by their values in another array
indices = list(range(n))
indices.sort(key=lambda i: values[i])
Binary Search with bisect
arr = [1, 3, 3, 3, 5, 7]
target = 3
bisect_left(arr, 3) -> 1 # leftmost insertion point
bisect_right(arr, 3) -> 4 # rightmost insertion point
Insert 3 here to keep sorted:
1 3 3 3 5 7
^ left ^ right
import bisect
# Both assume arr is sorted ascending
bisect.bisect_left(arr, x) # first index where x could be inserted
bisect.bisect_right(arr, x) # last index where x could be inserted
bisect.bisect(arr, x) # alias for bisect_right
# Insert while keeping sorted (O(n) due to shift!)
bisect.insort_left(arr, x)
bisect.insort_right(arr, x)
# Common patterns
# 1. Count elements <= x in sorted arr
count = bisect.bisect_right(arr, x)
# 2. Count elements < x
count = bisect.bisect_left(arr, x)
# 3. Find first element >= x
idx = bisect.bisect_left(arr, x)
# 4. Find first element > x
idx = bisect.bisect_right(arr, x)
# 5. Check if x exists
i = bisect.bisect_left(arr, x)
exists = i < len(arr) and arr[i] == x
Hand-rolled Binary Search
# Classic: find exact target
def binary_search(arr, target):
lo, hi = 0, len(arr) - 1
while lo <= hi:
mid = (lo + hi) // 2
if arr[mid] == target:
return mid
elif arr[mid] < target:
lo = mid + 1
else:
hi = mid - 1
return -1
# Lower-bound style: find first True in [F, F, F, T, T, T]
def first_true(predicate, lo, hi):
while lo < hi:
mid = (lo + hi) // 2
if predicate(mid):
hi = mid
else:
lo = mid + 1
return lo # first index where predicate is True
# Binary search on answer (e.g., min capacity such that ...)
def can_achieve(val):
# ... check if val is feasible ...
return True
lo, hi = 1, max_possible
while lo < hi:
mid = (lo + hi) // 2
if can_achieve(mid):
hi = mid # mid works, try smaller
else:
lo = mid + 1 # mid too small
return lo # smallest feasible value
(lo + hi) // 2 never overflows (big ints). In C++/Java you'd write lo + (hi - lo) // 2.
Two Pointers & Sliding Window
Two of the most common patterns in array problems. Both turn O(n²) brute force into O(n) by exploiting structure (sorted input, monotonicity).
Two Pointers
# Opposite ends — needs sorted input typically
def two_sum_sorted(arr, target):
lo, hi = 0, len(arr) - 1
while lo < hi:
s = arr[lo] + arr[hi]
if s == target:
return [lo, hi]
elif s < target:
lo += 1
else:
hi -= 1
return []
# Same direction — fast/slow (Floyd's cycle detection)
def has_cycle(head):
slow = fast = head
while fast and fast.next:
slow = slow.next
fast = fast.next.next
if slow == fast:
return True
return False
# Same direction — partition (e.g., remove duplicates in-place)
def remove_duplicates(nums):
slow = 0
for fast in range(1, len(nums)):
if nums[fast] != nums[slow]:
slow += 1
nums[slow] = nums[fast]
return slow + 1 # new length
Sliding Window
Fixed window (size k):
[a b c] d e f sum of window
a [b c d] e f add d, remove a
a b [c d e] f add e, remove b
a b c [d e f] add f, remove c
Variable window (expand right, shrink left until valid):
[a b c d e] → too big? shrink from left until constraint holds
# Fixed-size window: max sum of subarray length k
def max_sum_k(arr, k):
window_sum = sum(arr[:k])
result = window_sum
for i in range(k, len(arr)):
window_sum += arr[i] - arr[i - k] # slide right
result = max(result, window_sum)
return result
# Variable window: longest subarray with sum <= k
def longest_subarray_sum_le(arr, k):
left = 0
curr_sum = 0
best = 0
for right in range(len(arr)):
curr_sum += arr[right] # expand
while curr_sum > k: # shrink until valid
curr_sum -= arr[left]
left += 1
best = max(best, right - left + 1)
return best
# Variable window with frequency: longest substring with <= k distinct chars
def longest_substring_k_distinct(s, k):
from collections import defaultdict
count = defaultdict(int)
left = 0
distinct = 0
best = 0
for right, ch in enumerate(s):
if count[ch] == 0:
distinct += 1
count[ch] += 1
while distinct > k:
count[s[left]] -= 1
if count[s[left]] == 0:
distinct -= 1
left += 1
best = max(best, right - left + 1)
return best
Backtracking
Recursion + choice + undo. Think of exploring a tree of decisions: at each step you try an option, recurse, then undo it before trying the next. Three classic templates below.
The Universal Template
def backtrack(state, choices):
if is_goal(state):
record(state) # save a copy!
return
for choice in choices:
if is_valid(state, choice):
make_move(state, choice) # mutate
backtrack(state, choices) # recurse
undo_move(state, choice) # ALWAYS undo
Permutations, Subsets, Combinations
# Permutations — all orderings of nums
def permutations(nums):
res = []
def bt(path, used):
if len(path) == len(nums):
res.append(path[:]) # COPY the path!
return
for i in range(len(nums)):
if used[i]: continue
used[i] = True
path.append(nums[i])
bt(path, used)
path.pop()
used[i] = False
bt([], [False] * len(nums))
return res
# Subsets — all 2^n subsets (no duplicates)
def subsets(nums):
res = []
def bt(start, path):
res.append(path[:]) # every prefix is a subset
for i in range(start, len(nums)):
path.append(nums[i])
bt(i + 1, path) # only move forward
path.pop()
bt(0, [])
return res
# Combinations — choose k from [1, n]
def combinations(n, k):
res = []
def bt(start, path):
if len(path) == k:
res.append(path[:])
return
for i in range(start, n + 1):
path.append(i)
bt(i + 1, path)
path.pop()
bt(1, [])
return res
# Subsets with duplicates — sort + skip same value at same level
def subsets_with_dup(nums):
nums.sort() # group duplicates
res = []
def bt(start, path):
res.append(path[:])
for i in range(start, len(nums)):
if i > start and nums[i] == nums[i - 1]:
continue # skip duplicate at this level
path.append(nums[i])
bt(i + 1, path)
path.pop()
bt(0, [])
return res
N-Queens, Sudoku-Style Grid Backtracking
# Sudoku solver
def solve_sudoku(board):
def valid(r, c, ch):
for i in range(9):
if board[r][i] == ch or board[i][c] == ch: return False
br, bc = 3*(r//3), 3*(c//3)
for i in range(br, br+3):
for j in range(bc, bc+3):
if board[i][j] == ch: return False
return True
def bt():
for r in range(9):
for c in range(9):
if board[r][c] == '.':
for d in '123456789':
if valid(r, c, d):
board[r][c] = d
if bt(): return True
board[r][c] = '.'
return False # nothing fits — backtrack
return True # all filled
bt()
path[:] (or path.copy()) when recording results — Python lists are mutable, so without copying you'd capture the same list over and over.
Dynamic Programming
DP = recursion + memoization. Three styles in Python: top-down with @lru_cache (easiest), bottom-up with a table (most flexible), and space-optimized (when you only need the last row).
Top-down — Memoization
from functools import lru_cache
# Cleanest form — decorator handles memoization
@lru_cache(maxsize=None)
def fib(n):
if n < 2: return n
return fib(n - 1) + fib(n - 2)
# With multiple params (must all be hashable!)
@lru_cache(maxsize=None)
def grid_paths(r, c):
if r == 0 or c == 0: return 1
return grid_paths(r-1, c) + grid_paths(r, c-1)
# Explicit memo dict (when params aren't all hashable, or you need control)
def knapsack(values, weights, capacity):
n = len(values)
memo = {}
def solve(i, cap):
if i == n or cap == 0: return 0
if (i, cap) in memo: return memo[(i, cap)]
# Skip item i
best = solve(i + 1, cap)
# Take item i (if fits)
if weights[i] <= cap:
best = max(best, values[i] + solve(i + 1, cap - weights[i]))
memo[(i, cap)] = best
return best
return solve(0, capacity)
# House robber — classic linear DP
@lru_cache(maxsize=None)
def rob(nums, i=0):
if i >= len(nums): return 0
return max(nums[i] + rob(nums, i + 2), rob(nums, i + 1))
Bottom-up — Tabulation
# Fibonacci — 1D
def fib(n):
if n < 2: return n
dp = [0] * (n + 1)
dp[1] = 1
for i in range(2, n + 1):
dp[i] = dp[i-1] + dp[i-2]
return dp[n]
# Space-optimized (only need last 2)
def fib_opt(n):
if n < 2: return n
prev, curr = 0, 1
for _ in range(2, n + 1):
prev, curr = curr, prev + curr
return curr
# Longest Common Subsequence — 2D
def lcs(s1, s2):
m, n = len(s1), len(s2)
dp = [[0] * (n + 1) for _ in range(m + 1)]
for i in range(1, m + 1):
for j in range(1, n + 1):
if s1[i-1] == s2[j-1]:
dp[i][j] = dp[i-1][j-1] + 1
else:
dp[i][j] = max(dp[i-1][j], dp[i][j-1])
return dp[m][n]
# Longest Increasing Subsequence — O(n²) easy version
def lis(nums):
if not nums: return 0
dp = [1] * len(nums)
for i in range(1, len(nums)):
for j in range(i):
if nums[j] < nums[i]:
dp[i] = max(dp[i], dp[j] + 1)
return max(dp)
# 0/1 Knapsack — 2D
def knapsack(values, weights, capacity):
n = len(values)
dp = [[0] * (capacity + 1) for _ in range(n + 1)]
for i in range(1, n + 1):
for w in range(capacity + 1):
dp[i][w] = dp[i-1][w] # skip
if weights[i-1] <= w:
dp[i][w] = max(dp[i][w], values[i-1] + dp[i-1][w - weights[i-1]])
return dp[n][capacity]
# Unbounded knapsack (items reusable) — change dp[i-1] to dp[i] on take
def unbounded_knapsack(values, weights, capacity):
dp = [0] * (capacity + 1)
for w in range(capacity + 1):
for i in range(len(values)):
if weights[i] <= w:
dp[w] = max(dp[w], values[i] + dp[w - weights[i]])
return dp[capacity]
# Coin change — minimum coins
def coin_change(coins, amount):
dp = [float('inf')] * (amount + 1)
dp[0] = 0
for a in range(1, amount + 1):
for c in coins:
if c <= a:
dp[a] = min(dp[a], 1 + dp[a - c])
return dp[amount] if dp[amount] != float('inf') else -1
Classic Patterns
Two reusable building blocks: Union-Find for "are these connected?" queries, and Trie for prefix/word problems.
Union-Find (Disjoint Set)
class UnionFind:
def __init__(self, n):
self.parent = list(range(n))
self.rank = [0] * n # union by rank
self.size = [1] * n # for size tracking
self.count = n # number of components
def find(self, x):
# Path compression — flatten the tree
while self.parent[x] != x:
self.parent[x] = self.parent[self.parent[x]]
x = self.parent[x]
return x
# Recursive version (also path-compressing)
def find_rec(self, x):
if self.parent[x] != x:
self.parent[x] = self.find_rec(self.parent[x])
return self.parent[x]
def union(self, x, y):
px, py = self.find(x), self.find(y)
if px == py: return False # already same set
# Union by rank
if self.rank[px] < self.rank[py]:
px, py = py, px
self.parent[py] = px
if self.rank[px] == self.rank[py]:
self.rank[px] += 1
self.size[px] += self.size[py]
self.count -= 1
return True
def connected(self, x, y):
return self.find(x) == self.find(y)
# Use case: Kruskal's MST
def kruskal(n, edges):
edges.sort(key=lambda e: e[2]) # by weight
uf = UnionFind(n)
mst_weight = 0
for u, v, w in edges:
if uf.union(u, v):
mst_weight += w
return mst_weight
Trie (Prefix Tree)
class TrieNode:
def __init__(self):
self.children = {} # char -> TrieNode
self.is_end = False
class Trie:
def __init__(self):
self.root = TrieNode()
def insert(self, word):
node = self.root
for c in word:
if c not in node.children:
node.children[c] = TrieNode()
node = node.children[c]
node.is_end = True
def search(self, word):
node = self._walk(word)
return node is not None and node.is_end
def starts_with(self, prefix):
return self._walk(prefix) is not None
def _walk(self, s):
node = self.root
for c in s:
if c not in node.children:
return None
node = node.children[c]
return node
# Use case: word search with wildcard '.'
class WordDictionary:
def __init__(self):
self.root = TrieNode()
def addWord(self, word):
node = self.root
for c in word:
node = node.children.setdefault(c, TrieNode())
node.is_end = True
def search(self, word):
def dfs(node, i):
if i == len(word): return node.is_end
c = word[i]
if c == '.':
return any(dfs(child, i + 1) for child in node.children.values())
return c in node.children and dfs(node.children[c], i + 1)
return dfs(self.root, 0)
Monotonic Stack — Next Greater Element
# Next greater element (to the right)
def next_greater(arr):
n = len(arr)
result = [-1] * n
stack = [] # indices, values decreasing
for i in range(n):
while stack and arr[stack[-1]] < arr[i]:
result[stack.pop()] = arr[i]
stack.append(i)
return result
# Largest rectangle in histogram
def largest_rectangle(heights):
stack = []
max_area = 0
for i, h in enumerate(heights + [0]): # sentinel
while stack and heights[stack[-1]] > h:
height = heights[stack.pop()]
width = i if not stack else i - stack[-1] - 1
max_area = max(max_area, height * width)
stack.append(i)
return max_area
Pitfalls & Gotchas
The bugs that bite silently. Memorize these and you'll save hours of debugging.
# 1. MUTABLE DEFAULT ARGUMENTS — shared across calls!
def bad(arr=[]): # WRONG: arr persists between calls
arr.append(1)
return arr
bad(); bad() # [1, 1] — surprise!
def good(arr=None): # RIGHT
if arr is None: arr = []
arr.append(1)
return arr
# 2. INTEGER DIVISION — floor, not truncation!
5 / 2 # 2.5 (true division)
5 // 2 # 2 (floor)
-5 // 2 # -3 ← floors toward -inf, NOT -2!
# To get C-style truncation:
int(-5 / 2) # -2
# Or:
def trunc_div(a, b):
q = a // b
if (a % b != 0) and ((a < 0) != (b < 0)):
q += 1
return q
# 3. NEGATIVE MODULO — always non-negative if divisor positive
-5 % 3 # 1 (Python) — NOT -2 like C/Java!
-5 % -3 # -2
# Useful: cycle an index
next_idx = (curr + delta) % n # always in [0, n)
# 4. 2D LIST CREATION — shared row bug
bad = [[0] * cols] * rows # all rows ARE the same list!
good = [[0] * cols for _ in range(rows)] # independent rows
# 5. SHALLOW vs DEEP COPY
import copy
a = [[1, 2], [3, 4]]
b = a # same reference
b = a[:] # shallow: top-level new, inner same
b = a.copy() # same as [:]
b = copy.deepcopy(a) # fully independent
# 6. FLOAT EQUALITY — never use ==
0.1 + 0.2 == 0.3 # False!
abs(a - b) < 1e-9 # correct
# Or use fractions / Decimal for exact arithmetic
from fractions import Fraction
Fraction(1, 10) + Fraction(2, 10) == Fraction(3, 10) # True
# 7. CHAINED COMPARISONS — Python perk
if 0 <= x <= 100: ... # works as expected, evaluates once
# 8. SHORT-CIRCUIT in any/all
any(check(x) for x in huge) # stops at first True
all(check(x) for x in huge) # stops at first False
# 9. RECURRENCE / LOOP MUTATING DURING ITERATION
for x in arr:
arr.append(x) # infinite loop! iterate over a copy:
for x in arr[:]:
arr.append(x)
# 10. INTEGER VS FLOAT KEYS — but tuple hashing is fine
d[(1, 2)] = "ok" # tuples are hashable
# d[[1, 2]] = "no" # TypeError: lists aren't hashable
# 11. RANGE IS LAZY — materialize if you need it twice
list(range(10)) # use this, not range(10) directly
# range objects can be compared for equality though
range(3) == range(3) # True (Python 3)
# 12. STRING MULTIPLICATION — chaining
"-" * 50 # 50 dashes
["x"] * 3 # ['x', 'x', 'x'] (primitives OK)
[[]] * 3 # [[],[],[]] — SAME inner list! (mutable!)
Complexity Cheat Sheet
Quick reference for picking the right structure. Memorize these and you'll know instantly whether your approach will TLE.
Data Structure Operations
| Operation | List | Dict / Set | Deque | Heap |
|---|---|---|---|---|
| Access by index | O(1) | — | O(1) | O(1) peek min |
| Search by value | O(n) | O(1) avg | O(n) | O(n) |
| Insert end | O(1) amort. | O(1) | O(1) | O(log n) |
| Insert front | O(n) | — | O(1) | — |
| Insert middle | O(n) | — | O(n) | — |
| Delete end | O(1) | O(1) | O(1) | O(log n) |
| Delete front | O(n) | — | O(1) | — |
| Pop min/max | O(n) | — | — | O(log n) |
| Sort | O(n log n) | — | — | O(n log n) |
Algorithm Complexities
| Algorithm | Time | Space | When |
|---|---|---|---|
| BFS / DFS | O(V + E) | O(V) | traversal, shortest path (unweighted) |
| Dijkstra | O((V+E) log V) | O(V) | shortest path, non-negative weights |
| Binary search | O(log n) | O(1) | sorted input / answer space |
| Quicksort / Mergesort | O(n log n) | O(n) merge / O(log n) quick | general sorting |
| Heap push/pop | O(log n) | O(n) | priority queue, top-K |
| Union-Find (with path comp.) | O(α(n)) ≈ O(1) | O(n) | connectivity, MST |
| KMP string match | O(n + m) | O(m) | substring search |
| Backtracking (permutations) | O(n!) | O(n) | generate all orderings |
| Backtracking (subsets) | O(2ⁿ) | O(n) | generate all subsets |
| DP (states × transition) | O(states × trans) | O(states) | optimal substructure |
Operation Count Rule of Thumb
- n ≤ 20 → O(2ⁿ) or O(n!) is fine
- n ≤ 100 → O(n³) is fine
- n ≤ 1000 → O(n²) is fine
- n ≤ 10⁵ → need O(n log n)
- n ≤ 10⁶ → need O(n)
- n ≥ 10⁹ → need O(log n)