A14 · Part 3 · Advanced
Interview strategy
The algorithm is half the score — the other half is everything you say before, during and after writing it.
16On this page
- What is actually being scored
- The 45 minutes as checkpoints, not hopes
- The first five minutes: questions that change the answer
- A framework that works on a problem you have never seen
- Constraints → intended complexity: the highest-leverage table here
- Keywords to a first guess, and the check before you commit
- How to talk while you code
- Being stuck, handled well
- Pivoting off a wrong approach without losing the interview
- Proving it correct in one sentence
- Test before you say "done"
- Follow-ups: it is a stream, it is sorted, it does not fit on one machine
- Discussing tradeoffs at a senior level
- When the problem is NP-hard: recognise it, then say the right thing
- A full mock: one Hard problem, start to finish
- Signals that separate an advanced performance from an intermediate one
What is actually being scored
At the advanced level the interviewer is not checking whether you can produce a correct program. They are estimating one thing: what is it like to hand this person an ambiguous problem and come back in three days? Every behaviour in this chapter is a proxy for that. Clarifying questions predict whether you will build the wrong thing. Stating a brute force before optimizing predicts whether you ship something or stall. Testing your own code predicts whether QA finds your bugs or you do. Naming a tradeoff unprompted predicts whether you will make a defensible technical decision alone.
Two candidates can both produce a working O(n log n) solution and receive "strong hire" and "no hire." The difference is almost never the code. It is that one of them narrated a decision process the interviewer could follow and trust, and the other silently emitted a memorized answer that could not be probed, extended, or debugged out loud.
The 45 minutes as checkpoints, not hopes
The timeline above says where each phase should sit. A timeline you only glance at does nothing for you; what helps is knowing, at each checkpoint, what must already be true and what you do if it is not. These are guides to calibrate against, not rules: a Hard problem pushes the approach phase longer and a simple one shortens it.
| By minute | What should be true | If you are behind |
|---|---|---|
| 5 | The problem is restated, the example is answered by hand, and two or three questions that changed your plan have been asked. | Stop asking. State your assumptions aloud ("I'll assume non-empty input and say so in the code") and move on. |
| 12 | A brute force is on the board with its complexity, the bottleneck is named, the constraints have told you the target, and the interviewer has agreed to the plan. | No better idea by minute 10 is normal. Say so, and offer to code the brute force while you keep thinking: it is a baseline, partial credit, and often the skeleton of the optimization. |
| 25 | The main loop or recursion exists. Helpers have names even if their bodies are stubs. | Write the core loop first and stub the rest with a one-line contract ("isValid(state): true when no rule is broken"). Working structure beats polished fragments. |
| 33 | The code is complete enough to run in your head, and you start tracing the example with real values. | Trace what exists, say what remains, and finish it as you go. An unfinished function you traced is worth more than a finished one you did not. |
| 40 | The example and one edge case are traced, any bug is fixed, and you are naming trade-offs. | Give each trade-off two sentences. Offer the next improvement instead of starting to build it. |
| 45 | You have asked one question of your own about the problem or the team. | If time is gone, ask it anyway: it is the part you control. |
The checkpoint that does the most work is minute 12, because everything after it is expensive to undo. The one at minute 25 catches the commonest failure in the middle of the interview: a candidate who has been coding carefully for thirteen minutes and has a lovely helper function and no program. Look at the clock when you finish the approach phase, not only at the end.
The first five minutes: questions that change the answer
Silence here is the single strongest negative signal available, and it is the cheapest to fix. An engineer who starts coding from an under-specified problem statement is telling the interviewer exactly how they behave with an under-specified ticket. But not all questions are equal — "can I use a hashmap?" wastes the goodwill you are trying to build. Ask only questions whose answer would change your solution, and say why you are asking.
| Ask | Why it changes the solution |
|---|---|
| "How large can n get?" | The highest-value question in the interview. It fixes your target complexity before you have written anything — see the table below. |
| "Is the input sorted, or can I sort it?" | Sorted unlocks two pointers and binary search for free. If not sorted, an O(n log n) sort may be free anyway when the target is already O(n log n) — but it is not free if the target is O(n). |
| "Can there be duplicates?" | Changes whether you dedupe, whether a Set is safe, whether two-pointer needs a skip loop, and whether the expected output is unique. |
| "What's the range of the values? Negative? Zero? Floats?" | Negatives break sliding-window-on-sums and greedy arguments. Small bounded values unlock counting sort or a bitmask. Floats kill exact equality. |
| "What should I return for empty input / no valid answer?" | An explicit contract, decided up front, instead of an ad-hoc guess at minute 40. |
| "Can I mutate the input?" | In-place sorting or marking may be the whole space optimization; if the input is shared state, it isn't allowed. |
| "Is this called once, or repeatedly on the same data?" | Repeated queries change the answer entirely — preprocess into a prefix array, segment tree, or index map and amortize. |
| "Does the whole input fit in memory, or is it a stream?" | Streaming rules out sorting and random access; pushes toward heaps, reservoir sampling, count-min sketch. |
Say it like this → "Before I start — a few things
that would change my approach. How big is n? Can values be negative? And are
duplicates possible in the input? …n is up to 10⁵ and values can be negative
— good, that rules out the sliding-window approach I was about to reach for,
since a negative number means the window sum isn't monotonic."
That last clause is the part that scores. You are not collecting facts, you are demonstrating that each fact eliminates a branch of your decision tree. Two or three questions asked this way beat ten asked mechanically.
⚠ Don't ask questions you can answer from the examples
If the provided example contains a negative number, asking "can values be
negative?" reads as not having read the problem. Skim the examples first,
extract what they already settle, and ask only about what they leave open —
then say so: "the example has duplicates so I'll assume they're allowed; what
I can't tell from it is whether the array is guaranteed non-empty."
A framework that works on a problem you have never seen
You will not recognize the problem. That is the point of an advanced interview. What you need is not recall but a procedure that visibly makes progress even from zero. Run these seven steps out loud, in order.
| # | Step | What you actually say |
|---|---|---|
| 1 | Restate in your own words | "So: given X, return Y, where the constraint is Z. Is that right?" |
| 2 | Work the given example by hand | Say the answer for the example before writing anything. Catches misreads instantly. |
| 3 | State a brute force, with its complexity | "The obvious thing is to check every pair — O(n²) time, O(1) space. That's my baseline; let me see if I can beat it." |
| 4 | Name the bottleneck | "The expensive part is that for each i, I re-scan everything before i. That inner scan is the thing to remove." |
| 5 | Read the constraints for the target | "n goes to 10⁵, so O(n²) is 10¹⁰ — far too slow. They're steering me to O(n log n) or O(n)." |
| 6 | Ask what structure removes the bottleneck | "What would make that inner scan O(1) or O(log n)? A hashmap of seen values, a heap, a monotonic stack, or precomputed prefix sums." |
| 7 | Confirm, then code | "So: one pass, hashmap from value to index, O(n) time and O(n) space. Shall I code that?" |
Step 3 is non-negotiable and candidates skip it constantly, believing a brute force looks weak. The opposite is true: it guarantees you have a solution on the board within five minutes, it proves you understand the problem, and it gives you a concrete complexity to improve on. An interviewer will almost always let you skip implementing it. What they will not forgive is twenty minutes of silence hunting for the clever answer.
Dry run: the seven steps applied to one concrete problem
"Given a list of daily prices, find the maximum profit from buying once and selling once later. n ≤ 10⁵."
| Step | Applied here | Conclusion |
|---|---|---|
| 1. Restate | "Find the largest prices[j] - prices[i] with j > i." | matches the problem |
| 2. Hand-run the example | [7, 1, 5, 3, 6, 4]: buy at 1, sell at 6 | expected answer: 5 |
| 3. Brute force | check every pair (i, j) with j > i | O(n²), stated as the baseline |
| 4. Name the bottleneck | for each i, re-scanning every later j for its price | the re-scan is the wasted work |
| 5. Read the constraints | n ≤ 10⁵ → O(n²) is 10¹⁰, too slow | need O(n log n) or O(n) |
| 6. Structure that removes it | track the minimum price seen so far in one pass | no data structure needed — one running value |
| 7. Confirm, then code | "one pass, O(n) time, O(1) space" | ready to code |
The problem changes every time; the seven-step shape doesn't. That's the actual claim this framework makes — not that you'll recognize this specific problem, but that running the same seven questions on an unfamiliar one gets you from "no idea" to "one pass, O(n)" without needing to have seen it before.
Constraints → intended complexity: the highest-leverage table here
Interviewers and problem setters choose n deliberately. The
bound is a hint about the intended solution, and reading it correctly can
collapse a twenty-minute search into thirty seconds. Calibrate against the
rough industry rule that ~10⁸ simple operations is about one second.
| Constraint on n | Intended complexity | Pattern family it points at |
|---|---|---|
| n ≤ 10-12 | O(n!) · O(n! · n) | Full permutation search, brute-force TSP, "try every ordering" |
| n ≤ 20-25 | O(2ⁿ) · O(2ⁿ · n) | Bitmask — subset enumeration, bitmask DP, meet-in-the-middle (2^(n/2)) if n ≈ 40 |
| n ≤ 100 | O(n³) · O(n⁴) | Floyd-Warshall, interval/matrix-chain DP, triple nested loops are fine |
| n ≤ 1,000-5,000 | O(n²) | 2D DP over pairs — edit distance, LCS, palindromic substrings; all-pairs comparison |
| n ≤ 10⁵ | O(n log n) | Sort-then-scan, heap, binary search on the answer, balanced BST / ordered set, divide and conquer, segment tree |
| n ≤ 10⁶-10⁷ | O(n) · O(n log log n) | Single pass, two pointers, sliding window, hashmap, counting sort, prefix sums, sieve, Kadane |
| n ≤ 10⁹-10¹⁸ | O(log n) · O(√n) · O(1) | Math/closed form, binary search over the answer space, matrix exponentiation, digit DP — you cannot even read the input |
Read the second half of the constraints too. "Sum of all string lengths ≤ 10⁵" over many strings means linear in the total, which fits a Trie or Aho-Corasick built over all the strings at once. A value range like "values ≤ 100" alongside a huge n points at counting/bucketing. A memory limit matters as much as time: n = 5,000 with an O(n²) DP table is 25 million cells, easily 100-200 MB in JavaScript, which tells you the DP is meant to be rolled down to one or two rows. (At n = 10⁵ the table would be 10¹⁰ cells and the time 10¹⁰ steps, so no rolling saves it — the quadratic idea itself is out.)
Read the constraints backwards
Do not design a solution and then check whether it is fast enough. Read
n first, derive the complexity the setter intends, and use that
as a filter on which patterns are even eligible. "n ≤ 20" is not
trivia — it is the interviewer telling you the answer involves subsets. Very
few candidates do this, and it is visible in seconds when someone does.
Say it like this → "n is at most 20, which is a
strong hint — 2²⁰ is about a million, so exponential in n is affordable, and a
setter wouldn't cap n this low if a polynomial solution were intended. That points at enumerating subsets
with a bitmask, most likely bitmask DP over the set of already-used elements.
Let me check whether the state really is just 'which subset is used' or
whether I need an index too."
Keywords to a first guess, and the check before you commit
The phrasing of a problem leaks its pattern. The table below is a list of first guesses, not answers: each phrase makes a pattern worth considering first, and the right-hand column is the one question that tells you whether the guess survives. Saying the check out loud is what separates a pattern match from a hunch.
| The problem says | First guess | Check before you commit |
|---|---|---|
| "longest / shortest subarray or substring with at most k of something" | Sliding window | Does adding an element only ever push the window one way? Negative numbers break window sums. |
| "subarray sum equals k" | Prefix sums with a hash map | Are values possibly negative? If not, a window may also work. |
| "sorted array", "find the pair / position" | Binary search or two pointers | Is the order really given, and is the condition monotonic over it? |
| "minimize the maximum" or "maximize the minimum" | Binary search on the answer | Can you write a yes/no check "is value x achievable?" that flips only once as x grows? |
| "k-th largest", "top k", "median of a stream" | Heap (two heaps for the median) | Is k small against n? If everything is needed in order, sorting may be simpler. |
| "next greater / smaller element", "span", "histogram" | Monotonic stack | Does each element need only its nearest qualifying neighbour on one side? |
| "fewest steps", "shortest path" with equal-cost moves | Breadth-first search | Are all moves the same cost? With weights, you need Dijkstra or worse. |
| "connected", "groups", "merge accounts" | Union-find or a graph traversal | Do you need to answer many dynamic queries (union-find) or one pass (traversal)? |
| "prerequisites", "order of tasks", "build order" | Topological sort | Is a cycle possible, and what should the answer be then? |
| "number of ways", "minimum cost to reach" | Dynamic programming | Can you name a state that holds everything the future depends on, with small bounds? |
| "all combinations / permutations / partitions" | Backtracking | Is n small enough that the output size is affordable? Where can you prune? |
| "words with a common prefix", "autocomplete" | Trie | Are there many queries against a fixed dictionary, so building it pays off? |
| "overlapping intervals", "meeting rooms" | Sort by start, then sweep | Do you need the sorted order, or only the maximum overlap (a sweep over events)? |
| "design a class with O(1) operations" | A hash map composed with an ordered structure | Which two requirements pull in opposite directions? |
If two rows fit, that is information, not confusion: say both guesses and use the check to choose. "This looks like a window or a prefix-sum problem; the values can be negative, so it is prefix sums" is a complete and strong sentence.
How to talk while you code
The goal is a continuous, low-effort narration that lets the interviewer follow your reasoning without interrupting. Not a play-by-play of syntax — nobody needs "now I'll write a for loop." Narrate decisions, and name your patterns, because naming is what proves the choice was deliberate rather than lucky.
| Instead of | Say |
|---|---|
| silence | "I'll use a monotonic decreasing stack here, because I need the next greater element for every index and a stack lets each element be pushed and popped once — amortized O(n)." |
| "now a map" | "Map from value to index rather than a Set, because I need to return indices, not just detect membership." |
| "hmm, hold on" | "I'm deciding between sorting first and using a heap. Sorting is simpler but O(n log n) up front; the heap gets me the top k in O(n log k). Since k is small I'll take the heap." |
| "…" while fixing a bug | "That should be <=, not < — otherwise the last window never gets evaluated. Let me re-check the boundary." |
| "I'll handle that later" | "I'm deliberately deferring the empty-input case; noting it here as a TODO and I'll come back before I call this done." |
Two mechanical habits pay for themselves. First, write the function signature and the return type before the body — it forces the contract to be explicit. Second, when you defer something, say so and leave a visible marker; an acknowledged gap is a plan, an unacknowledged one is a bug.
⚠ Narrating and thinking are different modes, and forcing both at once stalls people
It is entirely fine to say "give me twenty seconds to think this through
quietly" and then go silent — that reads as controlled. What reads badly is
undeclared silence for two minutes. Buy the quiet explicitly, use
it, then come back with a statement rather than a mumble.
Being stuck, handled well
You will get stuck. It is expected and it is not disqualifying — freezing is. What is being measured is whether you have a procedure for it. Work this ladder out loud, in order, and say which rung you are on.
| Rung | Do this | Why it works |
|---|---|---|
| 1 | Re-read the constraints and the exact wording of the ask | Most stuckness is a misread — "subsequence" vs "subarray," "any" vs "all," "at most" vs "exactly." The constraint bound also re-states the target complexity you may have drifted from. |
| 2 | Work a tiny example by hand — n = 1, n = 2, n = 3 | You are looking for the rule your hand is following. Solving n = 3 manually and asking "what did I just do?" recovers the recurrence more often than staring at the general case. |
| 3 | Ask "what shape is this?" | Not "have I seen this problem" but: is it a graph? intervals? a tree of choices? a search over a monotonic answer space? Shape recall is far more reliable than problem recall. |
| 4 | Ask whether it is two known patterns composed | Advanced problems usually are. Sort + two pointers. Trie + backtracking. Binary search on the answer + a greedy feasibility check. Heap + hashmap. Topological order + DP. Say the two names out loud. |
| 5 | Relax the problem, solve the easier version | Drop a constraint (assume sorted, assume no duplicates, assume k = 1, assume the array is positive). Solve that, then re-introduce the constraint and see what breaks. Partial credit is real credit. |
| 6 | State where you are stuck, precisely, and take the hint | "I have an O(n²) solution and I know the bottleneck is re-scanning the prefix; what I can't see is a structure that gives me the max of a shrinking window in O(1)." That is a targeted request, and it lets the interviewer give a small hint rather than a large one. |
Say it like this → "I'm going to slow down for a
second and work n = 3 by hand, because I think the recurrence will be obvious
once I see what I'm doing manually. …Right — at each step I'm choosing
between taking this element and skipping it, and the choice only depends on
the remaining capacity. That's a knapsack shape, so the state is (index,
remaining) and I can memoize it."
Note what that phrasing does: it converts "stuck" into "executing a deliberate step." Interviewers are instructed to give hints; taking one gracefully costs far less than most candidates fear, and refusing to ask while burning ten minutes costs far more.
Pivoting off a wrong approach without losing the interview
Sooner or later the approach you announced stops working. Candidates treat this as a catastrophe and either defend a failing idea or fall silent. It is neither: finding that an approach is wrong, early and out loud, is a strong signal, because it shows you test your own ideas. What costs points is finding out late, or finding out and hiding it.
There are three ways an approach fails, and each has a different tell.
| How it fails | The tell | What you do next |
|---|---|---|
| It is incorrect | You can build a small input where it gives the wrong answer | Keep the counterexample on the board; it states exactly what the right idea must handle. |
| It is too slow | The constraints say n = 105 and your inner loop is O(n) | Name the repeated work and look for the structure that removes it. |
| It does not fit a constraint you assumed | You relied on sorted input, positive values, or distinct elements, and the problem does not promise them | Ask whether the assumption can be had cheaply (sort first?), or drop the approach. |
The incorrect case deserves a worked example, because greedy is where candidates most often commit to something unproven. "Make change with the fewest coins: take the largest coin that fits, repeatedly." For some coin systems this is right. For others it is quietly wrong, and one small input shows it.
function greedyCoins(coins, amount) {
const sorted = [...coins].sort((a, b) => b - a);
let count = 0;
for (const coin of sorted) {
count += Math.floor(amount / coin);
amount %= coin;
}
return amount === 0 ? count : -1;
}
function minCoins(coins, amount) {
const best = new Array(amount + 1).fill(Infinity);
best[0] = 0;
for (let a = 1; a <= amount; a++) {
for (const coin of coins) {
if (coin <= a && best[a - coin] + 1 < best[a]) best[a] = best[a - coin] + 1;
}
}
return best[amount] === Infinity ? -1 : best[amount];
}
With coins [1, 3, 4] and amount 6, greedyCoins
returns 3 (it takes 4, then 1, then 1) while minCoins returns 2
(3 and 3). Amount 6 is the smallest amount where they disagree for those
coins. With coins [1, 5, 10, 25] they agreed on every amount
from 1 to 500, which is why the greedy habit survives: it works on the
coins you grew up with.
Say it like this → "Let me test that before I code
it. Take coins 1, 3 and 4 and amount 6. My greedy picks 4 then 1 and 1, three
coins, but 3 and 3 is two. So the largest-first choice is not safe here. The
failure is that taking the big coin can block a better combination, which
means I need to consider both choices at each amount: that is a DP over
amounts, one entry per amount, O(amount × coins)."
Notice the structure of that speech: the claim, the smallest counterexample, the reason it fails, and what the reason tells you to build next. Three habits make a pivot cheap. Decide on a deadline: if you cannot make an idea work by a time you set in advance (say, five minutes), it is a wrong approach until proven otherwise. Keep what survives: the parsing, the helper functions and the examples usually carry over, and saying "the input handling stays, the core changes" shows control. Say what you learned: the counterexample or the missed constraint is the first line of the next approach, not an embarrassment to skip.
Proving it correct in one sentence
"Does it work?" is asked more often than "is it fast?", and "I tested it" is not an answer to it. A one-sentence correctness argument is short enough to say aloud and strong enough to convince. It names a fact that stays true throughout the algorithm (an invariant) or a reason a discarded option cannot matter. The table gives the shape for each family; fill the blanks with the specifics of your problem.
| Technique | The one-sentence shape |
|---|---|
| Two pointers on sorted data | "If a valid pair exists, both of its indices are always inside [lo, hi], because each move discards an index that cannot be in any valid pair." |
| Binary search | "The answer, if there is one, is always inside [lo, hi], and each step shrinks that range while keeping the answer in it." |
| Sliding window | "The window is the longest valid one ending at right, because shrinking from the left only stops once the constraint holds again." |
| Greedy | "Any optimal solution can be changed to start with my choice without getting worse, so committing to it loses nothing" (an exchange argument). |
| Dynamic programming | "The state holds everything the future depends on, and the recurrence considers every way to reach it." |
| Breadth-first search | "The queue always holds nodes in non-decreasing distance, so the first time a node is reached is by a shortest path." |
| Backtracking with pruning | "Every complete answer is either visited or sits under a branch I cut for a stated reason that no answer can satisfy." |
| Monotonic stack or deque | "An element is removed only when a later element makes it unable to ever be the answer." |
The first row, worked once on the sorted two-sum from the follow-ups below.
The loop moves lo up when nums[lo] + nums[hi] <
target. Why is that safe? nums[hi] is the largest value
still allowed, so nums[lo] with any partner in range is at most
nums[lo] + nums[hi], which is already too small: index
lo is in no valid pair, and discarding it cannot lose an answer.
The other branch is the mirror image. That is the entire proof, and it
also tells you the loop ends: each step discards one index.
⚠ "It passed my examples" is not a proof
Examples show the code does what you think on the inputs you thought of. An
invariant explains the inputs you did not think of. If you cannot state the
invariant, you do not yet know why the algorithm works, and that is the same
thing an interviewer's follow-up is built to find out.
Test before you say "done"
Declaring completion and letting the interviewer find the bug is the most avoidable score loss in the entire interview. Finding it yourself, out loud, converts the same bug into a positive signal. Trace the given example first — line by line, tracking real variable values, not vibes — then run a deliberately chosen edge case.
| Edge case | What it catches |
|---|---|
Empty input [] / "" | arr[0] on an empty array, Math.max() of nothing returning -Infinity, a while loop that assumed one element |
| Single element | Two-pointer and sliding-window loops whose body never executes; left < right vs left <= right |
All duplicates [5,5,5,5] | Dedupe logic, Set-vs-Map choices, two-pointer skip loops, "distinct" requirements |
| Already sorted / reverse sorted | Worst-case quicksort partitioning, degenerate BSTs, and off-by-one at the array ends |
| Two elements | The smallest case where a comparison or swap can be backwards |
| Negatives and zero | Greedy and sliding-window sum arguments that silently assume positivity; division and modulo by zero |
| All elements identical to the target / none matching | The "not found" return contract you agreed on in minute two |
| Maximum n from the constraints | Recursion depth (JS blows up around 10⁴-10⁵ frames), integer overflow past 2⁵³, O(n²) memory |
Say it like this → "Let me trace the given example
before I call this done. left = 0, right = 4, sum = 9, target is 9 — returns
[0, 4], matches. Now an edge case: single element. The
while (left <
right) never executes, so I fall through to the not-found return —
correct. And an empty array: nums.length - 1 is -1, the loop
still doesn't execute, still correct. I'm happy with this."
⚠ Tracing "in your head" is not tracing
Under pressure, silently re-reading code confirms what you intended to write,
not what you wrote. Say the actual variable values out loud, or write them in
a comment block. The whole value of the exercise comes from forcing yourself
to evaluate rather than recognize — and it is also the only way the
interviewer can see you doing it.
Follow-ups: it is a stream, it is sorted, it does not fit on one machine
The first solution is rarely the end. Interviewers keep the same problem and change one assumption, and each change has a standard move. Hearing the change as "which resource just got taken away?" is what turns the follow-up from a surprise into a lookup.
| The follow-up | What was taken away | The move |
|---|---|---|
| "The data arrives as a stream." | Random access, a second pass, and the right to store everything | Keep a small summary that one pass can update: a counter, a heap of size k, a sample. |
| "The input is already sorted." | Nothing: you are being given something | Drop the hash map or the sort. Two pointers or binary search now do it in O(1) extra space. |
| "There are many queries on the same data." | The right to pay O(n) per question | Preprocess once: prefix sums, a sorted copy, an index. |
| "It does not fit in memory." | Holding the whole input | Process in chunks and merge partial results (external sort, counting per chunk). |
| "It is spread over many machines." | One place to compute | Compute locally, send a small summary, combine. The question is whether the answer can be combined from the pieces' answers. |
Streaming: one pass, constant memory
A typical case: find the element that occurs more than half the time in a stream, without storing the stream. Keep a candidate and a counter. A matching element adds a vote; a different one cancels a vote; when the votes hit zero the next element becomes the candidate. If a strict majority exists, it cannot be cancelled out, because every cancellation removes one copy of it together with one copy of something else.
class MajorityStream {
constructor() {
this.candidate = undefined;
this.count = 0;
}
add(x) {
if (this.count === 0) {
this.candidate = x; // nothing is standing: x takes the floor
this.count = 1;
} else if (x === this.candidate) {
this.count++;
} else {
this.count--; // x cancels one vote for the candidate
}
}
}
The guarantee is conditional, and saying so is the point. If a strict
majority exists, candidate is that value: I tested 3,000 random
streams that each contained one. If no majority exists, the candidate is just
whatever survived: after adding 1, 2, 3 it is 3 with a count of 1, and a
second pass is the only way to confirm. A stream gives you no second pass, so
the honest statement is "this tells you the majority if there is one".
Sorted input: the hash map was only buying you order
For two-sum, the hash-map solution is O(n) time and O(n) space. Given sorted input, the same question needs no extra memory.
function twoSumSorted(nums, target) {
let lo = 0;
let hi = nums.length - 1;
while (lo < hi) {
const sum = nums[lo] + nums[hi];
if (sum === target) return [lo, hi];
if (sum < target) lo++; // nums[lo] is too small even with the largest partner left
else hi--; // nums[hi] is too large even with the smallest partner left
}
return [];
}
Time O(n), space O(1). It returns the pair's positions, or an empty array when there is none; I checked it against all pairs on 3,000 random sorted arrays, including empty ones. The follow-up lesson is general: a sorted input is a free resource, and a solution that ignores it is probably using more memory than it needs.
Distributed: can the answer be combined from the pieces?
For "the k largest of data spread over many machines", ask whether the global answer can be assembled from each machine's local answer. It can: every element of the global top k is also in the top k of the machine that holds it, so each machine sends only its own best k and one coordinator takes the best k of those.
function mergeTopK(shards, k) {
const candidates = shards.flatMap((shard) => [...shard].sort((a, b) => b - a).slice(0, k)); // each machine sends only its own best k
return candidates.sort((a, b) => b - a).slice(0, k);
}
The coordinator handles at most k times the number of machines items, however large the data is. I compared it with sorting everything on 2,000 random splits. Not every question combines this way: the median of the whole is not derived from the medians of the parts, so a distributed median needs a different design (counting values in ranges, then narrowing). Saying which kind of question you have is the senior answer.
Discussing tradeoffs at a senior level
The clearest seniority marker in the last ten minutes is raising a tradeoff before you are asked, and framing it against a use case rather than in the abstract. Intermediate candidates report complexity. Advanced candidates report complexity, name the alternative they rejected, and say what would change their mind.
| Axis | The sentence to have ready |
|---|---|
| Time vs space | "This is O(n) time with an O(n) hashmap. If memory were the binding constraint I'd sort in place and use two pointers — O(1) extra space, O(n log n) time. Which matters more here?" |
| Preprocess vs per-query | "If this is called once, the linear scan is right. If it's called a million times on the same array, I'd build prefix sums up front — O(n) once, then O(1) per query." |
| Worst case vs average case | "Quickselect is O(n) expected but O(n²) adversarially. If this is on a user-facing path where input could be hostile, I'd take the heap's guaranteed O(n log k) instead." |
| Simplicity vs constant factor | "The bitmask version is maybe 5× faster per node but noticeably harder to read. For n ≤ 12 I'd ship the readable one and leave a comment about the optimization." |
| Amortized vs bounded latency | "The dynamic array is amortized O(1) but a single resize is O(n). If this were in a real-time path I'd pre-size it." |
| Exact vs approximate | "For exact distinct counts I need O(n) memory. If an error of a percent or two is acceptable at this scale, HyperLogLog does it in kilobytes." |
| Mutating vs pure | "I'm sorting the input in place, which is faster but destroys the caller's array. If it's shared, I'd copy first and pay the O(n)." |
Every one of those ends in a question or a condition. That is deliberate — it turns a monologue into a design conversation and invites the interviewer to supply real-world context, which is the exact interaction they are trying to score. It is also honest: which side of a tradeoff is correct genuinely depends on information you do not have.
Two more things to have ready without being asked. How would this scale past one machine? — even a sentence ("if the array doesn't fit in memory, I'd external-sort by chunk, or shard by hash of the key and merge") shows the thinking extends past the whiteboard. And what would you test? — naming three unit tests and one property ("the output should always be a permutation of the input") signals engineering maturity that pure DSA never reaches.
When the problem is NP-hard: recognise it, then say the right thing
Some problems have no known algorithm that stays fast as n grows, and an interviewer will occasionally give you one on purpose. Failing to notice is the costly mistake: you spend thirty minutes hunting a polynomial solution that, as far as anyone knows, does not exist. Noticing is cheap, because the disguises are few.
| It sounds like | It is | When n is small | When n is large |
|---|---|---|---|
| "Visit every city once, shortest tour" | Travelling salesman | Bitmask DP over visited sets, O(n2 · 2n) | Heuristics or an approximation (for distances that obey the triangle inequality, a tour at most twice optimal from a spanning tree) |
| "A subset that adds up to exactly T" | Subset sum | Meet in the middle, O(2n/2 · n) | If the values are small, a DP over sums: pseudo-polynomial in T, not in n alone |
| "Split into k groups of equal total" | Partition and bin packing | Bucket backtracking with symmetry breaking, or bitmask DP | Greedy heuristics such as largest-first |
| "Fewest sets that cover everything" | Set cover | Backtracking with a bound, or bitmask DP | Repeatedly take the set that covers the most uncovered items (a known approximation) |
| "Longest simple path in a general graph" | Longest path | Backtracking or bitmask DP | No efficient exact method is known |
The clue that gives it away is almost always in the constraints: a bound like n <= 20, or n <= 40 with a sum question, is the setter telling you an exponential method is intended. The numbers decide how exponential. Counting steps against a budget of 108 operations, the largest n that fits is:
| Cost | Largest n within 108 | Cost at that n |
|---|---|---|
| n! | 11 | 39,916,800 |
| 3n | 16 | 43,046,721 |
| n2 · 2n | 18 | 84,934,656 |
| n · 2n | 22 | 92,274,688 |
| 2n | 26 | 67,108,864 |
Computed with a loop over n, so the cutoffs are exact for that budget. The table also explains the odd bounds: meet in the middle turns 240 into about 2 × 220 sums, which is why subset-sum questions go up to n = 40 while bitmask DP questions stop near 20.
Many problems look like the hard ones and are not, and mixing them up is the opposite error. The nearby pairs worth knowing: the shortest path is easy and the longest simple path in a general graph is hard (in a directed acyclic graph the longest path is easy again, by dynamic programming over a topological order); a tour using every edge once (Eulerian) is easy and a tour visiting every vertex once (Hamiltonian) is hard; deciding whether a graph can be coloured with two colours is easy and with three is hard.
Say it like this → "This is subset sum in disguise,
which is NP-hard in general, so I'm not going to look for a polynomial
algorithm. The constraint is n up to 40, so an exponential method is the
intended one, and 240 is too many but 220 is fine: I'll
split the items in two halves, list each half's sums, and match them. If the
values were small instead, I'd use a DP over the sum."
⚠ What not to say
"It's NP-hard, so it can't be done." It can, within the size the
constraints give. "I'll find a polynomial algorithm." Nobody knows how
for these problems, and promising one reads as not knowing the landscape.
Claiming a problem is NP-hard when you are not sure. Say "this
reminds me of" instead, and name the problem; the check is whether you can
see a clean reduction, and the near-miss pairs above show how often it fails.
A full mock: one Hard problem, start to finish
Everything above is easier to believe once you have watched it run on a real problem. Below is an interview on Sliding Window Maximum, written as a transcript. The notes in italics say which behaviour is being scored. Every piece of code shown was run: the brute force and the final function were compared on 3,000 random arrays, the trace table is the real output, and the wrong draft really returns what is claimed.
Interviewer: Given an integer array nums and a window
size k, the window slides one position at a time from the left
end to the right end. Return the maximum of each window.
You: Let me restate it: for every run of k consecutive
elements, output the largest, so there are n - k + 1 answers. A
few questions that change my approach. How large can n be? Can
values be negative? Is k always between 1 and
n?
Interviewer: n up to 105, values can be negative,
1 <= k <= n.
Scored: three questions, each tied to a decision (target complexity, sign of values, an empty-window case that cannot occur).
You: Let me do the example by hand. For [1, 3, -1, -3, 5, 3, 6,
7] with k = 3, the windows are [1,3,-1],
[3,-1,-3], [-1,-3,5], [-3,5,3],
[5,3,6], [3,6,7], so the answer is
[3, 3, 5, 5, 6, 7]. Brute force: for each window, scan its
k elements.
function maxSlidingWindowBrute(nums, k) {
const result = [];
for (let i = 0; i + k <= nums.length; i++) {
let max = nums[i];
for (let j = i + 1; j < i + k; j++) if (nums[j] > max) max = nums[j];
result.push(max);
}
return result;
}
You: That is O(n · k). With n = 105 and k = 50,000 that is
about 50,000 windows times 50,000 comparisons, roughly 2.5 billion, far past
the 108 a second I can count on. The bottleneck is that
consecutive windows share k - 1 elements and I rescan them every
time.
Scored: brute force with its complexity, the number checked against the constraint, the bottleneck named in one sentence.
You: First idea: remember the current maximum and only rescan when the
element leaving the window is that maximum. Most windows are then O(1).
Hold on, a decreasing array breaks it. In [8, 7, 6, 5, ...] the
maximum is always the element that just left, so I rescan on every window. I
counted it: on 20 decreasing numbers with k = 5 it rescans 15
times, on every window after the first, 75 comparisons; on 10,000 numbers with k = 5,000 it
does 25 million comparisons. That is the brute force again in its worst case,
so I'm dropping it.
Scored: a pivot. The idea was tested on an adversarial input before any code was written, and the counterexample was kept.
You: What the failed idea shows is that after the maximum leaves, I
need the next candidate ready. A max-heap of (value, index) pairs
does it: pop the top while its index is outside the window. That is O(n log n)
with lazy deletion and would pass. But look at what the heap keeps that it
never needs: if nums[j] <= nums[i] and j < i,
then j can never be a window maximum again, because
i is both larger and will stay in the window longer. So I can
throw such a j away immediately. What remains is a list of
indices whose values strictly decrease, oldest first. The front is the
maximum; the front expires when it leaves the window; a new element removes
smaller ones from the back. That is a monotonic deque, and each index is
pushed once and removed at most once, so O(n).
Scored: the optimization is a derivation from the bottleneck, an alternative (heap) is named with its cost, and the key fact ("can never be a maximum again") is stated as the reason.
Interviewer: Go ahead and code the deque version.
You: Signature first: takes nums and k,
returns an array. I'll use an array plus a head index rather than
shift(), which is O(n).
function maxSlidingWindow(nums, k) {
const result = [];
const deque = []; // indices; their values strictly decrease from front to back
let head = 0; // deque[head] is the front; moving head forward is a pop-front without shift()
for (let i = 0; i < nums.length; i++) {
if (head < deque.length && deque[head] <= i - k) head++; // the front slid out of the window
while (deque.length > head && nums[deque[deque.length - 1]] <= nums[i]) deque.pop(); // smaller or equal values can never be a max again
deque.push(i);
if (i >= k - 1) result.push(nums[deque[head]]);
}
return result;
}
Scored: narrating decisions (head index versus shift(), why <= pops equal values) instead of syntax.
You: Now I trace the example, with real values. The table shows the deque after each step as index:value.
| i | nums[i] | deque after the step | output |
|---|---|---|---|
| 0 | 1 | 0:1 | - |
| 1 | 3 | 1:3 (the 1 is smaller, popped) | - |
| 2 | -1 | 1:3, 2:-1 | 3 |
| 3 | -3 | 1:3, 2:-1, 3:-3 | 3 |
| 4 | 5 | 4:5 (index 1 expired, 2 and 3 popped) | 5 |
| 5 | 3 | 4:5, 5:3 | 5 |
| 6 | 6 | 6:6 | 6 |
| 7 | 7 | 7:7 | 7 |
You: That gives [3, 3, 5, 5, 6, 7], matching my hand
answer. Now edge cases. k = 1 should return the array itself.
Take [1, 0]. I expect [1, 0] and ... my first draft
of the expiry line was deque[head] < i - k, and it returns
[1, 1]. That is an off-by-one: the window holds indices
i - k + 1 to i, so an index equal to
i - k is already out and the test must be
<=. The final code above has the fix. It is worth knowing
that the buggy draft produces the right answer on the given example, so the
example alone would not have caught it.
Scored: tracing with values, then choosing an edge case that probes the boundary, and finding the bug before the interviewer does.
You: Complexity: each index is pushed once and popped at most once, so
O(n) time. The live part of the deque never holds more than
k indices; this array-and-head version does not free the
consumed front, so its memory is O(n), and a circular buffer would make it
O(k). Correctness in one sentence: the deque always holds,
in order, exactly the indices in the window that are not dominated by a later
index, so its front is the window's maximum. I also compared it with the brute
force on 3,000 random arrays with negatives and repeated values, plus
k = 1 and k = n.
Interviewer: What if the numbers arrive as a stream and you must report a maximum after each arrival?
You: The same code works almost unchanged: the loop body already
handles one element at a time and only needs the indices it has seen, so I
would keep the deque and a running index as fields and emit the front after
each push once k elements have arrived. With a circular buffer, memory stays O(k). If
the window were defined by time rather than by count, the expiry test would
compare timestamps instead of indices; the structure is the same.
Scored: a follow-up absorbed by naming what changed (nothing about the algorithm, only what the front is compared against).
What the transcript did, in order
- Questions that each changed the plan, then the example answered by hand.
- Brute force with its complexity, checked against the constraint, bottleneck named.
- A first improvement tested on an adversarial input and dropped before any code.
- The real idea derived from what the failure showed, with the alternative's cost stated.
- Code narrated, traced with values, an edge case found a bug, the fix explained.
- Complexity, a one-sentence correctness argument, then the follow-up answered by saying what changes.
Signals that separate an advanced performance from an intermediate one
- Constraints are read as a hint, not as trivia. "n ≤ 20, so they intend an exponential-in-n solution — that means bitmask" is said in the first two minutes, not discovered at minute thirty.
- A brute force is stated with its complexity before any optimizing starts. There is always something on the board, and the improvement is measured against a named baseline rather than asserted.
- Patterns are named out loud as they are chosen. "Monotonic stack, because I need the next greater element and each index is pushed and popped once" — not just correct code that happens to be a monotonic stack.
- The bottleneck is identified explicitly ("the expensive part is re-scanning the prefix") and the optimization is presented as buying that specific work back with a specific structure.
- Getting stuck produces a described procedure, not silence. Re-read constraints → hand-trace a tiny case → identify the shape → check for two composed patterns → relax a constraint → ask a precise question.
- The candidate tests their own code before declaring done — a real trace with real values, plus a deliberately chosen edge case, and finds their own off-by-one.
- Tradeoffs are raised unprompted and tied to a use case, with a stated condition that would flip the decision, rather than a memorized complexity table recited on request.
- A wrong approach is abandoned early and out loud. The counterexample or the missed constraint is stated, kept on the board, and used as the first line of the next approach.
- Correctness is claimed with an invariant, not with examples. One sentence says what stays true throughout, or why a discarded option cannot matter.
- Hard problems are recognised as hard. When the constraints point at an exponential method, the candidate says which problem it resembles and works within the stated size instead of hunting for a polynomial algorithm.
- Corrections are absorbed without defensiveness. A hint is taken, integrated, and credited — "good catch, that breaks when the values are negative; let me fix the invariant" — because the interviewer is simulating what code review with you feels like.
- Uncertainty is stated honestly and bounded. "I'm fairly sure this is O(n log n) amortized but I'd want to double-check the resize cost" beats a confident wrong claim every time, and it is the difference between an engineer you can trust and one you have to verify.
Before you move on
- Run the seven-step framework out loud on a problem you have never seen, including stating a brute force first.
- Read a constraint like n ≤ 10⁵ and say, before designing anything, which complexity class it's steering you toward.
- Name the bottleneck in a brute-force solution in one sentence, before naming the data structure that fixes it.
- Describe your procedure for being stuck, rather than going silent, and rehearse saying it out loud.
- Test your own solution against the original example plus one edge case before declaring it done.
- Say, for each checkpoint at minutes 5, 12, 25, 33 and 40, what must already be true and what you do if it is not.
- Take a keyword from a problem statement, name the first-guess pattern, and say the one check that would make you abandon it.
- Find a counterexample to a greedy idea on a tiny input, and turn it into the first line of your next approach.
- Write the one-sentence correctness argument for two pointers on sorted data, and for a monotonic deque.
- Given "it is a stream", "it is sorted" or "it is distributed", say which resource was taken away and what the standard move is.
- Look at a constraint like n <= 40 with a subset-sum question and say which exponential method is intended and why.
Opens in the editor: write it, run it, and check it against real tests.
All 159 for this level →5 questions, pass at 4. The check runs in your browser and the result stays on this device, so it is a self-check and not a certificate.
Finished reading?
0 of 48 chapters read in this series