Python’s integers have arbitrary precision — they never overflow. The math module provides float functions; int arithmetic uses built-in operators. float is 64-bit IEEE 754 (same as C double). Division with / always returns float; use // for integer (floor) division.
long.
2 ** 1000 works perfectly — produces a 302-digit number.Integer.MAX_VALUE equivalent needed. Use float('inf') for infinity sentinel.0.1 + 0.2 != 0.3 due to IEEE 754 representation.
// floors toward −∞ — -7 // 2 == -4 (not −3). This differs from C/Java truncation.
% follows floor division — -7 % 3 == 2 (not −1 like C/Java). Result always has the sign of the divisor.
** is exponentiation — not ^ (which is XOR). pow(base, exp, mod) provides built-in modular exponentiation in O(log exp).
math.gcd added in 3.5; math.lcm added in 3.9. Both accept multiple arguments from 3.9+.
| Category | Operation | Time |
|---|---|---|
| Arithmetic | ||
| Max | max(a, b) | O(1)2 args; O(n) for iterable |
| Min | min(a, b) | O(1)2 args; O(n) for iterable |
| Absolute | abs(x) | O(1)works on any numeric |
| Power & Root | ||
| Power | x ** n / pow(x, n) | O(1)float; O(log n) for int |
| Mod power | pow(base, exp, mod) | O(log exp)built-in 3-arg |
| Square root | math.sqrt(x) | O(1)returns float |
| Int sqrt | math.isqrt(x) | O(1)exact int, 3.8+; bounded-width values |
| Rounding | ||
| Floor | math.floor(x) | O(1)toward −∞ |
| Ceiling | math.ceil(x) | O(1)toward +∞ |
| Round | round(x) | O(1)banker’s rounding |
| Truncate | math.trunc(x) / int(x) | O(1)toward zero |
| Logarithms | ||
| Natural log | math.log(x) | O(1)base e |
| Log base 2 | math.log2(x) | O(1) |
| Log base 10 | math.log10(x) | O(1) |
| Number Theory | ||
| GCD | math.gcd(a, b) | O(log min)3.5+ |
| LCM | math.lcm(a, b) | O(log min)3.9+ |
| Special Values | ||
| Infinity | float('inf') | O(1) |
| Neg infinity | float('-inf') | O(1) |
| Is NaN | math.isnan(x) | O(1) |
| Random | ||
| Random int | random.randint(a, b) | O(1)[a, b] inclusive |
| Random float | random.random() | O(1)[0.0, 1.0) |
# — Arithmetic —
max(3, 7) # 7
min(3, 7) # 3
abs(-5) # 5 (no overflow trap unlike Java)
# — Power & Root —
2 ** 10 # 1024 (exact int)
pow(2, 10) # 1024 (same)
pow(2, 10, 1000) # 24 (modular: 1024 % 1000)
math.sqrt(49) # 7.0 (float)
math.isqrt(49) # 7 (exact int, 3.8+)
int(math.sqrt(49)) # 7
# Perfect square check (use isqrt for precision)
sq = math.isqrt(n)
is_perfect = sq * sq == n
# — Rounding —
math.floor(2.7) # 2
math.ceil(2.1) # 3
round(2.5) # 2 ← banker's rounding (rounds to even!)
round(3.5) # 4
int(2.7) # 2 (truncates toward zero)
int(-2.7) # -2
# — Logarithms —
math.log(math.e) # 1.0 (natural log)
math.log2(8) # 3.0
math.log10(1000) # 3.0
# Number of digits in n (n must be > 0; log10(0) raises ValueError!)
digits = len(str(n)) # simplest, works for any n
digits = math.floor(math.log10(n)) + 1 # math-based, n > 0 only
# — Integer division & modulo —
7 // 2 # 3 (floor division)
-7 // 2 # -4 (floors toward −∞, NOT truncate!)
-7 % 3 # 2 (sign of divisor, unlike C/Java)
# — Ceiling division (positive a, b) —
ceil_div = (a + b - 1) // b # or -(-a // b)
# — Safe infinity for DP / shortest path —
INF = float('inf') # safe for comparisons, no overflow
INF + 1 # still inf
INF > 10**18 # True
# GCD — built-in (3.5+)
math.gcd(12, 8) # 4
math.gcd(12, 8, 6) # 2 (multi-arg, 3.9+)
# LCM — built-in (3.9+)
math.lcm(4, 6) # 12
math.lcm(4, 6, 10) # 60 (multi-arg)
# LCM for < 3.9
def lcm(a, b): return a // math.gcd(a, b) * b
# isPrime — trial division O(√n)
def is_prime(n):
if n < 2: return False
if n < 4: return True
if n % 2 == 0 or n % 3 == 0: return False
i = 5
while i * i <= n:
if n % i == 0 or n % (i + 2) == 0: return False
i += 6
return True
# Modular exponentiation O(log exp) — built-in!
MOD = 1_000_000_007
pow(base, exp, MOD) # built-in 3-arg pow
# Safe modular add
(a % MOD + b % MOD) % MOD
# Safe modular multiply (no overflow in Python!)
a * b % MOD
# 1D DP with infinity
dp = [float('inf')] * n
dp[0] = 0 # base case
# 2D DP with infinity
dp = [[float('inf')] * cols for _ in range(rows)]
# Memoization (sentinel = -1 for uncomputed)
memo = [-1] * n
# Boolean DP
dp = [False] * (n + 1)
dp[0] = True # base: empty subset
# Space-optimized (two variables) — House Robber pattern
prev2, prev1 = 0, 0
for num in nums:
curr = max(prev1, prev2 + num)
prev2 = prev1
prev1 = curr
# @lru_cache for recursive memoization
from functools import lru_cache
@lru_cache(maxsize=None)
def solve(i, j):
if i >= n: return 0
return max(solve(i + 1, j), solve(i + 1, j - 1) + val[i])
# ❌ // floors toward -∞, NOT toward zero like C/Java
-7 // 2 # -4 (not -3!)
-1 // 2 # -1 (not 0!)
# ✅ Truncation toward zero (C/Java behavior)
int(-7 / 2) # -3
math.trunc(-7 / 2) # -3
# ❌ % follows floor division — sign of divisor
-7 % 3 # 2 (not -1 like C/Java!)
# This actually helps: Python modulo is always non-negative
# when divisor is positive — no ((a % n) + n) % n needed
# ❌ round() rounds to nearest EVEN for .5 (banker's rounding)
round(0.5) # 0 (not 1!)
round(1.5) # 2 (rounds to nearest even)
round(2.5) # 2 (not 3!)
# ✅ For always-round-up: math.floor(x + 0.5)
# ✅ For ceiling: math.ceil(x)
# ❌ Float comparison fails due to IEEE 754
0.1 + 0.2 == 0.3 # False!
0.1 + 0.2 # 0.30000000000000004
# ✅ Use math.isclose() or epsilon
math.isclose(0.1 + 0.2, 0.3) # True
abs(a - b) < 1e-9 # manual epsilon
# float('inf') is safe for comparisons but has edge cases
float('inf') - float('inf') # nan (not 0!)
float('inf') * 0 # nan
float('inf') == float('inf') # True
float('nan') == float('nan') # False! (NaN != NaN)
# ✅ For DP/shortest path, float('inf') works cleanly:
if dist[u] + w < dist[v]: # no overflow risk
dist[v] = dist[u] + w
Python is dynamically typed — conversions are explicit function calls. int(), float(), str() are the core converters. No silent truncation — invalid conversions raise ValueError. Python has no separate char type; a character is just a string of length 1.
| Category | Operation | Time |
|---|---|---|
| int ↔ str | ||
| Int to str | str(n) | O(d)d = digits |
| Str to int | int(s) | O(d)raises ValueError |
| Str to float | float(s) | O(d) |
| Radix conversions | ||
| Int to binary | bin(n) | O(b)"0b1010" |
| Int to hex | hex(n) | O(b)"0xff" |
| Int to octal | oct(n) | O(b)"0o10" |
| Parse with radix | int(s, base) | O(d) |
| char ↔ int | ||
| Char to int | ord(c) | O(1)Unicode code point |
| Int to char | chr(n) | O(1) |
| Collection conversions | ||
| Str to list | list(s) | O(n)list of chars |
| List to str | "".join(lst) | O(n) |
| Tuple to list | list(t) | O(n) |
| List to tuple | tuple(lst) | O(n) |
# — int ↔ str —
str(42) # "42"
int("42") # 42
float("3.14") # 3.14
f"{42:05d}" # "00042" (formatted)
# — char ↔ int (alphabet index) —
ord('c') - ord('a') # 2
ord('7') - ord('0') # 7
chr(ord('a') + 2) # 'c'
ord('A') # 65
# — Radix conversions —
bin(10) # "0b1010"
hex(255) # "0xff"
oct(8) # "0o10"
int("1010", 2) # 10
int("ff", 16) # 255
int("10", 8) # 8
f"{10:b}" # "1010" (no prefix)
f"{255:x}" # "ff" (no prefix)
# — str ↔ list of chars —
list("hello") # ['h', 'e', 'l', 'l', 'o']
"".join(['h', 'i']) # "hi"
# — Boolean —
bool(0) # False
bool("") # False
bool([]) # False
bool(None) # False
bool(1) # True (any non-zero)
bool("hi") # True (any non-empty)
# int() on float truncates toward zero (like C), not floor
int(2.9) # 2 (not 3)
int(-2.9) # -2 (not -3)
int(-0.5) # 0
# ✅ Use math.floor / math.ceil for directional rounding
math.floor(-2.9) # -3
math.ceil(-2.9) # -2
# ⚠ int("3.14") raises ValueError — must use float() first
int(float("3.14")) # 3
Python has no char type — a character is a str of length 1. Use ord() and chr() for Unicode conversions. String methods like isdigit(), isalpha() work on single characters too.
| Category | Operation | Time |
|---|---|---|
| Classification | ||
| Is digit | c.isdigit() | O(1) |
| Is letter | c.isalpha() | O(1) |
| Is alphanumeric | c.isalnum() | O(1) |
| Is space | c.isspace() | O(1) |
| Is upper | c.isupper() | O(1) |
| Is lower | c.islower() | O(1) |
| Conversion | ||
| To upper | c.upper() | O(1) |
| To lower | c.lower() | O(1) |
| Char to int | ord(c) | O(1) |
| Int to char | chr(n) | O(1) |
'7'.isdigit() # True
'A'.isalpha() # True
'3'.isalnum() # True
' '.isspace() # True
'A'.isupper() # True
'a'.islower() # True
# Inline ASCII checks (tight loops):
'a' <= c <= 'z' # lowercase
'A' <= c <= 'Z' # uppercase
'0' <= c <= '9' # digit
c.upper() # 'a' → 'A'
c.lower() # 'A' → 'a'
# Bit-level case tricks (ASCII letters only):
chr(ord(c) | 32) # force lowercase
chr(ord(c) & 95) # force uppercase
chr(ord(c) ^ 32) # toggle case
# Vowel check
c.lower() in 'aeiou' # pythonic
c.lower() in {'a', 'e', 'i', 'o', 'u'} # O(1) set lookup
Python integers have arbitrary width — no 32/64-bit boundary. & AND, | OR, ^ XOR, ~ NOT, << left shift, >> right shift. ^ is XOR (not power — use **). ~n returns -(n+1) due to arbitrary-width two’s complement. bin() gives the binary string.
~0 is -1 (not 0xFFFFFFFF). To get 32-bit behavior, mask with & 0xFFFFFFFF.
^ is XOR, not power — 2 ^ 3 == 1 (XOR), not 8. Use 2 ** 3 for exponentiation.
~n == -(n+1) — arbitrary-precision NOT. ~5 is -6, not some large positive number.
>> is always arithmetic — no unsigned right shift (>>>). For unsigned behavior, mask first: (n & 0xFFFFFFFF) >> k.
int.bit_count() — popcount built-in (3.10+). Before 3.10, use bin(n).count('1').
int.bit_length() — number of bits needed to represent value (excluding sign).
| Category | Operation | Time |
|---|---|---|
| Single-bit | ||
| Check bit i | (n >> i) & 1 | O(1) |
| Set bit i | n | (1 << i) | O(1) |
| Clear bit i | n & ~(1 << i) | O(1) |
| Toggle bit i | n ^ (1 << i) | O(1) |
| Intrinsics | ||
| Pop count | n.bit_count() | O(1)3.10+ |
| Pop count | bin(n).count('1') | O(b)any version |
| Bit length | n.bit_length() | O(1) |
| Common tricks | ||
| Lowest set bit | n & (-n) | O(1) |
| Clear lowest set | n & (n - 1) | O(1)Kernighan’s |
| Power of 2 | n > 0 and (n & (n - 1)) == 0 | O(1) |
| Bitmask n bits | (1 << n) - 1 | O(1) |
# — Single-bit operations —
(n >> i) & 1 # check bit i
n | (1 << i) # set bit i
n & ~(1 << i) # clear bit i
n ^ (1 << i) # toggle bit i
# — Lowest set bit, Kernighan's —
n & (-n) # isolate rightmost 1: 12(1100) → 4(0100)
n & (n - 1) # clear lowest set bit
# Count set bits
bin(n).count('1') # any version
n.bit_count() # 3.10+
# — Shifts, even/odd —
n << k # n × 2^k
n >> k # n ÷ 2^k (arithmetic, floors)
n & 1 == 0 # even
n & 1 == 1 # odd
# — XOR rules —
a ^ 0 == a # unchanged
a ^ a == 0 # self-cancels ← key for Single Number
a ^ b ^ a == b # associative + self-cancel
# — Swap without temp —
a, b = b, a # Pythonic swap (no XOR trick needed)
# — Bitmask & subset enumeration —
(1 << n) - 1 # n=4 → 0b1111 = 15
sub = mask
while sub > 0:
# process sub
sub = (sub - 1) & mask
# don't forget empty subset (sub = 0)
# — 32-bit mask (for LeetCode problems expecting 32-bit) —
n & 0xFFFFFFFF # simulate unsigned 32-bit
# ❌ Common mistake: ^ is XOR in Python, not exponentiation
2 ^ 3 # 1 (XOR: 10 ^ 11 = 01)
# ✅ Use ** for power
2 ** 3 # 8
# Python ints have infinite precision — ~0 is -1, not 0xFFFFFFFF
~0 # -1
~5 # -6 (-(5+1))
bin(-5) # '-0b101' (shows negative sign)
# For 32-bit problems (e.g. LeetCode Reverse Bits):
# ✅ Mask with 0xFFFFFFFF to simulate 32-bit unsigned
n = -1
n & 0xFFFFFFFF # 4294967295 (0xFFFFFFFF)
Python lists are dynamic arrays — contiguous, resizable, heterogeneous. append is amortized O(1); insert/pop(0) are O(n). For O(1) deque operations, use collections.deque. Lists are mutable and passed by reference.
# Empty list
arr = []
arr = list()
# Pre-filled
arr = [0] * n # [0, 0, ..., 0]
arr = [1, 2, 3]
# List comprehension
arr = [i * 2 for i in range(5)] # [0, 2, 4, 6, 8]
arr = [0 for _ in range(n)] # same as [0] * n
# 2D list
grid = [[0] * cols for _ in range(rows)]
# ❌ 2D trap: shared reference
grid = [[0] * cols] * rows # all rows point to SAME list!
# From string / iterable
list("hello") # ['h', 'e', 'l', 'l', 'o']
list(range(5)) # [0, 1, 2, 3, 4]
len(lst) is O(1) — stored as an attribute.[]. Check with if not lst: or if len(lst) == 0:lst[-1] is the last element, lst[-2] second to last. No IndexError unless out of range.
lst[i:j] returns a new list (shallow copy). lst[:] copies the entire list.
[x] * n shares references for mutable objects — [[]] * 3 creates three references to the SAME inner list. Use comprehension instead.
== compares element-by-element (value comparison). < / > use lexicographic order.
| Category | Operation | Time |
|---|---|---|
| Access | ||
| Length | len(lst) | O(1) |
| Index | lst[i] | O(1) |
| Slice | lst[i:j] | O(k)k = j-i, creates copy |
| Modify | ||
| Append | lst.append(x) | O(1)*amortized |
| Extend | lst.extend(other) | O(m) |
| Insert at i | lst.insert(i, x) | O(n) |
| Pop end | lst.pop() | O(1) |
| Pop at i | lst.pop(i) | O(n) |
| Remove val | lst.remove(x) | O(n)first occurrence |
| Delete at i | del lst[i] | O(n) |
| Search | ||
| Contains | x in lst | O(n) |
| Index of | lst.index(x) | O(n)raises if missing |
| Count | lst.count(x) | O(n) |
| Binary | bisect.bisect_left(lst, x) | O(log n)must be sorted |
| Sort & Transform | ||
| Sort in-place | lst.sort() | O(n log n)TimSort, stable |
| Sorted copy | sorted(lst) | O(n log n) |
| Reverse | lst.reverse() | O(n)in-place |
| Reversed copy | lst[::-1] | O(n) |
| Copy | lst.copy() / lst[:] | O(n)shallow |
| Min | min(lst) | O(n) |
| Max | max(lst) | O(n) |
| Sum | sum(lst) | O(n) |
# — Sort —
arr.sort() # ascending, in-place
arr.sort(reverse=True) # descending
sorted(arr) # returns new sorted list
# Sort 2D by first element
intervals.sort(key=lambda x: x[0])
# Sort by multiple keys
intervals.sort(key=lambda x: (x[0], -x[1])) # asc by [0], desc by [1]
# Sort by string length
words.sort(key=len)
# — Copy —
cp = arr[:] # shallow copy
cp = arr.copy() # same
cp = list(arr) # same
# 2D deep copy
import copy
cp2d = copy.deepcopy(grid)
# or manually:
cp2d = [row[:] for row in grid]
# — Aggregate —
sum(arr) # total
min(arr) # minimum
max(arr) # maximum
# — Reverse in-place —
arr.reverse()
arr[::-1] # reversed copy
# — Shuffle —
import random
random.shuffle(arr) # in-place
# — Remove at index —
arr.pop(i) # O(n), returns removed element
del arr[i] # O(n), no return
# — Rotate left by k —
k %= len(arr)
arr = arr[k:] + arr[:k]
# — Remove duplicates (preserve order) —
arr = list(dict.fromkeys(arr))
# — Remove duplicates (sorted) —
arr = sorted(set(arr))
# — Flatten 2D —
flat = [x for row in grid for x in row]
# — Zip —
list(zip([1, 2], ['a', 'b'])) # [(1, 'a'), (2, 'b')]
list(zip(*matrix)) # transpose
# — Enumerate —
for i, val in enumerate(arr): # index + value
pass
# — Print —
print(arr) # [1, 2, 3]
# ❌ All rows point to the SAME inner list
grid = [[0] * 3] * 3
grid[0][0] = 1
print(grid) # [[1,0,0], [1,0,0], [1,0,0]] all changed!
# ✅ Use list comprehension — each row is independent
grid = [[0] * 3 for _ in range(3)]
grid[0][0] = 1
print(grid) # [[1,0,0], [0,0,0], [0,0,0]] correct
# ✅ Guard before any logic
if not nums: return ...
if len(nums) < 2: return nums[0]
# Kadane's: init with nums[0], loop from i=1 → needs guard
# Two pointers: right = len(nums)-1 → -1 if empty
# Fixed window: k > n → index out of range
Python strings are immutable sequences of Unicode characters. Every modification returns a new string. Use list(s) for mutation, then "".join(lst) to convert back. == compares by value (no .equals() trap). String concatenation in a loop is O(n²) — use "".join() or list building instead.
s[0] = 'X' raises TypeError. Convert to list, modify, join back.
== is value comparison — no .equals() needed like Java. "abc" == "abc" is True.
s[-1] is the last character. s[::-1] reverses the string.
s[100:200] on a 5-char string returns "". Only direct indexing (s[100]) raises.
"".split() vs "".split(",") — "".split() returns [] (empty list). "".split(",") returns [""] (list with one empty string).
f"{var}" is the preferred formatting method (3.6+). Faster than % and .format().
| Category | Operation | Time |
|---|---|---|
| Access | ||
| Length | len(s) | O(1) |
| Char at i | s[i] | O(1) |
| Substring | s[i:j] | O(k)k = j-i, copy |
| IsEmpty | not s / len(s) == 0 | O(1) |
| Search | ||
| Contains | sub in s | O(n) |
| Find | s.find(sub) | O(n·m)−1 if not found |
| Index | s.index(sub) | O(n·m)raises if not found |
| Count | s.count(sub) | O(n) |
| Starts with | s.startswith(pre) | O(k) |
| Ends with | s.endswith(suf) | O(k) |
| Transform | ||
| Lower | s.lower() | O(n) |
| Upper | s.upper() | O(n) |
| Strip | s.strip() | O(n) |
| Replace | s.replace(old, new) | O(n) |
| Split | s.split(sep) | O(n) |
| Join | sep.join(lst) | O(n) |
| Comparison | ||
| Equals | s1 == s2 | O(n)value comparison |
| Compare | s1 < s2 | O(n)lexicographic |
# — Access —
len(s) # length
s[i] # char at index
s[i:j] # substring [i, j)
s[-1] # last char
s[::-1] # reversed string
# — Search —
s.find("sub") # first index, -1 if not found
s.rfind("sub") # last index, -1 if not found
s.index("sub") # first index, raises ValueError
"sub" in s # boolean
s.startswith("pre") # starts with
s.endswith("suf") # ends with
s.count("a") # count occurrences
# — Transform —
s.lower() # lowercase
s.upper() # uppercase
s.strip() # trim whitespace both ends
s.lstrip() # trim left
s.rstrip() # trim right
s.replace("old", "new") # replace all
s.replace("old", "new", 1) # replace first only
s.split(",") # split by separator
s.split() # split by any whitespace
",".join(["a", "b", "c"]) # "a,b,c"
"ab" * 3 # "ababab"
# — Mutate via list —
chars = list(s)
chars[0] = 'X'
s = "".join(chars)
# — Reverse (via slicing) —
s[::-1] # "hello" → "olleh"
# — Anagram key —
key = "".join(sorted(s)) # sorted chars
key = tuple(sorted(s)) # tuple key for dict
# Frequency array key (no sort needed)
freq = [0] * 26
for c in s: freq[ord(c) - ord('a')] += 1
key = tuple(freq) # hashable key for dict
# — Regex —
import re
re.findall(r'\d+', "a1b2c3") # ['1', '2', '3']
re.match(r'^[a-z]+$', s) # match from start
re.sub(r'\s+', ' ', s) # replace multiple spaces
# ❌ O(n²) — each += creates a new string
result = ""
for c in chars:
result += c # quadratic!
# ✅ O(n) — build list, join at end
parts = []
for c in chars:
parts.append(c)
result = "".join(parts)
# ✅ Or use list comprehension
result = "".join(c for c in chars)
# ⚠ Completely different behavior!
"".split() # [] (empty list)
"".split(",") # [""] (list with one empty string!)
# "a,,b".split(",") returns ["a", "", "b"] — empties between delims
# "a b".split() returns ["a", "b"] — no empties (collapses whitespace)
# Empty string → palindrome (True)
# Single char → palindrome (True)
# Valid Palindrome: strip non-alphanumeric, lowercase
def is_palindrome(s):
clean = [c.lower() for c in s if c.isalnum()]
return clean == clean[::-1]
# Two-pointer (no extra space):
def is_palindrome(s):
l, r = 0, len(s) - 1
while l < r:
while l < r and not s[l].isalnum(): l += 1
while l < r and not s[r].isalnum(): r -= 1
if s[l].lower() != s[r].lower(): return False
l += 1; r -= 1
return True
Python’s sorted() and list.sort() use TimSort (stable, O(n log n)). Sort by key= function — no subtraction-based comparators. For custom comparison logic, use functools.cmp_to_key.
# Sort ascending
arr.sort() # in-place
sorted(arr) # new list
# Sort descending
arr.sort(reverse=True)
sorted(arr, reverse=True)
# Sort by key function
arr.sort(key=lambda x: x[0]) # by first element
arr.sort(key=abs) # by absolute value
arr.sort(key=len) # by length
# Multi-key sort: asc by start, desc by end
intervals.sort(key=lambda x: (x[0], -x[1]))
# Largest Number #179 — concat comparison
from functools import cmp_to_key
strs.sort(key=cmp_to_key(lambda a, b: (b+a > a+b) - (b+a < a+b)))
# Stable sort — TimSort is always stable
# Equal elements preserve their original order
# Check if sorted
all(arr[i] <= arr[i+1] for i in range(len(arr)-1))
# Sort dict by value
sorted(d.items(), key=lambda x: x[1])
# Sort with None handling
arr.sort(key=lambda x: (x is None, x)) # None goes last
# Next permutation (no built-in, implement manually)
# Or use itertools.permutations for all permutations
# ✅ key= for simple transformations (preferred)
arr.sort(key=lambda x: -x) # descending
arr.sort(key=lambda x: (x[0], -x[1])) # multi-key
# ✅ cmp_to_key when comparison is pairwise (e.g. concatenation)
from functools import cmp_to_key
def compare(a, b):
if a + b > b + a: return -1 # a should come first
elif a + b < b + a: return 1
return 0
strs.sort(key=cmp_to_key(compare))
# ⚠ cmp_to_key is slower — prefer key= when possible
Python’s bisect module provides binary search on sorted sequences. bisect_left returns the leftmost insertion point; bisect_right returns the rightmost. No built-in “found” boolean — check the value at the returned index.
| Category | Operation | Time |
|---|---|---|
| Binary Search (sorted input) | ||
| Lower bound | bisect.bisect_left(a, x) | O(log n)first ≥ x |
| Upper bound | bisect.bisect_right(a, x) | O(log n)first > x |
| Insert sorted | bisect.insort(a, x) | O(n)search O(log n) + shift O(n) |
| Linear Search | ||
| Contains | x in lst | O(n) |
| Find index | lst.index(x) | O(n)raises if missing |
| Min | min(lst) | O(n) |
| Max | max(lst) | O(n) |
import bisect
# Binary search (sorted list)
idx = bisect.bisect_left(sorted_arr, target)
found = idx < len(sorted_arr) and sorted_arr[idx] == target
# Lower / upper bound
lb = bisect.bisect_left(arr, target) # first index where arr[i] >= target
ub = bisect.bisect_right(arr, target) # first index where arr[i] > target
count = ub - lb # number of occurrences of target
# Find minimum in rotated sorted array
def find_min(nums):
lo, hi = 0, len(nums) - 1
while lo < hi:
mid = (lo + hi) // 2
if nums[mid] > nums[hi]:
lo = mid + 1
else:
hi = mid
return lo
# Search in rotated array
def search(nums, target):
lo, hi = 0, len(nums) - 1
while lo <= hi:
mid = (lo + hi) // 2
if nums[mid] == target: return mid
if nums[lo] <= nums[mid]:
if nums[lo] <= target < nums[mid]:
hi = mid - 1
else: lo = mid + 1
else:
if nums[mid] < target <= nums[hi]:
lo = mid + 1
else: hi = mid - 1
return -1
# ❌ No progress when lo=0, hi=1: mid=0, lo stays 0
while lo < hi:
mid = (lo + hi) // 2
if valid(mid): hi = mid
else: lo = mid # INFINITE LOOP!
# ✅ Always advance by at least 1
else: lo = mid + 1
# Rule: lo < hi → boundary-finding
# Rule: lo <= hi → exact-match
# lo/hi must cover the complete valid range!
# Koko Eating Bananas:
lo = 1 # minimum possible rate
hi = max(piles) # one pile per hour
# Capacity to Ship:
lo = max(weights) # must fit heaviest package
hi = sum(weights) # ship everything in one day
# is_feasible must be strictly monotone!
dict is a hash map with O(1) average access, insertion-ordered since Python 3.7. set is a hash set with O(1) membership test. Keys must be hashable (immutable): int, str, tuple — not list or dict. collections.defaultdict and collections.Counter simplify common patterns.
| Category | Operation | Time |
|---|---|---|
| Dict | ||
| Put | d[key] = val | O(1)avg |
| Get | d[key] | O(1)KeyError if missing |
| Get + default | d.get(key, default) | O(1) |
| Delete | del d[key] | O(1) |
| Contains | key in d | O(1) |
| Size | len(d) | O(1) |
| Keys | d.keys() | O(1)view object |
| Values | d.values() | O(1)view |
| Items | d.items() | O(1)view |
| Pop | d.pop(key, default) | O(1) |
| Set | ||
| Add | s.add(x) | O(1) |
| Contains | x in s | O(1) |
| Remove | s.discard(x) | O(1)no error if missing |
| Remove | s.remove(x) | O(1)KeyError if missing |
| Size | len(s) | O(1) |
| Union | s1 | s2 | O(n+m) |
| Intersection | s1 & s2 | O(min(n,m)) |
| Difference | s1 - s2 | O(n) |
# — Dict operations —
d = {}
d[key] = val # put
val = d[key] # get (KeyError if missing)
val = d.get(key, 0) # get with default
del d[key] # remove
key in d # contains
len(d) # size
# Frequency counting
freq = {}
for n in nums: freq[n] = freq.get(n, 0) + 1
# — defaultdict (auto-creates missing keys) —
from collections import defaultdict
freq = defaultdict(int)
for n in nums: freq[n] += 1 # no KeyError
groups = defaultdict(list)
for key, item in items: groups[key].append(item)
# — Counter (frequency Swiss army knife) —
from collections import Counter
freq = Counter(nums) # {1: 3, 2: 2, ...}
freq.most_common(k) # top-k [(val, count), ...]
freq[missing_key] # 0 (no KeyError)
freq1 == freq2 # True if same frequencies
# Decrement and remove if zero
freq[key] -= 1
if freq[key] == 0: del freq[key]
# — Set operations —
s = set()
s = {1, 2, 3}
s.add(x) # add
s.discard(x) # remove (no error)
x in s # contains
s1 | s2 # union
s1 & s2 # intersection
s1 - s2 # difference
s1 ^ s2 # symmetric difference
# Set from list
s = set(arr)
# Visited pattern (BFS/DFS)
visited = set()
if node not in visited:
visited.add(node)
# — Ordered iteration (dict is insertion-ordered 3.7+) —
for k, v in d.items(): pass # insertion order
for k in sorted(d): pass # sorted keys
# — frozenset (hashable set, usable as dict key) —
fs = frozenset([1, 2, 3])
d[fs] = "value" # frozenset as key
# ❌ Default list/dict is shared across all calls
def add(val, lst=[]):
lst.append(val)
return lst
add(1) # [1]
add(2) # [1, 2] — NOT [2]!
# ✅ Use None as sentinel
def add(val, lst=None):
if lst is None: lst = []
lst.append(val)
return lst
Python’s heapq module provides a min-heap only. For max-heap, negate values. The heap is just a regular list maintained by heapq functions. Supports tuples for multi-key priority (compared element-by-element).
| Category | Operation | Time |
|---|---|---|
| Heapify | heapq.heapify(lst) | O(n) |
| Push | heapq.heappush(h, x) | O(log n) |
| Pop (min) | heapq.heappop(h) | O(log n) |
| Push + Pop | heapq.heappushpop(h, x) | O(log n)push then pop |
| Pop + Push | heapq.heapreplace(h, x) | O(log n)pop then push |
| Peek | h[0] | O(1) |
| Size | len(h) | O(1) |
| N smallest | heapq.nsmallest(k, lst) | O(n log k) |
| N largest | heapq.nlargest(k, lst) | O(n log k) |
import heapq
# — Min-heap —
h = []
heapq.heappush(h, 5)
heapq.heappush(h, 3)
heapq.heappush(h, 7)
top = heapq.heappop(h) # 3 (smallest)
peek = h[0] # peek without removing
# Heapify existing list
arr = [5, 3, 7, 1]
heapq.heapify(arr) # O(n), arr is now a valid heap
# — Max-heap (negate values) —
heapq.heappush(h, -val) # push negated
top = -heapq.heappop(h) # pop and negate back
# — Tuple heap (multi-key, compared element-by-element) —
# Dijkstra: (distance, node)
heapq.heappush(h, (dist, node))
d, u = heapq.heappop(h)
# Frequency-based: (-freq, val) for max-freq first
for val, cnt in freq.items():
heapq.heappush(h, (-cnt, val))
# — Top-K via heap —
h = []
for v in data:
heapq.heappush(h, v)
if len(h) > k:
heapq.heappop(h) # evict smallest
# h now holds the k largest elements
# — Merge sorted lists —
merged = list(heapq.merge(sorted1, sorted2))
# ❌ heapq is ALWAYS min-heap
heapq.heappush(h, val)
top = heapq.heappop(h) # returns SMALLEST
# ✅ Negate for max-heap
heapq.heappush(h, -val)
top = -heapq.heappop(h) # returns LARGEST
# ⚠ With tuples: negate the priority, not the value
heapq.heappush(h, (-priority, value))
p, v = heapq.heappop(h)
actual_priority = -p
Use list as a stack (append/pop are O(1)). For queues, use collections.deque — O(1) on both ends. Never use list.pop(0) for queue dequeue — it’s O(n). deque also serves as a deque with appendleft/popleft.
| Category | Operation | list | deque |
|---|---|---|---|
| Stack (LIFO) | |||
| Push | append(x) | O(1)* | O(1) |
| Peek | [-1] | O(1) | O(1) |
| Pop | pop() | O(1) | O(1) |
| Queue (FIFO) | |||
| Enqueue | append(x) | O(1)* | O(1) |
| Peek | [0] | O(1) | O(1) |
| Dequeue | pop(0) / popleft() | O(n) | O(1) |
| Deque | |||
| Push front | appendleft(x) | O(n) | O(1) |
| Push back | append(x) | O(1)* | O(1) |
| Pop front | popleft() | O(n) | O(1) |
| Pop back | pop() | O(1) | O(1) |
from collections import deque
# — Stack via list —
stack = []
stack.append(x) # push
top = stack[-1] # peek
stack.pop() # pop
not stack # isEmpty
# — Queue via deque (NOT list.pop(0)!) —
queue = deque()
queue.append(x) # enqueue
front = queue[0] # peek
queue.popleft() # dequeue O(1)
not queue # isEmpty
# — Deque —
dq = deque()
dq.append(x) # push back
dq.appendleft(x) # push front
dq.pop() # pop back
dq.popleft() # pop front
dq[0] # peek front
dq[-1] # peek back
# Deque with max length (auto-evicts from opposite end)
dq = deque(maxlen=k)
# — Sliding window maximum (monotonic decreasing deque) —
dq = deque() # stores indices
result = []
for i, num in enumerate(nums):
while dq and nums[dq[-1]] < num:
dq.pop() # remove smaller from back
dq.append(i)
if dq[0] <= i - k:
dq.popleft() # out of window
if i >= k - 1:
result.append(nums[dq[0]])
from collections import OrderedDict
# LRU cache via OrderedDict
class LRU:
def __init__(self, cap):
self.cap = cap
self.cache = OrderedDict()
def get(self, key):
if key not in self.cache: return -1
self.cache.move_to_end(key) # mark as recently used
return self.cache[key]
def put(self, key, val):
if key in self.cache:
self.cache.move_to_end(key)
self.cache[key] = val
if len(self.cache) > self.cap:
self.cache.popitem(last=False) # evict oldest
# Or use @functools.lru_cache for function memoization
# ❌ O(n) per dequeue — shifts all elements
queue = []
queue.append(x)
queue.pop(0) # O(n)!
# ✅ O(1) per dequeue
from collections import deque
queue = deque()
queue.append(x)
queue.popleft() # O(1)
# ❌ IndexError on empty
stack[-1] # IndexError
stack.pop() # IndexError
deque().popleft() # IndexError
# ✅ Always check before accessing
if stack: top = stack[-1]
if queue: front = queue[0]
Common patterns for 2D grid traversal, coordinate conversion, and matrix transforms. Direction arrays avoid repetitive if-else chains.
# 4-directional (up, right, down, left)
DIRS = [(-1, 0), (0, 1), (1, 0), (0, -1)]
# 8-directional (includes diagonals)
DIRS8 = [(-1,-1),(-1,0),(-1,1),(0,-1),(0,1),(1,-1),(1,0),(1,1)]
# Bounds check
def in_bounds(r, c, rows, cols):
return 0 <= r < rows and 0 <= c < cols
for dr, dc in DIRS:
nr, nc = r + dr, c + dc
if in_bounds(nr, nc, rows, cols):
pass # valid neighbor
# 2D ↔ 1D index
idx = r * cols + c
r, c = divmod(idx, cols)
# Transpose
list(zip(*matrix)) # returns list of tuples
[[row[i] for row in matrix] for i in range(len(matrix[0]))]
# Rotate 90° clockwise = transpose + reverse each row
rotated = [list(row)[::-1] for row in zip(*matrix)]
# Spiral traversal: 4 boundaries shrinking
top, bot, left, right = 0, m - 1, 0, n - 1
while top <= bot and left <= right:
# → top row, ↓ right col, ← bottom row, ↑ left col
top += 1; right -= 1; bot -= 1; left += 1
# Diagonal properties:
# Same diagonal: r - c is constant
# Same anti-diagonal: r + c is constant
# Grid DFS (4-directional)
def dfs(grid, vis, r, c):
if not in_bounds(r, c, len(grid), len(grid[0])) or vis[r][c]:
return
vis[r][c] = True
for dr, dc in DIRS:
dfs(grid, vis, r + dr, c + dc)
Standard node definitions and templates for linked lists, trees, tries, and graphs. Edge cases for each are the most common source of wrong answers in DSA problems.
class ListNode:
def __init__(self, val=0, next=None):
self.val = val
self.next = next
class TreeNode:
def __init__(self, val=0, left=None, right=None):
self.val = val
self.left = left
self.right = right
class TrieNode:
def __init__(self):
self.children = {}
self.is_end = False
# Adjacency list — unweighted undirected
from collections import defaultdict
adj = defaultdict(list)
for u, v in edges:
adj[u].append(v)
adj[v].append(u) # omit for directed
# Weighted: {neighbor: weight}
adj = defaultdict(list)
for u, v, w in edges:
adj[u].append((v, w))
# In-degree array (for Kahn's topo sort)
in_deg = [0] * n
for u, v in edges: in_deg[v] += 1
# Union-Find (DSU) with path compression + union by rank
parent = list(range(n))
rank = [0] * n
def find(x):
if parent[x] != x:
parent[x] = find(parent[x]) # path compression
return parent[x]
def union(x, y):
px, py = find(x), find(y)
if px == py: return False
if rank[px] < rank[py]: px, py = py, px
parent[py] = px
if rank[px] == rank[py]: rank[px] += 1
return True
if not head: return None
if not head.next: return head
# Iterate safely
while curr:
curr = curr.next
# Dummy head avoids None checks for head insertion/deletion
dummy = ListNode(0, head)
# ... work with dummy ...
return dummy.next
# ✅ Short-circuit: fast checked before fast.next
while fast and fast.next:
slow = slow.next
fast = fast.next.next
# ✅ Reverse: save next before overwriting
nxt = curr.next # 1. save
curr.next = prev # 2. reverse
prev = curr # 3. advance prev
curr = nxt # 4. advance curr
# Floyd's cycle: phase 2 — find cycle start
ptr = head
while ptr != slow:
ptr = ptr.next
slow = slow.next
return ptr # cycle entry point
# DFS base case — always first line
if not node: return 0
# BFS start
if not root: return result
# BST validate — use float('inf') bounds (no overflow in Python!)
def validate(n, lo, hi):
if not n: return True
if n.val <= lo or n.val >= hi: return False
return validate(n.left, lo, n.val) and validate(n.right, n.val, hi)
validate(root, float('-inf'), float('inf'))
# Max Path Sum: init to -inf (not 0!)
ans = float('-inf')
# Skewed tree (n=10^5 all left): use iterative to avoid stack overflow
# Python default recursion limit is 1000!
import sys
sys.setrecursionlimit(200000) # increase for deep recursion
# ❌ Only visits component containing node 0
dfs(graph, 0, visited)
# ✅ Start from every unvisited node
for i in range(n):
if i not in visited:
dfs(graph, i, visited)
components += 1
# Dijkstra: must skip stale entries
d, u = heapq.heappop(h)
if d > dist[u]: continue
# Dijkstra: unreachable nodes
if dist[dst] == float('inf'): return -1
# ❌ Dijkstra with negative weights → use Bellman-Ford
# Undirected DFS: ignore parent, not all visited neighbors
if neighbor == parent: continue
if neighbor in visited: return True # real cycle
# For directed: use 3-state (unvisited / in-stack / done)
# ❌ Python default recursion limit is 1000
# Deep DFS on n=100000 nodes → RecursionError!
# ✅ Increase limit
import sys
sys.setrecursionlimit(300000)
# ✅ Or convert to iterative with explicit stack
stack = [root]
while stack:
node = stack.pop()
if node.right: stack.append(node.right)
if node.left: stack.append(node.left)
Common patterns for sliding window, DP, backtracking, and palindrome that appear across many DSA problems. Edge cases from these patterns are the most frequent source of wrong answers.
# — String palindrome (two pointers) —
l, r = 0, len(s) - 1
while l < r:
if s[l] != s[r]: return False
l += 1; r -= 1
# — Integer palindrome (no string conversion) —
if x < 0 or (x % 10 == 0 and x != 0): return False
rev = 0
while x > rev:
rev = rev * 10 + x % 10
x //= 10
return x == rev or x == rev // 10
# — Expand around center (longest palindrome) —
def expand(s, l, r):
while l >= 0 and r < len(s) and s[l] == s[r]:
l -= 1; r += 1
return r - l - 1
# — Find minimum / maximum —
ans = float('inf')
ans = min(ans, candidate)
# — Collect results: always copy the list! —
result.append(current[:]) # shallow copy via slicing
result.append(list(current)) # or list() constructor
# — Frequency arrays —
freq = [0] * 26 # 26 lowercase letters
for c in s: freq[ord(c) - ord('a')] += 1
# Or use Counter
from collections import Counter
freq = Counter(s)
# — Early-return checklist —
if not nums: return ... # empty list
if not s: return ... # empty string
if not root: return ... # None tree
if not head: return head # None linked list
if k > len(nums): return 0 # window too large
if k > len(nums): return 0
if len(p) > len(s): return False # Permutation In String
# Matches counter — increment only at exact boundary
if window[c] == need[c]: formed += 1
# Decrement only when crossing below threshold
window[lc] -= 1
if window[lc] < need[lc]: formed -= 1
# Climbing Stairs: n=1 → dp[i-2] accesses dp[-1] (valid in Python, but wrong!)
if n <= 1: return n
# Decode Ways: s[0]='0' → 0 ways immediately
# Coin Change: amount=0 → 0 (no coins needed)
# Edit Distance: dp[i][0]=i, dp[0][j]=j → don't forget!
# House Robber II: can't use first and last together
if len(nums) == 1: return nums[0]
return max(rob(nums[:-1]), rob(nums[1:]))
# Unbounded (Coin Change): each coin reusable → loop FORWARD
for coin in coins:
for j in range(coin, amount + 1):
dp[j] = min(dp[j], dp[j - coin] + 1)
# 0/1 Knapsack: each item once → loop BACKWARD
for w, v in items:
for j in range(W, w - 1, -1):
dp[j] = max(dp[j], dp[j - w] + v)
# ❌ All results point to the SAME list object
result.append(current)
# ✅ Always copy on add
result.append(current[:]) # slice copy
result.append(list(current)) # list copy
# ❌ i > 0: skips valid combinations (too aggressive)
if i > 0 and nums[i] == nums[i - 1]: continue
# ✅ i > start: only skip duplicates at same recursion level
if i > start and nums[i] == nums[i - 1]: continue
# MUST sort array first!
nums.sort()
# Kadane's: ❌ init max_so_far = 0 → [-3,-1,-2] wrongly returns 0
# ✅ Init from first element
max_so_far = curr = nums[0]
# Max Product [2,3,0,4]: zero resets the subarray
# Product Except Self with zero: 2+ zeros → all products are zero
# 3Sum [0,0,0] → duplicate skip avoids repeated i starts, not valid triples
# if i > 0 and nums[i] == nums[i-1]: continue
# Longest Consecutive [1,1,1] → answer=1 (not 3); use set
# Rotated Sorted Array II [1,1,1,1]: can't determine sorted half
if nums[lo] == nums[mid] == nums[hi]:
lo += 1; hi -= 1 # linear fallback
# Subarray length: right - left + 1
# Substring s[i:j]: [i, j), length = j - i
# for l < r vs for l <= r
# Binary search: lo <= hi for exact-match
# Binary search: lo < hi for boundary-finding
# Return conventions:
# Binary Search not found → -1
# Coin Change impossible → -1 (NOT 0!)
# Min Window no window → ""
# Subsets: empty set [] → valid result
# ⚠ Python negative indexing: s[-1] is valid (last char)
# This means s[right] with right=-1 does NOT crash —
# it silently returns the wrong element!
Python’s itertools provides combinatoric generators and lazy iterators. functools provides lru_cache for memoization and cmp_to_key for custom sorting. These are essential for competitive programming and interviews.
import itertools
from functools import lru_cache, cmp_to_key
# — Combinatorics —
itertools.permutations([1,2,3]) # all 3! = 6 permutations
itertools.permutations([1,2,3], 2) # 2-length permutations (P(3,2) = 6)
itertools.combinations([1,2,3], 2) # C(3,2) = 3: (1,2),(1,3),(2,3)
itertools.combinations_with_replacement([1,2], 2) # (1,1),(1,2),(2,2)
itertools.product([0,1], repeat=3) # all 2^3 = 8 binary strings
# — Iterators —
itertools.chain([1,2], [3,4]) # 1, 2, 3, 4 (flatten)
itertools.accumulate([1,2,3,4]) # 1, 3, 6, 10 (prefix sums)
itertools.groupby(sorted(data), key=lambda x: x[0]) # group by first element
itertools.zip_longest([1,2], [3]) # (1,3), (2,None)
# — Memoization —
@lru_cache(maxsize=None)
def fib(n):
if n < 2: return n
return fib(n - 1) + fib(n - 2)
# ⚠ lru_cache only works with hashable arguments
# Lists → convert to tuple first
# Clear cache: fib.cache_clear()
# — Prefix sums via accumulate —
from itertools import accumulate
prefix = list(accumulate(nums)) # [a, a+b, a+b+c, ...]
prefix = [0] + list(accumulate(nums)) # 0-indexed prefix sum
# — Enumerate from 1 —
for i, val in enumerate(arr, 1): pass # i starts at 1
The print() function and f-strings handle most output. For fast input in competitive programming, use sys.stdin. input() strips the trailing newline automatically.
# — Print —
print("hello", 42) # hello 42
print(f"name={name} age={age}") # f-string (3.6+)
print(f"{3.14159:.2f}") # "3.14"
print(*arr, sep=", ") # 1, 2, 3
print(arr) # [1, 2, 3]
print("no newline", end="") # suppress newline
# — Input —
s = input() # read line (str)
n = int(input()) # read int
a, b = map(int, input().split()) # read two ints
arr = list(map(int, input().split())) # read int array
# Fast input for competitive programming
import sys
input = sys.stdin.readline # much faster
# — File I/O —
with open("file.txt") as f:
data = f.read() # entire file as string
lines = data.splitlines() # list of lines (from already-read data)
with open("file.txt", "w") as f:
f.write("hello\n")
# — String formatting —
f"{42:05d}" # "00042" (zero-padded)
f"{255:08b}" # "11111111" (binary, 8-wide)
f"{255:#x}" # "0xff" (hex with prefix)
f"{3.14:.4f}" # "3.1400" (4 decimal places)