Skip to main content
Reference

Python Cheatsheet for Algorithms

The Python you actually reach for while solving algorithm problems: the built-in data structures, the standard-library modules that replace hand-written code, and the idioms that make solutions short and fast. Features marked 3.10+ need Python 3.10 or newer (LeetCode's Python 3 runtime supports them). Each section ends with the full signatures of the functions and methods it uses, written the way the Python documentation writes them: arguments in square brackets are optional, arguments before / can only be passed by position, and arguments after * can only be passed by keyword.

Complexity at a glance

Know these before you pick a structure. Most "Time Limit Exceeded" results in Python come from an O(n) operation hiding inside a loop.

StructureFast (O(1) or O(log n))Slow (O(n)): watch out
lista[i], a.append(x), a.pop()x in a, a.pop(0), a.insert(0, x), a.remove(x), a.index(x)
dict / setx in d, d[k], d[k] = v, del d[k], s.add(x) (O(1) average)iterating all keys
dequeappend, appendleft, pop, popleftdq[i] in the middle
heapqheap[0] O(1), heappush / heappop O(log n)searching for an arbitrary item
bisectbisect_left / bisect_right O(log n)insort is O(n): the search is fast, the list shift is not
SortedListadd, remove, pop, bisect_left, sl[i] (all ~O(log n))nothing common
strs[i], len(s)s += c in a loop (can be O(n²)), s.find(t), slicing

list: arrays, stacks, and 2D grids

REFERENCE
Loading...
YOUR PRACTICE
Loading...

Trap: a.pop(0) and a.insert(0, x) shift every element, so each is O(n). If you pop from the front repeatedly (a queue, BFS), use collections.deque.

append vs extend vs +: a.append(x) adds x as one element, even when x is a list. a.extend(iterable) walks the iterable and adds each of its elements one by one. Both change a in place and return None.

REFERENCE
Loading...
YOUR PRACTICE
Loading...
OperationChanges the original list?Cost
a.extend(b)Yes, in placeO(k), where k = len(b)
c = a + bNo, builds a new listO(n + k): copies every element of a and b
a += bYes, in place (it calls extend under the hood)O(k), same as extend

extend costs O(k) time and O(k) extra space to grow the list by k elements.

Trap: nums = nums.extend([3, 4]) sets nums to None, because extend returns None. Call nums.extend([3, 4]) on its own line. The same holds for append, sort and reverse.

Trap: a.extend("dog") adds three characters 'd', 'o', 'g', not the word. To add the word as one element, use a.append("dog").

Full signatures

SignatureWhat it does
list.append(x)Adds x at the end, O(1)
list.extend(iterable)Adds every element of iterable at the end, one by one, O(k) for k elements; returns None
list.pop([i])Removes and returns the item at index i (the last item when i is left out); O(1) at the end, O(n) anywhere else
list.insert(i, x)Inserts x before index i, O(n)
list.remove(x)Removes the first item equal to x, O(n); ValueError if there is none
list.index(x[, start[, end]])Index of the first item equal to x, O(n); ValueError if there is none
list.sort(*, key=None, reverse=False)Sorts the list in place and returns None
sorted(iterable, /, *, key=None, reverse=False)Returns a new sorted list; the input is unchanged
list.copy()Returns a shallow copy, same as a[:]
max(iterable, *, key=None), max(iterable, *, default, key=None), max(arg1, arg2, *args, key=None)Largest item; ValueError on an empty iterable unless default is given
min(iterable, *, key=None), min(iterable, *, default, key=None), min(arg1, arg2, *args, key=None)Smallest item; ValueError on an empty iterable unless default is given
sum(iterable, /, start=0)start plus every item

dict: hash maps

REFERENCE
Loading...
YOUR PRACTICE
Loading...
  • Dicts keep insertion order (guaranteed since 3.7).
  • Keys must be hashable: str, int, tuple, frozenset work, list / set / dict don't. That's why anagram keys are tuple(count) or "".join(sorted(word)).
  • d1 == d2 is True when both have the same key-value pairs; insertion order doesn't matter, and the check is O(n).
  • Trap: adding or removing keys while iterating over the dict raises RuntimeError. Iterate over list(d) if you need to delete as you go.

Full signatures

SignatureWhat it does
dict.get(key, default=None)The value for key, or default when key is missing (never raises)
dict.setdefault(key, default=None)The value for key; when key is missing, first inserts key with the value default
dict.items()A view of (key, value) pairs
dict.pop(key[, default])Removes key and returns its value; returns default when key is missing, or raises KeyError when no default is given
sorted(iterable, /, *, key=None, reverse=False)Returns a new sorted list

defaultdict: dicts that create missing values

defaultdict(factory) calls factory() the first time you touch a missing key, so you never write "if key not in d" setup code.

REFERENCE
Loading...
YOUR PRACTICE
Loading...

Trap: just reading d[missing_key] inserts it. Use key in d to test membership without creating the key.

Full signatures

SignatureWhat it does
collections.defaultdict(default_factory=None, /[, ...])A dict that calls default_factory() to create the value of a missing key; the optional extra arguments are the same as dict()'s
set.add(elem)Adds elem to the set

Counter: frequency maps

Counter (from collections) builds a frequency map in one call: Counter(iterable) counts occurrences of every element. It behaves like a dict but with .most_common(k), and missing keys default to 0 instead of raising KeyError.

REFERENCE
Loading...
YOUR PRACTICE
Loading...

More that comes up constantly:

REFERENCE
Loading...
YOUR PRACTICE
Loading...

The leftover-count line solves Ransom Note (LC 383) in one expression.

Trap in sliding windows: after c[x] -= 1 a key with count 0 still exists, so len(c) still counts it. Delete it (if c[x] == 0: del c[x]) when len(c) must mean "distinct values in the window".

Turning {key: frequency} into {frequency: key}. Top-K and bucket-sort problems need the map the other way round. Pick the form by one question: can two keys share the same frequency? They usually can, so the first four forms keep every key.

REFERENCE
Loading...
YOUR PRACTICE
Loading...

Here n is the number of items counted (len(s)) and u is the number of distinct keys (len(counts)). Forms 1, 3 and 4 make one pass over counts.items(), so they cost O(u) on top of the O(n) spent building counts; form 4 also allocates n + 1 empty lists, so it is O(n) in total. Form 2 sorts u pairs, so it costs O(u log u). Form 4 is what makes Top K Frequent Elements (LC 347) run in O(n): the last loop above walks buckets from the highest index down, and it can stop as soon as top holds k keys. The heapq section shows the same problem solved with a heap of size k.

Trap in form 5: {freq: key for key, freq in counts.items()} stores one key per frequency. When 'a' and 'c' both have count 2, the later key overwrites the earlier one and a key disappears without any error. Use form 3 or form 4 unless you know every count is different.

Trap in form 1: in on a list compares whole elements, so 3 in pairs is False even though (3, 'b') is in the list: every element is a tuple, and 3 != (3, 'b'). To ask "does any key have frequency 3?", look the frequency up in form 3 (3 in by_freq) or form 4 (buckets[3], which is a non-empty list), or scan the pairs with any(freq == 3 for freq, key in pairs).

defaultdict(int) vs Counter() — which one for frequency counting? For plain counting, counts[x] += 1 and counts[x] -= 1 behave the same on both: neither one deletes a key automatically when its count drops to 0 (that's why the trap above, and the defaultdict trap further up, both exist). The one real behavior difference is what happens on a missing-key read:

defaultdict(int)Counter()
Reading a missing key (counts["z"])Returns 0 and inserts "z": 0 into the dictReturns 0 without inserting "z"
len(counts) after only reads, no writesGrows with every missing key you merely looked upStays accurate — unread keys are never added
Extra methodsNone — it is a plain dict.most_common(), .elements(), set-style +/-/&/`

Use Counter() when you want those extra methods, or when you're building the whole frequency map in one call from an iterable (Counter(nums)). Use defaultdict(int) when the counting is a means to something else — e.g. a sliding-window frequency map you mutate key-by-key and never call .most_common() on — since a plain dict subclass with no extra methods is the more standard choice there. Either way, if your code also reads a key just to check it (not to count it), prefer defaultdict(int) only when you want that read to insert the key; otherwise reach for Counter(), or guard the read with key in counts first.

Full signatures

SignatureWhat it does
collections.Counter([iterable-or-mapping])Counts every item of an iterable, or copies counts from a mapping
Counter.most_common([n])The n most common (item, count) pairs, most common first (all of them when n is left out)
Counter.total()Sum of all counts (3.10+)
Counter.elements()An iterator that repeats each item as many times as its count; items with a count below 1 are left out
dict.items()A view of (key, value) pairs
dict.setdefault(key, default=None)The value for key; when key is missing, first inserts key with the value default

set: membership and uniqueness

REFERENCE
Loading...
YOUR PRACTICE
Loading...

Full signatures

SignatureWhat it does
set.add(elem)Adds elem
set.discard(elem)Removes elem if present; no error if missing
set.remove(elem)Removes elem; KeyError if missing
set.union(*others)Same as a | b, but accepts any iterables
set.intersection(*others)Same as a & b, but accepts any iterables
set.difference(*others)Same as a - b, but accepts any iterables
set.symmetric_difference(other)Same as a ^ b, but accepts any iterable
set.issubset(other)Same as a <= b, but accepts any iterable
dict.fromkeys(iterable, value=None, /)A new dict with the items of iterable as keys, in first-seen order, each mapped to value
frozenset([iterable])An immutable, hashable set

Intersections: sets, lists, and dicts

Sets. a & b (see set above) needs a set on both sides; s.intersection(any_iterable) also accepts a list or tuple: {1, 2, 3, 4}.intersection([3, 4, 5, 6]) is {3, 4}.

Lists (keep order or duplicates). A list comprehension keeps list1's order and its repeated values.

REFERENCE
Loading...
YOUR PRACTICE
Loading...

Dictionaries. keys() and items() views behave like sets, so & finds common keys or common key-value pairs.

REFERENCE
Loading...
YOUR PRACTICE
Loading...

Trap: x in list2 inside the comprehension is O(n), so the plain list version is O(n·m). Build set(list2) once first.

Full signatures

SignatureWhat it does
set.intersection(*others)Items present in the set and in every one of others; each of others can be any iterable
dict.keys()A set-like view of the keys
dict.items()A set-like view of the (key, value) pairs (set-like only when every value is hashable)

deque: queues and BFS

A double-ended queue with O(1) pushes and pops at both ends. Use it for every queue and every BFS.

REFERENCE
Loading...
YOUR PRACTICE
Loading...

BFS template (shortest path in steps on a grid):

REFERENCE
Loading...
YOUR PRACTICE
Loading...

Full signatures

SignatureWhat it does
collections.deque([iterable[, maxlen]])A new deque; with maxlen, adding to a full deque drops an item from the other end
deque.append(x)Adds x at the right end, O(1)
deque.appendleft(x)Adds x at the left end, O(1)
deque.pop()Removes and returns the rightmost item, O(1); IndexError if empty
deque.popleft()Removes and returns the leftmost item, O(1); IndexError if empty

heapq: useful methods for algorithms

Before Python 3.14, heapq only has min-heap functions, so for a max-heap you negate the values you push. Python 3.14 adds heapify_max, heappush_max and heappop_max; check that your judge runs 3.14 before you use them.

MethodWhat it doesComplexity
heapq.heapify(x)Turns the list x into a heap, in placeO(n)
heapq.heappush(heap, item)Pushes item onto the heapO(log n)
heapq.heappop(heap)Pops and returns the smallest itemO(log n)
heapq.heappushpop(heap, item)Pushes then pops, faster than separate callsO(log n)
heapq.heapreplace(heap, item)Pops then pushes (assumes heap non-empty)O(log n)
heapq.nlargest(n, iterable, key=None)Returns the n largest elementsO(len(iterable) log n)
heapq.nsmallest(n, iterable, key=None)Returns the n smallest elementsO(len(iterable) log n)
Example: Top K Frequent Elements (LC 347)
REFERENCE
Loading...
YOUR PRACTICE
Loading...
Heap patterns
REFERENCE
Loading...
YOUR PRACTICE
Loading...
Keep a sorted top-k list without a heap
REFERENCE
Loading...
YOUR PRACTICE
Loading...

top_k[i:-1] is a copy of the slice, so assigning it to top_k[i+1:] is safe. The last element drops off, and the list keeps its length k.

Cost: each number takes O(k) time to find its place and to shift. The whole stream takes O(n · k). A size-k heap takes O(n log k), so use this version only when k is small.

Example: Find the Kth Largest Integer in the Array (LC 1985)

The numbers arrive as strings. The size-k min-heap from the Heap patterns block above still works, but the heap must order the strings by numeric value, not alphabetically, so each push is keyed by the pair (len(s), s).

REFERENCE
Loading...
YOUR PRACTICE
Loading...

Tuples compare element by element, starting at index 0. With (len(s), s) the comparison takes two steps:

  1. Length first. A positive integer with more digits is always larger, so (2, "10") beats (1, "7") on the lengths alone and the characters are never looked at.
  2. The string as a tie-break. When two numbers have the same number of digits, their lengths tie and Python compares the strings. For strings of equal length, character-by-character order is the same as numeric order, so "21" > "12" is the right answer.
PairKeys pushedWhich key is largerNumeric truth
"10" vs "7"(2, "10") vs (1, "7")(2, "10"), decided by the lengths10 > 7
"21" vs "12"(2, "21") vs (2, "12")(2, "21"), decided by the strings21 > 12

Each tuple comparison looks at up to L characters, so the heap work is O(n · L · log k) in total. The strings are never converted to int.

Full signatures

SignatureWhat it does
heapq.merge(*iterables, key=None, reverse=False)Lazily merges inputs that are each already sorted into one sorted iterator
heapq.heapify_max(x), heapq.heappush_max(heap, item), heapq.heappop_max(heap)Max-heap versions of heapify, heappush and heappop (3.14+)
collections.Counter([iterable-or-mapping])Counts every item of an iterable
dict.items()A view of (key, value) pairs
itertools.count(start=0, step=1)An endless iterator: start, start + step, …
next(iterator[, default])The next item of iterator; default (or StopIteration) when it is exhausted

bisect: binary search on sorted lists

bisect finds insertion points in a sorted list in O(log n), so you don't have to hand-write binary search.

REFERENCE
Loading...
YOUR PRACTICE
Loading...
You want…Use
First index with a[i] >= xbisect_left(a, x)
First index with a[i] > xbisect_right(a, x)
Last index with a[i] <= xbisect_right(a, x) - 1
Last index with a[i] < xbisect_left(a, x) - 1
Is x present?i = bisect_left(a, x); i < len(a) and a[i] == x
Example: Longest Increasing Subsequence (LC 300) in O(n log n)

tails[k] is the smallest tail of any increasing subsequence of length k + 1. Each number either extends the longest one or replaces a tail with a smaller value.

REFERENCE
Loading...
YOUR PRACTICE
Loading...
Binary search on a condition: key= over a range (3.10+)

bisect_left only needs len() and indexing, so a range works as a lazy list of candidates (nothing is built in memory). Pass a key that turns each candidate into False or True and search for True: the result is the position of the first candidate where the condition holds. This is the Lower Bound binary search in one line, and key runs only O(log n) times.

H-Index II (LC 275), Koko Eating Bananas (LC 875) and Sqrt(x) (LC 69), each in one line:

REFERENCE
Loading...
YOUR PRACTICE
Loading...
You want…Use
First candidate in range(lo, hi) where cond is Truebisect_left(range(lo, hi), True, key=cond): a position, so add lo (or index the range)
Last candidate where cond is True (True, …, True, False, …)bisect_left(range(n), True, key=lambda i: not cond(i)) - 1
No candidate is TrueThe result equals len(range): check it before using it
  • The condition must be monotonic: all False, then all True. If it can flip back, the answer is quietly wrong.
  • bisect never passes x through key: search for True (or a key value), not for a candidate.
key= on sorted records and descending lists
REFERENCE
Loading...
YOUR PRACTICE
Loading...

Full signatures

SignatureWhat it does
bisect.bisect_left(a, x, lo=0, hi=len(a), *, key=None)First index i in a[lo:hi] with a[i] >= x (compares key(a[i]) with x when key is given, 3.10+)
bisect.bisect_right(a, x, lo=0, hi=len(a), *, key=None)First index i in a[lo:hi] with a[i] > x; bisect.bisect is the same function
bisect.insort_left(a, x, lo=0, hi=len(a), *, key=None)Inserts x at bisect_left's position; key is applied to x too
bisect.insort_right(a, x, lo=0, hi=len(a), *, key=None)Inserts x after any equal items; bisect.insort is the same function
range(stop), range(start, stop[, step])A lazy sequence of integers; supports len() and indexing, so bisect can search it
max(iterable, *, key=None)Largest item
sum(iterable, /, start=0)start plus every item

Keeping a hashmap of values sorted automatically

Maintain sorted order on insertion (recommended)

Use SortedList (from the sortedcontainers library) with defaultdict so every new key gets its own auto-sorted list:

REFERENCE
Loading...
YOUR PRACTICE
Loading...
SortedList.pop()

Works like a normal list's pop(), but each variant runs in O(log N):

REFERENCE
Loading...
YOUR PRACTICE
Loading...
  • Use sl.pop(index) when you know the position.
  • Use sl.remove(value) or sl.discard(value) when you know the value instead of its index.
More SortedList operations

sortedcontainers is available on LeetCode. It is the standard substitute for a balanced BST or C++ multiset.

REFERENCE
Loading...
YOUR PRACTICE
Loading...

Full signatures

SignatureWhat it does
sortedcontainers.SortedList(iterable=None, key=None)A list that keeps its items sorted; with key, it keeps them sorted by key(item)
SortedList.add(value)Inserts value in sorted position, ~O(log n)
SortedList.pop(index=-1)Removes and returns the item at index (the largest by default), ~O(log n)
SortedList.remove(value)Removes one copy of value; ValueError if missing
SortedList.discard(value)Removes one copy of value; no error if missing
SortedList.bisect_left(value)Index where value would be inserted before equal items
SortedList.bisect_right(value)Index where value would be inserted after equal items
SortedList.index(value, start=None, stop=None)Index of the first copy of value; ValueError if missing
SortedList.count(value)How many copies of value the list holds
collections.defaultdict(default_factory=None, /[, ...])A dict that calls default_factory() to create the value of a missing key

Custom sort rules (no comparator classes in Python)

Python doesn't take a two-argument comparator class the way some other languages do. Instead:

1. The key parameter (most common & Pythonic)

A single-argument function that transforms each item into something Python already knows how to sort.

REFERENCE
Loading...
YOUR PRACTICE
Loading...

SortedList from sortedcontainers accepts the same key: SortedList(data, key=lambda x: len(x)).

Python's sort is stable: items that compare equal keep their original order. For "descending by one field, ascending by another", negate the numeric field: key=lambda p: (-p.score, p.name).

2. functools.cmp_to_key (traditional two-argument comparator)

For logic too complex for a key transform, return -1/0/1 like a classic comparator:

REFERENCE
Loading...
YOUR PRACTICE
Loading...
Example: Largest Number (LC 179)

No single key works here: 9 must come before 34, but 3 must come after 34. Compare the two orders directly: a goes first when a + b is the bigger string.

REFERENCE
Loading...
YOUR PRACTICE
Loading...
3. Magic methods (__lt__ + @total_ordering)

For custom objects that need a default sort order everywhere:

REFERENCE
Loading...
YOUR PRACTICE
Loading...

Full signatures

SignatureWhat it does
list.sort(*, key=None, reverse=False)Sorts the list in place, stable, returns None
sorted(iterable, /, *, key=None, reverse=False)Returns a new sorted list, stable
functools.cmp_to_key(func)Turns a two-argument comparator (negative, zero or positive result) into a key function
@functools.total_orderingClass decorator: from __eq__ and one of __lt__, __le__, __gt__, __ge__, fills in the other comparisons
str.join(iterable)Joins strings with the string it is called on between them
sortedcontainers.SortedList(iterable=None, key=None)A list that keeps its items sorted by key(item)

key=: one-argument functions across the standard library

The key idea from sorting works in many other functions: pass a one-argument function, and Python compares key(item) instead of item. The functions still return the original items, not the keys.

FunctionWhat key changesNotes
sorted(iterable, /, *, key=None, reverse=False), list.sort(*, key=None, reverse=False)Order by key(item)Stable; add reverse=True for descending
min(iterable, *, key=None), max(iterable, *, key=None)Picks the item with the smallest / largest keyReturns the item; min(iterable, *, default, key=None) returns default for empty input
heapq.nsmallest(n, iterable, key=None), heapq.nlargest(n, iterable, key=None)Top n by keyO(len(iterable) log n)
heapq.merge(*iterables, key=None, reverse=False)Merges inputs that are each sorted by the same keyLazy
bisect.bisect_left(a, x, lo=0, hi=len(a), *, key=None), bisect.bisect_right(a, x, lo=0, hi=len(a), *, key=None) 3.10+Compares key(a[i]) with xx is not passed through key
bisect.insort(a, x, lo=0, hi=len(a), *, key=None) 3.10+Inserts x in key orderx is passed through key
itertools.groupby(iterable, key=None)Groups consecutive items with an equal keySort first to group all equal keys together
sortedcontainers.SortedList(iterable=None, key=None)Keeps items ordered by keySearch by key with bisect_key_left(key) / bisect_key_right(key)

The heapq.nsmallest line below is K Closest Points to Origin (LC 973) in one call.

heapq.heappush, heappop and heapify take no key: push (key, tie_breaker, item) tuples instead (see heapq above).

REFERENCE
Loading...
YOUR PRACTICE
Loading...

Full signatures

SignatureWhat it does
dict.get(key, default=None)As a key function, counts.get maps each item to its count
operator.itemgetter(*items)As a key function, fetches one or more items/keys by index; faster than an equivalent lambda
operator.attrgetter(*attrs)As a key function, fetches one or more attributes by name (dotted names reach nested attributes)
SortedList.add(value)Inserts value in key order
SortedKeyList.bisect_key_left(key)First index whose item's key is >= key
SortedKeyList.bisect_key_right(key)First index whose item's key is > key
heapq.heapify(x), heapq.heappush(heap, item), heapq.heappop(heap)No key parameter: push (key, tie_breaker, item) tuples

Iteration idioms

enumerate: index + value together
REFERENCE
Loading...
YOUR PRACTICE
Loading...
Example: Two Sum (LC 1)
REFERENCE
Loading...
YOUR PRACTICE
Loading...
zip, range, any / all, grids
REFERENCE
Loading...
YOUR PRACTICE
Loading...

Full signatures

SignatureWhat it does
enumerate(iterable, start=0)Yields (index, item) pairs; the first index is start
zip(*iterables, strict=False)Yields tuples with one item from each input; stops at the shortest input (strict=True raises ValueError on unequal lengths, 3.10+)
range(stop), range(start, stop[, step])A lazy sequence of integers from start up to but not including stop
reversed(seq)An iterator over seq from the last item to the first
itertools.pairwise(iterable)Yields each pair of adjacent items (3.10+)
any(iterable)True if at least one item is truthy; stops at the first one
all(iterable)True if every item is truthy; stops at the first falsy one
iter(object, sentinel=None)An iterator over object; without sentinel, repeated calls to next() (or membership checks like in) advance it one item at a time and never restart

Generators vs. list comprehensions

A generator (yield) produces values lazily, one at a time; wrapping it in list(...) forces it to run fully and collect every value.

REFERENCE
Loading...
YOUR PRACTICE
Loading...

A list comprehension does the same thing directly, without needing yield, when you don't need laziness:

REFERENCE
Loading...
YOUR PRACTICE
Loading...

Full signatures

SignatureWhat it does
list([iterable])A new list holding every item of iterable; runs a generator to the end
range(stop), range(start, stop[, step])A lazy sequence of integers from start up to but not including stop
next(iterator[, default])Takes one value from a generator; default (or StopIteration) when it is exhausted

itertools: loops you don't have to write

REFERENCE
Loading...
YOUR PRACTICE
Loading...

With prefix sums p = list(accumulate(nums, initial=0)), the sum of nums[i..j] inclusive is p[j + 1] - p[i].

Full signatures

SignatureWhat it does
itertools.accumulate(iterable[, function, *, initial=None])Running totals (or running results of function); initial is emitted first
itertools.permutations(iterable, r=None)Every ordered arrangement of r items (all items when r is None)
itertools.combinations(iterable, r)Every unordered choice of r items, in input order
itertools.product(*iterables, repeat=1)Cartesian product, like nested for-loops
itertools.groupby(iterable, key=None)Yields (key, group) for each run of consecutive items with an equal key
itertools.chain.from_iterable(iterable)Flattens one level: yields the items of each inner iterable in turn

functools: memoization and folding

REFERENCE
Loading...
YOUR PRACTICE
Loading...
  • Arguments to a cached function must be hashable: pass tuples, not lists.
  • Inside a LeetCode class Solution, define the cached helper inside the method so the cache is fresh for every test case.
Deep recursion: three ways past the limit

A recursive DFS or backtracking whose depth can pass ~1000 (for example, a skewed tree with 10^4 nodes) hits Python's default limit of 1000 frames and raises RecursionError. In order of preference:

1. Go iterative with an explicit stack (the usual interview answer). The stack is an ordinary list, so there is no recursion limit at all.

Recursive (hits the limit):

REFERENCE
Loading...
YOUR PRACTICE
Loading...

Iterative (bypasses the limit):

REFERENCE
Loading...
YOUR PRACTICE
Loading...

Pushing the neighbors in reversed order makes the stack pop them first-to-last, so both versions process nodes in the same preorder. Both, as written, assume a tree: on a graph with cycles, add a visited set (mark a node when you push it, or skip already-visited nodes when you pop), or the loop never ends and nodes are processed more than once.

2. Raise the limit at the top of your file:

REFERENCE
Loading...
YOUR PRACTICE
Loading...

Caveat: sys.setrecursionlimit only raises Python's frame counter; it does not give the thread a bigger C stack. On an older Python that can crash the interpreter (a segfault, with no RecursionError to catch): in our test on Python 3.9, the code above crashed at a depth of 22,000 even with the limit at 200,000. Python 3.12 ran the same code to a depth of 199,990, but an @lru_cache recursion 10,000 deep still raised RecursionError there with the limit at 100,000.

3. Run the solver in a thread with a bigger stack, when the recursive form has to stay. The stack size has to be set before the thread starts:

REFERENCE
Loading...
YOUR PRACTICE
Loading...

On Python 3.9 this ran to a depth of 199,990, where the plain version crashed. It doesn't work in the in-browser Try-it: the browser's Python (Pyodide) can't start threads.

Full signatures

SignatureWhat it does
@functools.cache(user_function)Unbounded memoization (3.9+); same as lru_cache(maxsize=None)
@functools.lru_cache(maxsize=128, typed=False)Memoization that keeps the maxsize most recent results; maxsize=None keeps all of them
functools.reduce(function, iterable[, initial])Folds the items left to right: function(function(a, b), c), …
operator.mul(a, b), operator.xor(a, b)Function forms of a * b and a ^ b
reversed(seq)An iterator over seq from the last item to the first
sys.setrecursionlimit(limit)Sets the maximum depth of the Python call stack
threading.stack_size([size])Sets the stack size in bytes for threads created after the call; returns the old size
threading.Thread(group=None, target=None, name=None, args=(), kwargs={}, *, daemon=None)A thread that runs target(*args, **kwargs)
Thread.start()Starts the thread
Thread.join(timeout=None)Waits until the thread finishes

Numbers, math, and bits

REFERENCE
Loading...
YOUR PRACTICE
Loading...

Bit tricks:

REFERENCE
Loading...
YOUR PRACTICE
Loading...

^= as a running parity toggle: XOR (^) outputs 1 when its two inputs differ and 0 when they're the same: 00=0, 11=0, 01=1, 10=1. A number's parity is 0 when it's even and 1 when it's odd, and parity adds the same way XOR does: even+even=even, odd+odd=even, even+odd=odd. That's why running_parity ^= value % 2 works as a toggle: it flips running_parity exactly when value is odd, and leaves it unchanged when value is even -- one ^= per step tracks a running sum's parity without ever recomputing sum(...) % 2 from scratch.

REFERENCE
Loading...
YOUR PRACTICE
Loading...

Full signatures

SignatureWhat it does
float(x=0.0)float("inf") and float("-inf") build infinities
min(arg1, arg2, *args, key=None)Smallest argument
divmod(a, b)(a // b, a % b)
pow(base, exp, mod=None)base ** exp; with mod, computed fast as base ** exp % mod
math.gcd(*integers)Greatest common divisor
math.lcm(*integers)Least common multiple (3.9+)
math.isqrt(n)Largest integer r with r * r <= n
math.comb(n, k)Number of ways to choose k of n items, order ignored
math.perm(n, k=None)Number of ordered arrangements of k of n items
abs(x)Absolute value
bin(x)Binary string with a 0b prefix
str.count(sub[, start[, end]])Number of non-overlapping copies of sub
int.bit_count()Number of 1 bits in the absolute value (3.10+)

**: power and keyword unpacking

** means two different things, depending on where it sits.

1. Between two numbers, it is the power operator: a ** b is a raised to the power b.

REFERENCE
Loading...
YOUR PRACTICE
Loading...
  • ^ is XOR, not power: 2 ^ 3 is 1.
  • ** binds tighter than a leading minus and groups right to left: -2 ** 2 is -4, and 2 3 2 is 2 ** 9, i.e. 512.
  • x ** 0.5 goes through a float, so it can be off by one for very large ints. For an exact integer square root, use math.isqrt(x) (see Numbers, math, and bits above).

2. In a function call or definition, it unpacks keyword arguments.

Unpacking a dict into a function call:

REFERENCE
Loading...
YOUR PRACTICE
Loading...

Capturing any number of keyword arguments inside a function:

REFERENCE
Loading...
YOUR PRACTICE
Loading...
  • The dict's keys must match the function's parameter names; an unknown key raises TypeError.
  • The same ** merges dicts: {a, b} (see dict above).

Full signatures

SignatureWhat it does
pow(base, exp, mod=None)Function form of base ** exp
int(x=0), int(x, base=10)Converts to an integer, truncating a float toward zero
math.isqrt(n)Exact integer square root

Strings

REFERENCE
Loading...
YOUR PRACTICE
Loading...

Full signatures

SignatureWhat it does
str.lower(), str.upper()Lowercase / uppercase copy
str.split(sep=None, maxsplit=-1)List of parts; with no sep, splits on runs of whitespace
str.strip([chars])Copy without leading and trailing whitespace (or the characters in chars)
str.startswith(prefix[, start[, end]])True if the string starts with prefix (which can be a tuple of prefixes)
str.find(sub[, start[, end]])Lowest index of sub, or -1 if missing
str.index(sub[, start[, end]])Like find, but raises ValueError if missing
str.isalnum(), str.isdigit(), str.isalpha()True if the string is non-empty and every character is a letter or digit / a digit / a letter
ord(c)Integer code of a one-character string
chr(i)One-character string for the integer code i
str.join(iterable)Joins strings with the string it is called on between them
list.append(x)Adds x at the end, O(1)

String prefix: Longest Common Prefix

Longest Common Prefix (LC 14). You don't need sorting or a Trie for this. Horizontal scanning is the simplest:

REFERENCE
Loading...
YOUR PRACTICE
Loading...

How the options compare (M strings, S characters in total):

  • Horizontal scanning (above): no sorting and no custom objects, just startswith and slicing. Every shrink re-checks the prefix from its start, so the worst case is O(S + L²) with L = len(strs[0]) (one very long first string and a short second one). Start from the shortest string, prefix = min(strs, key=len), to cap L at the shortest length.
  • Length-sorting (O(M log M + M·N_min)): relies on sort()/enumerate, fast C-level built-ins, but pays a sorting step.
  • Trie approach (O(S) to build): clean on paper, but it creates an object and a dict lookup per character, which adds real Python overhead.

Full signatures

SignatureWhat it does
str.startswith(prefix[, start[, end]])True if the string starts with prefix
min(iterable, *, key=None)Smallest item; with key=len, the shortest string

Group Anagrams: two approaches

Group Anagrams (LC 49) can be keyed two ways.

Approach 1: sort the word as the key (simpler, O(M · N log N))

All anagrams become the identical string once sorted, so use that as the dict key:

REFERENCE
Loading...
YOUR PRACTICE
Loading...

sorted(word) returns a list of characters, so join it back into a string ("aet") to use it as the key.

Approach 2: character-count tuple as the key (faster, O(M · N))

Count each letter into a fixed-size array (26 letters), then use the tuple as the key. This avoids the N log N sort cost per word:

REFERENCE
Loading...
YOUR PRACTICE
Loading...

Full signatures

SignatureWhat it does
collections.defaultdict(default_factory=None, /[, ...])A dict that calls default_factory() to create the value of a missing key
sorted(iterable, /, *, key=None, reverse=False)Returns a new sorted list; for a string, a list of characters
str.join(iterable)Joins strings with the string it is called on between them
ord(c)Integer code of a one-character string
tuple([iterable])An immutable, hashable copy of iterable
dict.values()A view of the values