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.
| Structure | Fast (O(1) or O(log n)) | Slow (O(n)): watch out |
|---|---|---|
list | a[i], a.append(x), a.pop() | x in a, a.pop(0), a.insert(0, x), a.remove(x), a.index(x) |
dict / set | x in d, d[k], d[k] = v, del d[k], s.add(x) (O(1) average) | iterating all keys |
deque | append, appendleft, pop, popleft | dq[i] in the middle |
heapq | heap[0] O(1), heappush / heappop O(log n) | searching for an arbitrary item |
bisect | bisect_left / bisect_right O(log n) | insort is O(n): the search is fast, the list shift is not |
SortedList | add, remove, pop, bisect_left, sl[i] (all ~O(log n)) | nothing common |
str | s[i], len(s) | s += c in a loop (can be O(n²)), s.find(t), slicing |
list: arrays, stacks, and 2D grids
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.
| Operation | Changes the original list? | Cost |
|---|---|---|
a.extend(b) | Yes, in place | O(k), where k = len(b) |
c = a + b | No, builds a new list | O(n + k): copies every element of a and b |
a += b | Yes, 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
| Signature | What 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
- Dicts keep insertion order (guaranteed since 3.7).
- Keys must be hashable:
str,int,tuple,frozensetwork,list/set/dictdon't. That's why anagram keys aretuple(count)or"".join(sorted(word)). d1 == d2is True when both have the same key-value pairs; insertion order doesn't matter, and the check isO(n).- Trap: adding or removing keys while iterating over the dict raises
RuntimeError. Iterate overlist(d)if you need to delete as you go.
Full signatures
| Signature | What 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.
Trap: just reading d[missing_key] inserts it. Use key in d to test membership without creating the key.
Full signatures
| Signature | What 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.
More that comes up constantly:
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.
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 dict | Returns 0 without inserting "z" |
len(counts) after only reads, no writes | Grows with every missing key you merely looked up | Stays accurate — unread keys are never added |
| Extra methods | None — 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
| Signature | What 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
Full signatures
| Signature | What 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.
Dictionaries. keys() and items() views behave like sets, so & finds common keys or common key-value pairs.
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
| Signature | What 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.
BFS template (shortest path in steps on a grid):
Full signatures
| Signature | What 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.
| Method | What it does | Complexity |
|---|---|---|
heapq.heapify(x) | Turns the list x into a heap, in place | O(n) |
heapq.heappush(heap, item) | Pushes item onto the heap | O(log n) |
heapq.heappop(heap) | Pops and returns the smallest item | O(log n) |
heapq.heappushpop(heap, item) | Pushes then pops, faster than separate calls | O(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 elements | O(len(iterable) log n) |
heapq.nsmallest(n, iterable, key=None) | Returns the n smallest elements | O(len(iterable) log n) |
Example: Top K Frequent Elements (LC 347)
Heap patterns
Keep a sorted top-k list without a heap
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).
Tuples compare element by element, starting at index 0. With (len(s), s) the comparison takes two steps:
- 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. - 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.
| Pair | Keys pushed | Which key is larger | Numeric truth |
|---|---|---|---|
"10" vs "7" | (2, "10") vs (1, "7") | (2, "10"), decided by the lengths | 10 > 7 |
"21" vs "12" | (2, "21") vs (2, "12") | (2, "21"), decided by the strings | 21 > 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
| Signature | What 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.
| You want… | Use |
|---|---|
First index with a[i] >= x | bisect_left(a, x) |
First index with a[i] > x | bisect_right(a, x) |
Last index with a[i] <= x | bisect_right(a, x) - 1 |
Last index with a[i] < x | bisect_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.
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:
| You want… | Use |
|---|---|
First candidate in range(lo, hi) where cond is True | bisect_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 True | The result equals len(range): check it before using it |
- The condition must be monotonic: all
False, then allTrue. If it can flip back, the answer is quietly wrong. bisectnever passesxthroughkey: search forTrue(or a key value), not for a candidate.
key= on sorted records and descending lists
Full signatures
| Signature | What 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:
SortedList.pop()
Works like a normal list's pop(), but each variant runs in O(log N):
- Use
sl.pop(index)when you know the position. - Use
sl.remove(value)orsl.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.
Full signatures
| Signature | What 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.
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:
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.
3. Magic methods (__lt__ + @total_ordering)
For custom objects that need a default sort order everywhere:
Full signatures
| Signature | What 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_ordering | Class 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.
| Function | What key changes | Notes |
|---|---|---|
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 key | Returns 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 key | O(len(iterable) log n) |
heapq.merge(*iterables, key=None, reverse=False) | Merges inputs that are each sorted by the same key | Lazy |
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 x | x is not passed through key |
bisect.insort(a, x, lo=0, hi=len(a), *, key=None) 3.10+ | Inserts x in key order | x is passed through key |
itertools.groupby(iterable, key=None) | Groups consecutive items with an equal key | Sort first to group all equal keys together |
sortedcontainers.SortedList(iterable=None, key=None) | Keeps items ordered by key | Search 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).
Full signatures
| Signature | What 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
Example: Two Sum (LC 1)
zip, range, any / all, grids
Full signatures
| Signature | What 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.
A list comprehension does the same thing directly, without needing yield, when you don't need laziness:
Full signatures
| Signature | What 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
With prefix sums p = list(accumulate(nums, initial=0)), the sum of nums[i..j] inclusive is p[j + 1] - p[i].
Full signatures
| Signature | What 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
- 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):
Iterative (bypasses the limit):
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:
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:
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
| Signature | What 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
Bit tricks:
^= 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.
Full signatures
| Signature | What 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.
^is XOR, not power:2 ^ 3is1.**binds tighter than a leading minus and groups right to left:-2 ** 2is-4, and2 3 2is2 ** 9, i.e.512.x ** 0.5goes through a float, so it can be off by one for very large ints. For an exact integer square root, usemath.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:
Capturing any number of keyword arguments inside a function:
- The dict's keys must match the function's parameter names; an unknown key raises
TypeError. - The same
**merges dicts:{a, b}(seedictabove).
Full signatures
| Signature | What 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
Full signatures
| Signature | What 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:
How the options compare (M strings, S characters in total):
- Horizontal scanning (above): no sorting and no custom objects, just
startswithand slicing. Every shrink re-checks the prefix from its start, so the worst case isO(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 onsort()/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
| Signature | What 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:
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:
Full signatures
| Signature | What 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 |