Python Syntax for DSA
The Python built-ins, collections and standard-library helpers you type in every coding round, with tested snippets and the Big-O of each operation.
Python lets you write a working solution in very few lines, but only if the right built-in comes to mind instantly: deque instead of list.pop(0), heapq with negation for a max-heap, Counter instead of a hand-rolled frequency map. This sheet groups that everyday syntax by data structure and lists the cost of each operation.
Everything assumes Python 3.10 or newer. Every snippet was run and the comment on each print line is its real output, so you can paste it into the editor and tweak it. Tick items off once you can type them from memory, and read the bullets: they are the gotchas that cost time in interviews.
Lists & slicing
Python's dynamic array: your default container for almost everything.
1Create a list and a 2D grid (correctly)
Build each row of a grid with a comprehension so every row is its own list.
[[0] * c] * rrepeats the SAME row object r times: writing one cell changes every row.[0] * nis fine for a 1D list of immutable values like numbers or None.
a = [0] * 3 grid = [[0] * 3 for _ in range(2)] # 2 rows x 3 cols, independent grid[0][0] = 1 print(grid) # [[1, 0, 0], [0, 0, 0]] bad = [[0] * 3] * 2 # both rows are the same list object bad[0][0] = 1 print(bad) # [[1, 0, 0], [1, 0, 0]] print(a, len(grid), len(grid[0])) # [0, 0, 0] 2 3
2append, pop, insert, remove, extend
Add and remove at the end in O(1); anything at the front or middle shifts elements and costs O(n).
a.pop(0)anda.insert(0, x)are O(n); usecollections.dequefor a queue.a.remove(x)deletes the first match and raises ValueError if x is absent.a[-1]reads the last element; it raises IndexError on an empty list.
a = [1, 2, 3] a.append(4) a.insert(0, 0) print(a) # [0, 1, 2, 3, 4] print(a.pop()) # 4 print(a.pop(0)) # 0 a.remove(2) print(a, a[-1]) # [1, 3] 3 a.extend([7, 8]) print(a) # [1, 3, 7, 8]
3Slicing a[i:j:k] and reversing
A slice copies the items from index i up to (not including) j, taking every k-th; negative indexes count from the end.
a[::-1]returns a reversed copy;a.reverse()reverses in place and returns None.- Slices never raise on out-of-range bounds, and
a[:]is a shallow copy (handy aspath[:]in backtracking).
a = [0, 1, 2, 3, 4, 5] print(a[1:4]) # [1, 2, 3] print(a[:2], a[4:]) # [0, 1] [4, 5] print(a[::2], a[-2:]) # [0, 2, 4] [4, 5] print(a[::-1]) # [5, 4, 3, 2, 1, 0] b = a[:] b[0] = 99 print(a[0], b[0]) # 0 99 print(a[10:20]) # []
4len, in, index, count
Quick questions about a list: its size, whether it holds a value, where, and how often.
x in ascans the whole list (O(n)); build a set once if you test membership repeatedly.a.index(x)raises ValueError when x is missing, so checkx in afirst.
a = [4, 7, 4, 9] print(len(a)) # 4 print(7 in a, 5 in a) # True False print(a.index(4)) # 0 print(a.count(4)) # 2 print(not []) # True
5enumerate and zip
enumerategives (index, value) pairs;zipwalks several sequences side by side.enumerate(a, start=1)begins counting at 1 without touching the list.zipstops at the shortest input;list(zip(*grid))transposes a matrix.
names = ['ann', 'bob', 'cat'] scores = [90, 75, 88] for i, n in enumerate(names, start=1): print(i, n, end=' ') # 1 ann 2 bob 3 cat print() print(list(zip(names, scores))) # [('ann', 90), ('bob', 75), ('cat', 88)] print(list(zip([1, 2, 3], 'ab'))) # [(1, 'a'), (2, 'b')] print(list(zip(*[[1, 2], [3, 4]]))) # [(1, 3), (2, 4)]
6List comprehensions
Build a new list from a loop and an optional filter in one readable line.
- Nested loops read left to right, the same order as written-out for loops.
- Use a generator
(x for x in ...)insidesum/any/maxto skip building a list.
sq = [x * x for x in range(5)] print(sq) # [0, 1, 4, 9, 16] evens = [x for x in range(10) if x % 2 == 0] print(evens) # [0, 2, 4, 6, 8] flat = [v for row in [[1, 2], [3]] for v in row] print(flat) # [1, 2, 3] tags = ['even' if x % 2 == 0 else 'odd' for x in range(3)] print(tags) # ['even', 'odd', 'even']
7sum, min, max with key; any and all
Aggregate a sequence in O(n);
key=decides what is compared anddefault=covers empty input.min([])andmax([])raise ValueError; passdefault=when the input may be empty.all([])is True andany([])is False.
a = [3, -7, 5] print(sum(a), min(a), max(a)) # 1 -7 5 print(max(a, key=abs)) # -7 print(min(['kiwi', 'fig', 'banana'], key=len)) # fig print(max([], default=0)) # 0 print(any(x < 0 for x in a)) # True print(all(x > 0 for x in a)) # False print(all([]), any([])) # True False
| Operation | Code | Time |
|---|---|---|
| Index read / write | a[i], a[i] = x | O(1) |
| Append / pop last | a.append(x), a.pop() | O(1) amortised |
| Insert / pop at front or middle | a.insert(i, x), a.pop(i) | O(n) |
| Membership / index / count | x in a, a.index(x) | O(n) |
| Slice copy | a[i:j] | O(j - i) |
| Length | len(a) | O(1) |
| Sort | a.sort(), sorted(a) | O(n log n) |
| Min / max / sum | min(a), sum(a) | O(n) |
Strings
Immutable sequences of characters with a rich set of methods.
8Indexing, slicing and reversing
Strings index and slice exactly like lists, so
s[::-1]is the quickest palindrome check.- Strings are immutable:
s[0] = 'x'raises TypeError. reversed(s)gives an iterator; wrap it in''.join(...)to get a string back.
s = "python" print(s[0], s[-1], len(s)) # p n 6 print(s[1:4]) # yth print(s[::-1]) # nohtyp print(''.join(reversed(s))) # nohtyp w = "level" print(w == w[::-1]) # True
- Strings are immutable:
9Build strings with a list + join
Collect pieces in a list and join once at the end instead of growing a string in a loop.
s += cinside a loop may copy the string each time (O(n^2) in the worst case);''.join(parts)is linear.- To edit characters, convert with
list(s), change the list, then join it back.
s = "abc" chars = list(s) chars[0] = 'z' print(''.join(chars)) # zbc parts = [] for i in range(4): parts.append(str(i)) print('-'.join(parts)) # 0-1-2-3 print(''.join(map(str, [1, 2, 3]))) # 123
10split, join and strip
splitbreaks text into tokens,joinglues them back,striptrims the ends.split()with no argument splits on any run of whitespace and drops empty tokens;split(',')keeps them.joinonly accepts strings, so map numbers withstrfirst.
line = " the quick brown " print(line.split()) # ['the', 'quick', 'brown'] print("a,b,,c".split(',')) # ['a', 'b', '', 'c'] print(' '.join(['x', 'y', 'z'])) # x y z print(line.strip() + '|') # the quick brown| print("xxhixx".strip('x')) # hi print("a-b-c".replace('-', '')) # abc
11find, index, count, startswith
Search inside a string for a substring, its position, or how many times it occurs.
findreturns -1 when missing;indexraises ValueError.countcounts non-overlapping matches.
s = "banana" print(s.find("an"), s.find("z")) # 1 -1 print(s.index("na")) # 2 print(s.count("a"), s.count("ana")) # 3 1 print(s.startswith("ban"), s.endswith("na")) # True True print("nan" in s) # True
12Character tests and case
isalpha,isdigit,isalnum,isspaceandlower/uppercover most input-cleaning tasks.- These methods work on whole strings too:
'123'.isdigit()is True,''.isdigit()is False.
Practice: Valid Palindrome →print("abc".isalpha(), "123".isdigit()) # True True print("a1".isalnum(), " ".isspace()) # True True print("Q".isupper(), "Hi".lower(), "hi".upper()) # True hi HI s = "A man, a plan" clean = [c.lower() for c in s if c.isalnum()] print(''.join(clean)) # amanaplan
- These methods work on whole strings too:
13ord and chr
Convert between a character and its code point;
ord(c) - ord('a')maps a-z to 0-25.- A 26-slot list indexed this way is a fast fixed-size counter for lowercase letters.
Practice: Valid Anagram →print(ord('a'), chr(98)) # 97 b counts = [0] * 26 for c in "abca": counts[ord(c) - ord('a')] += 1 print(counts[:3]) # [2, 1, 1] print(chr(ord('a') + 25)) # z print(tuple(counts) == tuple([2, 1, 1] + [0] * 23)) # True
14f-strings
Embed expressions in a string with
f"...{expr}..."and an optional format spec after a colon.{x:.2f}rounds to 2 decimals,{n:03d}zero-pads,{n:b}prints binary,{x=}shows name and value.
name, score = "Ada", 9.5678 print(f"{name} scored {score:.2f}") # Ada scored 9.57 n = 5 print(f"{n:03d} {n:b} {n=}") # 005 101 n=5 print(f"[{'x':>3}]") # [ x] print(f"{1234567:,}") # 1,234,567
| Operation | Code | Time |
|---|---|---|
| Index / length | s[i], len(s) | O(1) |
| Slice / reverse | s[i:j], s[::-1] | O(k) for k chars |
| Concatenate | s + t | O(n + m) |
| Join many pieces | ''.join(parts) | O(total length) |
| Substring search | t in s, s.find(t) | O(n * m) worst |
| Split / strip / replace | s.split(), s.strip() | O(n) |
| Compare | s == t | O(n) |
dict, defaultdict & Counter
Hash maps: O(1) average lookups for counting, grouping and caching.
15dict basics and get with a default
Store key-value pairs;
get(key, default)reads safely when the key may be missing.d[key]raises KeyError for a missing key;getreturns None or your default.- Dicts remember insertion order (guaranteed since 3.7).
- Keys must be hashable: ints, strings, tuples - not lists or sets.
Practice: Two Sum →d = {'a': 1} d['b'] = 2 print(d.get('z'), d.get('z', 0)) # None 0 print('a' in d, len(d)) # True 2 d['a'] = d.get('a', 0) + 1 print(d) # {'a': 2, 'b': 2} del d['b'] print(d.pop('a'), d) # 2 {}
16Iterate items, keys, values; setdefault
Loop with
.items()to get key and value together;setdefaultinserts a starting value only if the key is new.- Adding or deleting keys while looping raises RuntimeError; loop over
list(d)if you must modify. setdefault(k, [])builds the empty list on every call, even when unused;defaultdictis cleaner.
ages = {'ann': 30, 'bob': 25} for k, v in ages.items(): print(k, v, end='; ') # ann 30; bob 25; print() print(list(ages), list(ages.values())) # ['ann', 'bob'] [30, 25] graph = {} for u, v in [(1, 2), (1, 3), (2, 3)]: graph.setdefault(u, []).append(v) print(graph) # {1: [2, 3], 2: [3]}
- Adding or deleting keys while looping raises RuntimeError; loop over
17defaultdict for grouping and adjacency lists
A dict that creates a default value (list, int, set) the first time a key is touched.
- Merely reading
dd[k]inserts k, which can grow the dict or break a 'key exists' check; usek in ddto test. - Pass the factory, not a call:
defaultdict(list), notdefaultdict([]).
Practice: Group Anagrams →from collections import defaultdict groups = defaultdict(list) for w in ["eat", "tea", "tan", "ate"]: groups[''.join(sorted(w))].append(w) print(dict(groups)) # {'aet': ['eat', 'tea', 'ate'], 'ant': ['tan']} cnt = defaultdict(int) cnt['x'] += 1 print(cnt['x'], cnt['missing'], len(cnt)) # 1 0 2
- Merely reading
18Counter: counting, most_common, arithmetic
A dict subclass that counts hashable items in one call and supports multiset maths.
- Missing keys read as 0 without being inserted.
a - bkeeps only positive counts, so an empty result means b covers a (ransom-note check).most_common(k)breaks ties by first appearance.
Practice: Ransom Note →from collections import Counter c = Counter("mississippi") print(c['s'], c['z']) # 4 0 print(c.most_common(2)) # [('i', 4), ('s', 4)] print(Counter("listen") == Counter("silent")) # True a, b = Counter("aabc"), Counter("abd") print(a - b) # Counter({'a': 1, 'c': 1}) print(a & b, (a + b)['a']) # Counter({'a': 1, 'b': 1}) 3 print(not (Counter("ab") - Counter("aabb"))) # True
19Dict comprehension
Build a dict from any iterable in one expression, e.g. value-to-index maps.
- When a key repeats, the later value wins.
dict(zip(keys, values))is another one-liner for pairing two lists.
nums = [3, 1, 3] last = {v: i for i, v in enumerate(nums)} print(last) # {3: 2, 1: 1} sq = {x: x * x for x in range(3)} print(sq) # {0: 0, 1: 1, 2: 4} print(dict(zip("ab", [1, 2]))) # {'a': 1, 'b': 2}
20Sort a dict by value
Sort the keys (or the item pairs) with a key function that reads the value.
- Use
-countin the key for descending counts with ascending ties. max(d, key=d.get)finds the key with the largest value in O(n).
Practice: Top K Frequent Elements →freq = {'a': 3, 'b': 1, 'c': 2} print(sorted(freq, key=freq.get)) # ['b', 'c', 'a'] print(sorted(freq.items(), key=lambda kv: -kv[1])) # [('a', 3), ('c', 2), ('b', 1)] print(max(freq, key=freq.get)) # a
- Use
| Operation | Code | Time |
|---|---|---|
| Get / set / delete | d[k], d[k] = v, del d[k] | O(1) avg |
| Membership | k in d | O(1) avg |
| Safe read | d.get(k, default) | O(1) avg |
| Iterate | for k, v in d.items() | O(n) |
| Count items | Counter(iterable) | O(n) |
| Top k counts | c.most_common(k) | O(n log k) |
| Sort by value | sorted(d, key=d.get) | O(n log n) |
set
Unordered unique values with O(1) average membership tests.
21add, remove, discard and membership
Use a set whenever you only need to know 'have I seen this?'.
{}is an empty dict; writeset()for an empty set.removeraises KeyError if the value is absent;discarddoes not.list(dict.fromkeys(a))removes duplicates while keeping the original order.
Practice: Contains Duplicate →s = set() for x in [3, 1, 3]: s.add(x) print(len(s), 3 in s) # 2 True s.discard(99) s.remove(1) print(s) # {3} nums = [3, 1, 3, 2] print(len(set(nums)) != len(nums)) # True print(list(dict.fromkeys(nums))) # [3, 1, 2]
22Union, intersection, difference
Operators combine sets in time linear in their sizes.
|union,&intersection,-difference,^symmetric difference,<=subset.- Printed set order is not guaranteed in general; do not rely on it in answers.
a, b = {1, 2, 3}, {2, 3, 4} print(a | b) # {1, 2, 3, 4} print(a & b) # {2, 3} print(a - b) # {1} print(a ^ b) # {1, 4} print({1, 2} <= a) # True
23Tuples and frozenset as keys
Only hashable values can go into a set or be dict keys, so store coordinates as tuples and sets as frozensets.
- Adding a list raises
TypeError: unhashable type: 'list'; convert withtuple(lst). - A frozenset key ignores order, which is handy for 'same group of items' lookups.
visited = set() visited.add((0, 1)) print((0, 1) in visited) # True state = [2, 3] visited.add(tuple(state)) print(len(visited)) # 2 groups = {frozenset("abc"): "g1"} print(groups[frozenset("cba")]) # g1
- Adding a list raises
| Operation | Code | Time |
|---|---|---|
| Add / remove / discard | s.add(x), s.discard(x) | O(1) avg |
| Membership | x in s | O(1) avg |
| Build from list | set(a) | O(n) |
| Union | a | b | O(len(a) + len(b)) |
| Intersection | a & b | O(min(len(a), len(b))) |
| Difference | a - b | O(len(a)) |
heapq (heap)
A binary min-heap stored inside a plain list.
24heappush and heappop
heapqkeeps a list arranged so the smallest element is always at index 0.heapqis a min-heap only; there is no comparator argument.h[0]peeks at the minimum; the rest of the list is NOT sorted.
import heapq h = [] for x in [5, 1, 4]: heapq.heappush(h, x) print(h[0]) # 1 print(heapq.heappop(h)) # 1 print(heapq.heappop(h), len(h)) # 4 1
25heapify in O(n)
Turn an existing list into a heap in place in linear time, faster than n pushes.
heapifyreturns None; keep using the same list.- Popping everything off yields sorted order (heap sort in O(n log n)).
import heapq a = [7, 2, 9, 1] heapq.heapify(a) print(a[0]) # 1 print([heapq.heappop(a) for _ in range(len(a))]) # [1, 2, 7, 9]
26Max-heap via negation
Push negated values into the min-heap and negate again when you read them.
- Python 3.14 adds
heapq.heappush_max/heappop_max, but negation works on every version and judge. - For tuples, negate only the field you want reversed:
(-count, word).
import heapq nums = [3, 8, 5] h = [-x for x in nums] heapq.heapify(h) print(-h[0]) # 8 heapq.heappush(h, -10) print(-heapq.heappop(h)) # 10 print(-heapq.heappop(h)) # 8
- Python 3.14 adds
27Tuples (priority, item)
Push tuples so the heap orders by the first field, then the next field on ties.
- This is the standard shape for Dijkstra:
(dist, node).
Pattern: Shortest path →import heapq tasks = [] heapq.heappush(tasks, (2, "write")) heapq.heappush(tasks, (1, "plan")) heapq.heappush(tasks, (2, "test")) while tasks: pri, name = heapq.heappop(tasks) print(pri, name, end=' | ') # 1 plan | 2 test | 2 write | print()
- This is the standard shape for Dijkstra:
28Tie-breaking gotcha: add a counter
When priorities tie, Python compares the next field, which crashes if it is a dict, node or other non-comparable object.
- Insert a unique, increasing counter as the second field:
(priority, next(counter), item). - Heaps of lists like
[dist, node]hit the same problem when nodes are custom objects.
import heapq, itertools h, counter = [], itertools.count() for dist, node in [(1, {'id': 'A'}), (1, {'id': 'B'})]: heapq.heappush(h, (dist, next(counter), node)) print(heapq.heappop(h)[2]) # {'id': 'A'} try: heapq.heappush(h, (1, {'id': 'C'})) heapq.heappush(h, (1, {'id': 'D'})) except TypeError: print("dicts are not comparable") # dicts are not comparable
- Insert a unique, increasing counter as the second field:
29nlargest and nsmallest
Return the k biggest or smallest items, optionally ordered by a key.
- Cost is about O(n log k); for k = 1 just use
min/max, for k near n just sort.
Practice: K Closest Points to Origin →import heapq a = [5, 1, 9, 3, 7] print(heapq.nlargest(2, a)) # [9, 7] print(heapq.nsmallest(2, a)) # [1, 3] pts = [(1, 2), (3, 0), (0, 1)] print(heapq.nsmallest(1, pts, key=lambda p: p[0]**2 + p[1]**2)) # [(0, 1)]
- Cost is about O(n log k); for k = 1 just use
30K-th largest with a size-k min-heap
Keep only the k largest values seen so far; the heap's top is then the k-th largest.
- O(n log k) time, O(k) space.
heapq.heappushpop(h, x)pushes then pops in one faster step.
Practice: Kth Largest Element in an Array →import heapq def kth_largest(nums, k): h = [] for x in nums: heapq.heappush(h, x) if len(h) > k: heapq.heappop(h) return h[0] print(kth_largest([3, 2, 1, 5, 6, 4], 2)) # 5
| Operation | Code | Time |
|---|---|---|
| Peek min | h[0] | O(1) |
| Push / pop | heappush(h, x), heappop(h) | O(log n) |
| Push then pop | heappushpop(h, x) | O(log n) |
| Build heap | heapify(a) | O(n) |
| k largest / smallest | nlargest(k, a) | O(n log k) |
deque, queue & stack
O(1) operations at both ends for BFS, stacks and sliding windows.
31collections.deque
A double-ended queue with O(1) append and pop on both sides.
- Indexing the middle
q[i]is O(n); deque is for the ends. deque(maxlen=k)silently drops from the other end when full.
from collections import deque q = deque([2, 3]) q.append(4) q.appendleft(1) print(q) # deque([1, 2, 3, 4]) print(q.popleft(), q.pop()) # 1 4 print(q[0], q[-1], len(q)) # 2 3 2 q.rotate(1) print(list(q)) # [3, 2]
- Indexing the middle
32BFS skeleton
Pop from the left, push neighbours on the right; the first time you reach a node is via a shortest path in an unweighted graph.
- Mark nodes as visited when you enqueue them, not when you pop them, to avoid duplicates.
- For level-by-level work, loop
for _ in range(len(q))inside the while.
Pattern: Graph BFS →from collections import deque graph = {0: [1, 2], 1: [3], 2: [3], 3: []} dist = {0: 0} q = deque([0]) while q: u = q.popleft() for v in graph[u]: if v not in dist: dist[v] = dist[u] + 1 q.append(v) print(dist) # {0: 0, 1: 1, 2: 1, 3: 2}
33List as a stack
A plain list is the idiomatic stack:
appendto push,pop()to pop,[-1]to peek.- Check
if stackbeforestack[-1]orpop(); both raise IndexError on empty. - Python has no separate Stack class; do not use
queue.LifoQueue(it is for threads).
Practice: Valid Parentheses →def valid(s): pairs = {')': '(', ']': '[', '}': '{'} stack = [] for c in s: if c in pairs: if not stack or stack.pop() != pairs[c]: return False else: stack.append(c) return not stack print(valid("([]{})"), valid("(]"), valid("((")) # True False False
- Check
34Monotonic deque: sliding window max
Keep indices whose values decrease from front to back; the front is always the window maximum.
- Each index is pushed and popped at most once, so the whole pass is O(n).
- Store indices, not values, so you can tell when the front has left the window.
Practice: Sliding Window Maximum →from collections import deque def window_max(nums, k): dq, out = deque(), [] for i, x in enumerate(nums): while dq and nums[dq[-1]] <= x: dq.pop() dq.append(i) if dq[0] <= i - k: dq.popleft() # front left the window if i >= k - 1: out.append(nums[dq[0]]) return out print(window_max([1, 3, -1, -3, 5, 3, 6, 7], 3)) # [3, 3, 5, 5, 6, 7]
| Operation | Code | Time |
|---|---|---|
| Push / pop right | q.append(x), q.pop() | O(1) |
| Push / pop left | q.appendleft(x), q.popleft() | O(1) |
| Peek ends | q[0], q[-1] | O(1) |
| Index middle | q[i] | O(n) |
| Stack push / pop (list) | st.append(x), st.pop() | O(1) amortised |
| Queue via list (avoid) | a.pop(0) | O(n) |
Sorting & comparators
Stable O(n log n) sorting driven by key functions.
35sorted() vs .sort()
sortedreturns a new list from any iterable;list.sort()sorts in place and returns None.a = a.sort()is a classic bug: it setsato None.- Both are stable, so equal keys keep their original order;
reverse=Trueflips the order.
a = [3, 1, 2] b = sorted(a) print(a, b) # [3, 1, 2] [1, 2, 3] r = a.sort() print(a, r) # [1, 2, 3] None print(sorted("bca", reverse=True)) # ['c', 'b', 'a']
36key=lambda and multi-key tuples
A key function maps each element to the value to compare; return a tuple to sort by several fields.
- Negate a numeric field inside the tuple to make just that field descending.
operator.itemgetter(1)is a fast alternative tolambda p: p[1].
ps = [("bo", 25), ("al", 30), ("cy", 25)] print(sorted(ps, key=lambda p: p[1])) # [('bo', 25), ('cy', 25), ('al', 30)] print(sorted(ps, key=lambda p: (-p[1], p[0]))) # [('al', 30), ('bo', 25), ('cy', 25)] words = ["bb", "a", "ccc", "dd"] print(sorted(words, key=lambda w: (len(w), w))) # ['a', 'bb', 'dd', 'ccc']
37functools.cmp_to_key
Wrap an old-style two-argument comparator when the order cannot be expressed as a simple key.
- Return a negative number if a should come first, positive if b should, 0 if equal.
- Classic use: ordering numbers as strings to form the largest number.
from functools import cmp_to_key def cmp(a, b): if a + b > b + a: return -1 return 1 if a + b < b + a else 0 nums = [3, 30, 34, 5, 9] s = sorted(map(str, nums), key=cmp_to_key(cmp)) print(''.join(s)) # 9534330
38Sorting intervals
Sort by start time first; overlapping intervals then sit next to each other and can be merged in one pass.
- For 'fewest removals' or 'arrows' problems, sort by end time instead.
Practice: Merge Intervals →intervals = [[8, 10], [1, 3], [2, 6]] intervals.sort(key=lambda x: x[0]) merged = [] for s, e in intervals: if merged and s <= merged[-1][1]: merged[-1][1] = max(merged[-1][1], e) else: merged.append([s, e]) print(merged) # [[1, 6], [8, 10]]
| Operation | Code | Time |
|---|---|---|
| Sort in place | a.sort(key=f) | O(n log n) |
| New sorted list | sorted(it, key=f) | O(n log n) |
| Reverse order | sorted(a, reverse=True) | O(n log n) |
| Custom comparator | sorted(a, key=cmp_to_key(c)) | O(n log n) |
| Min / max only | min(a, key=f) | O(n) |
Binary search & bisect
Find positions in sorted data in O(log n).
39bisect_left and bisect_right
Return the insertion point that keeps a sorted list sorted: before equal elements (left) or after them (right).
bisect_right(a, x) - bisect_left(a, x)counts occurrences of x.- Check
i < len(a) and a[i] == xbefore treating the result as a hit. - Since 3.10 both accept
key=for searching by a field.
Pattern: Binary search →import bisect a = [1, 2, 2, 2, 5] print(bisect.bisect_left(a, 2)) # 1 print(bisect.bisect_right(a, 2)) # 4 print(bisect.bisect_right(a, 2) - bisect.bisect_left(a, 2)) # 3 i = bisect.bisect_left(a, 4) print(i, i < len(a) and a[i] == 4) # 4 False
40insort
Insert a value into a sorted list at the right place.
- Finding the spot is O(log n) but shifting elements makes it O(n) overall.
- For many inserts, a heap or
sortedcontainers.SortedList(available on LeetCode) is faster.
import bisect a = [1, 4, 9] bisect.insort(a, 5) bisect.insort(a, 0) print(a) # [0, 1, 4, 5, 9]
41Manual lo/hi template (first True)
Binary search on the answer: find the smallest value where a monotone condition flips from False to True.
- Use
lo < hi,hi = midon True andlo = mid + 1on False; the loop always ends at the answer. - Python ints never overflow, so
(lo + hi) // 2is safe.
Practice: Koko Eating Bananas →def first_true(lo, hi, ok): while lo < hi: mid = (lo + hi) // 2 if ok(mid): hi = mid else: lo = mid + 1 return lo print(first_true(0, 100, lambda x: x * x >= 50)) # 8 piles, h = [3, 6, 7, 11], 8 fits = lambda k: sum((p + k - 1) // k for p in piles) <= h print(first_true(1, max(piles), fits)) # 4
- Use
| Operation | Code | Time |
|---|---|---|
| Insertion point | bisect_left(a, x) | O(log n) |
| After equal items | bisect_right(a, x) | O(log n) |
| Sorted insert | insort(a, x) | O(n) |
| Search on answer | while lo < hi | O(log range * check) |
itertools & functools
Ready-made combinatorics, prefix sums and memoisation.
42permutations, combinations, product
Generate orderings, subsets of a fixed size, or the cartesian product without writing recursion.
- Outputs are tuples and are produced lazily; wrap in
listonly if you need them all. - Counts explode fast: n! permutations, C(n, r) combinations.
Pattern: Backtracking →from itertools import permutations, combinations, product print(list(permutations([1, 2, 3], 2))) # [(1, 2), (1, 3), (2, 1), (2, 3), (3, 1), (3, 2)] print(list(combinations('abc', 2))) # [('a', 'b'), ('a', 'c'), ('b', 'c')] print(list(product([0, 1], repeat=2))) # [(0, 0), (0, 1), (1, 0), (1, 1)] print(len(list(permutations(range(4))))) # 24
- Outputs are tuples and are produced lazily; wrap in
43accumulate for prefix sums
Running totals in one call; prepend 0 so that
pre[j] - pre[i]is the sum ofa[i:j].initial=0(3.8+) prepends the 0 for you.- Pass another function, e.g.
max, for running maximums.
Pattern: Prefix sum →from itertools import accumulate a = [3, 1, 4, 1] pre = list(accumulate(a, initial=0)) print(pre) # [0, 3, 4, 8, 9] print(pre[3] - pre[1]) # 5 print(list(accumulate(a, max))) # [3, 3, 4, 4]
44groupby
Group consecutive equal elements (or equal keys) into runs.
- It only groups neighbours; sort first if you want all equal items together.
- Each group is a one-shot iterator: consume it before moving to the next.
from itertools import groupby s = "aaabccdd" print([(k, len(list(g))) for k, g in groupby(s)]) # [('a', 3), ('b', 1), ('c', 2), ('d', 2)] print(''.join(f"{k}{len(list(g))}" for k, g in groupby(s))) # a3b1c2d2
45lru_cache / cache for memoised DP
Decorate a recursive function so repeated arguments return a stored result: top-down DP in one line.
- Arguments must be hashable; pass tuples, not lists.
@cache(3.9+) equals@lru_cache(maxsize=None); callf.cache_clear()between test cases if the function is global.- Deep recursion still hits the recursion limit; raise it or switch to bottom-up.
Practice: Climbing Stairs →from functools import cache @cache def ways(n): if n <= 1: return 1 return ways(n - 1) + ways(n - 2) print(ways(40)) # 165580141 print(ways.cache_info().currsize) # 41
46reduce
Fold a sequence into one value by applying a two-argument function left to right.
- Give an initial value as the third argument so empty input does not raise.
math.prodandsumare clearer for products and sums.
Practice: Single Number →from functools import reduce import operator print(reduce(operator.mul, [1, 2, 3, 4], 1)) # 24 print(reduce(lambda a, b: a ^ b, [4, 1, 2, 1, 2])) # 4 print(reduce(operator.add, [], 0)) # 0
| Tool | Code | Output size / time |
|---|---|---|
| Permutations | permutations(a, r) | n! / (n - r)! |
| Combinations | combinations(a, r) | C(n, r) |
| Cartesian product | product(a, repeat=k) | n^k |
| Prefix sums | accumulate(a, initial=0) | O(n) |
| Runs of equal items | groupby(a) | O(n) |
| Memoised call | @cache | O(1) avg per cached hit |
Classes & nodes
Linked-list and tree nodes, quick records, and function gotchas.
47ListNode and a dummy head
A tiny class with
valandnext; a dummy head lets you build or edit a list without special-casing the first node.- Return
dummy.next, notdummy. - Check
node is None(ornot node) before readingnode.next.
Pattern: Linked list →class ListNode: def __init__(self, val=0, next=None): self.val, self.next = val, next def build(vals): dummy = tail = ListNode() for v in vals: tail.next = ListNode(v) tail = tail.next return dummy.next head = build([1, 2, 3]) print(head.val, head.next.val, head.next.next.next) # 1 2 None
- Return
48TreeNode and recursive traversal
Nodes with
leftandright; most tree answers are a short recursive function returning a value per subtree.- The base case
if not nodehandles empty trees and leaves' missing children. - Use
nonlocal bestinside a nested helper when it must update an outer variable.
Pattern: Tree DFS →class TreeNode: def __init__(self, val=0, left=None, right=None): self.val, self.left, self.right = val, left, right # 1 # / \ # 2 3 root = TreeNode(1, TreeNode(2), TreeNode(3)) def inorder(n): return inorder(n.left) + [n.val] + inorder(n.right) if n else [] def depth(n): return 1 + max(depth(n.left), depth(n.right)) if n else 0 print(inorder(root), depth(root)) # [2, 1, 3] 2
- The base case
49dataclass for quick records
@dataclasswrites__init__,__repr__and__eq__for you;order=Truealso adds comparisons by field order.- Fields compare in declaration order, so put the sort field first.
collections.namedtupleis a lighter immutable alternative.
from dataclasses import dataclass @dataclass(order=True) class Job: end: int start: int name: str = "" jobs = [Job(5, 1, "b"), Job(3, 0, "a")] print(min(jobs)) # Job(end=3, start=0, name='a') print(sorted(jobs)[-1].name) # b print(Job(1, 0) == Job(1, 0)) # True
50__lt__ so objects work in a heap
Define
__lt__and your objects can go straight intoheapqorsorted.- heapq only ever uses
<, so__lt__is enough. - Without it, pushing two objects raises TypeError; the counter-tuple trick is the alternative.
Practice: Merge k Sorted Lists →import heapq class Node: def __init__(self, cost, name): self.cost, self.name = cost, name def __lt__(self, other): return self.cost < other.cost h = [] for c, n in [(3, "c"), (1, "a"), (2, "b")]: heapq.heappush(h, Node(c, n)) print([heapq.heappop(h).name for _ in range(3)]) # ['a', 'b', 'c']
- heapq only ever uses
51Mutable default arguments
Default values are created once when the function is defined, so a default list is shared by every call.
- Use
Noneas the default and create the list inside the function. - The same sharing bites backtracking: append
path[:], notpath, to the results.
def add_bad(x, acc=[]): acc.append(x) return acc print(add_bad(1), add_bad(2)) # [1, 2] [1, 2] def add_ok(x, acc=None): acc = [] if acc is None else acc acc.append(x) return acc print(add_ok(1), add_ok(2)) # [1] [2]
- Use
Handy utilities
Infinity, integer maths, recursion, fast input and bit tricks.
52Infinity: float('inf') and math.inf
Start a running minimum at infinity (or maximum at minus infinity) so the first real value always wins.
- Infinity is a float; convert the final answer back, e.g.
return -1 if best == inf else best. int(math.inf)raises OverflowError.
import math best = float('inf') for x in [7, 3, 9]: best = min(best, x) print(best) # 3 print(math.inf == float('inf')) # True print(-math.inf < -10**18) # True print([math.inf] * 3) # [inf, inf, inf]
- Infinity is a float; convert the final answer back, e.g.
53// and % with negatives, divmod
//floors toward minus infinity and%takes the sign of the divisor, unlike Java or C++.- For C-style truncation use
int(a / b)(fine for normal sizes) or adjust the sign yourself. - Ceiling division without floats:
-(-a // b)or(a + b - 1) // bfor positive b. /always returns a float, even for4 / 2.
print(7 // 2, -7 // 2) # 3 -4 print(-7 % 3, 7 % -3) # 2 -2 print(int(-7 / 2)) # -3 print(divmod(17, 5)) # (3, 2) print(4 / 2, -(-7 // 2)) # 2.0 4
- For C-style truncation use
54pow with mod, gcd, lcm and other math helpers
Built-ins for modular exponentiation, gcd/lcm, integer square roots and binomials.
- Python ints never overflow, but reduce modulo
10**9 + 7as you go to keep them fast. pow(a, -1, m)(3.8+) gives a modular inverse when gcd(a, m) is 1.
import math MOD = 10**9 + 7 print(pow(2, 10, 1000), pow(3, -1, 7)) # 24 5 print(pow(2, 100, MOD)) # 976371285 print(math.gcd(12, 18), math.lcm(4, 6)) # 6 12 print(math.isqrt(17), math.comb(5, 2)) # 4 10 print(2**64) # 18446744073709551616
- Python ints never overflow, but reduce modulo
55sys.setrecursionlimit
The default limit is about 1000 frames, which deep DFS or memoised recursion can exceed.
- Raise it at the top of the file for recursion depths in the thousands.
- Very deep recursion can still crash the interpreter; rewrite as iterative with an explicit stack if depth may hit ~10^5.
import sys sys.setrecursionlimit(10**6) def depth(n): return 0 if n == 0 else 1 + depth(n - 1) print(depth(5000)) # 5000
56Fast input with sys.stdin
On stdin-based judges,
sys.stdin.readlineis much faster thaninput()for large inputs.readlinekeeps the trailing newline;int()andsplit()ignore it, but strip it before using the line as text.- For huge inputs, read everything at once:
data = sys.stdin.buffer.read().split().
import sys input = sys.stdin.readline # stdin: # 3 # 1 2 3 n = int(input()) nums = list(map(int, input().split())) print(n, sum(nums)) # 3 6
57Bit tricks
Test, clear and count bits with a handful of operators.
x & 1is odd/even,x & (x - 1)clears the lowest set bit,x & -xisolates it.x.bit_count()(3.10+) counts set bits;bin(x).count('1')works everywhere.a ^ a == 0, so XOR-ing everything cancels pairs.
x = 12 print(bin(x), x & 1, x >> 1, x << 2) # 0b1100 0 6 48 print(x & (x - 1), x & -x) # 8 4 print(bin(x).count('1'), x.bit_count()) # 2 2 print(x > 0 and x & (x - 1) == 0) # False print(5 ^ 5, 5 ^ 0) # 0 5
58Swap and unpacking
Tuple assignment swaps without a temp variable and unpacks sequences in one line.
- The right side is fully evaluated first, then targets are assigned left to right; order matters in tricks like
cur.next, prev, cur = prev, cur, cur.next. first, *rest = acollects the remainder into a list.
a, b = 1, 2 a, b = b, a print(a, b) # 2 1 nums = [5, 6, 7] nums[0], nums[2] = nums[2], nums[0] print(nums) # [7, 6, 5] first, *rest = nums print(first, rest) # 7 [6, 5]
- The right side is fully evaluated first, then targets are assigned left to right; order matters in tricks like
59Grid neighbours with chained comparisons
Loop over four direction offsets and bounds-check with
0 <= r < R, which Python reads as two comparisons.- Grids are
grid[row][col]; get sizes withR, C = len(grid), len(grid[0]).
Practice: Number of Islands →grid = [[1, 1, 0], [0, 1, 0]] R, C = len(grid), len(grid[0]) r, c = 0, 1 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] == 1: print((nr, nc), end=' ') # (1, 1) (0, 0) print()
- Grids are
Quick tips
- Use
dequefor queues and BFS;list.pop(0)silently turns an O(n) solution into O(n^2). heapqis min-only: negate values (or the priority field) for a max-heap, and add a counter to tuples when items are not comparable.- Build 2D grids with a comprehension, never
[[0] * c] * r, and default mutable parameters toNone. - Reach for
Counter,defaultdict,bisectand@cachebefore writing them by hand - interviewers value knowing the standard library. - State complexities as you code: dict/set lookups are O(1) average,
x in listis O(n), sorting is O(n log n).
Topic list based on the free Everything You Need to Get Placed checklist by Mohammad Fraz. Explanations and examples are original.