Coding-Round Walkthroughs & Method¶
What this lesson gives you
A repeatable four-step process, live narration technique, and the art of recovering when you’re stuck — with a fully narrated LRU cache walkthrough.
Estimated time: 38 min read · Part: Cracking the Tier-1 Interview
A coding interview is a performance, and like any performance it rewards preparation of process, not just material. Two candidates can produce identical solutions; the one who narrates reasoning clearly, handles curveballs gracefully, and catches edge cases before the interviewer points them out will win the debrief. This lesson gives you the method: a four-step protocol that works on every problem, plus the communication moves that score well when you are uncertain.
The silent coder who lost to a weaker solution
In a paired-debrief at a tier-1 firm, two candidates solved the same problem. Alex produced a clean O(n log n) solution but said nothing while typing — head down, fingers moving, fifteen silent minutes. Jordan produced an O(n²) brute force while speaking in full sentences: “My first instinct is to sort, but let me check whether that meets the time constraint given n is up to 10&sup5…” followed by a pivot, followed by an acknowledged mistake, followed by the correct fix. Jordan got the offer. The debrief note on Alex read: “We have no signal on how he thinks. Could not evaluate.”
The Four-Step Method¶
Jazz improvisation vs free-form noise
Expert jazz musicians do not play whatever comes to mind — they improvise within a chord structure. The four-step method is your chord structure: it keeps you from spinning when adrenaline spikes, and it gives the interviewer predictable checkpoints where they can insert feedback rather than watching you disappear into a code tunnel.
Step 1 — Clarify. Before touching the keyboard, ask questions. Never assume. The questions are not stalling; they demonstrate engineering instinct. Ask about: data types and ranges, whether the input is sorted, whether the answer must be unique, what to return if no answer exists, and what the interviewer considers the most important performance constraint (time vs memory vs simplicity).
Step 2 — Plan. Name the pattern you intend to use and state the complexity you expect. Sketch the algorithm in one to three sentences. Only move to code when the interviewer nods or says “go ahead.” This gate prevents the worst failure mode: coding fifteen minutes in the wrong direction.
Step 3 — Code. Write clean code while narrating. Announce each non-obvious decision: “I’m using a sentinel node here to avoid a special-case for an empty list.” Use descriptive variable names; left and right over l and r, frequency over f. Interviewers copy your code into the debrief tool — readable code is a proxy for clean production code.
Step 4 — Test. Walk through your code with a concrete small example by hand — not just “it looks right,” but: “let me trace this with [3, 1, 4], target 5. i=0, num=3, complement=2, not in seen, add 3→0. i=1, num=1, complement=4, not in seen, add 1→1. i=2, num=4, complement=1, 1 is in seen at index 1, return [1, 2]. Correct.” Then explicitly test edge cases: empty input, single element, all-same elements, sorted in reverse.
How to Clarify: Constraint Reading¶
"Before I start, a few quick questions to make sure I understand the constraints.
1. What is the range of n? [If n can be 10^5, I need O(n log n) or better.
If n <= 1000, O(n^2) might be acceptable.]
2. Are the values bounded? [If values are in [-10^4, 10^4], I can use an
array as a frequency table instead of a hash map for O(1) guaranteed.]
3. Can input contain duplicates? [This affects what 'unique pair' means
and whether I need to skip repeated elements.]
4. What should I return if no answer exists? -1? Empty list? Raise an error?
5. Should I handle None / empty input, or can I assume valid input?
6. Is memory a concern, or should I optimise purely for time?"
Narrated Walkthrough: Design a LRU Cache¶
This is one of the most common mid-level interview problems. The requirement: implement a Least Recently Used (LRU) cache with O(1) get and O(1) put. Here is the full four-step execution narrated as you would speak it in the room.
Why OrderedDict solves this elegantly
A doubly-linked list gives O(1) move-to-front and O(1) remove-from-back. A hash map gives O(1) key lookup. An LRU cache is exactly the combination: hash map keys pointing into a doubly-linked list ordered by recency. Python’s OrderedDict is exactly this data structure under the hood. In production you would implement the list explicitly for clarity; in an interview you reach for OrderedDict and explain what it does — then offer to implement the list manually if asked.
from collections import OrderedDict
class LRUCache:
"""O(1) get and put using Python's OrderedDict (doubly-linked list + hash map).
Narration: "I'll use OrderedDict which maintains insertion order.
The most-recently-used key lives at the right (last). When I access
a key I move it to the right; when capacity is exceeded I pop from
the left (the LRU end)."
"""
def __init__(self, capacity: int) -> None:
self.capacity = capacity
self.cache: OrderedDict[int, int] = OrderedDict()
def get(self, key: int) -> int:
if key not in self.cache:
return -1
self.cache.move_to_end(key) # mark as most recently used
return self.cache[key]
def put(self, key: int, value: int) -> None:
if key in self.cache:
self.cache.move_to_end(key)
self.cache[key] = value
if len(self.cache) > self.capacity:
self.cache.popitem(last=False) # evict LRU (the leftmost item)
# ── inline test (do this during the interview) ────────────────────────────
lru = LRUCache(2)
lru.put(1, 1) # cache: {1:1}
lru.put(2, 2) # cache: {1:1, 2:2}
assert lru.get(1) == 1 # cache: {2:2, 1:1} — 1 is now most recent
lru.put(3, 3) # evicts key 2; cache: {1:1, 3:3}
assert lru.get(2) == -1
lru.put(4, 4) # evicts key 1; cache: {3:3, 4:4}
assert lru.get(1) == -1
assert lru.get(3) == 3
assert lru.get(4) == 4
print("LRU tests passed")
"""Manual doubly-linked list + hash map — show this if asked to go deeper."""
class DLNode:
def __init__(self, key: int = 0, val: int = 0) -> None:
self.key = key; self.val = val
self.prev: 'DLNode | None' = None
self.next: 'DLNode | None' = None
class LRUCacheManual:
def __init__(self, capacity: int) -> None:
self.cap = capacity
self.map: dict[int, DLNode] = {}
# sentinel head (LRU end) and tail (MRU end) simplify edge cases
self.head = DLNode(); self.tail = DLNode()
self.head.next = self.tail; self.tail.prev = self.head
def _remove(self, node: DLNode) -> None:
node.prev.next = node.next # type: ignore[union-attr]
node.next.prev = node.prev # type: ignore[union-attr]
def _insert_at_tail(self, node: DLNode) -> None:
prev = self.tail.prev
prev.next = node # type: ignore[union-attr]
node.prev = prev
node.next = self.tail
self.tail.prev = node
def get(self, key: int) -> int:
if key not in self.map:
return -1
node = self.map[key]
self._remove(node)
self._insert_at_tail(node)
return node.val
def put(self, key: int, value: int) -> None:
if key in self.map:
self._remove(self.map[key])
node = DLNode(key, value)
self.map[key] = node
self._insert_at_tail(node)
if len(self.map) > self.cap:
lru = self.head.next # leftmost real node
self._remove(lru) # type: ignore[arg-type]
del self.map[lru.key] # type: ignore[union-attr]
Handling Curveballs Mid-Problem¶
Interviewers deliberately inject follow-up constraints to see how you adapt. Common curveballs and their moves:
| Curveball | What the interviewer is testing | The right move |
|---|---|---|
| “What if the input is a stream?” | Can you generalize beyond batch processing? | Switch to a generator; process lazily; discuss buffer size |
| “What if it doesn’t fit in memory?” | Do you know external algorithms? | External merge sort; sketch a chunk-and-merge approach |
| “Can you do it in O(1) space?” | Do you know in-place techniques? | Identify if the input array can be the work space; explain trade-offs |
| “Can you make it thread-safe?” | Do you think about concurrency? | Wrap critical sections in a Lock; mention GIL limitations for CPU-bound tasks |
| “What if k is very large?” | Do you re-examine your earlier complexity claim? | Re-derive complexity with the new k; suggest a streaming alternative |
Python-Specific Interview Tips¶
Communication Phrases That Score Well¶
WHEN STARTING:
"My first instinct is [X], but before I code it let me check the
complexity given the constraint n <= 10^5..."
WHEN RECOGNISING A PATTERN:
"I notice the array is sorted — that opens up binary search, which
would bring this from O(n) to O(log n)."
WHEN STUCK:
"Let me restate the problem in my own words and try a small example.
[trace]. OK — I see the structure now. The issue is..."
WHEN FINDING A BUG DURING TEST:
"Good — tracing through this, when left == right I'm returning an
empty string but the problem guarantees a non-empty result. Let me
add that guard at the top."
WHEN ASKED TO OPTIMISE:
"My current solution is O(n^2). I could get to O(n) by trading O(n)
space for a hash map. Whether that's worth the trade depends on your
memory constraints — is that a concern here?"
WHEN YOU DON'T KNOW THE OPTIMAL:
"I know there's likely an O(n log n) approach here — possibly
involving a sorted data structure. I don't have it at my fingertips
right now. Can I code the O(n^2) approach first and we discuss
the optimisation together?"
Over-optimising without narration
Prematurely diving for the optimal solution while silent is the most common failure mode among strong engineers. If you cannot narrate the optimal solution confidently, code the brute force while narrating it, then say “I know I can improve this — let me think out loud.” An O(n2) solution that is clearly explained beats an O(n) solution produced silently in every debrief rubric that matters.
Write your own test harness in the editor
Do not rely solely on the platform’s test runner. At the bottom of your solution file, write three to five inline assertions that cover: the happy path, an edge case (empty input, single element), and a known tricky case. Interviewers notice candidates who proactively test their own code — it mirrors professional practice and often catches bugs before the interviewer needs to.
Consulting lens: the interview as a client meeting simulation
Solutions consulting interviews deliberately mimic client engagement dynamics. When a client asks “how would you build our data pipeline?” they are watching whether you ask clarifying questions before scoping, whether you explain your reasoning during design, and whether you acknowledge uncertainty gracefully. The coding interview is the same dynamic, compressed into 45 minutes. Candidates who treat it as a collaborative problem-solving session rather than a solo performance score significantly higher on “would work well with clients.”
Key takeaways
- The four steps — clarify, plan, code, test — are not suggestions; they are a protocol that prevents the most common failure modes.
- Narrate every non-obvious decision; silence is the single biggest debrief risk.
- Use constraint values (n ≤ 105) to set your complexity budget before writing a line.
- Write inline assertions at the bottom of your solution and trace through them out loud.
- When stuck: restate the problem, try a tiny example, identify what kind of object the algorithm should operate on.
- Python idioms (enumerate, zip, comprehensions, any/all) signal fluency and cut implementation time.
- Communicate trade-offs (“I can trade O(n) space to get O(n) time”) rather than just presenting one solution.
Knowledge check
InterviewA candidate solves a problem correctly but says nothing during the 30-minute session. The interviewer fills out the debrief. What is the most likely outcome?
- Strong hire — a correct solution is the only signal that matters
- Hire — the interviewer will infer good thinking from clean code
- No hire or weak hire — the debrief will say “no signal on how they think”
- No hire — silence is an automatic disqualifier at all firms
Answer
No hire or weak hire — the debrief will say “no signal on how they think”
Debrief rubrics at tier-1 firms evaluate problem-solving process , not just correctness. A silent candidate leaves the interviewer unable to assess communication skill, client-readiness, debugging instinct, or how they would behave in a team setting. The debrief note “no signal on how they think” is a real failure mode that strong engineers fall into regularly. The correct answer is “no hire or weak hire” — but note that silence is not an automatic disqualifier everywhere; some firms weight correctness more heavily. At solutions consulting firms and client-facing roles, communication is weighted at least equally to correctness.
Exercise
Exercise 15.2 · LRU Cache with Explicit Doubly-Linked List Without using OrderedDict, implement LRUCache with O(1) get and O(1) put using a hash map and a hand-written doubly-linked list. Use sentinel head and tail nodes to avoid edge-case branches. Verify it passes the same five assertions used in the OrderedDict version above.
Show solution
class Node:
__slots__ = ("key", "val", "prev", "next")
def __init__(self, key: int = 0, val: int = 0) -> None:
self.key = key; self.val = val
self.prev: "Node | None" = None
self.next: "Node | None" = None
class LRUCache:
def __init__(self, capacity: int) -> None:
self.cap = capacity
self.store: dict[int, Node] = {}
self.head, self.tail = Node(), Node() # sentinels
self.head.next = self.tail
self.tail.prev = self.head
def _cut(self, n: Node) -> None:
n.prev.next = n.next # type: ignore[union-attr]
n.next.prev = n.prev # type: ignore[union-attr]
def _mru(self, n: Node) -> None:
"""Insert node just before tail (MRU position)."""
n.prev = self.tail.prev
n.next = self.tail
self.tail.prev.next = n # type: ignore[union-attr]
self.tail.prev = n
def get(self, key: int) -> int:
if key not in self.store:
return -1
node = self.store[key]
self._cut(node); self._mru(node)
return node.val
def put(self, key: int, value: int) -> None:
if key in self.store:
self._cut(self.store[key])
del self.store[key]
node = Node(key, value)
self.store[key] = node
self._mru(node)
if len(self.store) > self.cap:
lru = self.head.next # type: ignore[union-attr]
self._cut(lru) # type: ignore[arg-type]
del self.store[lru.key] # type: ignore[union-attr]
lru = LRUCache(2)
lru.put(1, 1); lru.put(2, 2)
assert lru.get(1) == 1
lru.put(3, 3)
assert lru.get(2) == -1
lru.put(4, 4)
assert lru.get(1) == -1
assert lru.get(3) == 3
assert lru.get(4) == 4
print("All assertions passed")