Ten interview patterns built around the exact point where intuition usually breaks: what the state means, why a pointer moves, and which invariant makes the code safe.
Decode constraints.Decide which operations are safe and affordable.
State the invariant.Name what must stay true before writing the loop.
Expose help gradually.Use a nudge, derivation, trace, then code—in that order.
Return later.Record the outcome and let the lab schedule retrieval.
This device
Your retrieval queue
Progress stays in this browser. This device stores lesson IDs, outcomes, confidence, assistance level, and review dates—never your code or private reasoning.
0reviewed
10due now
0independent
01
Hash setCore medium
Longest Consecutive Sequence
Consecutive values, unsorted input, and an O(n) target.
Due now
Cold prompt
How can each sequence be counted once without sorting?
Say your answer aloud. Name the state and the rule that moves it. No typing is collected.
Constraints decoder
Duplicates do not change a sequence, so a set is the natural representation.
Fast membership matters more than order.
The value range is safe for stepping by one in the stated problem constraints.
One nudge
Ask what fact proves that n is the first value—not the middle—of its run.
Invariant
Only a value with no predecessor may start a forward scan.
Derive it
A set turns “does n + 1 exist?” into average O(1) membership.
Starting from every value repeats work: 2 scans 2→5, then 3 scans 3→5 again.
If n - 1 exists, n cannot be a start. Skip it.
Every value is therefore visited by at most one forward sequence scan.
Failure mode
Converting to a set and then treating iteration order as sorted. A set removes duplicates; it does not establish order.
Walk the smallest useful trace
[2, 3, 4, 5, 10, 20]
2 has no predecessor 1 → scan 2, 3, 4, 5 → length 4.
3, 4, and 5 have predecessors → skip them.
10 and 20 start one-value runs.
Reveal the Rust implementation
Rust · accepted pattern
use std::collections::HashSet;
impl Solution {
pub fn longest_consecutive(nums: Vec<i32>) -> i32 {
let numbers: HashSet<i32> = nums.into_iter().collect();
let mut best = 0;
for &start in &numbers {
if numbers.contains(&(start - 1)) {
continue;
}
let mut current = start;
let mut length = 1;
while numbers.contains(&(current + 1)) {
current += 1;
length += 1;
}
best = best.max(length);
}
best
}
}
Say this in the interview
“The set gives constant-average membership, and the predecessor check makes each run start exactly once.”
O(n) average time · O(n) space
Transfer check
If the problem asked for the longest run with step size 2, what replaces the predecessor and successor checks?
02
Hash mapFoundation
Group Anagrams
Group different inputs that are equivalent under one transformation.
Due now
Cold prompt
What representation is identical for every word in an anagram group?
Say your answer aloud. Name the state and the rule that moves it. No typing is collected.
Constraints decoder
Words contain lowercase ASCII letters, so byte sorting is safe.
The grouped output order is irrelevant.
A hash-map key must own stable, hashable data.
One nudge
Transform “eat”, “tea”, and “ate” in a way that produces one identical key.
Invariant
Every word with the same canonical key belongs to the same bucket.
Derive it
Sorting a word’s bytes creates a canonical representation.
“eat”, “tea”, and “ate” all become the byte sequence for “aet”.
Use that sequence as the map key and append the original owned String.
Collect the buckets after every word has been classified.
Failure mode
Comparing every word with every other word. That hides the stronger idea: compute one reusable identity, then group by it.
Walk the smallest useful trace
["eat", "tea", "tan", "ate", "nat", "bat"]
aet → ["eat", "tea", "ate"]
ant → ["tan", "nat"]
abt → ["bat"]
Reveal the Rust implementation
Rust · accepted pattern
use std::collections::HashMap;
impl Solution {
pub fn group_anagrams(strs: Vec<String>) -> Vec<Vec<String>> {
let mut groups: HashMap<Vec<u8>, Vec<String>> = HashMap::new();
for word in strs {
let mut key = word.as_bytes().to_vec();
key.sort_unstable();
groups.entry(key).or_default().push(word);
}
groups.into_values().collect()
}
}
Say this in the interview
“I am not comparing words pairwise; I am mapping every equivalent word to the same canonical sorted key.”
O(n · k log k) time · O(n · k) space
Transfer check
If words were very long but restricted to a–z, what fixed-size key could remove the sorting cost?
03
Hash map + heapCore medium
Top K Frequent Elements
Repeatedly retrieve the highest-ranked items, where rank is not the item itself.
Due now
Cold prompt
What belongs first in each heap tuple: the number or its frequency?
Say your answer aloud. Name the state and the rule that moves it. No typing is collected.
Constraints decoder
Frequency counting needs one pass and one map entry per distinct number.
Rust BinaryHeap is a max-heap by default.
Tuple ordering compares the first field first, so frequency must be the primary score.
One nudge
A heap only knows what you place in its comparison position. Rank by the score, not the identity.
Invariant
The heap ranks entries by frequency, while preserving the number as the payload.
Derive it
Count each number with a HashMap.
Push (frequency, number), not just number.
The maximum tuple now has the highest frequency.
Pop k times and return only the number payload.
Failure mode
Asking for the largest dictionary keys instead of the keys with the largest dictionary values.
Walk the smallest useful trace
[1, 1, 1, 2, 2, 3], k = 2
Counts: 1→3, 2→2, 3→1.
Heap entries: (3,1), (2,2), (1,3).
Two pops return numbers 1 and 2.
Reveal the Rust implementation
Rust · accepted pattern
use std::collections::{BinaryHeap, HashMap};
impl Solution {
pub fn top_k_frequent(nums: Vec<i32>, k: i32) -> Vec<i32> {
let mut counts = HashMap::new();
for number in nums {
*counts.entry(number).or_insert(0) += 1;
}
let mut heap = BinaryHeap::new();
for (number, frequency) in counts {
heap.push((frequency, number));
}
(0..k)
.filter_map(|_| heap.pop().map(|(_, number)| number))
.collect()
}
}
Say this in the interview
“The heap item is (frequency, value), so the comparison key and returned payload are deliberately separate.”
O(n + m log m) time · O(m) space, where m is the number of distinct values
Transfer check
How would a min-heap of size k change the complexity when k is much smaller than the number of unique values?
04
Sorted two pointersCore medium
3Sum
Find unique triples whose sum hits a target.
Due now
Cold prompt
After fixing one number, what three cases decide which pointer moves?
Say your answer aloud. Name the state and the rule that moves it. No typing is collected.
Constraints decoder
Sorting is allowed and exposes monotonic pointer movement.
The result must not contain duplicate triplets.
Use i64 for the temporary sum so addition cannot overflow i32.
One nudge
Compare the complete three-number sum with zero: too small, exact, or too large.
Invariant
For a fixed i, every remaining candidate lies between left and right.
Derive it
Sort the numbers and fix nums[i].
Use left and right to search the remaining sorted suffix.
If the total is too small, only moving left can increase it.
If too large, only moving right can decrease it.
After a match, move both and skip equal neighboring values.
Failure mode
Comparing left + right with nums[i] instead of -nums[i], or using only two movement cases and missing valid pairs.
Walk the smallest useful trace
[-2, 0, 1, 1, 2]
Fix -2; target pair sum is 2.
0 + 2 = 2 → record [-2, 0, 2], then move both.
1 + 1 = 2 → record [-2, 1, 1].
Reveal the Rust implementation
Rust · accepted pattern
impl Solution {
pub fn three_sum(mut nums: Vec<i32>) -> Vec<Vec<i32>> {
nums.sort_unstable();
let mut result = Vec::new();
if nums.len() < 3 {
return result;
}
for i in 0..nums.len() - 2 {
if i > 0 && nums[i] == nums[i - 1] {
continue;
}
let (mut left, mut right) = (i + 1, nums.len() - 1);
while left < right {
let total = nums[i] as i64 + nums[left] as i64 + nums[right] as i64;
if total < 0 {
left += 1;
} else if total > 0 {
right -= 1;
} else {
result.push(vec![nums[i], nums[left], nums[right]]);
left += 1;
right -= 1;
while left < right && nums[left] == nums[left - 1] {
left += 1;
}
while left < right && nums[right] == nums[right + 1] {
right -= 1;
}
}
}
}
result
}
}
Say this in the interview
“Sorting gives a monotonic decision: too small moves left, too large moves right, and equality moves both.”
O(n²) time · O(1) auxiliary space excluding output
Transfer check
How would the fixed-value target change if the triples had to sum to 12 instead of zero?
05
Running optimumFoundation
Best Time to Buy and Sell Stock
Choose an earlier value and a later value to maximize their difference.
Due now
Cold prompt
What single fact about the past is sufficient for every future selling day?
Say your answer aloud. Name the state and the rule that moves it. No typing is collected.
Constraints decoder
The buy must occur before the sell, so process days from left to right.
Duplicate prices are legal and irrelevant.
No transaction is allowed, so initialize profit to zero.
One nudge
For any future selling price, a cheaper earlier buy dominates every more expensive earlier buy.
Invariant
When today is evaluated, min_price is the cheapest price seen through today.
Derive it
Treat each current day as the possible sell day.
The best matching buy is the minimum price among earlier days.
Include the current price in the running minimum.
Update profit with current price minus that minimum; buying and selling today simply contributes zero.
Failure mode
Using a set or duplicate-removal window. This problem needs one best historical value, not the contents of a window.
Walk the smallest useful trace
[10, 1, 5, 6, 7, 1]
10 establishes the first minimum.
1 replaces it; future profits are measured from 1.
7 gives profit 6, the maximum.
Reveal the Rust implementation
Rust · accepted pattern
impl Solution {
pub fn max_profit(prices: Vec<i32>) -> i32 {
let mut min_price = i32::MAX;
let mut best = 0;
for price in prices {
min_price = min_price.min(price);
best = best.max(price - min_price);
}
best
}
}
Say this in the interview
“For each sell day, I only need the cheapest valid buy day seen before it.”
O(n) time · O(1) space
Transfer check
What breaks if the problem permits multiple transactions, and what new state would you need?
06
Sliding windowCore medium
Longest Substring Without Repeating Characters
Longest contiguous range satisfying a condition that can be repaired from the left.
Due now
Cold prompt
What must be true immediately before the window length is recorded?
Say your answer aloud. Name the state and the rule that moves it. No typing is collected.
Constraints decoder
The standard problem uses ASCII characters, so byte indexing is valid.
The answer is a length, not the substring itself.
A previous position can move left forward, never backward.
One nudge
Store the latest index of each byte, then jump left past a repeated byte when necessary.
Invariant
The window [left, right] contains no duplicate byte.
Derive it
Expand right one byte at a time.
If the byte was seen inside the current window, move left to previous + 1.
Use max so an old occurrence never moves left backward.
After repair, the window is valid; update its length.
Failure mode
Moving left to previous + 1 without max, which can reopen characters that already fell outside the window.
Walk the smallest useful trace
"abba"
"ab" reaches length 2.
The second b moves left from 0 to 2.
The final a was seen at 0, outside the current window, so left stays 2.
Reveal the Rust implementation
Rust · accepted pattern
use std::collections::HashMap;
impl Solution {
pub fn length_of_longest_substring(s: String) -> i32 {
let mut last_seen: HashMap<u8, usize> = HashMap::new();
let mut left = 0;
let mut best = 0;
for (right, &byte) in s.as_bytes().iter().enumerate() {
if let Some(&previous) = last_seen.get(&byte) {
left = left.max(previous + 1);
}
last_seen.insert(byte, right);
best = best.max(right - left + 1);
}
best as i32
}
}
Say this in the interview
“I expand right, repair the no-duplicate invariant by moving left forward, then update the longest valid length.”
O(n) time · O(min(n, alphabet)) space
Transfer check
How would the state change for “longest substring with at most k distinct characters”?
07
Monotonic stackCore medium
Daily Temperatures
For every item, find the next later item that is greater.
Due now
Cold prompt
What unfinished work should the stack remember?
Say your answer aloud. Name the state and the rule that moves it. No typing is collected.
Constraints decoder
The answer is a distance, so store indices rather than temperatures alone.
Equal temperatures do not resolve each other.
Every index can be pushed once and popped at most once.
One nudge
Treat the stack as a waiting room: who can today’s temperature resolve?
Invariant
The stack contains unresolved indices with temperatures decreasing from bottom to top.
Derive it
Push an index when its next warmer day is unknown.
When today is warmer than the top index, today is that index’s first valid answer.
Pop and store current index minus previous index.
Repeat because one hot day can resolve several colder days.
Failure mode
Counting warmer values already seen. The question asks for the first warmer future day and its index distance.
Walk the smallest useful trace
[30, 38, 30, 36, 35, 40, 28]
38 resolves day 0 after 1 day.
36 resolves day 2 after 1 day.
40 resolves days 4, 3, and 1 after 1, 2, and 4 days.
Reveal the Rust implementation
Rust · accepted pattern
impl Solution {
pub fn daily_temperatures(temperatures: Vec<i32>) -> Vec<i32> {
let mut answer = vec![0; temperatures.len()];
let mut stack: Vec<usize> = Vec::new();
for (day, &temperature) in temperatures.iter().enumerate() {
while let Some(&previous_day) = stack.last() {
if temperatures[previous_day] >= temperature {
break;
}
stack.pop();
answer[previous_day] = (day - previous_day) as i32;
}
stack.push(day);
}
answer
}
}
Say this in the interview
“The stack stores unresolved indices; each index enters once and leaves once, so the nested loop is still linear.”
O(n) amortized time · O(n) space
Transfer check
Which comparison would change for “next smaller element,” and would the stack be increasing or decreasing?
08
Stack machineCore medium
Evaluate Reverse Polish Notation
Operators appear after their operands and intermediate results feed later operations.
Due now
Cold prompt
When an operator arrives, what exactly leaves and re-enters the stack?
Say your answer aloud. Name the state and the rule that moves it. No typing is collected.
Constraints decoder
Tokens may be negative numbers, so numeric-string tests are fragile.
An operator is an instruction to act now—not a token to save for later.
Invariant
The stack contains only completed values waiting to become operands.
Derive it
Push every number immediately.
For an operator, pop the right operand first and the left operand second.
Compute left operator right.
Push the result so it can participate in a later operation.
Failure mode
Keeping one global running result, leaving consumed operands behind, or reversing subtraction and division operands.
Walk the smallest useful trace
["2", "1", "+", "3", "*"]
Push 2, push 1.
+ consumes 2 and 1, then produces 3.
Push 3; * consumes 3 and 3, then produces 9.
Reveal the Rust implementation
Rust · accepted pattern
impl Solution {
pub fn eval_rpn(tokens: Vec<String>) -> i32 {
let mut stack: Vec<i32> = Vec::new();
for token in tokens {
let value = match token.as_str() {
"+" | "-" | "*" | "/" => {
let right = stack.pop().unwrap();
let left = stack.pop().unwrap();
match token.as_str() {
"+" => left + right,
"-" => left - right,
"*" => left * right,
"/" => left / right,
_ => unreachable!(),
}
}
_ => token.parse().unwrap(),
};
stack.push(value);
}
stack.pop().unwrap()
}
}
Say this in the interview
“Each operator consumes the two most recent completed values and pushes one completed result back.”
O(n) time · O(n) space
Transfer check
Why does the same stack idea not directly evaluate ordinary infix notation without handling precedence?
09
Linked list ownershipOwnership gate
Reverse Linked List in Rust
Reverse every next pointer in place while preserving access to the unprocessed suffix.
Due now
Cold prompt
How do you move ownership of next without partially moving the current node?
Say your answer aloud. Name the state and the rule that moves it. No typing is collected.
Constraints decoder
Nodes are owned through Option<Box<ListNode>>.
The next link must be detached before the current node is linked backward.
Only a constant number of owned pointers is needed.
One nudge
Use take() to replace node.next with None and obtain ownership of the old link.
Invariant
prev owns the reversed prefix; head owns the untouched suffix.
Derive it
Move the current boxed node out of head with while let Some.
Detach and save its next node using node.next.take().
Point the current node to the reversed prefix.
Move the current node into prev and continue with the saved suffix.
Failure mode
Rewiring next before saving it, or trying to copy Box-owned nodes as if pointers were Python references.
Walk the smallest useful trace
1 → 2 → 3 → None
prev = 1; head owns 2 → 3.
prev = 2 → 1; head owns 3.
prev = 3 → 2 → 1; head = None.
Reveal the Rust implementation
Rust · accepted pattern
// Definition supplied by the judge:
// pub struct ListNode {
// pub val: i32,
// pub next: Option<Box<ListNode>>,
// }
impl Solution {
pub fn reverse_list(
mut head: Option<Box<ListNode>>,
) -> Option<Box<ListNode>> {
let mut prev = None;
while let Some(mut node) = head {
head = node.next.take();
node.next = prev;
prev = Some(node);
}
prev
}
}
Say this in the interview
“At every step, prev owns the reversed prefix and head owns the untouched suffix; take safely transfers the next link.”
O(n) time · O(1) auxiliary space
Transfer check
How would you find the middle first without taking ownership of the nodes?
10
Binary searchCore medium
Search a 2D Matrix
Rows are sorted and every row begins after the previous row ends.
Due now
Cold prompt
How can one index address the matrix without allocating a flattened copy?
Say your answer aloud. Name the state and the rule that moves it. No typing is collected.
Constraints decoder
The matrix and first row are non-empty in the standard problem contract.
The global row-order property makes the entire matrix monotonically sorted.
Use usize for Rust indexing.
One nudge
For cols columns, quotient chooses the row and remainder chooses the column.
Invariant
If the target exists, its virtual index remains inside the closed interval [left, right].
Derive it
Pretend the m × n matrix is one sorted array of length m × n.
Map virtual index mid to row = mid / cols and col = mid % cols.
Compare the matrix value with the target.
Discard half using the standard closed-interval binary-search rules.
Failure mode
Binary-searching rows but using a linear “contains” scan inside one row, or choosing a row from only its first value.
Walk the smallest useful trace
3 × 4 matrix, target 16
mid 5 maps to [1][1] = 11 → move right.
mid 8 maps to [2][0] = 23 → move left.
mid 6 maps to [1][2] = 16 → found.
Reveal the Rust implementation
Rust · accepted pattern
impl Solution {
pub fn search_matrix(matrix: Vec<Vec<i32>>, target: i32) -> bool {
if matrix.is_empty() || matrix[0].is_empty() {
return false;
}
let cols = matrix[0].len();
let mut left = 0usize;
let mut right = matrix.len() * cols;
while left < right {
let mid = left + (right - left) / 2;
let value = matrix[mid / cols][mid % cols];
if value < target {
left = mid + 1;
} else {
right = mid;
}
}
left < matrix.len() * cols
&& matrix[left / cols][left % cols] == target
}
}
Say this in the interview
“The row-order guarantee makes this one sorted virtual array, so ordinary binary search applies without flattening.”
O(log(m · n)) time · O(1) space
Transfer check
Why does this virtual-array method fail when rows and columns are sorted independently but row ranges overlap?
Scope
Built from learning signals, not private transcripts.
These lessons are an original synthesis of recurring misconceptions and durable interview patterns. No chat transcript, private answer, or learner-authored code is published or uploaded. OpenGrind is an independent open-source project and is not affiliated with LeetCode, NeetCode, Google, or any other interview platform or employer.