This first session maps the technical interview itself and then the small set of patterns most questions are built from. It covers what a 45-minute round actually contains and the four axes it is scored on, the six-step loop to run on any unseen problem, and how to read a problem's constraints as a statement of the complexity you are expected to hit. It then works the eight patterns that carry most interview questions - hash map, two pointers, sliding window, binary search, stack, BFS/DFS, heap, and dynamic programming - each with a runnable Python solution and a real execution trace, including Two Sum, valid palindrome, longest substring without repeating characters, daily temperatures, number of islands, climbing stairs, and merge intervals. It closes with the twenty most frequently asked problems mapped to their patterns, a narration and self-testing script for the round itself, and a ten-week practice plan. Traps target the mistakes that actually cost rounds: coding before clarifying, the accidental quadratic from a list membership test, rebuilding a window instead of sliding it, sorting away the indices the question asked for, and handing verification back to the interviewer.
Subject: DSA Interview Prep · 85 slides · code lesson
Open the interactive version of this deck · Homework for this lesson
Title
DSA Interview Prep ยท Session 1
A map of the interview itself, then the handful of patterns that most questions are wearing a costume over. Python throughout.
Objectives
Interview questions look infinite and are not. This first session builds the map: what a round actually contains, and which shapes keep coming back. By the end you can:
Section
Section 1
Estimation
Before we look at any list. LeetCode alone has well over 3,000 problems, and companies pull from all of them.
Predict first
Roughly how many distinct problem patterns would you have to know to have a workable approach to the large majority of interview questions?
Correct: Around 15 - and about 8 of them carry most of the weight.
Why: Interviewers reuse a small set of structural ideas: hash lookup, two pointers, sliding window, binary search, stack, tree/graph traversal, heap, and dynamic programming. Problems differ in story and in edge cases far more than in structure, which is exactly why memorizing solutions scales badly and recognizing patterns scales well.
Concept
Almost every company runs some version of this ladder. The DSA content is concentrated in the middle two.
The online assessment is scored only on passing tests - so speed and edge cases decide it. Every other round is scored by a person who is listening to your reasoning, which is a different game and the one we mostly train for.
Concept
A round is not 45 minutes of coding. Here is where the time actually goes in a round that goes well.
| minutes | what is happening | who is talking |
|---|---|---|
| 0-3 | Intro, problem statement | them |
| 3-8 | You clarify, restate, walk an example | you |
| 8-12 | Brute force stated out loud, complexity given | you |
| 12-18 | Optimize: the pattern is named and justified | both |
| 18-35 | Code it | you, narrating |
| 35-42 | Dry run on a real example, edge cases | you |
| 42-45 | Their questions, your questions | both |
Notice that coding is under half of it. Candidates who spend minutes 3-12 silently typing are the ones who get 'weak communication' on the feedback form even when the code works.
Intuition
Interview feedback forms at most large companies rate roughly four things separately. This is why a working solution can still fail a round.
Two candidates can produce the same final code and get opposite decisions. The difference is almost always communication and verification - the two you can improve fastest, and the two we drill every session.
McDowell, Cracking the Coding Interview, 6th ed. Ch. VI — the same four axes, described from the interviewer's side
Two truths and a lie
Sort each claim. Commit before you scroll - the wrong one is wrong for a reason worth having.
Sort into buckets
Which of these hold up, and which is folklore?
Concept
Every DSA question you get will sit in one of three families, and the family tells you how to spend the first five minutes.
| family | what it tests | example prompt | your first move |
|---|---|---|---|
| Implementation | Careful coding, edge cases | Merge two sorted lists | Clarify the edge cases, then just write it |
| Pattern recognition | Do you see the structure | Longest substring without repeating characters | Name the pattern out loud before coding |
| Open / design-flavoured | Trade-offs and judgement | Design a data structure with O(1) insert, delete, getRandom | Ask about scale and operations, then choose structures |
Most of a 2-3 month prep window should go to the middle family - it is the biggest, and it is the one that transfers to problems you have not seen.
Sorting
Take thirty seconds per prompt. You are not solving them.
Sort into buckets
Sort each prompt into its family.
Valid parentheses is borderline - it needs a stack, but the stack is the first thing anyone thinks of. When a pattern is that obvious, the question has become an implementation question.
Missing information
Interviewers under-specify on purpose. The unasked question is the trap.
Prompt: "Given an array of numbers, return the two that add up to a target."
Discussion prompt
Write down every question you would ask before touching the keyboard.
Hint: Think about the input's shape, its contents, and what 'return' means.
Answer:
None, or raise?Six questions, about forty seconds, and every one of them changes the code you would write. This is the cheapest signal you can send.
Trap
Interviewer: "Find the two numbers that sum to the target."
Immediately type for i in range(len(nums)):
Why: The silence felt like failure, so typing felt like progress.
Four minutes later, discover the array was sorted and indices were wanted
Why: Now the nested loop has to be thrown away and the clock has moved.
What the interviewer wrote down: jumped to code, did not scope the problem, needed a hint to find the sorted-array optimization.
Interviewer: "Find the two numbers that sum to the target."
Say: "Let me restate it and ask two things first."
Why: This buys thinking time and reads as method, not hesitation.
Ask about sorting and about indices-versus-values, then state the brute force and its cost out loud
Why: Both answers redirect the solution before any code exists, and the brute force gives you a working fallback you can always retreat to.
What the interviewer wrote down: scoped the problem, identified the quadratic baseline unprompted, chose the right structure.
Pattern
This is the procedure. It does not change with the problem, and it is what we will rehearse until it is automatic.
Steps 1-4 should take about a third of the round. If you have said nothing for two minutes at any point, you are off the loop.
Explain it to yourself
Discussion prompt
Step 3 costs you about ninety seconds and produces code you are about to throw away. Argue for it - why is it worth the time?
Hint: Think about what happens if you run out of clock at minute 40.
Answer:
A candidate who says "brute force is quadratic, and the waste is the repeated inner scan" has already half-derived the hash map.
Check
Answer before you look. This one is about method, not code.
Check your understanding
The interviewer finishes reading a problem you have never seen. What is the highest-value use of your next three minutes?
Answer: B
Why: Restating plus one hand-worked example catches almost every misunderstanding, and it does so while the clock is cheap. It also generates the test case you will dry-run at minute 38, so the three minutes are spent twice.
Section
Section 2
Warm-up
Discussion prompt
Without looking anything up, write the time complexity of each: (a) looking up a key in a Python dict, (b) x in my_list, (c) my_list.sort(), (d) my_list.insert(0, x), (e) my_set.add(x).
Hint: Two of these are the ones candidates get wrong under pressure.
Answer:
| operation | average cost | why |
|---|---|---|
d[k] | O(1) | hash straight to a bucket |
x in my_list | O(n) | linear scan, no index |
my_list.sort() | O(n log n) | Timsort |
my_list.insert(0, x) | O(n) | every later element shifts right |
my_set.add(x) | O(1) | same hashing as dict |
x in my_list versus x in my_set is the difference between a quadratic and a linear solution, and it is one character of Python. This is the most common accidental O(n squared) in interviews.
Python Wiki - TimeComplexity (per-operation costs for list, dict, set) list / dict / set — the authoritative per-operation table
Concept
Big-O answers exactly one question: when the input gets 10 times bigger, what happens to the work? It says nothing about milliseconds on your laptop.
O(f(n)) — The shape of the growth curve as n gets large, with constant factors and lower-order terms dropped - because those stop mattering exactly when n stops being small.
| complexity | n = 10 | n = 1,000 | n = 1,000,000 | verdict at n = 10^6 |
|---|---|---|---|---|
| O(log n) | 3 | 10 | 20 | instant |
| O(n) | 10 | 1,000 | 1,000,000 | fine |
| O(n log n) | 33 | 10,000 | 20,000,000 | fine |
| O(n^2) | 100 | 1,000,000 | 10^12 | hopeless |
| O(2^n) | 1,024 | astronomical | astronomical | hopeless past n = 30 |
Read the bottom two rows again. The gap between n log n and n^2 is not a tuning problem - at a million elements it is 2 x 10^7 operations against 10^12, which at a realistic ten million operations per second is a couple of seconds against more than a day.
Scale up
count_pairs compares every element with every later element. Step through what happens as the input grows - these counts are exact, not estimates.
Step through it
At which step does this stop being a viable interview answer?
The shape to remember: the input grew 10x, the work grew 100x. That squaring is what O(n^2) means, and it is why the constraint line in the problem statement is the most important line in it.
Notation
Candidates say 'oh of en log en' fluently and cannot say where the log came from. Read each piece.
Annotate
On: \( T(n) = O(n \log n) \)
If you can say where the log comes from, you can derive the complexity of a solution you have never analysed. If you cannot, you are reciting.
Concept
Every well-posed problem states a bound on n. That bound is the interviewer telling you which complexity they will accept.
| n up to | budget | target complexity | typical shape |
|---|---|---|---|
| 10-20 | anything | O(2^n) or O(n!) | backtracking, permutations |
| ~500 | generous | O(n^3) | triple loop, some DP |
| ~5,000 | moderate | O(n^2) | DP table, all pairs |
| ~10^5 | tight | O(n log n) or O(n) | sort, heap, window, hash |
| ~10^9 | extreme | O(log n) or O(1) | binary search, math, formula |
Read the table right to left in the interview: you are given n up to 10^5, so you need at most n log n, so sorting is affordable but a nested loop is not. That reasoning, said out loud, is worth real points.
Discrimination
For each constraint, choose the complexity you should be aiming for. No code.
Sort into buckets
Sort each constraint into the complexity you should target.
Cost model
The trap is not the loop you can see. Read this and find the expensive line.
Annotate
n in seen scans a list linearly, so it is O(n) by itself - inside an O(n) loop. Total: O(n^2).seen = set(). Membership becomes O(1) and the whole function becomes O(n). One word.This function is the single most common accidental quadratic in Python interviews, and it is invisible unless you cost the operations inside the loop rather than counting the loops.
Worked example
Interviewers ask 'what is the complexity of what you just wrote?' every single round. Here is the procedure, on a function with two loops that are not nested and one that is.
def summarize(nums):
total = 0
for n in nums: # A
total += n
nums_sorted = sorted(nums) # B
pairs = 0
for i in range(len(nums)): # C
for j in range(i + 1, len(nums)):
pairs += 1
return total, nums_sorted[0], pairsCost each region on its own, in terms of n
Why: Regions that run one after another are added, not multiplied - the sequencing is what most candidates get wrong first.
| region | what it does | cost |
|---|---|---|
| A | one pass, O(1) work per element | O(n) |
| B | Timsort | O(n log n) |
| C | every pair (i, j) with j > i | O(n^2) |
Add them, then keep only the fastest-growing term
Why: O(n) + O(n log n) + O(n^2) = O(n^2). Lower-order terms vanish because at large n the square dwarfs the rest.
Check the space separately
Why: sorted() allocates a new list of n elements, so space is O(n) even though total and pairs are O(1). Interviewers ask for both and candidates volunteer only time.
Verify against the real counts
Why: Region C on n = 1,000 executes 499,500 times, which is n(n-1)/2 - exactly the quadratic the analysis predicted.
Trap
for n in nums:
total += n
for n in nums:
biggest = max(biggest, n)Say "two loops over nums, so it's O(n^2)"
Why: Counting loops instead of counting the work each element causes.
The real cost is O(n) + O(n) = O(2n) = O(n). Sequential loops add.
for i in range(len(nums)):
for j in range(len(nums)):
check(nums[i], nums[j])Ask: for each element of the outer loop, how much work happens inside?
Why: n elements, each causing n units of inner work, so the costs multiply: O(n^2). Nesting multiplies, sequencing adds.
| shape | n = 1,000 iterations | cost |
|---|---|---|
| two loops in sequence | 1,000 + 1,000 = 2,000 | O(n) |
| one loop inside another | 1,000 x 1,000 = 1,000,000 | O(n^2) |
The test is not how many for keywords are on screen - it is whether one loop runs inside another.
Concept
Every complexity answer has two halves. Saying only the time half is a missed point in almost every round.
| what you allocate | space | note |
|---|---|---|
| A few counters and pointers | O(1) | loop variables are free |
| A hash map of every element | O(n) | the usual price of a hash solution |
A sorted copy via sorted() | O(n) | list.sort() in place is O(1) extra |
| A recursion stack of depth d | O(d) | for a tree, that is the height |
| A DP table of n by m | O(n*m) | often reducible to one row |
Say both halves without being asked: "O(n) time, O(n) extra space - and I can get space down to O(1) if I'm allowed to sort in place." That sentence is a trade-off, which is what the design half of the score is looking for.
Check
Read it carefully. The answer is not the number of loops.
Check your understanding
What is the time complexity of first_duplicate, where seen is a list and the membership test is if n in seen?
Answer: B
Why: n in seen on a list is a linear scan, so each of the n iterations can do up to n comparisons: O(n^2). Changing seen to a set makes membership O(1) on average and the whole function O(n) - a one-word fix worth naming out loud in a round.
Section
Section 3
Concept
Interview problems announce themselves. This table is the reflex we are building - phrase on the left, structure on the right.
| the prompt says... | reach for | typical cost |
|---|---|---|
| "has it appeared before", "count of each" | hash map / set | O(n) |
| "sorted array", "pair that sums to" | two pointers | O(n) |
| "substring", "subarray", "consecutive" | sliding window | O(n) |
| "sorted", "find the boundary", "minimum such that" | binary search | O(log n) |
| "matching", "nesting", "next greater" | stack | O(n) |
| "shortest path in steps", "connected region" | BFS / DFS | O(V + E) |
| "top K", "K largest", "running median" | heap | O(n log k) |
| "number of ways", "maximum over choices" | dynamic programming | O(n) to O(n*m) |
Saying "the word substring plus a constraint on characters makes me think sliding window" is not guessing. It is the reasoning interviewers most want to hear, because it is what transfers to the next problem.
Matching
No solving. Just pair each phrase with the structure it should trigger.
Match the pairs
Match each phrase from a problem statement to the pattern it signals.
Why: "Sorted" is the loudest word in any prompt - it means you can move two pointers inward or halve the search space, and it is free information you paid nothing for. "Longest ... such that" is a window growing and shrinking. "K most" is a heap of size k, not a full sort. "Every combination" is exhaustive search with pruning. "Appeared before" is membership, which is a set.
Concept
The hash map is the most common single idea in interview questions. It turns "have I seen this?" from a scan into a lookup.
# The whole pattern, in three lines
seen = {} # value -> where I saw it
for i, n in enumerate(nums):
if n in seen: # O(1) average, not a scan
return seen[n], i
seen[n] = i| question | without a hash map | with a hash map |
|---|---|---|
| Is x in the collection? | O(n) scan | O(1) average |
| How many times does x occur? | O(n) per query | O(1) after one O(n) pass |
| Where did I last see x? | O(n) scan | O(1) average |
| Total for n queries | O(n^2) | O(n) |
The cost is O(n) extra space. That is the trade you should say out loud: "I'll spend O(n) memory to drop the time from quadratic to linear."
Python Wiki - TimeComplexity (per-operation costs for list, dict, set) dict — average-case O(1) get and set
Prediction
Commit to an answer. This is the shape of a dozen real questions.
def first_repeat(nums):
seen = set()
for n in nums:
if n in seen:
return n
seen.add(n)
return None
print(first_repeat([5, 3, 9, 3, 5]))Predict first
What does this print - and how many elements does it look at before it stops?
Correct: 3, after looking at 4 elements.
It prints 3, and it never reads the last element.
Why: The loop returns on the first value that has already been seen, not on the value that repeats earliest in the array. 5 repeats too, but its second occurrence is at index 4, while 3's second occurrence is at index 3 - so the function stops there and never reads the final 5. Interviewers love this distinction because 'first duplicate' is ambiguous until you ask.
| index | n | seen before this step | action |
|---|---|---|---|
| 0 | 5 | {} | add 5 |
| 1 | 3 | {5} | add 3 |
| 2 | 9 | {5, 3} | add 9 |
| 3 | 3 | {5, 3, 9} | return 3 - index 4 is never reached |
"First duplicate" is ambiguous: first by second occurrence (what this code does) or first by first occurrence (which would be 5). Ask. This is a real clarifying question, not a pedantic one.
Worked example
Given nums and a target, return the indices of the two numbers that add to the target. This is the single most commonly assigned interview problem, and its one-pass solution is the template for a dozen others.
State the brute force and its cost first
Why: Every pair is O(n^2) time and O(1) space. It is correct, and it is the fallback you keep in your pocket.
Name the waste: the inner loop re-asks a question the outer loop already answered
Why: For each n we are searching the rest of the array for target - n. That is a membership question, and membership is what a hash map does in O(1).
def two_sum(nums, target):
seen = {} # value -> index
for i, n in enumerate(nums):
need = target - n
if need in seen:
return [seen[need], i]
seen[n] = i
return []Trace it on nums = [2, 7, 11, 15], target = 26
Why: The table below is the actual execution - each row is one iteration, showing what seen held before the check.
| i | n | need = 26 - n | need in seen? | seen before this step |
|---|---|---|---|---|
| 0 | 2 | 24 | no | {} |
| 1 | 7 | 19 | no | {2: 0} |
| 2 | 11 | 15 | no | {2: 0, 7: 1} |
| 3 | 15 | 11 | yes | {2: 0, 7: 1, 11: 2} |
Verify the returned indices against the input
Why: It returns [2, 3]; nums[2] + nums[3] = 11 + 15 = 26. Correct, in one pass, O(n) time and O(n) space.
The move worth stealing: store what you have seen keyed by what a future element would need. That single idea also solves subarray-sum-equals-k, pairs-with-difference-k, and contains-duplicate-within-distance-k.
Trap
"The array is unsorted, so let me sort it and use two pointers."
nums.sort() # destroys the original positions
lo, hi = 0, len(nums) - 1
# ... two-pointer scan finds the values 11 and 15Return the two indices found after sorting
Why: Those indices point into the sorted array. The caller asked about the original one, so the answer is wrong even though the values are right.
| original | after sort | |
|---|---|---|
| array | [15, 2, 11, 7] | [2, 7, 11, 15] |
| index of 11 | 2 | 2 |
| index of 15 | 0 | 3 |
Ask at minute 2: "Do you want the indices or the values?"
If indices: use the hash map, leave the array untouched
Why: O(n) time, O(n) space, positions preserved - and it beats the sorted approach's O(n log n) anyway.
If values, and the array is already sorted: two pointers
Why: O(n) time and O(1) space, which is strictly better than hashing - so the right answer genuinely depends on the clarifying question.
| asked for | array state | best approach | cost |
|---|---|---|---|
| indices | unsorted | hash map | O(n) time, O(n) space |
| values | sorted | two pointers | O(n) time, O(1) space |
| values | unsorted | sort, then two pointers | O(n log n) time |
Concept
When the data is sorted, or the answer depends on a pair, two indices moving toward each other replace a nested loop with a single pass.
lo, hi = 0, len(a) - 1
while lo < hi:
total = a[lo] + a[hi]
if total == target:
return lo, hi
if total < target:
lo += 1 # need more: only the left end can grow
else:
hi -= 1 # need less: only the right end can shrink| variant | pointers start | they move | example question |
|---|---|---|---|
| converging | both ends | inward | two-sum on a sorted array |
| same direction | both at 0 | fast one leads | remove duplicates in place |
| fast / slow | both at head | fast moves 2x | linked-list cycle detection |
| read / write | both at 0 | write lags read | compact an array in place |
The reason it is O(n) and not O(n^2): each pointer only ever moves in one direction, so together they take at most n steps in total.
Invariant
Step through a palindrome check on "Race car!" and watch for the thing that never changes. The cleaned string is racecar.
Step through it
What is true about the characters OUTSIDE lo and hi at every single step?
The invariant: everything outside lo and hi has already been verified to match. When the pointers meet, 'outside' is the whole string, so the answer is True. That sentence is the proof of correctness, and being able to state it is what separates a memorized loop from an understood one.
Worked example
Given a string, ignore case and non-alphanumeric characters, and decide whether it reads the same forwards and backwards.
Clarify before coding
Why: What counts as a character? Are digits included? Is the empty string a palindrome? (Conventionally yes - and saying so is worth a point.)
def is_palindrome(s):
t = [c.lower() for c in s if c.isalnum()]
lo, hi = 0, len(t) - 1
while lo < hi:
if t[lo] != t[hi]:
return False
lo += 1
hi -= 1
return TrueTrace it on "Race car!"
Why: After cleaning, t = racecar. Each row is one pass of the while loop, taken from the real run.
| pass | lo | t[lo] | hi | t[hi] | equal? |
|---|---|---|---|---|---|
| 1 | 0 | r | 6 | r | yes |
| 2 | 1 | a | 5 | a | yes |
| 3 | 2 | c | 4 | c | yes |
| - | 3 | - | 3 | - | lo is not < hi, loop ends |
Verify: the loop ended without returning False, so the result is True
Why: is_palindrome("Race car!") returns True and is_palindrome("hello") returns False on the first comparison, h against o.
Cost: O(n) time, and O(n) space because of the cleaned list. If asked for O(1) space, skip non-alphanumeric characters in place with two while loops inside the main one - a natural follow-up, so have the answer ready.
Intuition
The words substring, subarray, and consecutive all mean the same thing structurally: a contiguous stretch with two ends.
The naive solution rebuilds every stretch from scratch: n starting points times n lengths, and O(n) work to evaluate each - cubic, or quadratic if you are careful. The window keeps one stretch and edits its ends.
| approach | work per new stretch | total |
|---|---|---|
| rebuild each substring | O(n) to re-scan it | O(n^2) or worse |
| slide the window | O(1) to add one char and drop some | O(n) |
Mental image: you are not printing a new photo each time, you are dragging the edges of a crop box. Everything already inside the box stays valid.
Concept
Which one you need is decided by a single question: is the window's size given to you, or is it whatever the constraint allows?
# fixed size k: one in, one out
window = sum(nums[:k])
best = window
for i in range(k, len(nums)):
window += nums[i] - nums[i - k]
best = max(best, window)
# variable size: grow always, shrink while the rule is broken
start = 0
for end in range(len(nums)):
add(nums[end])
while broken():
remove(nums[start])
start += 1| clue in the prompt | flavour | example |
|---|---|---|
| "...of size k", "of length k" | fixed | max sum subarray of size k |
| "longest ... such that" | variable, maximize | longest substring with no repeats |
| "shortest ... containing" | variable, minimize | minimum window substring |
| "at most k distinct" | variable with a counter | longest substring with k distinct characters |
Both are O(n) because every index enters the window once and leaves at most once - the inner while does not make it quadratic, and being able to say why is a frequent follow-up question.
Fill the middle
Everything here is standard except one line. Fill it in - this is the line that decides whether the window is correct.
Fill in the blanks
Complete the two blanks.
def longest_unique(s):
last = last[ch] + 1 # char -> most recent index
start = 0 # left edge of the window
best = 0
for i, ch in enumerate(s):
if ch in last and last[ch] >= start:
start = i - start + 1
last[ch] = i
best = max(best, ___)
return best
Why: start = last[ch] + 1 jumps the left edge to just past the previous copy of this character, which is what makes the whole scan O(n) - creeping forward one index at a time would re-check characters and make it quadratic. The guard last[ch] >= start matters just as much: without it, a character last seen before the window would drag start backwards and shrink the answer. And the window length is i - start + 1 because both ends are inclusive - the off-by-one here is the most common bug in this pattern.
Worked example
A top-five most-asked question, and the cleanest example of a variable window. Input: "abcabcbb".
Clarify first
Why: Is the input ASCII or unicode? Do we return the length or the substring itself? Is an empty string valid input?
Brute force, out loud, not typed
Why: Check every substring for uniqueness: O(n^2) substrings, O(n) to check each, so O(n^3). The waste is re-checking characters that were already known unique.
def longest_unique(s):
last = {}
start = 0
best = 0
for i, ch in enumerate(s):
if ch in last and last[ch] >= start:
start = last[ch] + 1
last[ch] = i
best = max(best, i - start + 1)
return bestTrace every index on "abcabcbb" - this is the real execution
Why: Watch start jump rather than creep, and watch best only ever grow.
| i | ch | start after | window | length | best |
|---|---|---|---|---|---|
| 0 | a | 0 | a | 1 | 1 |
| 1 | b | 0 | ab | 2 | 2 |
| 2 | c | 0 | abc | 3 | 3 |
| 3 | a | 1 | bca | 3 | 3 |
| 4 | b | 2 | cab | 3 | 3 |
| 5 | c | 3 | abc | 3 | 3 |
| 6 | b | 5 | cb | 2 | 3 |
| 7 | b | 7 | b | 1 | 3 |
Verify against the edge cases
Why: "abcabcbb" gives 3, "bbbbb" gives 1, "pwwkew" gives 3 (wke, not the non-contiguous pwke). All three match the real output.
Cost: O(n) time - each index is visited once - and O(min(n, alphabet)) space for the map. Say the alphabet bound; it is the kind of precision that reads as experience.
Trap
best = 0
for i in range(len(nums) - k + 1):
best = max(best, sum(nums[i:i + k]))Call sum() on a fresh slice for every window
Why: It looks like one loop, so it looks linear. But nums[i:i+k] copies k elements and sum adds k of them, inside a loop that runs n times.
| n | k | operations |
|---|---|---|
| 1,000 | 100 | about 100,000 |
| 100,000 | 1,000 | about 100,000,000 - times out |
window = sum(nums[:k])
best = window
for i in range(k, len(nums)):
window += nums[i] - nums[i - k]
best = max(best, window)Add the entering element, subtract the leaving one
Why: The window's value is repaired in O(1) instead of rebuilt in O(k). This is the whole point of the pattern.
| n | k | operations |
|---|---|---|
| 1,000 | 100 | about 1,000 |
| 100,000 | 1,000 | about 100,000 - instant |
Concept
Any time the data is sorted, or the answer is monotonic ("if 7 works then 8 works"), you can halve the space instead of scanning it.
def bsearch(a, x):
lo, hi = 0, len(a) - 1
while lo <= hi: # inclusive range [lo, hi]
mid = (lo + hi) // 2
if a[mid] == x:
return mid
if a[mid] < x:
lo = mid + 1 # x must be to the right
else:
hi = mid - 1 # x must be to the left
return -1Trace it: find 23 in [2, 5, 8, 12, 16, 23, 38, 56]
Why: Eight elements, and it finishes in two comparisons - that is the log.
| pass | lo | hi | mid | a[mid] | decision |
|---|---|---|---|---|---|
| 1 | 0 | 7 | 3 | 12 | 12 < 23, go right: lo = 4 |
| 2 | 4 | 7 | 5 | 23 | found at index 5 |
The version worth memorizing is not this one but its cousin: the smallest index where a condition first becomes true. Most real binary-search interview questions - rotated array, first bad version, minimum capacity - are that boundary search wearing a costume.
Error analysis
It compiles, it passes the happy path, and on some inputs it never returns. Read it as if reviewing a colleague's pull request.
Annotate
lo = mid does not shrink the range when mid == lo. With lo = 0, hi = 1, mid is 0, and if a[0] < x we set lo = 0 again - the loop spins forever.lo = mid + 1. Since a[mid] has already been compared and rejected, excluding it is safe, and it guarantees the range strictly shrinks.hi = len(a) with while lo < hi is a half-open range, which is fine on its own - but it must be paired consistently. Mixing hi = len(a) - 1 with lo < hi silently drops the last element.hi = mid is correct in the half-open convention because hi is exclusive. Pick one convention per problem and say which you are using out loud.The rule that prevents every version of this bug: each iteration must make the range strictly smaller. Before you write the loop, say which of lo/hi is inclusive - most binary-search bugs are convention drift, not logic errors.
Faded example
The boundary search - find the first index where the condition holds. Fill the three blanks.
Fill in the blanks
Complete the boundary search.
def first_true(a, condition):
lo, hi = 0, len(a) - 1
answer = -1
while lo <= hi:
mid = (lo + hi) // 2
if condition(a[mid]):
answer = mid
hi = mid - 1 # a better answer may still be left of mid
else:
lo = mid + 1
return answer
Why: This template answers 'first bad version', 'minimum eating speed', 'smallest divisor', 'search insert position' and every rotated-array variant - which is most of the binary-search questions asked. The key difference from plain search is that finding a match does not return: it records the match and keeps shrinking toward the left edge, so the loop always ends at the first index that works.
Concept
A stack is the right structure whenever the thing you need next is the thing you saw most recently: nesting, matching, undo, and 'next greater element'.
def valid(s):
pairs = {')': '(', ']': '[', '}': '{'}
stack = []
for ch in s:
if ch in '([{':
stack.append(ch)
else:
if not stack or stack.pop() != pairs[ch]:
return False
return not stackTrace it on "([{}])"
Why: Every closer must match the most recent unmatched opener - which is exactly the top of the stack.
| char | action | stack after |
|---|---|---|
| ( | push | ['('] |
| [ | push | ['(', '['] |
| { | push | ['(', '[', '{'] |
| } | pop { - matches | ['(', '['] |
| ] | pop [ - matches | ['('] |
| ) | pop ( - matches | [] |
Two edge cases carry the whole question, and both are on line 8 and line 10: a closer arriving when the stack is empty ("())"), and leftovers on the stack at the end ("(("). Candidates who only check the middle case get one of the hidden tests wrong.
Prediction
Four inputs to the bracket checker above. Decide all four before revealing.
Predict first
Which of these return True: "([{}])", "(]", "((", "())("?
Correct: Only the first.
Why: "([{}])" is properly nested. "(]" pops ( and compares it with the [ that ] requires - mismatch. "((" never fails during the loop but ends with two unmatched openers, so the final return not stack catches it. "())(" fails on the third character, where a closer arrives with an empty stack. Those last two are exactly the cases a rushed implementation misses.
| input | result | which guard catches it |
|---|---|---|
| ([{}]) | True | - |
| (] | False | stack.pop() != pairs[ch] |
| (( | False | return not stack at the end |
| ())( | False | not stack when a closer arrives |
Worked example
Given daily temperatures, return for each day how many days until a warmer one. This is the standard 'next greater element' question, and it is the one that separates candidates who know a stack from candidates who know when.
Brute force first, out loud
Why: For each day, scan forward until a warmer day: O(n^2). The waste is that a cold day gets re-scanned by every earlier day.
The insight: keep the days still waiting for an answer on a stack
Why: When today is warmer than the day on top of the stack, today is that day's answer - and we can pop it, permanently.
def daily(temps):
out = [0] * len(temps)
stack = [] # indices, temperatures decreasing
for i, t in enumerate(temps):
while stack and temps[stack[-1]] < t:
j = stack.pop()
out[j] = i - j
stack.append(i)
return outTrace it on [73, 74, 75, 71, 69, 72, 76, 73]
Why: The stack always holds indices whose temperatures decrease from bottom to top - that is the invariant that makes the pops safe.
| i | temp | indices popped | stack after | out so far |
|---|---|---|---|---|
| 0 | 73 | - | [0] | [0,0,0,0,0,0,0,0] |
| 1 | 74 | 0 | [1] | [1,0,0,0,0,0,0,0] |
| 2 | 75 | 1 | [2] | [1,1,0,0,0,0,0,0] |
| 3 | 71 | - | [2,3] | unchanged |
| 4 | 69 | - | [2,3,4] | unchanged |
| 5 | 72 | 4, 3 | [2,5] | [1,1,0,2,1,0,0,0] |
| 6 | 76 | 5, 2 | [6] | [1,1,4,2,1,1,0,0] |
| 7 | 73 | - | [6,7] | final: [1,1,4,2,1,1,0,0] |
Verify the interesting entries by hand
Why: Day 2 (75 degrees) waits until day 6 (76): 6 - 2 = 4. Correct. Days 6 and 7 never get a warmer day, so they keep 0 - which is why the output array is initialised with zeros rather than left empty.
Why it is O(n) despite the inner while: every index is pushed once and popped at most once, so the total number of pops across the whole run is at most n. That amortized argument is the follow-up question, every time.
Concept
Every tree question and every grid question is one of these two walks. The only structural difference is which end of the pending collection you take from.
from collections import deque
def bfs(start, neighbors):
seen = {start}
q = deque([start])
while q:
node = q.popleft() # FIFO -> level by level
for nxt in neighbors(node):
if nxt not in seen:
seen.add(nxt)
q.append(nxt)
def dfs(node, neighbors, seen):
seen.add(node)
for nxt in neighbors(node): # LIFO via the call stack
if nxt not in seen:
dfs(nxt, neighbors, seen)| BFS | DFS | |
|---|---|---|
| structure | queue (deque.popleft) | recursion or an explicit stack |
| visits | nearest first, level by level | one branch to the bottom first |
| memory | O(width of the level) | O(depth) |
| shortest path? | yes, on unweighted graphs | no |
| typical use | min steps, level order | connected regions, cycles, paths |
The seen set is not optional and is not an optimization - without it a graph with any cycle loops forever. Forgetting it on a grid problem, where every cell has a path back, is the most common graph mistake in interviews.
Trade off
Complete the blanks from what you just read. Both are correct answers to different questions.
Comparison matrix
| question | BFS | DFS |
|---|---|---|
| Fewest moves on an unweighted graph | yes | no - the first path found may be long |
| Extra memory used | O(width) | O(depth) |
| Deep, narrow tree (a linked list, say) | safe - the level is tiny | risky - recursion depth |
| Count connected regions in a grid | works fine | works fine, usually shorter code |
The one that decides real rounds: "shortest" or "fewest" plus "unweighted" means BFS. If edges have weights, neither is right and the answer is Dijkstra - a good sentence to have ready.
Worked example
Given a grid of "1" (land) and "0" (water), count the connected regions of land. Grids show up constantly, and the trick is seeing that a cell's neighbours are its four adjacent cells.
Clarify
Why: Is diagonal adjacency connected? (Usually no.) May I modify the input grid? That answer decides whether you need a separate visited set.
def num_islands(grid):
rows, cols = len(grid), len(grid[0])
count = 0
def sink(r, c):
if r < 0 or c < 0 or r >= rows or c >= cols or grid[r][c] != '1':
return
grid[r][c] = '0' # mark visited by sinking it
sink(r + 1, c); sink(r - 1, c)
sink(r, c + 1); sink(r, c - 1)
for r in range(rows):
for c in range(cols):
if grid[r][c] == '1':
count += 1 # a new island starts here
sink(r, c)
return countTrace the outer scan on a 4x5 grid
Why: Rows: 11000, 11000, 00100, 00011. The scan only ever starts a flood fill on land it has not already sunk.
| cell reached by the scan | grid value there | action | count |
|---|---|---|---|
| (0,0) | 1 | new island, sink 4 cells | 1 |
| (0,1) ... (1,1) | 0 (already sunk) | skip | 1 |
| (2,2) | 1 | new island, sink 1 cell | 2 |
| (3,3) | 1 | new island, sink 2 cells | 3 |
| everything else | 0 | skip | 3 |
Verify by eye and by cost
Why: Three regions: the 2x2 block, the single cell, and the pair - the function returns 3. Every cell is visited a constant number of times, so it is O(rows x cols) time and O(rows x cols) worst-case recursion depth.
Mention the recursion depth unprompted: on a 1000x1000 all-land grid, recursive DFS can exceed Python's stack limit, and the fix is an explicit stack or BFS. Naming a failure mode of your own solution scores well.
Ranking
These five steps solve nearly every grid question. Put them in the order you would actually perform them.
Put in order
Order the steps for attacking a grid traversal problem.
Why: Clarifying comes first because the answers change the code. The BFS/DFS choice comes next because it decides the skeleton. The bounds-and-visited guard is written before the recursive calls - writing it last is how infinite recursion gets shipped. Hand-walking a small example catches sign errors in the neighbour offsets before the interviewer does, and the cost statement closes the round with the analysis they were going to ask for anyway.
Intuition
Dynamic programming has a fearsome name and a small idea: some recursive problems ask the same sub-question over and over, so you write the answer down the first time.
Naive fib(40) makes about 205 million calls, and roughly 15 million of them are recomputing fib(5) alone. Storing each answer once turns an exponential tree into a linear walk - the algorithm did not get cleverer, it just stopped forgetting.
| plain recursion | with memo | bottom-up | |
|---|---|---|---|
| calls for fib(40) | about 205 million | 40 | 40 |
| time | O(2^n) | O(n) | O(n) |
| space | O(n) stack | O(n) + stack | O(1) if you keep two values |
Two questions identify a DP problem: is the answer built from answers to smaller versions of the same problem, and do those smaller versions repeat? If yes to both, write the recurrence before any code.
Concept
Top-down memoization and bottom-up tabulation compute identical values. Write whichever makes the recurrence obvious - then say the other exists.
from functools import cache
@cache # top-down: recursion + a memo
def climb(n):
if n <= 2:
return n
return climb(n - 1) + climb(n - 2)
def climb_iter(n): # bottom-up: no recursion at all
a, b = 1, 1
for _ in range(n - 1):
a, b = b, a + b
return b| top-down (memo) | bottom-up (table) | |
|---|---|---|
| how you write it | the recurrence, literally | a loop filling an array |
| risk | recursion depth on large n | none |
| space | O(n) memo + O(n) stack | O(n), often reducible to O(1) |
| best when | the state space is sparse | every state is needed anyway |
In an interview, write the memoized version first - it is closer to the recurrence you just said aloud, so it is easier to justify - then offer the iterative rewrite as the optimization. That sequence is itself a signal.
Worked example
You can climb 1 or 2 steps at a time. How many distinct ways to reach step n? This is the DP everyone is asked first, and its recurrence is the one to be able to derive cold.
Derive the recurrence instead of recalling it
Why: The last move onto step n was either a 1-step from n-1 or a 2-step from n-2. Those two sets of paths are disjoint and cover everything, so ways(n) = ways(n-1) + ways(n-2).
Pin the base cases with tiny hand cases, not intuition
Why: ways(1) = 1 (one step). ways(2) = 2 (1+1, or 2). Getting these wrong shifts the whole sequence, and it is the most common error on this problem.
def climb(n):
a, b = 1, 1 # a = ways(n-2), b = ways(n-1)
for _ in range(n - 1):
a, b = b, a + b
return bTrace it to n = 6
Why: Each row is one pass of the loop, from the real execution.
| pass | a | b | meaning |
|---|---|---|---|
| start | 1 | 1 | ways(1) = 1 |
| 1 | 1 | 2 | ways(2) = 2 |
| 2 | 2 | 3 | ways(3) = 3 |
| 3 | 3 | 5 | ways(4) = 5 |
| 4 | 5 | 8 | ways(5) = 8 |
| 5 | 8 | 13 | ways(6) = 13 |
Verify against a hand count
Why: For n = 4 the paths are 1111, 112, 121, 211, 22 - five of them, matching the table. The sequence 1, 2, 3, 5, 8, 13 is Fibonacci shifted by one, which is a good sanity check to mention.
Cost: O(n) time and O(1) space because only two values are kept. House Robber, Min Cost Climbing Stairs, and Decode Ways are all this same loop with a different line inside.
Reverse engineer
Interviewers often hand you a recurrence and ask what it computes. Work backwards from this one.
Fill in the blanks
Fill in the missing half of the recurrence, then name the problem.
best[i] = max(best[i - 1], best[i - 2] + nums[i]) -> the problem is House Robber
Why: At each index there are exactly two choices: skip i, keeping best[i-1]; or take i, which forbids i-1 and so builds on best[i-2]. The max of those two is the answer at i. Recognising a recurrence by its shape - a choice between skip and take - is far more transferable than remembering the story about houses, and the same shape appears in Delete and Earn and in Maximum Sum of Non-Adjacent Elements.
Concept
Sorting to answer "the k largest" costs O(n log n). A heap of size k costs O(n log k), and when k is small that is effectively linear.
import heapq
from collections import Counter
def top_k(nums, k):
counts = Counter(nums) # O(n)
return [n for n, _ in heapq.nlargest(
k, counts.items(), key=lambda p: p[1])]
print(top_k([1, 1, 1, 2, 2, 3], 2)) # -> [1, 2]| approach | time | when to prefer it |
|---|---|---|
| sort everything | O(n log n) | you need the full order anyway |
| heap of size k | O(n log k) | k is much smaller than n |
| bucket by frequency | O(n) | counts are bounded by n - the optimal answer |
heapq.heappush/heappop | O(log n) each | a stream, where n is unknown |
Two Python facts worth knowing cold: heapq is a min-heap, so push negated values for a max-heap; and Counter(nums).most_common(k) exists, but interviewers usually want to hear the heap or bucket reasoning behind it.
Python 3 documentation - heapq heapq.nlargest — the size-k heap used here
Comparison
Complete this from memory. This card is the thing to be able to reproduce before your first real interview.
Comparison matrix
| pattern | signature clue | typical cost |
|---|---|---|
| Hash map | "seen before", "count of each" | O(n) |
| Two pointers | sorted array, or a pair | O(n) |
| Sliding window | "substring", "consecutive" | O(n) |
| Binary search | sorted, or monotonic condition | O(log n) |
| Stack | nesting, or next greater | O(n) |
| BFS / DFS | grid, tree, "connected", "fewest steps" | O(V + E) |
| Heap | "top k", "k largest" | O(n log k) |
| Dynamic programming | "number of ways", "maximum over choices" | O(n) to O(n*m) |
Elimination
Prompt: "Given an unsorted array of up to 100,000 integers, return the length of the longest run of consecutive integers - for example [100, 4, 200, 1, 3, 2] gives 4, for 1,2,3,4."
Eliminate the wrong options
Which approach survives?
Survives elimination: B
Why: The set solution is O(n): each value is the start of a run only if value - 1 is absent, and the upward walk from each start visits each value at most once across the whole run, so the total work is linear despite the nested loop. The reasoning to say out loud is 'n up to 100,000 means I want linear or n log n; sorting gives me n log n immediately, and a hash set gets me to linear by replacing the order with membership.'
Section
Section 4
Concept
These are the problems that appear again and again across company question banks and every widely used study list. Learn them as instances of patterns, in the third column - that is the part that transfers.
| problem | pattern | target cost |
|---|---|---|
| Two Sum | hash map | O(n) |
| Contains Duplicate | hash set | O(n) |
| Valid Anagram | counting with a hash map | O(n) |
| Group Anagrams | hash map keyed by sorted word | O(n k log k) |
| Best Time to Buy and Sell Stock | one pass, running minimum | O(n) |
| Maximum Subarray (Kadane) | one pass, running best | O(n) |
| Valid Parentheses | stack | O(n) |
| Merge Two Sorted Lists | two pointers | O(n + m) |
NeetCode - pattern-grouped practice list — a widely used pattern-grouped list, if you want a practice order
Concept
| problem | pattern | target cost |
|---|---|---|
| Longest Substring Without Repeating Characters | sliding window | O(n) |
| Binary Search / Search Insert Position | binary search | O(log n) |
| Reverse Linked List | three pointers | O(n) |
| Linked List Cycle | fast and slow pointers | O(n) |
| Invert / Maximum Depth of Binary Tree | DFS | O(n) |
| Binary Tree Level Order Traversal | BFS | O(n) |
| Number of Islands | DFS or BFS on a grid | O(rows x cols) |
| Course Schedule | topological sort / cycle detection | O(V + E) |
| Top K Frequent Elements | heap or bucket by count | O(n log k) |
| Climbing Stairs / House Robber | dynamic programming | O(n) |
| Coin Change | dynamic programming | O(n x amount) |
| Merge Intervals | sort, then sweep | O(n log n) |
Twenty problems, eight patterns. Working these until the pattern is instant - not the code - is most of what a 2-3 month runway is for.
Pattern
Four prompts you have not seen in this deck. Thirty seconds each, pattern only.
Predict first
Which pattern does each of the four prompts signal?
Correct: Sliding window (minimum window substring); hash set or XOR (single number); binary search (rotated array minimum); dynamic programming (stock with fee).
| prompt | pattern | clue word |
|---|---|---|
| shortest substring containing | sliding window | substring |
| every element twice except one | hash set / XOR | appears |
| rotated sorted array | binary search | sorted |
| maximize profit with a fee | dynamic programming | maximize |
Why: Prompt 1 says 'shortest substring containing' - contiguous plus a constraint is a variable window. Prompt 2 is a membership question, so a set works in O(n) space, and XOR does it in O(1) if you spot that a ^ a = 0. Prompt 3 still has a monotonic structure despite the rotation, so the halving argument survives. Prompt 4 has a state you carry forward (holding versus not holding), which is the signature of DP rather than a greedy scan.
Concept
Interviewers know the famous problems are famous. The defence is to change the story, not the structure - which is precisely why pattern recognition beats memorization.
| the classic | the disguise you will actually get |
|---|---|
| Two Sum | "Find two transactions that reconcile to this amount." |
| Number of Islands | "Count the distinct clusters in this sensor grid." |
| Valid Parentheses | "Validate that these HTML tags are properly nested." |
| Climbing Stairs | "How many ways can a robot cross n tiles moving 1 or 2 at a time?" |
| Top K Frequent | "Show the k most-viewed products from this event log." |
When a prompt has an unusual story, strip the nouns: what is the input shape, what is the output, and what would the brute force be? The pattern usually appears the moment the story is gone.
Real world
Prompt: "Our logging service receives a stream of request IDs. Report the first ID that we have already handled, so we can drop the retry."
Discussion prompt
Rewrite this as an abstract problem in one sentence, then name the pattern and the cost.
Hint: What is the input's type? What single question is being asked of it?
Answer:
first_repeat function from earlier in this deck, word for word.That last point is where a story-wrapped question earns its keep: the context introduces a constraint the abstract version does not have.
Counterexample
A confident-sounding statement. It is false. Find the case that kills it.
Claim: "If the array is sorted, two pointers always beat a hash map, so you should never hash a sorted array."
Discussion prompt
Produce a counterexample, or state the condition under which the claim fails.
Hint: Two pointers need the pair to be found by moving inward. What if the question is not about a pair?
Answer:
Sorted data makes two pointers available, not automatically correct. The test is what the question asks for, not what the input looks like.
Hypothesis
An online assessment gives you n up to 200,000 and a 2-second limit. Your candidate solution sorts the array, then for each element does a binary search.
Predict first
Will this pass, and what is its complexity? State your prediction, then check it against the constraint table from Section 2.
Correct: Yes - it is O(n log n), which is comfortable at n = 200,000.
| part | cost | at n = 200,000 |
|---|---|---|
| sort | O(n log n) | ~3.6 million |
| n binary searches | O(n log n) | ~3.6 million |
| total | O(n log n) | ~7 million - fine |
Why: The sort is O(n log n). The loop runs n times and does an O(log n) binary search each time, which is also O(n log n). Added, the whole thing is O(n log n) - roughly 200,000 x 18, about 3.6 million operations, far inside a 2-second budget even in Python. The habit worth building is doing this arithmetic before coding: it takes fifteen seconds and it tells you whether to keep going or find a better approach.
Worked example
Given a list of intervals, merge all the overlapping ones. It is the canonical member of a whole family: insert interval, meeting rooms, non-overlapping intervals.
The insight comes from sorting, and it is worth stating explicitly
Why: Once the intervals are sorted by start, any interval can only overlap the one immediately before it in the output - so a single sweep suffices, and no pairwise comparison is needed.
def merge(intervals):
intervals.sort(key=lambda p: p[0])
out = []
for start, end in intervals:
if out and start <= out[-1][1]:
out[-1][1] = max(out[-1][1], end)
else:
out.append([start, end])
return outTrace it on [[1,3], [8,10], [2,6], [15,18]]
Why: After sorting by start: [1,3], [2,6], [8,10], [15,18].
| interval | out[-1] before | overlap? | out after |
|---|---|---|---|
| [1,3] | - | no (empty) | [[1,3]] |
| [2,6] | [1,3] | 2 <= 3, yes | [[1,6]] |
| [8,10] | [1,6] | 8 > 6, no | [[1,6],[8,10]] |
| [15,18] | [8,10] | 15 > 10, no | [[1,6],[8,10],[15,18]] |
Verify the nesting case separately - it is the one that breaks naive code
Why: merge([[1,4],[2,3]]) returns [[1,4]], not [[1,3]]. That is what max(...) on line 6 is for: the second interval ends earlier, so overwriting the end blindly would shrink the merged interval.
Cost: O(n log n), dominated entirely by the sort. When a solution's cost is all sorting, say so - it tells the interviewer you know the sweep itself is linear.
Check
A prompt you have not seen. Pattern only - do not solve it.
Check your understanding
"Given an array of integers and an integer k, return the maximum sum of any contiguous subarray of exactly k elements. n can be 100,000." Which approach should you propose?
Answer: B
Why: 'Contiguous' plus 'exactly k elements' is the fixed-size window, which is O(n) time and O(1) space. Each step repairs the running sum in constant time instead of re-adding k elements, which is exactly the trap slide from Section 3.
Explain it
Discussion prompt
In four sentences, and without using the words 'window', 'pointer' or 'index', explain to a first-year student why the sliding window is faster than checking every substring.
Hint: What work does the fast version refuse to redo?
Answer:
A model answer: "Checking every stretch means re-reading characters you have already read, over and over. Instead, keep one stretch and edit its ends: when you take in a new character on the right, you only have to drop characters from the left until the stretch is legal again. Every character gets taken in once and dropped at most once, so the total work is proportional to the length of the string. The naive version does that much work for each starting position."
If you cannot explain it without the jargon, you are relying on the words rather than the idea - and an interviewer's follow-up will find that out. This is the single most useful drill between sessions.
Commit first
Rate your confidence honestly. Confident-and-wrong is the state worth finding now rather than in a real round.
Predict first
You need the k largest elements of an unsorted array of 1,000,000 numbers, with k = 10. What is the best complexity achievable, and with what structure?
Correct: O(n log k) with a min-heap of size k - about 1,000,000 x 3 comparisons rather than 1,000,000 x 20.
Why: Keep a min-heap holding the k largest seen so far: push each element, and if the heap exceeds size k, pop the smallest. Each operation is O(log k) with k = 10, so log k is about 3 while log n is about 20 - a real speedup, not a theoretical one. A hash map cannot help because the question is about order, not membership; and building a max-heap of all n elements is O(n) to heapify but then O(k log n) to extract, which is fine too but keeps O(n) memory instead of O(k).
Section
Section 5
Concept
Communication is scored separately, and it is the easiest score to raise. These are sentences to have ready - they work on any problem.
last_seen because it holds ..."Notice how many of these state a trade-off. That is the sentence type that separates a mid-level from a junior read, and it costs nothing to add.
Step zero
Prompt: "Given a list of words, group together the ones that are anagrams of each other."
Discussion prompt
Write your plan in plain English - no code, no Python. Three or four sentences.
Hint: What single fact makes two words anagrams? Could that fact be a dictionary key?
Answer:
Every line of that plan becomes one or two lines of code. Candidates who write this plan finish; candidates who start typing usually discover the sorted-key idea at minute 30.
Pattern
Verification is a separate score. This checklist is what to run at minute 35, out loud, without being prompted.
nums = [] crash on nums[0] or on len(nums) - 1?-1, None, an empty list? Whatever you clarified at minute 3.range(n) or range(n + 1) correct here?Say what you are testing as you test it: "empty input - line 3 would index into an empty list, so I need a guard." Finding your own bug scores higher than never having one, because it is evidence of a habit.
Anomaly
This function is supposed to remove every even number. It does not, and it raises no error - the worst kind of bug.
def remove_evens(nums):
for n in nums:
if n % 2 == 0:
nums.remove(n)
return nums
print(remove_evens([1, 2, 4, 5])) # -> [1, 4, 5]Predict first
Why does 4 survive? Be specific about what the loop is doing when the list changes underneath it.
Correct: Removing 2 shifts every later element left by one, and the loop's internal index has already advanced - so 4 slides into the position the loop just left, and is skipped.
The list shrank under the iterator, so one element was stepped over.
Why: Python's list iterator walks by index. At index 1 the value is 2, which is removed, so the list becomes [1, 4, 5] and 4 now sits at index 1 - but the iterator moves on to index 2, which holds 5. The element is never examined. The fix is to build a new list ([n for n in nums if n % 2]) or to iterate over a copy (for n in nums[:]). Mutating a collection while iterating over it is the most common silent bug in Python interviews, and it shows up in linked-list and grid problems too.
| iterator index | list at that moment | value seen | action |
|---|---|---|---|
| 0 | [1, 2, 4, 5] | 1 | odd, keep |
| 1 | [1, 2, 4, 5] | 2 | remove -> [1, 4, 5] |
| 2 | [1, 4, 5] | 5 | odd, keep - 4 was skipped entirely |
Constraint
You have solved 'find the first repeated value' with a hash set: O(n) time, O(n) space.
Discussion prompt
Now the interviewer says: "Nice. Now do it in O(1) extra space." What do you ask, and what do you propose?
Hint: What extra information about the input would make an in-place approach possible? And what does sorting cost you?
Answer:
This kind of follow-up is not a trap. It is testing whether you know why your solution costs what it costs, and whether you will ask before assuming.
Trap
Minute 34. The code is written. You say: "I think that's it - should I run it?"
Hand the verification back to the interviewer
Why: They now have to find your bugs, which converts the verification score into a hint - and hints are recorded.
| what happens next | how it is scored |
|---|---|
| They spot an empty-input crash | needed a hint on correctness |
| They ask 'what about duplicates?' | did not test independently |
| Code was actually fine | still no evidence of a testing habit |
Minute 34. The code is written. You say: "Let me trace it on our example and then check three edge cases."
Walk the real values through the real lines, out loud
Why: This catches the majority of bugs, and it is observable - which is what the verification score is measuring.
| what happens next | how it is scored |
|---|---|
| You find your own off-by-one | strong: self-corrected |
| You note the empty case and add a guard | strong: anticipates edges |
| Everything passes | strong: verified independently |
Check
A fixed-size window that looks correct. One of these inputs makes it silently wrong - no exception, just a wrong number.
def max_sum_k(nums, k):
window = sum(nums[:k])
best = window
for i in range(k, len(nums)):
window += nums[i] - nums[i - k]
best = max(best, window)
return best| candidate input | what is unusual about it |
|---|---|
| nums = [1, 2, 3], k = 2 | nothing - the ordinary case |
| nums = [1, 2, 3], k = 5 | k is larger than the list |
| nums = [-5, -2, -9], k = 1 | every value is negative |
| nums = [4, 4, 4, 4], k = 4 | k equals the length |
Check your understanding
Which input makes max_sum_k return a wrong answer with no error?
Answer: B
Why: With k larger than the list, nums[:5] silently returns the whole 3-element list rather than raising, so window is the sum of all three, the loop body never runs, and the function confidently returns a sum over a window that does not exist. Python's forgiving slicing is what hides it - a guard such as if k > len(nums): return None (after clarifying what the caller wants) is the fix.
best starts at the first window rather than at 0 - which is the bug this code deliberately avoids.Section
Section 6
Concept
You said interviews start in two to three months and that you want the solution to click on unseen problems. That means pattern reps, spaced out - not a race through a problem list.
| weeks | focus | volume | the test that you are ready |
|---|---|---|---|
| 1-2 | Arrays, hashing, two pointers | 3-4 problems/week | You name the pattern before reading the constraints |
| 3-4 | Sliding window, stack, binary search | 4 problems/week | You write the boundary search from memory |
| 5-6 | Trees and graphs (BFS/DFS) | 4 problems/week | You write BFS without looking up deque |
| 7-8 | Dynamic programming, heaps | 3-4 problems/week | You derive a recurrence before coding |
| 9-10 | Mixed review, timed, out loud | 2 mocks/week | 45 minutes, narrated, no pausing |
Two rules that matter more than the schedule: always talk while you solve, even alone; and redo a problem you got wrong three days later rather than moving on. Volume without recall is the most common way a 10-week plan produces no improvement.
Concept
The difference between 400 problems and 120 problems is much smaller than the difference between these two habits.
| instead of | do this | why |
|---|---|---|
| Reading the solution when stuck | Sitting with it 20 minutes, then reading only the hint | The struggle is what builds retrieval |
| Moving on once it passes | Writing the pattern and clue in a one-line log | The log becomes your revision deck |
| Solving silently | Narrating as if someone is watching | Communication is a separate score, and it needs reps |
| Never repeating a problem | Redoing wrong ones after 3 days, then 10 | Spaced repetition is what makes it automatic |
Your one-line log for Two Sum: "unsorted + indices wanted -> hash map of value to index, one pass, O(n)/O(n)." Twenty of those lines is the revision sheet you read the morning of an interview.
Connect it up
Do this on paper too. Reproducing it from memory tomorrow is the actual homework.
Draw it
Draw the eight patterns as boxes. For each one, write (a) the clue phrase that triggers it, (b) its typical complexity, and (c) one named problem from the short list. Then draw an arrow between any two patterns that can solve the same problem - for example two pointers and hash map on Two Sum - and label the arrow with what decides between them.
The arrows are the part that matters. Interviewers rarely ask "do you know BFS" - they ask "why did you choose that over the alternative", and the arrows are where those answers live.
Exit ticket
Predict first
Of the eight patterns, which one could you not write from scratch in ten minutes today? Name it, and name what specifically is missing - the structure, the loop condition, or the complexity argument.
Correct: Whatever you named is the first thing we work in session two, and the answer is almost always a loop condition rather than a whole structure.
Why: Being able to say 'I can write BFS but I always fumble whether to mark seen on push or on pop' is a far more useful self-assessment than 'I'm bad at graphs'. Specific gaps get fixed in a single session; vague ones turn into months of unfocused practice. Send me your answer before we meet and I will build the next session around it.
Recap
You have the map. Sessions from here are reps on it - narrated, timed, and with the weak patterns first.
| before next session | why |
|---|---|
| Redo Two Sum and Valid Palindrome, narrated aloud | Two patterns, one session's worth of reps |
| Write the eight-pattern card from memory | Recall, not recognition |
| Send me your exit-ticket answer | So session two starts on your weak pattern |
Want this taught 1-on-1? Alexander tutors DSA Interview Prep — $55/session, free consultation.