Part 15 · Lesson 59 · read

The Interview Playbook

Ask, plan, code, test: the 45 minutes, step by step.

On this page

  1. The interview is a conversation
  2. The 45 minutes at a glance
  3. Step 1: clarify the problem
  4. Step 2: examples and edge cases, out loud
  5. Step 3: brute force, then make it faster
  6. Step 4: clean code when time is short
  7. Step 5: test with a dry run
  8. Step 6: state time and space complexity
  9. Using hints well
  10. JavaScript-specific tips
  11. Common mistakes
  12. Mock interview walk-throughs (4)
  13. Make it stick
  14. What's next

The interview is a conversation

You now know the main data structures and patterns. A coding interview tests one more thing. Can you use them while another person watches, in about 45 minutes, on a problem you have not seen before? Interviewers do not only ask "did the code pass?" They also ask: Can I follow how this person thinks? Would I enjoy working with them? Do they notice problems before I point them out?

That is good news, because thinking and speaking clearly are skills you can practise. This lesson gives you a fixed routine to follow every time. On interview day you can then spend your energy on the problem, and not on deciding what to do next. The routine has six steps: clarify, examples, plan, code, test, analyse. We use Two Sum as our example all the way through, because you already know it.

The 45 minutes at a glance

Dry run · a 45-minute interview, minute by minute
MinutesPhaseWhat you doWhat you might say
0 – 5ClarifySay the problem again in your own words. Ask about input size, value ranges, duplicates, empty input, and what to return."So I get an array of integers and must return the indices of two numbers adding up to target. Can there be negatives? Is exactly one answer guaranteed?"
5 – 10Examples and edgesWork through the given example by hand. Then make up one or two of your own: the smallest input, duplicates, no answer."For [3,3] and target 6 the answer is [0,1]. For an empty array I would return an empty array."
10 – 17Brute force, then betterSay the obvious solution and what it costs. Find what is wasteful. Name the improvement and ask if you may start."Brute force checks every pair: O(n²). The waste is searching for the partner each time. A map makes that O(1). Shall I go with the map?"
17 – 32CodeWrite calmly, say what each part does, and use clear names. You may leave a small helper for later ("I'll fill this in after")."I loop once. For each number I look for target minus that number in the map, then I store this number."
32 – 39TestDo a dry run (follow your code by hand) on the example and on an edge case. Go line by line and track the variables. Fix what you find."i = 0, nums[0] = 2, needed 7, map is empty, store 2 → 0. i = 1, needed 2, found at 0, so return [0, 1]."
39 – 42Complexity and alternativesSay the time and the memory, and what n means. Mention a trade-off, or what you would do for sorted or huge input."O(n) time and O(n) memory for the map. If memory mattered, I could sort a copy of the pairs (value, index) and use two pointers. That is O(n log n) time, and it still needs O(n) memory for the pairs."
42 – 45Your questionsAsk one or two real questions about the team or the work. Say thank you."What does a typical week look like for someone on this team?"

Real interviews start with a few minutes of introductions and end with your questions. So you often have only about 35 minutes for the problem itself. Keep the same balance: roughly a third of the time for questions and planning, a third for coding, and the rest for testing and talking.

If you run short on time
Never skip the testing step. A brute-force answer that works and that you have tested is worth more than a faster answer that is not finished. If time is almost over, say so: "I won't finish the faster version. Here is how I would finish it."

Step 1: clarify the problem

Problem statements are short and a little vague on purpose. Asking questions is not a sign of weakness. It is the first thing a real engineer does with a new task. First, say the problem again in your own words. Then a misunderstanding shows up now, and not at minute 30. Next, ask the questions whose answers would change your solution.

Dry run · the clarifying questions that matter most
AskWhy the answer changes your solutionWhere we covered it
How big can the input be?Tells you if O(n²) is fine (n up to a few thousand), or if you need O(n log n) or O(n) (n up to 100,000 or more).Lesson 12
What are the value ranges? Negatives, zero, huge numbers?Negatives break sliding windows. Huge numbers may be too big to stay exact. Zero breaks division tricks.Lessons 23, 24
Can there be duplicates?Decides if you need a Set or a Map of counts, and how to skip repeats.Lessons 10, 26
Is the input sorted?Sorted input suggests binary search or two pointers. Unsorted input may need a sort first.Lessons 21, 28
What if the input is empty, or has one element?Tells you the base case (the simplest case) and what the function must return.Lesson 11
What should I return when there is no answer?It could be -1, null, an empty array or false. The interviewer decides, so ask.Lesson 11
May I modify the input? Do I need to preserve order?If yes, you can change the data in place (sink an island, swap). If no, you must make a copy.Lesson 8
Is there exactly one answer, or several (and do you want all of them)?With one answer you can stop early. With many answers you must collect them all.Lesson 33

Do not ask all eight every time. Pick the ones that are really open. If the statement already says the array is sorted, do not ask. Lesson 11 has the full method for reading a problem.

Step 2: examples and edge cases, out loud

Take the example from the problem and solve it by hand. Say each step out loud: "nums is 2, 7, 11, 15 and the target is 9. 2 and 7 add up to 9, so the answer is indices 0 and 1." This shows you understood the problem. The way you solved it by hand is often the algorithm itself.

Then make up edge cases. These are small, odd inputs where code often breaks. Here is a checklist you can trust: empty input, one element, two elements, all elements equal, duplicates, negatives and zero, already sorted, sorted in reverse, the largest allowed size, and "no answer exists". Say them out loud and write the expected answers next to them. You will use the same list again in step 5.

Step 3: brute force, then optimise

Always say the brute force first, even if you already see a better answer. Brute force means trying every possibility in the simplest way. It is correct, it shows you can solve the problem, and it gives you a cost to beat. Then ask: where is the wasted work? It is usually one of these. You search for something that a map could look up (lesson 26). Or you work out again something that a running total could remember (lesson 20). Or you scan a sorted range that binary search could cut in half (lesson 28). Or you solve the same small problem many times (lesson 54).

JavaScript
// "Does the array contain a duplicate?" in three steps of improvement, said aloud as you go.function hasDuplicateBrute(nums) {            // 1. brute force: compare every pair, O(n^2) time, O(1) space  for (let i = 0; i < nums.length; i++) {    for (let j = i + 1; j < nums.length; j++) {      if (nums[i] === nums[j]) return true;    }  }  return false;}function hasDuplicateSort(nums) {             // 2. sort, then equal values sit side by side: O(n log n) time  const sorted = [...nums].sort((a, b) => a - b);   // copy first so the caller's array is untouched  for (let i = 1; i < sorted.length; i++) {    if (sorted[i] === sorted[i - 1]) return true;  }  return false;}function hasDuplicateSet(nums) {              // 3. remember what we have seen: O(n) time, O(n) space  const seen = new Set();  for (const x of nums) {    if (seen.has(x)) return true;    seen.add(x);  }  return false;}for (const f of [hasDuplicateBrute, hasDuplicateSort, hasDuplicateSet]) {  console.log(f([1, 2, 3, 1]), f([1, 2, 3]), f([]));}// true false false// true false false// true false false

Say it like climbing a ladder: "Brute force is O(n²) time. Sorting gives O(n log n) time with O(1) extra memory, if I may change the order. A Set (a collection that remembers values and checks "seen it?" in O(1) time) gives O(n) time but uses O(n) memory. Which trade-off would you like?" Offering the choice is a strong move. Then agree on the plan before you type: "Does that approach sound reasonable to you?"

Step 4: clean code under time pressure

You write for two readers: the interviewer, and yourself ten minutes from now, when you must dry-run the code. So:

  • Write one function with a clear name and inputs, in the same form the problem gave you.
  • Choose names that mean something (needed, left, best), not a, t, m.
  • Handle the special cases first with an early return (leave the function at once), then write the main logic.
  • Explain in blocks, not word by word: "now the loop that builds the map".
  • Use built-in tools you know well (Map, Set, Math.max). Do not write a heap from memory unless asked. JavaScript has no built-in priority queue, so just say "I would use a priority queue here".
  • If a helper function slows you down, give it a name, use it, and write its body later.
JavaScript
// The same function twice. Both are correct; only one is pleasant to read, dry-run and fix at minute 35.function f(a, t) {  const m = new Map();  for (let i = 0; i < a.length; i++) {    if (m.has(t - a[i])) return [m.get(t - a[i]), i];    m.set(a[i], i);  }  return [];}function twoSum(nums, target) {  const indexOf = new Map();                   // value -> index where we saw it  for (let i = 0; i < nums.length; i++) {    const needed = target - nums[i];    if (indexOf.has(needed)) return [indexOf.get(needed), i];    indexOf.set(nums[i], i);                   // set AFTER the check so an element never pairs with itself  }  return [];}console.log(f([2, 7, 11, 15], 9));        // [ 0, 1 ]console.log(twoSum([2, 7, 11, 15], 9));   // [ 0, 1 ]

Step 5: test with a dry run

When the code is written, do not say "I think it works." Run it by hand. This is the dry-run habit you built in lesson 1. Point at each line and say the value of each variable. Write the variables in a small table next to the code. Use three kinds of input. First, the example from the problem. Second, one edge case from step 2. Third, if you have time, one case made to break your idea (for Two Sum: the same number used twice).

JavaScript
// A tiny test table you can type in two minutes, and run in your head just as well.function twoSum(nums, target) {  const indexOf = new Map();  for (let i = 0; i < nums.length; i++) {    const needed = target - nums[i];    if (indexOf.has(needed)) return [indexOf.get(needed), i];    indexOf.set(nums[i], i);  }  return [];}const cases = [  ["the given example",     [2, 7, 11, 15], 9,  [0, 1]],  ["duplicates",            [3, 3],         6,  [0, 1]],  ["negatives",             [-1, -2, -3, -4], -6, [1, 3]],  ["no element used twice", [3, 2, 4],      6,  [1, 2]],  ["no answer",             [1, 2],         10, []],  ["empty array",           [],             5,  []],];for (const [name, nums, target, want] of cases) {  const got = twoSum(nums, target);  console.log(JSON.stringify(got) === JSON.stringify(want) ? "pass" : "FAIL", name, got);}// pass the given example [ 0, 1 ]// pass duplicates [ 0, 1 ]// pass negatives [ 1, 3 ]// pass no element used twice [ 1, 2 ]// pass no answer []// pass empty array []

If you find a bug yourself, you earn credit ("good catch, that is an off-by-one", which means a mistake of 1 in a count or index). If the interviewer finds it, it costs you. When you find a bug, change the smallest thing. Run the case that failed. Then run the earlier cases again to check that nothing else broke.

Step 6: state time and space complexity

Give the analysis before you are asked. A good answer has the same few parts every time:

Dry run · how to state complexity
Do thisExampleWhy
Say what n is"n is the length of nums." In a grid: "R rows, C columns". In a graph: "V vertices (points), E edges (links)".A complexity means nothing if you do not say what n is.
Name the biggest cost"Time is O(n log n) because the sort is the slowest part. The loop after it is only O(n)."Shows you can find the slowest part (the bottleneck).
Count extra memory, including the call stack"Memory is O(n) for the map. The recursion also uses O(h) call stack, where h is the tree height."Interviewers often ask about the hidden memory that recursion uses.
Say best, average and worst case when they differ"A hash lookup is O(1) on average. Quick sort is O(n log n) on average but O(n²) in the worst case."Lesson 12.
Offer one trade-off"We could save memory by sorting in place. The cost is O(n log n) time, and we lose the original order."Turns a single number into a talk about design choices.

Practise one sentence like this: “Time is O(…) because …, memory is O(…) because …, where n is …”.

The interviewer may then ask "can you do better?" They may be hinting that a better solution exists, or they may just want to see how you react. Think out loud about the slowest part. Is it the sort? The lookup? The repeated work? You do not need to find a miracle. It is also a good answer to explain the lowest possible cost ("I must at least read every element, so O(n) time is the minimum").

Using hints well

Hints are normal. Almost every interviewer plans to give some. What they watch is how you use them.

  1. Get stuck out loud. Say what you have tried and where it fails: "sorting loses the original indices, so I need a way to keep them". That is a clear question, and it earns a clear hint.
  2. Ask for a direction, not the answer. "Should I think more about the data structure or about the algorithm?"
  3. Say the hint again in your own words and say what it changes: "So you suggest a map from value to index. Then each lookup is O(1)."
  4. Do not feel bad about it. A hint at minute 15 costs far less than a silent dead end until minute 40.
  5. Do not ignore it. If the interviewer mentions a data structure or a limit, use it straight away.
A rule of thumb
If you have made no progress for about five minutes, say so and ask for a hint. If you have a working but slow solution, say "I have a correct O(n²) solution. I would like to improve it. May I think for a minute?" Both are good signs to an interviewer.

JavaScript-specific tips

JavaScript has a few behaviours that can quietly break an answer that is otherwise correct. We ran every sample below to check the printed values. Knowing these behaviours shows that you have experience.

sort() compares text by default

If you give sort() no comparator (a small function that says which of two values comes first), it turns the values into strings. Then 10 sorts before 9. For numbers, always pass (a, b) => a - b (lesson 18). It also sorts in place, which means it changes the original array. Copy first with [...arr] if you need to keep the original.

JavaScript
const nums = [10, 9, 1, 100];console.log([...nums].sort());                // [ 1, 10, 100, 9 ]      default order compares TEXT: "10" < "9"console.log([...nums].sort((a, b) => a - b)); // [ 1, 9, 10, 100 ]      a comparator fixes itconsole.log(nums);                            // [ 10, 9, 1, 100 ]      the spread copied it, so the original is intactnums.sort((a, b) => a - b);                   // sort() with no copy changes the array in placeconsole.log(nums);                            // [ 1, 9, 10, 100 ]console.log(["b", "a", "c"].sort().join("")); // abc                    fine for single letters: text order is what we want

-0, NaN and decimals

Three kinds of values break the usual idea of "equal". First, -0 (negative zero) is a real value. It equals 0 under ===, but Object.is and console.log treat it as different. It appears from 0 * -5 and similar sums, and it can make a check of the expected output fail. Second, NaN (“not a number”) is never equal to anything, even itself. Use Number.isNaN to test for it. Third, computers store decimals as binary fractions, so 0.1 + 0.2 is not exactly 0.3.

JavaScript
console.log(0 === -0);                  // true     === says they are equalconsole.log(Object.is(0, -0));          // false    Object.is can tell them apartconsole.log(0 * -5, Math.round(-0.4));  // -0 -0    -0 appears whenever a zero is multiplied by a negativeconsole.log(String(-0), JSON.stringify(-0)); // 0 0 it prints as a plain 0 in text, but console.log shows -0console.log((-0) + 0, new Set([0, -0]).size); // 0 1   adding 0 normalises it; Set and Map treat them as one keyconsole.log(NaN === NaN);               // false    NaN is not equal to anything, itself includedconsole.log([NaN].includes(NaN));       // true     includes, Set and Map use "same value" logic...console.log([NaN].indexOf(NaN));        // -1       ...but indexOf uses ===, so it never finds NaNconsole.log(Number.isNaN(Number("abc")), Number.isNaN("abc"), isNaN("abc")); // true false trueconsole.log(0.1 + 0.2);                 // 0.30000000000000004console.log(0.1 + 0.2 === 0.3);         // falseconsole.log(Math.abs(0.1 + 0.2 - 0.3) < 1e-9); // true   compare floats with a tolerance, or work in whole cents

Map and Set against plain objects

A plain object is fine for counting letters. But its keys are always strings, keys that look like whole numbers get reordered, and it already has inherited names like constructor. A Map keeps the type of each key and the order in which you added them. Neither one compares arrays by their contents. For pairs, build a text key such as "row,col" (lesson 10).

JavaScript
const obj = {};obj[1] = "number key";obj["1"] = "string key";console.log(Object.keys(obj).length);   // 1       an object turns every key into a string, so 1 and "1" collideconst map = new Map();map.set(1, "number key");map.set("1", "string key");console.log(map.size);                  // 2       a Map keeps the typeconst order = {};order.b = 1; order[2] = 1; order[1] = 1;console.log(Object.keys(order));        // [ '1', '2', 'b' ]   integer-like keys jump to the front, sorted: not insertion orderconsole.log("constructor" in {});       // true    an empty object already "has" inherited names such as constructor and toStringconst byReference = new Map();byReference.set([1, 2], "x");console.log(byReference.get([1, 2]));   // undefined   arrays are compared by identity, not by contentsconst visited = new Set();visited.add(2 + "," + 3);               // build a text key for a pair such as a grid cell (row 2, column 3)console.log(visited.has("2,3"));        // true

Integers: 2^53 and 32 bits

JavaScript numbers are 64-bit decimal-style numbers (called doubles). They do not wrap around like a 32-bit intdoes in Java or C++. But whole numbers are exact only up to 2^53 - 1. This leads to two traps. First, a product or sum above that limit loses digits, and nothing warns you. For "give the answer modulo 1e9 + 7" questions (the remainder after dividing by 1e9 + 7), use BigInt (a number type for very big whole numbers) or split the multiplication. Second, the bitwise operators (|, &, ^, <<, >>) first turn the number into 32 bits. So they are the wrong tool for big values (lesson 58).

JavaScript
console.log(Number.MAX_SAFE_INTEGER);   // 9007199254740991    this is 2^53 - 1console.log(2 ** 53 + 1 === 2 ** 53);   // true     beyond it, whole numbers silently lose precisionconsole.log(12345678901234567890);      // 12345678901234567000console.log(12345678901234567890n);     // 12345678901234567890n    a BigInt (the n suffix) stays exactconsole.log((2 ** 31) | 0);             // -2147483648   bitwise operators work on 32-bit integersconsole.log(5000000000 | 0);            // 705032704console.log(1 << 31, 1 << 32);          // -2147483648 1// "Return the answer modulo 1e9 + 7" with big factors: the multiplication itself can exceed 2^53.const a = 999999999, b = 999999998, mod = 1000000007;console.log((a * b) % mod);             // 70   wrong: a * b is about 1e18, already roundedconsole.log(Number(BigInt(a) * BigInt(b) % BigInt(mod))); // 72   right: BigInt multiplies exactly

Array holes

new Array(3) makes three empty slots (holes), not three undefined values. Methods such as map and forEach skip these holes. Build arrays with fill or Array.from. Holes also appear when you write delete arr[i] or assign to an index past the end of the array.

JavaScript
const holes = [1, , 3];                 // a hole: the slot does not exist at all (not the same as undefined)console.log(holes.length, 1 in holes);  // 3 falseconsole.log(holes.map((x) => x * 2));   // [ 2, <1 empty item>, 6 ]   map, forEach and friends skip holesconsole.log(new Array(3).map((_, i) => i)); // [ <3 empty items> ]    so this "fill with 0..2" does nothingconsole.log(new Array(3).fill(0));      // [ 0, 0, 0 ]console.log(Array.from({ length: 3 }, (_, i) => i)); // [ 0, 1, 2 ]   the safe way to build an array from an index

Recursion depth is limited

Each function call uses space on the call stack (the list of calls that are still waiting to finish). JavaScript limits this to about ten thousand calls for simple functions. In our test the function below reached about 10,900 calls in Node 22. Browsers and bigger functions give other numbers. A recursion that goes n = 100,000 calls deep, such as a depth-first search down a long chain, gives a RangeError. In an interview, ask for the maximum depth. If it can reach 10^5, say that you will switch to your own stack (an array that you push to and pop from).

JavaScript
function countDown(n) { return n === 0 ? 0 : 1 + countDown(n - 1); }console.log(countDown(1000));           // 1000try {  countDown(100000);} catch (e) {  console.log(e instanceof RangeError, e.message); // true Maximum call stack size exceeded}// The same walk with your own array as the stack never hits that limit.function countDownLoop(n) {  const stack = [n];  let steps = 0;  while (stack.length) {    const v = stack.pop();    if (v > 0) { steps++; stack.push(v - 1); }  }  return steps;}console.log(countDownLoop(100000));     // 100000

shift() can be slow; use a head index

queue.shift() removes the first element and moves all the others down by one. So it can cost O(n) for each call. (JavaScript engines speed this up for small arrays, but you cannot count on it.) In our test run for this lesson, emptying 100,000 elements with shift took over ten seconds. With an index it took about a millisecond. For BFS (breadth-first search: visit everything one step away, then two steps away, and so on, using a queue; lessons 39 and 50), move a head index forward instead. The same advice applies to unshift, and to splice in the middle of a big array.

JavaScript
// A queue as an array: advance a head index instead of calling shift().const queue = [1, 2, 3];let head = 0;const order = [];while (head < queue.length) {  const x = queue[head++];              // O(1) dequeue  order.push(x);  if (x < 3) queue.push(x + 10);        // new work joins the back, exactly as in BFS}console.log(order);                     // [ 1, 2, 3, 11, 12 ]

Destructuring swaps

[a, b] = [b, a] swaps two values without a temporary variable. It also works on array slots. There is one trap. A line that starts with [ is joined to the line before it if that line has no semicolon. So if you write without semicolons, the swap can give an error or do something else without warning. End the line before it with ;.

JavaScript
let x = 1, y = 2;[x, y] = [y, x];                        // swap with no temporary variableconsole.log(x, y);                      // 2 1const arr = [5, 6, 7];[arr[0], arr[2]] = [arr[2], arr[0]];    // swap two array slots: the heart of reverse, partition and permutationsconsole.log(arr);                       // [ 7, 6, 5 ]const [first, ...rest] = [1, 2, 3];console.log(first, rest);               // 1 [ 2, 3 ]

2-D arrays: fill and copying

The most common bug with grids: Array(3).fill(Array(3).fill(0)) puts one inner array into all three rows. So changing one row changes all of them. Build each row separately. Copying has the same problem. [...grid] is a shallow copy. This means it copies only the outer array, and the rows are still shared. Copy each row, or use structuredClone, which copies at any depth (lesson 25).

JavaScript
const bad = Array(3).fill(Array(3).fill(0));    // fill puts the SAME inner array in every rowbad[0][0] = 1;console.log(bad);                       // [ [ 1, 0, 0 ], [ 1, 0, 0 ], [ 1, 0, 0 ] ]const good = Array.from({ length: 3 }, () => Array(3).fill(0));   // a fresh inner array per rowgood[0][0] = 1;console.log(good);                      // [ [ 1, 0, 0 ], [ 0, 0, 0 ], [ 0, 0, 0 ] ]const grid = [[1, 2], [3, 4]];const shallow = [...grid];              // copies the outer array only; the rows are sharedshallow[0][0] = 99;console.log(grid[0][0]);                // 99const rowsCopied = grid.map((row) => [...row]);   // one level deeper: a real copy of a 2-D arrayrowsCopied[0][0] = 5;console.log(grid[0][0]);                // 99   unchangedconst deep = structuredClone(grid);     // copies any depth (numbers, arrays, objects, Map, Set)deep[0][0] = 7;console.log(grid[0][0], deep[0][0]);    // 99 7

Smaller gotchas, all checked

Dry run · expressions that surprise people
ExpressionResultWhat to remember
-7 % 3-1The result has the sign of the left number. For a result that is never negative, use ((x % m) + m) % m, which gives 2.
Math.floor(-7 / 2) and Math.trunc(-7 / 2)-4 and -3floor rounds down. trunc cuts off the fraction. They differ only for negative numbers.
Math.round(-2.5) and Math.round(2.5)-2 and 3A half is rounded towards +Infinity (up).
"10" < "9"trueTwo strings are compared character by character, as text. Convert them with Number(...) first.
(2 ** 31 + 2 ** 31) >> 10A shift first turns the number into 32 bits. Use Math.floor((lo + hi) / 2) instead. It stays exact below 2^53.
Math.max() and Math.min() with no arguments-Infinity and InfinityHandy as starting values for "best so far", but surprising on an empty array.
[].reduce((a, b) => a + b)TypeErrorWithout a starting value, reduce on an empty array gives an error. Write reduce(fn, 0).
Math.max(...hugeArray)RangeErrorSpreading hundreds of thousands of values into arguments uses up the call stack (150,000 values failed in Node 22, 125,000 still worked; the limit differs between engines). Use a loop.
for (const i in [5])i is the string "0"for…in walks the keys as text. Use for…of to get the values, or use an index loop.

Common mistakes

Dry run · what loses offers, and the fix
MistakeWhy it hurtsDo this instead
Starting to code in the first minuteYou may solve the wrong problem, or find an edge case at minute 30.Spend the first 10 minutes on questions, examples and a plan.
Coding in silenceThe interviewer cannot give you credit or hints for thinking they cannot hear.Say your decisions in short sentences: first what, then why.
Jumping to the clever solution and never mentioning brute forceIf the clever idea fails, you have nothing. You also lose the chance to show how you improved the answer.Always say the brute force and its cost first, even if it is one sentence.
Short, unclear names (a, m, t, x2)You will mix them up when you do a dry run and feel stressed.Use needed, indexOf, left, right, best.
Saying "done" without testingA one-character bug (< instead of <=) goes unseen.Always follow one example and one edge case through the code.
Testing only the example from the problemThe example is chosen so that it works. Bugs hide at the edges.Add empty input, one element, duplicates and negatives.
Going quiet and staying stuck for minutesSilence looks like panic, and it wastes time.Say what you tried and where it fails. Ask for a hint after about 5 minutes.
Arguing with a hintInterviewers give hints to help you do well.Say "let me think about that", try the hint, and tell them what happened.
Forgetting to talk about complexity until askedIt is part of the answer. It is not an extra question.Say the time and memory cost before the interviewer has to ask.
Rewriting everything when you find a bugIt costs minutes and adds new bugs.Point to the line, change that line, and run the failing case again.
Making it too complicated (classes, generics, input checks)Fewer lines mean fewer bugs and faster dry runs.Write one function that solves the problem as stated. Mention input checks instead of writing them.

Pick the two that you know from yourself. Practise the fix on purpose in your next five problems.

Mock interview walk-throughs

These four are different from the practice questions in earlier lessons. Each one is a script. It has the problem, the examples, the approaches, and a green box at the end with what you would say out loud at each step. Read the problem, close the solutions, and first try to say your own version out loud. Then compare.

Q1Mock interview: Two SumEasy

Given an array of integers nums and an integer target, return the indices of the two numbers that add up to target. You may not use the same element twice. Return an empty array if there is no such pair.

Example 1
Input: nums = [2,7,11,15], target = 9
Output: [0,1]
Explanation: nums[0] + nums[1] = 2 + 7 = 9.
Example 2
Input: nums = [3,2,4], target = 6
Output: [1,2]
Explanation: 2 + 4 = 6. The 3 cannot pair with itself, so [0,0] is not allowed.
Example 3
Input: nums = [3,3], target = 6
Output: [0,1]
Explanation: Repeated numbers are fine, because they are at different positions.
Stuck? Open one hint at a time
Hint 1
Say the brute force first. What does it cost, and which part of it repeats the same kind of search?
Hint 2
For each number you are really asking one question: "is target minus this number somewhere else in the array?"
Hint 3
Which data structure can answer "have I seen this value, and where?" in O(1) time?
Show all 2 solutions with explanations
Approach 1Brute force: every pair
The idea

Try each index i with every later index j. Return the first pair whose values add up to the target.

JavaScript
function twoSum(nums, target) {  for (let i = 0; i < nums.length; i++) {    for (let j = i + 1; j < nums.length; j++) {   // j starts after i: no element is used twice      if (nums[i] + nums[j] === target) return [i, j];    }  }  return [];}console.log(twoSum([2, 7, 11, 15], 9)); // [ 0, 1 ]console.log(twoSum([3, 2, 4], 6));      // [ 1, 2 ]console.log(twoSum([3, 3], 6));         // [ 0, 1 ]console.log(twoSum([1, 2], 10));        // []
How it works

O(n²) time and O(1) memory. It is clearly correct. So it is a good starting point, and also a good way to check the faster version (the answers must match).

Approach 2One pass with a Map
The idea

Walk through the array once and keep a Map from value to index. For each number, check if target - number is already in the Map. If it is, return both indices. If not, store this number.

JavaScript
function twoSum(nums, target) {  const indexOf = new Map();                       // value -> index where we saw it  for (let i = 0; i < nums.length; i++) {    const needed = target - nums[i];    if (indexOf.has(needed)) return [indexOf.get(needed), i];    indexOf.set(nums[i], i);                       // store after checking, so an element cannot pair with itself  }  return [];}console.log(twoSum([2, 7, 11, 15], 9)); // [ 0, 1 ]console.log(twoSum([3, 2, 4], 6));      // [ 1, 2 ]console.log(twoSum([3, 3], 6));         // [ 0, 1 ]console.log(twoSum([1, 2], 10));        // []
How it works

O(n) time, O(n) memory. Dry run (follow the code by hand) on [3,2,4], target 6. At i = 0 we need 3, the map is empty, so store 3 → 0. At i = 1 we need 4, it is not there, so store 2 → 1. At i = 2 we need 2, it is at index 1, so return [1, 2]. We check first and store after. That is why the lone 3 never pairs with itself.

Which approach should you use?

Clarify (minutes 0–5). "So I get an array of integers and a target. I return the indices of two different positions whose values add up to the target. Can the numbers be negative? Can the same position be used twice? Is there always exactly one answer? What should I return if there is none? How big can the array be?" (Suppose the answers are: negatives allowed, no reuse, at most one answer, empty array when there is none, n up to 10,000.)

Examples and edges (5–10). "For 2, 7, 11, 15 and target 9 the answer is 0 and 1. Edge cases: two equal numbers like 3 and 3 with target 6 should return 0 and 1. A single 3 with target 6 must not return the same index twice. An empty array, or no answer, returns an empty array."

Brute force, then better (10–17). "Brute force checks every pair. That is O(n²) time and O(1) memory. For 10,000 elements that is about fifty million checks. That would pass, but we can do better. The waste is that for each number I search the rest of the array for its partner. If I remember the numbers I have already seen in a map, that search becomes an O(1) lookup. Then the time is O(n) and the memory is O(n). Do you want me to go with the map?"

Code (17–32). "I make a map from value to index. For each position I work out the partner I need, target - nums[i]. If the map has it, I return its index and mine. If not, I store my own value. I store it after the check on purpose, so a number cannot match itself."

Test (32–39). "Dry run on 3, 2, 4 with target 6. At i = 0 I need 3, the map is empty, so I store 3. At i = 1 I need 4, it is not there, so I store 2. At i = 2 I need 2, it is at index 1, so I return 1 and 2. For 3 and 3: at i = 0 I store 3. At i = 1 I need 3, it is at 0, so I return 0 and 1. For an empty array the loop does not run, and we return an empty array."

Complexity (39–42). "The time is O(n), because there is one pass and each map operation is O(1) on average. The memory is O(n) for the map, because in the worst case it holds every element. If memory were tight, I would sort a copy of (value, index) pairs and use two pointers. That is O(n log n) time and still O(n) memory for the pairs, so the trade-off is not free." (LeetCode 1; this is lesson 26.)

Q2Mock interview: first invalid bracket (a Valid Parentheses variant)Medium

A string contains brackets (), [], {} and other characters, which you ignore. Return the index of the first character that makes the brackets invalid. That is a closing bracket that does not match the most recent open one, or a closing bracket when nothing is open. If the string ends with brackets still open, return the index of the earliest one that was never closed. If everything is balanced, return -1.

Example 1
Input: s = "a(b)[c]{d}"
Output: -1
Explanation: Every bracket is matched and in the right order. Letters are ignored.
Example 2
Input: s = "(]"
Output: 1
Explanation: The ] at index 1 arrives while ( is the open bracket. A ] cannot close a (.
Example 3
Input: s = "(()"
Output: 0
Explanation: Nothing goes wrong along the way, but the ( at index 0 is never closed. Report the first opener that is never closed.
Example 4
Input: s = "())"
Output: 2
Explanation: The second ) at index 2 has nothing left to close.
Stuck? Open one hint at a time
Hint 1
First solve the plain yes/no version. Which data structure matches the most recent opener with the next closer?
Hint 2
To report an index, store indices on the stack, not characters. How do you get the character back from an index?
Hint 3
There are two ways to fail while scanning (a wrong closer, or a closer when the stack is empty) and one way at the end (openers left over). Which index goes with each?
Show all 2 solutions with explanations
Approach 1Plain yes/no: delete matched pairs again and again
The idea

Start with the easy version first, as you would in the interview. Keep removing neighbouring pairs (), [] and {} until nothing changes. The string is valid if it ends up empty. This tells you only valid or not valid. It does not tell you the position.

JavaScript
function isValid(s) {  let brackets = s.replace(/[^()\[\]{}]/g, "");  // drop every character that is not a bracket  let previous;  do {    previous = brackets;    brackets = brackets.replace("()", "").replace("[]", "").replace("{}", "");  } while (brackets !== previous);  return brackets === "";}console.log(isValid("a(b)[c]{d}")); // trueconsole.log(isValid("(]"));         // falseconsole.log(isValid("(()"));        // falseconsole.log(isValid("{[()]}"));     // true
How it works

Each pass removes at least one pair or stops, and each pass costs O(n). So the worst case is O(n²). It shows you understand the problem, but it loses the positions that we need for this variant.

Approach 2Stack of indices
The idea

Scan from left to right. A stack is a pile where you add and remove only at the top. Push the index of every opener onto it. When you see a closer, pop the top. If the stack was empty, or the opener you popped does not match, return this index. After the scan, if openers are left, the earliest one (the bottom of the stack) is the answer.

JavaScript
function firstInvalid(s) {  const pairs = { ")": "(", "]": "[", "}": "{" };  const stack = [];                                  // indices of openers not yet closed  for (let i = 0; i < s.length; i++) {    const ch = s[i];    if (ch === "(" || ch === "[" || ch === "{") {      stack.push(i);    } else if (ch in pairs) {      if (stack.length === 0 || s[stack[stack.length - 1]] !== pairs[ch]) return i;   // nothing to close, or wrong type      stack.pop();    }                                                // any other character is ignored  }  return stack.length === 0 ? -1 : stack[0];         // the earliest opener that was never closed}console.log(firstInvalid("a(b)[c]{d}")); // -1console.log(firstInvalid("(]"));         // 1console.log(firstInvalid("(()"));        // 0console.log(firstInvalid("())"));        // 2console.log(firstInvalid(""));           // -1console.log(firstInvalid("}"));          // 0
How it works

O(n) time, O(n) memory for the stack. ch in pairs is safe here because ch is one character. So it can never equal inherited names like "constructor". Dry run on (]: push 0. At index 1 the closer ] needs [, but the top of the stack is ( at index 0, so return 1.

Which approach should you use?

Clarify. "Besides round brackets there are square and curly ones. Other characters may appear, and I should ignore them? What should I return: a true/false value or the position? If the string is valid, do I return -1? If the only problem is an opener that is never closed, which index do you want: the first such opener, or the last?" (Suppose the answers are: ignore other characters, return the index of the first problem, -1 if valid, and for leftovers the earliest opener that is never closed.)

Examples and edges. "Letters mixed with correct brackets give -1. A wrong closer such as (] gives 1. A closer with nothing open, such as ), gives 0. Openers that are never closed, like (((, give the first one. The empty string is valid."

Plan. "The plain version is the classic stack problem. The most recent opener that is not yet matched must be closed first. This is called last in, first out. A brute force would be to delete matching pairs until none are left. That is O(n²) and it destroys the indices. So I will use a stack and store indices instead of characters. Then I can report a position and still find the character with s[index]."

Code. "For an opener I push its index. For a closer, I fail at once if the stack is empty or the top opener is the wrong type, and I return this index. Otherwise I pop. I skip other characters. At the end, a stack that is not empty means some openers were never closed. The bottom of the stack is the earliest one."

Test. "On (() : push 0, push 1, the closer pops 1, the stack is [0], the loop ends, so I return stack[0] = 0. On ()) : push 0, pop it, the next closer finds an empty stack, so I return index 2. On the empty string the stack is empty, so I return -1."

Complexity. "The time is O(n): one pass, and each stack operation is O(1). The memory is O(n) in the worst case, for example a string of only openers. I cannot do better than O(n) time, because the very last character can decide the answer." (LeetCode 20 plus a variant; this is lesson 38.)

Q3Mock interview: Merge IntervalsMedium

Given a list of intervals [start, end], merge all overlapping intervals. Return the intervals that do not overlap and that cover exactly the same ranges. Intervals that touch at one point count as overlapping.

Example 1
Input: intervals = [[1,3],[2,6],[8,10],[15,18]]
Output: [[1,6],[8,10],[15,18]]
Explanation: [1,3] and [2,6] overlap and become [1,6]. The others have no overlap and stay as they are.
Example 2
Input: intervals = [[1,4],[4,5]]
Output: [[1,5]]
Explanation: Intervals that only touch at 4 count as overlapping.
Example 3
Input: intervals = [[1,4],[2,3]]
Output: [[1,4]]
Explanation: The second interval is inside the first one. The merged end is the larger end, 4, not 3.
Stuck? Open one hint at a time
Hint 1
Sort the intervals by their start. After that, which intervals could overlap the one you are building?
Hint 2
Keep the last merged interval. Compare only the next interval's start with the end of that last interval.
Hint 3
When they overlap, what should the new end be: the old end, the next interval's end, or the larger of the two?
Show all 2 solutions with explanations
Approach 1Brute force: keep merging any pair that overlaps
The idea

Look for any two intervals that overlap. Replace them with one interval that covers both (their union). Then start again. Stop when a full scan finds no overlap.

JavaScript
function merge(intervals) {  const list = intervals.map(([a, b]) => [a, b]);        // copy, so the caller's data is not changed  let changed = true;  while (changed) {    changed = false;    search: for (let i = 0; i < list.length; i++) {      for (let j = i + 1; j < list.length; j++) {        const [a, b] = list[i], [c, d] = list[j];        if (a <= d && c <= b) {                           // they overlap (or touch)          list[i] = [Math.min(a, c), Math.max(b, d)];          list.splice(j, 1);          changed = true;          break search;                                   // start the scan again        }      }    }  }  return list.sort((p, q) => p[0] - q[0]);}console.log(merge([[1, 3], [2, 6], [8, 10], [15, 18]])); // [ [ 1, 6 ], [ 8, 10 ], [ 15, 18 ] ]console.log(merge([[1, 4], [4, 5]]));                    // [ [ 1, 5 ] ]console.log(merge([[1, 4], [2, 3]]));                    // [ [ 1, 4 ] ]console.log(merge([]));                                  // []
How it works

Each merge removes one interval, so there are at most n merges. Each restart scans O(n²) pairs, so the worst case is O(n³). It is useful for checking the faster version, and it is a good thing to say out loud before you improve it.

Approach 2Sort by start, then sweep
The idea

Sort a copy by start. Put the first interval in the result. Then look at each next interval. If its start is not more than the end of the last interval in the result, stretch that end to the larger of the two ends. Otherwise, start a new interval.

JavaScript
function merge(intervals) {  const sorted = intervals.map(([a, b]) => [a, b]).sort((p, q) => p[0] - q[0]);   // by start; a copy  const result = [];  for (const [start, end] of sorted) {    const last = result[result.length - 1];    if (last && start <= last[1]) {      last[1] = Math.max(last[1], end);               // overlap: extend, never shrink    } else {      result.push([start, end]);                      // gap: begin a new merged interval    }  }  return result;}console.log(merge([[1, 3], [2, 6], [8, 10], [15, 18]])); // [ [ 1, 6 ], [ 8, 10 ], [ 15, 18 ] ]console.log(merge([[1, 4], [4, 5]]));                    // [ [ 1, 5 ] ]console.log(merge([[1, 4], [2, 3]]));                    // [ [ 1, 4 ] ]console.log(merge([]));                                  // []
How it works

O(n log n) time for the sort, then one O(n) sweep. The memory is O(n) for the copy and the result. After sorting, an interval can only overlap the most recent merged one, so one comparison is enough. Dry run on [[1,4],[2,3]]: the result is [[1,4]]. The next start is 2, and 2 ≤ 4, so the end becomes max(4, 3) = 4.

Which approach should you use?

Clarify. "Each interval is a pair [start, end] with start ≤ end. Is the input sorted? Do intervals that touch, like [1,4] and [4,5], count as overlapping? May I change the input, or should I return a new array? Can the input be empty? Roughly how many intervals are there?" (Suppose the answers are: unsorted, touching counts, do not change the input, empty gives an empty array, up to 10^4.)

Examples and edges. "The sample merges the first two. Edge cases: touching intervals merge. An interval inside another one, like [1,4] and [2,3], must keep the larger end. Also unsorted input, a single interval, an empty list, and many intervals that all join into one."

Brute force, then better. "Brute force: again and again, find any two overlapping intervals, replace them with their union, and scan again until none overlap. That is O(n³) in the worst case. The waste is that I search all pairs, because the order is random. If I sort by start, an interval can only overlap the latest merged interval, so one pass is enough. The main cost is then the sort: O(n log n)."

Code. "I copy the input and sort the copy by start, so I do not change the caller's data. For each interval I look at the last interval in my result. If the start is less than or equal to its end, they overlap, and I set the end to the larger of the two ends. I use the larger one because of the case where one interval is inside another. Otherwise I add a new interval."

Test. "For [[1,3],[2,6],[8,10],[15,18]]: the result starts as [1,3]. 2 ≤ 3, so the end becomes 6. 8 > 6, so I add [8,10]. 15 > 10, so I add [15,18]. For [[1,4],[2,3]] the end stays 4. For an empty input the loop never runs and I return an empty array. One more check: I wrote start <= last[1] with less-than-or-equal, so touching intervals merge, as we agreed."

Complexity. "The time is O(n log n), and the sort is the biggest part. The sweep is only O(n). The memory is O(n) for the copy and the result. If the input were already sorted, I could skip the sort and get O(n)." (LeetCode 56; this is lesson 48.)

Q4Mock interview: can you finish all courses? (a graph problem, short form)Medium

There are numCourses courses, numbered 0 to numCourses - 1. Each pair [a, b] in prerequisites means you must take course b before course a. Return true if you can finish all the courses.

Example 1
Input: numCourses = 2, prerequisites = [[1,0]]
Output: true
Explanation: To take course 1 you must first take course 0. So take 0, then 1.
Example 2
Input: numCourses = 2, prerequisites = [[1,0],[0,1]]
Output: false
Explanation: Each course needs the other one first. This is a cycle (a loop that never ends), so neither can start.
Example 3
Input: numCourses = 4, prerequisites = [[1,0],[2,0],[3,1],[3,2]]
Output: true
Explanation: Order 0, 1, 2, 3 works. Course 3 needs both 1 and 2.
Stuck? Open one hint at a time
Hint 1
Draw each pair [a, b] as an arrow b → a (b must come first). What does an impossible schedule look like in that drawing?
Hint 2
It is a cycle in a directed graph (a graph where the links are one-way arrows). Which two methods can find a cycle?
Hint 3
Kahn's idea: a course with no unmet prerequisite can be taken now. Keep taking such courses and count how many you can take.
Show all 2 solutions with explanations
Approach 1Depth-first search with three states
The idea

Give each course one of three colours: not visited, in progress (on the path we are walking now), or done. Depth-first search (DFS) means following one path as far as it goes before trying another. If DFS reaches a course that is in progress, we walked a path back to ourselves. That is a cycle, so return false.

JavaScript
function canFinish(numCourses, prerequisites) {  const next = Array.from({ length: numCourses }, () => []);  for (const [course, before] of prerequisites) next[before].push(course);   // arrow: before -> course  const state = new Array(numCourses).fill(0);       // 0 = unvisited, 1 = on the current path, 2 = finished  function hasCycle(v) {    if (state[v] === 1) return true;                 // we came back to a course on our own path    if (state[v] === 2) return false;    state[v] = 1;    for (const w of next[v]) if (hasCycle(w)) return true;    state[v] = 2;    return false;  }  for (let v = 0; v < numCourses; v++) if (hasCycle(v)) return false;  return true;}console.log(canFinish(2, [[1, 0]]));                          // trueconsole.log(canFinish(2, [[1, 0], [0, 1]]));                  // falseconsole.log(canFinish(4, [[1, 0], [2, 0], [3, 1], [3, 2]]));  // trueconsole.log(canFinish(1, []));                                // true
How it works

O(V + E) time and O(V + E) memory, where V is the number of courses and E is the number of pairs. The memory is for the adjacency list (each course's list of next courses) plus the recursion. We mark finished courses, so we never explore the same part twice. That keeps the time linear. A very long chain of prerequisites could use up the call stack in JavaScript.

Approach 2Kahn's algorithm (count the waiting prerequisites)
The idea

For each course, count how many prerequisites it still waits for (this number is called its in-degree). Put the courses with zero into a queue (a line where the first one in is the first one out). Take one course. Lower the count of the courses that depended on it. Add any course that reaches zero to the queue. If you took every course, there was no cycle.

JavaScript
function canFinish(numCourses, prerequisites) {  const next = Array.from({ length: numCourses }, () => []);  const waiting = new Array(numCourses).fill(0);     // prerequisites each course still needs  for (const [course, before] of prerequisites) {    next[before].push(course);    waiting[course]++;  }  const queue = [];  for (let v = 0; v < numCourses; v++) if (waiting[v] === 0) queue.push(v);  let head = 0;  while (head < queue.length) {    const v = queue[head++];                         // take this course    for (const w of next[v]) {      waiting[w]--;      if (waiting[w] === 0) queue.push(w);    }  }  return queue.length === numCourses;                // anything stuck inside a cycle is never queued}console.log(canFinish(2, [[1, 0]]));                          // trueconsole.log(canFinish(2, [[1, 0], [0, 1]]));                  // falseconsole.log(canFinish(4, [[1, 0], [2, 0], [3, 1], [3, 2]]));  // trueconsole.log(canFinish(1, []));                                // true
How it works

O(V + E) time and memory. Dry run on [[1,0],[0,1]]: both courses wait for one prerequisite, so the queue starts empty. We took 0 of 2 courses, so the answer is false. There is no recursion, so there is no worry about the call stack.

Which approach should you use?

Clarify. "So [a, b] means b must be taken before a. Courses are numbered 0 to n minus 1. Can the same pair appear twice? Can a course be its own prerequisite? How big can n and the number of pairs be?" (Suppose the answers are: repeated pairs are possible, a course can need itself, n up to 2,000 and up to 5,000 pairs. A course that needs itself is a cycle, so the answer is false.)

Examples and edges. "With no prerequisites the answer is simply true. Two courses that need each other give false. A chain 0, 1, 2 gives true. A cycle with a tail, like 0 → 1 → 2 → 1, gives false even though course 0 is fine. Courses that are not linked to the others should not confuse me."

Model and brute force. "I treat each course as a point (vertex) and each prerequisite as an arrow (directed edge) from the required course to the course that depends on it. The question becomes: does this graph have a cycle? The brute force is to try every possible order of the courses and check each one against the pairs. That is n factorial orders, which is far too slow. A smarter idea: a valid order exists exactly when the graph has no cycle, and I can find a cycle in linear time."

Plan. "I will use Kahn's algorithm. I build an adjacency list (for each course, a list of the courses that come after it) and count how many prerequisites each course has. A course with zero can be taken now, so I put those in a queue. When I take a course, every course that depended on it has one prerequisite fewer. If that number reaches zero, I add it to the queue. If I can take all n courses, it is possible. If the queue becomes empty too early, the courses that are left are stuck in a cycle. I prefer this to the recursive DFS, because it cannot run out of call stack."

Code and test. "I write the loop with a head index instead of shift. Test on 2 courses with pairs [1,0] and [0,1]: both wait for one, the queue starts empty, so I took 0 of 2, and the answer is false. Now the diamond shape with 4 courses: course 0 has zero waiting, so I take it. Courses 1 and 2 drop to zero, so I take them. Course 3 drops from two to zero, so I take it. That is 4 of 4, so the answer is true."

Complexity. "The time is O(V + E). Each vertex (course) is queued once and each edge (pair) is handled once. The memory is O(V + E) for the adjacency list and the counts. If you want the real order of courses, the queue already holds it. That is the follow-up question, Course Schedule II." (LeetCode 207; this is lesson 51.)

Make it stick

Make it stick — close this page, then
  1. Say the six steps (clarify, examples, plan, code, test, analyse) and about how many minutes each one gets.
  2. Write down five clarifying questions that fit almost any array problem.
  3. Say the step-by-step path from brute force to the best answer out loud for Two Sum. Then do it for another problem you choose.
  4. List six JavaScript surprises from this lesson, with a one-line fix for each.
  5. Explain why Array(3).fill(Array(3).fill(0)) is a bug. Give two ways to fix it.
  6. Pick a practice question from any earlier lesson. Solve it on paper in 30 minutes and explain it out loud as you go.

Revisit schedule: solve two of the questions again tomorrow, again in 3 days, and once more in a week — without looking at the answers. Each time it will feel easier. That is the memory forming.

What's next

The last lesson, Lesson 60, turns everything into a plan. It has a cheat sheet that links the words of a problem to the right method and lesson. It has an eight-week practice schedule. It has a spaced-repetition routine (you review things again after longer and longer gaps). It also has mock interviews where you must spot the pattern from the words alone.

Published by Jainik · Last updated · How these lessons are made

Keep learning

Part of the study guide

Related series