← AlgoVision ALP

Algorithm Patterns

Five reusable skeletons power almost every interview problem.
Learn the pattern once — recognize it instantly — apply it everywhere.
Click the + lines in code blocks to reveal deeper context.

Which Pattern Do I Need?

Master Decision Flowchart

Start here. Ask these questions about every new problem:

New Problem
→
Need O(1) lookup?
YES→
Hash Map / Set
NO↓
Linear scan with conditions?
YES→
Two Pointers / Window
NO↓
Sorted + find / first True / 3-way partition?
YES→
lo / mid / hi
NO↓
Tree / graph level-by-level?
YES→
BFS + Queue
NO↓
Bottom-up value / explore all?
YES→
DFS / Recursion
NO↓
Optimal / count ways / overlapping subproblems?
YES→
Dynamic Programming
Signal Phrases → Pattern

When you see these words in a problem statement, reach for the corresponding pattern:

You hear…You think…You need…
"find pair", "duplicate", "frequency", "group by" O(1) lookup or counting Hash Map
"sorted array", "palindrome", "subarray sum", "substring" Converge or slide through data Two Pointers
"level order", "shortest path", "flood fill", "components" Explore neighbors layer by layer BFS
"max depth", "validate tree", "all paths", "combinations" Recurse, aggregate, or backtrack DFS
"minimum / maximum", "number of ways", "can you reach", "optimal substructure" Reuse answers to smaller subproblems (skip/jump to prior states) Dynamic Programming
"sorted + pair from both ends" Pointers move toward each other Converging Pointers
"longest / shortest contiguous", "window", "shrink while invalid" One pointer always advances; the other moves only when a condition fails One-way Contingent
"take or skip", "previous house", "i−1 / i−2", "fibonacci-like" Not scanning pointers — dependency offsets into a DP table Skip Pointers (DP)
"search sorted", "lower / upper bound", "first True", "rotated sorted" Halve the range with a computed midpoint lo / mid / hi (Search)
"sort colors", "3-way partition", "0s then 1s then 2s" Three live cursors: mid walks; lo/hi collect buckets lo / mid / hi (Partition)

Pattern 1: Hash Map / Frequency Counter

When to use O(1) lookup

Whenever you need to check existence, count occurrences, or group items without nested loops.

find complement count frequency detect duplicate group by key two values that sum to
Data structures: dict set defaultdict Counter
Core Skeleton
def solve(nums, target):
Step 1 — Identify the problem type.
Are you searching for a pair, counting, or grouping? If you need to turn O(n²) into O(n), a hash map is almost certainly the answer.
seen = {} # val → index (or count)
Step 2 — Choose your map type.
  • dict — complement lookup (Two Sum)
  • set — existence check (Contains Duplicate)
  • Counter — frequency counting (Valid Anagram)
  • defaultdict(list) — grouping (Group Anagrams)
for i, num in enumerate(nums):
Step 3 — Single pass.
Process each element exactly once. The hash map gives you O(1) lookup for anything you've already seen, eliminating the inner loop.
complement = target - num
The key insight.
Instead of asking "which other element pairs with this?" (O(n) scan), compute the complement and check the map (O(1)).
# Two Sum: complement = target - num # Contains Dup: complement = num (just check set) # Anagram: decrement count for each char in t
if complement in seen:
O(1) lookup — the whole point.
in dict is O(1) average. This replaces the O(n) inner loop of the brute-force approach.
return [seen[complement], i]
Found it — return early.
For Two Sum you return indices. For Contains Duplicate you return True. For frequency problems you continue counting.
seen[num] = i # build map as you go
Interview tip: Always build the map as you iterate, not in a separate pass. This handles edge cases (like not pairing an element with itself) and keeps the code to one loop.
Variations

Complement Lookup

complement = target - num if complement in seen:

Two Sum, Two Sum II

Frequency Count

count[c] += 1 # for s count[c] -= 1 # for t

Valid Anagram, Top K

Group by Key

key = tuple(sorted(s)) groups[key].append(s)

Group Anagrams

Pattern 2: Two Pointers / Sliding Window

When to use linear scan

When the data is sorted or sequential and you need to find a pair, shrink/expand a range, or process a contiguous subarray.

sorted array palindrome subarray sum substring minimum window container / area
Data structures: left, right indices set (window) dict (freq)
When you hear… pointer family

Interviewers say “two pointers,” but several different mechanics hide under that phrase. Pick the right family before you code:

Family When you hear… How they move Not this if…
Converging “sorted,” “pair that sums to,” “palindrome from both ends,” “max area between lines” Start at both ends; each step moves at least one pointer inward until they meet. You need a contiguous window whose size grows/shrinks (that’s one-way contingent).
One-way contingent “longest substring,” “minimum window,” “shrink while invalid,” “same-direction scan” Right (or the lead) always advances. Left advances only when a condition fails (contingent). Both ends are active decision-makers that move toward each other (that’s converging).
lo / mid / hi (3 pointers) “binary search,” “first True / lower bound,” “rotated sorted,” “sort colors / 3-way partition” Three named indices. Search: mid = (lo+hi)//2, then shrink lo or hi. Partition: mid walks once; swap with lo/hi to bucket values. You only need a pair sum from both ends (converging) or a growing/shrinking window (one-way contingent). Also not DP skip offsets.
Skip pointers (DP) “take or skip,” “previous house,” “ways from n−1 and n−2,” “optimal up to i” Not scanning the array live. You read earlier DP cells (i-1, i-2, …) as dependency offsets. You are comparing nums[L] and nums[R] while sliding indices — that is still two-pointers, not DP skips.
Rule of thumb: Live cursors on the input → Two Pointers / Window / lo-mid-hi. Offsets into a filled table (dp[i-1]) → Dynamic Programming. A computed midpoint that halves a range → lo / mid / hi search.
Core Skeleton — Converging Pointers
def solve(nums, target):
Step 1 — Is the input sorted?
Converging pointers require sorted data. If it's not sorted, consider sorting first (O(n log n)) or using a hash map instead.
left, right = 0, len(nums) - 1
Step 2 — Place pointers at extremes.
Left starts at the beginning, right at the end. They converge toward each other, eliminating candidates based on the comparison.
while left < right:
Step 3 — The convergence loop.
Each iteration moves at least one pointer inward, guaranteeing O(n) time. The loop ends when the pointers meet.
total = nums[left] + nums[right]
Step 4 — Compute and compare.
The comparison result tells you which pointer to move:
  • total == target → found it
  • total < target → need larger sum → move left right
  • total > target → need smaller sum → move right left
if total == target: return [left, right] elif total < target: left += 1 else: right -= 1
Interview tip: When asked "can you do better than O(n²)?", sorting + two pointers gives O(n log n). If input is already sorted, it's O(n).
Core Skeleton — One-way Contingent (Sliding Window)
def solve(s):
Step 1 — Recognize the window pattern.
You need a contiguous subarray or substring that satisfies some condition. The window expands right and shrinks left.
left = 0; window = set() # or dict for freq
Step 2 — Choose your window state.
  • set — track unique chars (Longest Substring Without Repeating)
  • dict — track frequencies (Minimum Window Substring, Character Replacement)
  • int — running sum (Min Size Subarray Sum, Best Time to Buy)
for right in range(len(s)):
Step 3 — Expand right (always).
The right pointer advances every iteration, adding the new element to the window state.
window.add(s[right]) # expand
Add the new element to your window state. For frequency problems: freq[s[right]] += 1. For sum problems: total += nums[right].
while window_invalid: # shrink
Step 4 — Shrink left (conditionally).
When the window violates the constraint, remove from the left until valid again. This inner while loop is still O(n) total because left only moves right.
# Longest Substring: while s[right] in window # Min Subarray Sum: while total >= target # Min Window: while have == need
window.remove(s[left]); left += 1 result = max(result, right - left + 1)
Interview tip: The expand-right / shrink-left pattern is O(n) even with the inner while loop, because each element enters and leaves the window at most once. This is the textbook one-way contingent pattern: right always moves; left moves only while the window is invalid.
Core Skeleton — lo / mid / hi (Search) 3 pointers
def search(nums, target):
When you hear “sorted” + “find / first True / bound”.
Three indices: lo and hi bound the remaining search space; mid is computed each step (not walked like a window). Halve until the range collapses.
lo, hi = 0, len(nums) - 1
Place the outer pointers.
Inclusive range [lo, hi] is the classic interview default. Write the loop invariant aloud: “if the answer exists, it is still inside [lo, hi].”
while lo <= hi:
Use <= when the range is inclusive.
Each iteration must shrink the range (move lo or hi) so the loop terminates. Off-by-one bugs almost always come from mixing inclusive/exclusive updates.
mid = (lo + hi) // 2
mid is derived, not a third scanner.
Unlike converging L/R, you do not independently “decide” mid — you recompute it from the current lo/hi. Prefer lo + (hi - lo) // 2 in languages that overflow on lo + hi.
if nums[mid] == target: return mid elif nums[mid] < target: lo = mid + 1 else: hi = mid - 1 return -1
Interview tip: Same three names power rotated-array search and “first True” / lower-bound problems — only the predicate on mid changes, not the lo/mid/hi skeleton.
Core Skeleton — lo / mid / hi (3-way Partition) Dutch flag
def sortColors(nums):
When you hear “three categories in place”.
Here all three are live cursors: lo grows the left bucket, hi grows the right bucket, mid walks the unknown middle once. One pass, O(1) extra space.
lo = mid = 0; hi = len(nums) - 1
Invariant: [0..lo) is bucket 0, [lo..mid) is bucket 1, (hi..n) is bucket 2, and [mid..hi] is unprocessed.
while mid <= hi: if nums[mid] == 0: nums[lo], nums[mid] = nums[mid], nums[lo] lo += 1; mid += 1 elif nums[mid] == 1: mid += 1 else: nums[mid], nums[hi] = nums[hi], nums[mid] hi -= 1 # do not mid++ yet
Discern from search: Partition mid walks and swaps. Search mid is recomputed and never swapped with lo/hi. Same names, different jobs.
Variations

Converging

L, R = 0, n-1 while L < R:

Both ends move inward. Two Sum II, Valid Palindrome, 3Sum, Container

One-way Contingent

for R in range(n): while invalid: L++

Lead always advances; trail moves only when the window breaks. Sliding Window family.

lo / mid / hi

mid = (lo+hi)//2 # or Dutch: mid walks

Binary Search, Rotated Search, Sort Colors / 3-way partition

Fast / Slow

slow = fast = head fast = fast.next.next

Same direction, different speeds. Cycle Detection, Middle Node

DP skip pointers (i-1/i-2) are covered under Dynamic Programming — they are not live scan cursors.

Pattern 3: BFS (Breadth-First Search)

When to use queue

When you need to explore level by level, find the shortest path, or process all nodes at the same distance before moving deeper.

level order shortest path minimum depth flood fill connected components nearest neighbor
Data structures: deque (queue) visited set level list
Core Skeleton — Tree BFS
from collections import deque
Always use deque, never a list.
list.pop(0) is O(n) — it shifts every element. deque.popleft() is O(1). This matters when the queue holds thousands of nodes.
def bfs(root):
Step 1 — Is this a BFS problem?
Ask: "Do I need to process nodes layer by layer?" If yes, BFS. If you need a bottom-up aggregate (depth, diameter), prefer DFS.
if not root: return []
Step 2 — Guard the empty case.
Every tree solution starts with a null check. For BFS, return an empty result ([], 0, False — depends on the problem).
queue = deque([root]); result = []
Step 3 — Seed the queue.
Start with the root. For graph problems, seed with the starting node and mark it visited. For multi-source BFS (e.g., all land cells), seed with all starting points.
while queue:
Step 4 — The BFS loop.
Process until the queue is empty. Each iteration of the outer while loop handles one level. The inner for loop processes all nodes at that level.
level_size = len(queue) # snapshot current level
The level-size trick.
Capture len(queue) before the inner loop. New children added during the loop belong to the next level. This is what separates level-order BFS from a simple queue drain.
current_level = [] for _ in range(level_size): node = queue.popleft() current_level.append(node.val) if node.left: queue.append(node.left) if node.right: queue.append(node.right) result.append(current_level) return result
Interview tip: The level-size snapshot (len(queue)) is the single line that separates "I know BFS" from "I understand BFS." Always mention it.
Core Skeleton — Graph BFS (Flood Fill)
def bfs_grid(grid, sr, sc):
Graph BFS vs Tree BFS.
Trees have no cycles, so you never revisit. Grids/graphs can have cycles, so you need a visited set (or mark cells in-place).
queue = deque([(sr, sc)]); visited = {(sr, sc)}
Mark visited on enqueue, not on dequeue.
If you wait until dequeue to mark, the same cell can be added to the queue multiple times by different neighbors, causing O(n²) behavior.
while queue: r, c = queue.popleft() for dr, dc in [(-1,0),(1,0),(0,-1),(0,1)]: nr, nc = r + dr, c + dc if 0 <= nr < rows and 0 <= nc < cols \ and (nr,nc) not in visited: visited.add((nr, nc)) queue.append((nr, nc))
Interview tip: The 4-direction array [(-1,0),(1,0),(0,-1),(0,1)] is a reusable constant. Name it DIRS for clarity.

Pattern 4: DFS / Recursion

When to use recursion

When you need to explore to the deepest point first, compute a bottom-up value from subtrees, or explore all possible paths.

max depth validate tree invert / mirror all paths bottom-up aggregate memoization
Data structures: call stack (implicit) explicit stack memo dict
Core Skeleton — Post-Order Aggregation
def solve(root):
Step 1 — Identify the aggregation.
You're computing a value that depends on both subtrees. The answer at each node combines the answers from its children. This is post-order: left → right → combine.
if not root: return 0 # base case
Step 2 — The null base case.
Every recursive tree solution starts here. The return value depends on the problem:
  • Depth problems → return 0
  • Boolean problems → return True
  • Collection problems → return []
left = solve(root.left)
Step 3 — Recurse into subtrees.
Trust that the recursive call returns the correct answer for the subtree. Don't try to trace every call — focus on one node and assume children are solved.
right = solve(root.right)
return 1 + max(left, right) # combine
Step 4 — Combine results.
This one line is the only thing that changes between problems:
# Max Depth: return 1 + max(left, right) # Min Depth: return 1 + min(left, right) + guards # Same Tree: return p.val == q.val and left and right # Invert Tree: swap children, return root
Interview tip: Say "I'll use post-order because the answer at each node depends on both subtrees." This shows you understand why, not just how.
Core Skeleton — DFS with Bounds (Validation)
def isValid(node, lo, hi):
Passing constraints down the tree.
Unlike post-order (bottom-up), validation passes bounds top-down. Each node must satisfy lo < node.val < hi. Initial call: isValid(root, -inf, inf).
if not node: return True
Empty subtree is always valid. This is the base case that stops the recursion.
if node.val <= lo or node.val >= hi:
The constraint check.
If the current value violates the bounds, the entire subtree is invalid. Return False immediately.
return False return isValid(node.left, lo, node.val) \ and isValid(node.right, node.val, hi)
Interview tip: The bounds approach avoids the common mistake of only checking parent-child relationships. A node must be valid against all ancestors, not just its parent.
Core Skeleton — Memoization Bridge to DP
def solve(n, memo={}):
When DFS meets overlapping subproblems.
Same recursive call with the same arguments → cache it. This is the on-ramp to Dynamic Programming. The “pointers” here (n-1, n-2) are skip offsets, not live scan cursors.
if n in memo: return memo[n]
Check cache first.
Before computing, check if you've already solved this subproblem. This is the memoization guard — it prevents recomputation.
if n <= 1: return 1 # base case
Base cases stop the recursion.
For Climbing Stairs: ways(0) = 1, ways(1) = 1. For other DP problems, identify the smallest subproblem that has a known answer.
memo[n] = solve(n-1) + solve(n-2) # skip pointers return memo[n]
Interview tip: Start recursive, then add memo, then convert to iterative DP. Full bottom-up skeletons, skip-pointer vocabulary, and problem links live in the DP pattern section.

Pattern 5: Dynamic Programming

When to use optimal / count

When the answer at step i is built from a small set of earlier answers (optimal substructure) and the same subproblems would be recomputed without a table or memo (overlapping subproblems).

minimum / maximum number of ways can you reach / is possible take or skip fibonacci-like grid paths
Data structures: dp array / table memo dict rolling vars
When you hear… DP signals

Translate the prompt language into a DP move:

You hear… You think… Likely recurrence
“number of ways to climb / decode / reach” Count paths through states dp[i] = dp[i-1] + dp[i-2] (or more branches)
“maximum money without adjacent” / “take or skip” Skip pointer choice at each house/index dp[i] = max(dp[i-1], nums[i] + dp[i-2])
“fewest coins / min edits / min path sum” Minimize over prior feasible states dp[x] = 1 + min(dp[x - choice])
“paths in a grid (only right/down)” 2D table; each cell from up + left dp[r][c] = dp[r-1][c] + dp[r][c-1]
“longest common / increasing subsequence” Compare chars/values; extend or reset Diagonal extend on match; max of neighbors on mismatch
Skip Pointers vs Scan Pointers

DP “pointers” look like indices, but they behave differently from Two Pointers:

Converging / One-way Contingent Skip Pointers (DP)
What moves? Live cursors on the input (L, R) The fill index i; you read earlier cells
Why move? Compare values / keep a window valid Express the recurrence: take, skip, or combine prior answers
Example while L < R or while invalid: L++ dp[i-1] (skip current) vs dp[i-2] (take & jump)
Gotcha Do not call window shrink “DP” just because you have two indices Do not invent a second live cursor when you only need prior states
Say this in interview: “I’m not scanning with two pointers — i-1 and i-2 are skip pointers into the DP table for the take-or-skip recurrence.”
Core Skeleton — 1D Bottom-up (Skip Pointers)
def rob(nums):
House Robber shape.
At each index you either skip this house (dp[i-1]) or take it and skip the previous (nums[i] + dp[i-2]). Those i-1 / i-2 reads are skip pointers.
if not nums: return 0
Guard empty input. Then seed base cases for the first one or two houses before the loop.
dp = [0] * len(nums)
Table of answers so far.
dp[i] = best result using only the prefix nums[0..i]. Climbing Stairs uses the same skeleton with + instead of max.
dp[0] = nums[0] if len(nums) == 1: return dp[0] dp[1] = max(nums[0], nums[1]) for i in range(2, len(nums)): dp[i] = max(dp[i-1], nums[i] + dp[i-2]) # skip vs take return dp[-1]
Interview tip: After the table works, offer O(1) space: keep only prev2, prev1 rolling variables — the skip pointers become two scalars.
Core Skeleton — 2D Grid DP
def uniquePaths(m, n):
Each cell depends on up and left.
Skip pointers become 2D: (r-1, c) and (r, c-1). Same idea as Minimum Path Sum with min + cell weight.
dp = [[0] * n for _ in range(m)]
Seed first row and first column to 1 (only one way along the border), then fill the interior.
for r in range(m): dp[r][0] = 1 for c in range(n): dp[0][c] = 1 for r in range(1, m): for c in range(1, n): dp[r][c] = dp[r-1][c] + dp[r][c-1] # skip to up / left return dp[m-1][n-1]
Interview tip: Draw a tiny 3×3 table on the whiteboard first. If you cannot fill cell (1,1) from earlier cells, the recurrence is wrong.
Variations

1D Skip / Take

dp[i] = max( dp[i-1], take+dp[i-2])

House Robber, Climbing Stairs, Decode Ways

Knapsack / Coins

for c in coins: dp[x] = min(dp[x], 1+dp[x-c])

Coin Change, Partition Equal Subset Sum

2D Strings / Grid

match → diag+1 else → max(up,left)

LCS, Edit Distance, Unique Paths, Min Path Sum

Which Shape Do I Need?

Interview decision flow C++

Custom problems rarely hand you LeetCode stubs. Model the domain first, then algorithm.

New custom problem
→
Just fields + no invariants?
YES→
struct
NO↓
Owns resources / API?
YES→
class + methods
NO↓
Shared graph nodes?
YES→
pointers / indices
TreeNode / ListNode initializer list const methods unordered_map vector ownership
Signal Phrases → C++ Shape
You hear…You think…You need…
"node with left/right"tiny public data bagstruct TreeNode
"cache / queue / stack API"encapsulate + invariantsclass with methods
"graph of sensors / rooms"identity + sharingindices or Node*
"don't copy this object"alias without ownership transferconst T&
"this method shouldn't mutate"document + enforceconst method

Pattern 1: struct for Interview Nodes

Core Skeleton — TreeNode / ListNode
struct TreeNode {
public by default. Interviewers expect a minimal node type. Prefer struct over a heavy class when there are no invariants to protect.
int val;
Keep fields obvious. Add string id; / double weight; for custom problems.
TreeNode *left, *right;
Children are pointers (or indices into a vector<TreeNode>). Never store by-value children — that copies subtrees.
TreeNode(int v) : val(v), left(nullptr), right(nullptr) {} };
Interview tip: Say the initializer list out loud — it shows you initialize before the body runs.
struct
  • Default public
  • Great for POD / nodes
  • Still can have methods
class
  • Default private
  • Hide state, expose API
  • Prefer when invariants matter

Pattern 2: class API + Composition

Core Skeleton — Domain object
class CircularBuffer {
Name the system from the prompt (Robot, Cache, Scheduler). Methods become the verbs in the problem statement.
public:
Public surface = what the interviewer will call in tests.
explicit CircularBuffer(int cap) : buf(cap), head(0), size_(0) {}
explicit avoids accidental conversions. Init list seeds members.
bool enQueue(int x);
Keep methods small. Return bools/errors instead of throwing in interviews unless asked.
int size() const { return size_; }
const = "safe on a const object / won't mutate". Instant credibility.
private: vector<int> buf; int head, size_; };
Rule of Zero: Prefer vector/string/unique_ptr so you don't hand-write copy constructors under time pressure.
Composition — Graph owns nodes
struct Edge { int to; double w; }; class Graph { public: explicit Graph(int n) : adj(n) {} void addEdge(int u, int v, double w) { adj[u].push_back({v, w}); } const vector<Edge>& neighbors(int u) const { return adj[u]; } private: vector<vector<Edge>> adj; };

Custom problems become: build the type → fill it from input → run BFS/Dijkstra on the API.

Pattern 3: Fast Debug Habits

  • Print address + children when links look wrong: cout << node << " L=" << node->left;
  • Assert invariants early: assert(cap > 0);
  • Prefer indices into a vector when ownership is messy — no dangling deletes mid-interview.
  • Pass large objects as const T&; mutate via non-const ref only when intentional.
Loading C++ Primer…

Which Shape Do I Need?

Interview decision flow Python

For custom prompts, name the nouns first. @dataclass keeps models clean; classic classes still win for heavy behavior.

New custom problem
→
Mostly fields?
YES→
@dataclass
NO↓
LeetCode-style node?
YES→
plain class __init__
NO↓
Invariants / complex API?
YES→
class + methods
@dataclass field(default_factory=list) Optional[T] frozen / slots asdict debug
Signal Phrases → Python Shape
You hear…You think…You need…
"reading with ts + value"immutable-ish data bag@dataclass
"list of children / edges"mutable default hazardfield(default_factory=list)
"left/right may be missing"nullable childOptional[TreeNode]
"use as dict key"hashable recordfrozen=True
"enqueue / get / put API"behavior + stateclassic class

Pattern 1: @dataclass for Clean Models

Core Skeleton — attributes without boilerplate
from dataclasses import dataclass, field
Generates __init__, __repr__, __eq__. Perfect under interview time pressure.
@dataclass
Add slots=True for many tiny objects; frozen=True when instances should be keys.
class Reading:
Name domain nouns from the prompt: Robot, Waypoint, Event, Order.
ts: int
Typed fields are documentation for you and the interviewer.
tags: list[str] = field(default_factory=list)
Never write tags: list = [] — that shared list bites everyone. default_factory is the fix.
Interview tip: Say "I'll model this as a dataclass so equality and repr are free, then implement the algorithm against that type."
Composition — nested dataclasses
from dataclasses import dataclass @dataclass class Location: x: float y: float @dataclass class Event: loc: Location kind: str meta: dict # or field(default_factory=dict) e = Event(Location(1.0, 2.0), "ping") # debug: from dataclasses import asdict; print(asdict(e))

Pattern 2: Classic class for Nodes & APIs

Core Skeleton — ListNode / TreeNode
class ListNode:
Matches LeetCode stubs. Dataclass works too, but interviewers recognize this instantly.
def __init__(self, val=0, next=None):
Defaults matter — empty node and missing child are common.
self.val = val self.next = next # Optional[ListNode]
Typing: next: Optional["ListNode"] (or ListNode | None) documents nullability without slowing you down.
Core Skeleton — behavior-heavy class
class LRUCache: def __init__(self, capacity: int): self.cap = capacity self.order = OrderedDict() def get(self, key: int) -> int: if key not in self.order: return -1 self.order.move_to_end(key) return self.order[key] def put(self, key: int, value: int) -> None: self.order[key] = value self.order.move_to_end(key) if len(self.order) > self.cap: self.order.popitem(last=False)

When the prompt is an API surface, skip dataclass — methods are the product.

Pattern 3: Fast Debug Habits

  • print(node) is useless without __repr__ — dataclasses give you one free.
  • Dump nested state with asdict(obj) + pprint.
  • Watch the mutable default: def f(xs=[]) and items: list = [] in dataclasses.
  • For graphs, prefer dict[int, list[int]] adjacency unless you truly need object identity.
Loading Python Primer…
Tree-Specific Patterns • All Problems
AlgoVision — Algorithm · C++ · Python Patterns • See the code. Trace the logic. Ace the interview.