Interviewers grade how you think as much as whether your code passes. A candidate who clarifies, explains a brute force, improves it and tests it out loud usually beats one who silently writes a perfect answer. A fixed flow also keeps you calm: when you get stuck, you always know the next step.
The flow at a glance#
The whole flow fits in seven steps. Each one is covered in more detail below.
| Step | What you do | What it sounds like |
|---|---|---|
| 1. Clarify | Restate the problem; pin down inputs, outputs, constraints | "So I return the indices, not the values?" |
| 2. Edge cases | List the awkward inputs and agree how to handle them | "What should happen on an empty array?" |
| 3. Brute force | Give the simplest correct idea and its complexity | "Checking every pair is time, space." |
| 4. Optimise | Find the bottleneck and the technique that removes it | "The inner loop is a lookup, so a hash map makes it ." |
| 5. Commit | Pick an approach, list data structures, outline steps, state complexity | "Given up to , I'll go with the version." |
| 6. Code | Write clean code while narrating | "This map stores value to index." |
| 7. Test | Dry-run a normal case, then the edge cases | "Walking through [2, 7, 11] with target 9..." |
1. Clarify the problem#
Don't touch the keyboard yet. Restate the problem in your own words, then ask:
- Inputs: types, sizes, can they be empty or
None? Sorted? Unique values? Negative numbers? - Outputs: return a value, modify in place, or print? Any order required? What if there's no answer?
- Constraints: how large is ? The answer tells you the target complexity (see Which Technique When?).
2. List edge cases early#
Saying these out loud before coding shows you're thinking about correctness:
- Empty input, or input of size 1
- All elements the same, or all different
- Duplicates
- Negative numbers or zeros
- Already in the final state (sorted, balanced, ...)
- No valid answer: return
-1,[],None, or raise?
Note them down, agree what each should return, and come back to them in step 7.
3. Start with brute force#
Explain the simplest correct solution and its time and space complexity, even if it's slow. It proves you understand the problem, gives you a fallback, and often points straight at the bottleneck. Walk through an example with it so you and the interviewer agree on the expected output.
"First, I can solve this by checking every pair, which is . Then I'll look at what's repeated."
4. Optimise the bottleneck#
Ask: which part of the brute force is doing repeated work?
- A repeated search → hash map/set, sorting + binary search, or a heap.
- Repeated range sums → prefix sums.
- Recomputed subproblems → memoisation / DP.
- A window that slides → two pointers or a sliding window.
If you're stuck:
- Describe the solution in plain English as if doing it by hand, then translate.
- Go through data structures one by one and rule them out: "Would a stack help? A heap?"
- Try a smaller example, or a sorted version of the input.
Compare the options by time and space, then pick one:
"Considering can be , I'll go with the hash map approach: time, space."
5. Commit before coding#
Before writing code, say briefly:
- Data structures you'll use and why (e.g. a frequency map, a min-heap).
- The algorithm in a few steps: initialise, loop, update, return.
- Time and space complexity, with the reason: " because each element enters and leaves the window once."
- "If this makes sense, I'll start coding." Waiting for a nod catches misunderstandings before they cost you 15 minutes.
6. Code it#
Now write the code you just outlined, keeping the interviewer with you as you go.
- Keep narrating: what each variable means, why each condition is there.
- Use clear names (
left,right,seen, noti2,tmp). - Handle the agreed edge cases explicitly, often as early returns.
- Re-read the code for syntax slips: missing colons, wrong indentation, off-by-one ranges.
7. Test it#
Don't wait for the interviewer to find bugs. Test your own code first:
- Dry-run the example from step 3, line by line, tracking variables.
- Run each edge case from step 2.
- Say: "That's complete. Let's run it against a test case." If you can execute code, use the template in Test Your Solution Live.
If the interviewer asks "can you make this faster?", look for repeated work, a lookup that could be a hash map or set, a search that could be binary, or branches you could prune.
Codebase-style rounds#
Some rounds give you an existing codebase or API instead of a blank editor ("implement get_execution_order using this service"). The same flow applies with two extra steps.
- Clarify requirements: input/output types, constraints, what counts as valid data, and how failures should be handled (retry? empty result? raise?).
- Read the code first: what each module is responsible for, what the API returns, and where your change belongs. Propose the location and confirm it before writing anything.
- Outline the core logic: state the algorithm, trade-offs and complexity; dry-run one happy example.
- Code in layers: fetch (with retries), sanitise, then run the core logic, returning early whenever there's nothing left to process.
- Dry-run the happy path, then the edge cases, and confirm the return format.
A template for the fetch → sanitise → core shape:
import time
from collections import deque
from typing import List
class PipelineScheduler:
def __init__(self, api_service):
self.api_service = api_service
def get_execution_order(self, pipeline_id: str) -> List[str]:
# 1. Fetch with retry and exponential backoff
resp = None
delay = 0.1
for _ in range(3):
resp = self.api_service.fetch_pipeline_tasks(pipeline_id)
if resp and resp.status_code == 200:
break
time.sleep(delay)
delay *= 3
if not resp or resp.status_code != 200:
return [] # fail gracefully after retries
# 2. Extract and sanitise
raw_tasks = resp.tasks or []
valid_tasks = self._sanitize_tasks(raw_tasks)
if not valid_tasks:
return []
# 3. Core logic: topological sort (Kahn's algorithm)
indegree = {t["id"]: 0 for t in valid_tasks}
children = {t["id"]: [] for t in valid_tasks}
for task in valid_tasks:
for dep in task["depends_on"]:
if dep in indegree:
children[dep].append(task["id"])
indegree[task["id"]] += 1
ready = deque(t for t, d in indegree.items() if d == 0)
execution_order = []
while ready:
task_id = ready.popleft()
execution_order.append(task_id)
for child in children[task_id]:
indegree[child] -= 1
if indegree[child] == 0:
ready.append(child)
has_cycle = len(execution_order) != len(valid_tasks)
return [] if has_cycle else execution_order
def _sanitize_tasks(self, raw_tasks):
# Keep only well-formed tasks; drop anything without an id
return [
{"id": t["id"], "depends_on": t.get("depends_on") or []}
for t in raw_tasks
if isinstance(t, dict) and t.get("id")
]
Afterwards, talk about extensibility: what changes if requirements grow (new task fields, partial failures, very large pipelines)?
Naming that reads well#
| Rule | Weak | Strong |
|---|---|---|
| Methods are verbs | def data_sanitization(self, tasks) |
def _sanitize_tasks(self, tasks) |
| Collections are plural | for item in task_list: |
for task in tasks: |
| Booleans ask a question | if cycle_detected: |
if has_cycle: |
| Avoid generic names | data = resp.data |
raw_tasks = resp.tasks |
| Name by state | tasks = filter(tasks) |
valid_tasks = _sanitize_tasks(raw_tasks) |
Key takeaways#
- Clarify inputs, outputs and constraints before anything else; constraints hint at the target complexity.
- Always state a brute force with its complexity, then optimise the bottleneck.
- Commit out loud (data structures, steps, complexity) and get a nod before coding.
- Narrate while coding, then dry-run a normal case and every edge case you listed.
- In codebase rounds, read the existing code first and structure your change as fetch → sanitise → core logic with early returns.