Reading the problem
A coding interview is a specification exercise wearing an algorithms costume. This is the seven-minute routine that turns a two-sentence prompt into something you can actually solve — and the reason interviewers score it separately from your code.
What you'll be able to do
- Convert an underspecified prompt into a written signature, constraints, and three concrete examples before writing any logic
- Name the four ambiguities that appear in almost every prompt, and ask about them in a way that reads as rigour rather than stalling
- Recognise when a stated constraint is really a hint about the intended complexity
- Narrate your reasoning at a level that gives a hiring signal instead of a monologue
Here is the uncomfortable thing about coding interviews: the prompt you are given is deliberately incomplete. "Given a string, find the length of the longest substring without repeating characters" does not say what a character is, what happens on empty input, or whether "substring" means contiguous. A candidate who starts typing has answered all three questions silently, and will be marked down for two of them even if the code is perfect.
Published rubrics from hiring companies almost always break the score into four parts — problem solving, technical competency, testing, and communication — and only one of those four is the code. The other three are decided largely in the first and last five minutes.
A useful corrective to the advice everyone repeats. The argument is not that you should go silent — it is that narrating *while* you think produces mush, and that separating the thinking from the telling is a skill you can practise. Watch it before you do the exercises in this lesson, because the reading phase is exactly where this goes wrong. No chapter markers.
What the prompt is hiding
Every prompt withholds the same four things. Learn them as a checklist and you will never sit in silence at the start of a round again.
| Hidden thing | What to ask | Why it changes your code |
|---|---|---|
| Input domain | "Is this ASCII, Unicode, or arbitrary bytes? Can it be empty?" | Decides whether a 128-slot array works or you need a hash map |
| Size | "Roughly how long can the input be — thousands, or hundreds of millions?" | Decides whether O(n²) is acceptable or disqualifying |
| Output shape | "Do you want the length, or the substring itself? If there are ties, which one?" | A tie-breaking rule you invent may not be the one being graded |
| Mutability & memory | "Can I modify the input in place? Is extra O(n) space fine?" | In-place is often the intended answer to array questions |
The size question is the highest-value one, because the answer is nearly always a hint. A stated bound of n ≤ 10⁵ means the grader expects something around O(n log n) — a quadratic loop would run about 10¹⁰ operations, which is minutes, not milliseconds. A bound of n ≤ 20 is the opposite hint: it is small enough for 2ⁿ, so the intended answer is probably exhaustive search or bitmask dynamic programming, and you are being tested on whether you notice that brute force is allowed.
The routine
1Restate the problem in your own words▾
Not a recital — a compression. "So: I'm given a sequence of characters, and I need the length of the longest run of consecutive characters where no character repeats. Is that right?"
Two things happen. If you have misread it, you find out now instead of eleven minutes in, which is the single most expensive failure mode in a coding round. And you have handed the interviewer a chance to correct course, which they will take, because they want you to pass.
2Write the signature and the constraints down▾
Physically write it, in the editor, as a comment. This is a small act with an outsized effect: it converts a conversation into a specification, and it gives you something to point at later when you argue that your solution is correct.
// longestUnique(s: string) -> number
// s: ASCII, 0 <= s.length <= 1e5
// return: length of the longest substring with all-distinct characters
// ties: length only, so ties don't matter
3Build three examples — typical, empty, and adversarial▾
Three is the right number, and each has a job.
The typical example is for shared understanding: "abcabcbb" → 3. The empty or minimal example is where off-by-one bugs live: "" → 0, "a" → 1. The adversarial example is chosen to break the first approach you thought of: "bbbbb" → 1 if you assumed characters vary, and "tmmzuxt" → 5 if you assumed a repeat means you can restart from scratch.
That last one is worth dwelling on, because it is the actual trap in this problem. A plausible-but-wrong approach on seeing a repeat is "clear the set and start over from the current character." On "tmmzuxt" that gives 4 ("mzux"), missing the correct answer "mzuxt", which is 5. Finding this example before coding is worth more than finding it after.
4State the brute force, out loud, with its cost▾
"The obvious solution is: for every start index, extend as far as I can, tracking seen characters. That's O(n²) time, O(min(n, alphabet)) space."
Say it even when — especially when — you already know the fast answer. It establishes a baseline you can beat, it proves you can analyse cost, and it gives you something to fall back to if the optimisation stalls. A working O(n²) with a clear explanation of the O(n) you were reaching for outscores a broken O(n) every time.
5Name the bottleneck, then reach for a pattern▾
Optimisation in interviews is not inspiration; it is a small lookup table from observed waste to technique.
- Recomputing over a contiguous range you already scanned → sliding window
- Repeated "does this exist / how many times" questions → hash map or set
- Input is sorted, or sorting is free relative to the bound → two pointers or binary search
- Need the smallest or largest of a changing set → heap
- Overlapping subproblems on a sequence → dynamic programming
- "Next greater / previous smaller" → monotonic stack
The whole rest of this module and track is that table, expanded, with the invariant that makes each one correct.
6Code, narrating structure rather than syntax▾
Say what the block does before you write it, not what you are typing. "I'll keep a map from character to its last index, and a left edge. For each right edge, if the character was seen at or after left, I jump left past it." Then write it. Nobody needs to hear "open paren, i, less than."
7Test by walking the adversarial example, by hand▾
Out loud, on the example you built in step 3 that was designed to break you. Point at the variables and say their values. This is the "testing" rubric line, and it is the cheapest one to earn, because you already did the work in step 3 — most candidates simply forget to cash it in.
The whole thing, once, in code
Here is the finished solution to the example problem, written the way step 6 describes it. Edit it and run it — the assertions below are the three examples from step 3.
function longestUnique(s) {
// lastSeen: character -> the most recent index it appeared at.
const lastSeen = new Map();
let left = 0;
let best = 0;
for (let right = 0; right < s.length; right++) {
const ch = s[right];
const prev = lastSeen.get(ch);
// Only jump forward. `prev` may point BEHIND left, in which case that
// occurrence is already outside the window and must be ignored --
// this is the "tmmzuxt" trap.
if (prev !== undefined && prev >= left) {
left = prev + 1;
}
lastSeen.set(ch, right);
best = Math.max(best, right - left + 1);
}
return best;
}
const cases = [["abcabcbb", 3], ["", 0], ["a", 1], ["bbbbb", 1], ["tmmzuxt", 5]];
for (const [input, want] of cases) {
const got = longestUnique(input);
console.log(`${JSON.stringify(input).padEnd(10)} -> ${got} ${got === want ? "ok" : `WRONG, want ${want}`}`);
}
import json
def longest_unique(s):
# last_seen: character -> the most recent index it appeared at.
last_seen = {}
left = 0
best = 0
for right, ch in enumerate(s):
prev = last_seen.get(ch)
# Only jump forward. `prev` may point BEHIND left, in which case that
# occurrence is already outside the window and must be ignored --
# this is the "tmmzuxt" trap. Note `is not None`, not a truth test:
# index 0 is falsy, so `if prev:` would silently skip the first
# character of the string.
if prev is not None and prev >= left:
left = prev + 1
last_seen[ch] = right
best = max(best, right - left + 1)
return best
cases = [("abcabcbb", 3), ("", 0), ("a", 1), ("bbbbb", 1), ("tmmzuxt", 5)]
for text, want in cases:
got = longest_unique(text)
verdict = "ok" if got == want else f"WRONG, want {want}"
print(f"{json.dumps(text).ljust(10)} -> {got} {verdict}")
"abcabcbb" -> 3 ok "" -> 0 ok "a" -> 1 ok "bbbbb" -> 1 ok "tmmzuxt" -> 5 ok
Notice how much of that code is a direct transcription of the clarification work. The prev >= left guard exists because of the "tmmzuxt" example. The best = 0 initial value exists because of the empty-string example. Steps 1–3 are not ceremony; they are where the code comes from.
How long this should take
Seven minutes out of forty-five, of which most is steps 1–3. If you are eleven minutes in and still clarifying, you are stalling and it reads as stalling. If you are ninety seconds in and already typing, you have skipped the part that three of the four rubric lines measure.