Revision notes
Read only this page on the day. No new problems — new material on the morning of an interview adds panic, not knowledge.
Say: “Let me make sure I understand the problem first.” Then restate it in your own words. It buys you twenty seconds and it is a scored behaviour, not a stall.
For a trainee role, how you reason is weighted more heavily than whether you reach the optimal solution.
Don’t memorise solutions. Memorise which words in the question point to which pattern, and say the mapping out loud before you code.
| When you hear | Reach for |
|---|---|
| count · frequency · duplicate · seen before · group by | hash map / Set |
| sorted · in place · pair that sums to · palindrome | two pointers |
| subarray · substring · consecutive · window · longest | sliding window |
| brackets · matching · valid · most recent | stack |
| sorted + find something | binary search |
“This asks for the longest substring with a condition, so I’ll use a sliding window with a hash map tracking what’s inside the window.” That one sentence tells them you know what you’re doing.
| Notation | Name | Where it shows up |
|---|---|---|
| O(1) | constant | array index, Map/Set get·has·set, push·pop |
| O(log n) | logarithmic | binary search — halving each step |
| O(n) | linear | one pass, includes, indexOf |
| O(n log n) | linearithmic | sorting |
| O(n²) | quadratic | nested loop over the same array |
Rule of thumb: a nested loop over one array is O(n²) and they want better. Adding a Set or Map to avoid the inner loop is the better — it makes it O(n). Sorting first gives O(n log n), which is often good enough.
Space is the extra memory you allocate. A Map holding n items is O(n) space. Reversing with two pointers is O(1).
The syntax you need to write DSA at all. Without it the problems feel harder than they are.
// WRONG — default sort compares as STRINGS, so "10" < "9"
[10, 9, 1].sort() // [1, 10, 9]
// RIGHT
[10, 9, 1].sort((a, b) => a - b) // ascending
[10, 9, 1].sort((a, b) => b - a) // descending
// sort() MUTATES. Copy first:
[...arr].sort((a, b) => a - b)
const freq = {};
for (const ch of s) freq[ch] = (freq[ch] || 0) + 1;
// Map version — same idea
const m = new Map();
m.set(k, (m.get(k) || 0) + 1);
const seen = new Set();
seen.add(1); seen.has(1); // O(1) both
[...new Set(arr)] // dedupe in one line
s[0] = 'x' // does nothing — no error, no change
s.split('').reverse().join('') // reverse a string
s.split(' ') // to words
s.toLowerCase()
s.replace(/[^a-z0-9]/gi, '') // strip punctuation
'a'.charCodeAt(0) // 97 ('z'=122, 'A'=65)
// MUTATES: push pop shift unshift splice sort reverse fill
// RETURNS NEW: map filter slice concat reduce flat join
arr.push(x) / arr.pop() // O(1) — the END is cheap
arr.unshift(x) / arr.shift() // O(n) — shifts everything
arr.includes(x) / indexOf(x) // O(n) — scans
[x, y] = [y, x]; // swap, no temp variable
Math.max(...[1, 9, 3]) // 9 — spread into arguments
Math.floor(7 / 2) // 3 — integer division
7 % 2 // 1 — remainder, even/odd tests
Array(3).fill(0) // [0, 0, 0]
Array.from({length: 2}, () => Array(3).fill(0)) // 2D grid
break out of .forEach(). If you might need to stop early, use for or for...of.Asked as “what does this print?”. Guess first, then read the why — the why is what’s scored.
if (true) { var a = 1; let b = 2; }
a // 1 — var is FUNCTION-scoped, leaks out
b // ReferenceError — let is BLOCK-scoped
const o = { n: 1 };
o.n = 2; // ALLOWED
o = {}; // TypeError
const prevents reassignment, not mutation.
typeof varX // "undefined" — var hoists, initialised to undefined
var varX = 5;
letY // ReferenceError — temporal dead zone
let letY = 5;
hoisted() // works — function DECLARATIONS hoist entirely
function hoisted() {}
0 == '0' // true — == converts types
0 === '0' // false — === checks type too
null == undefined // true
null === undefined // false
NaN === NaN // false — the famous one
Number.isNaN(NaN) // true — how you actually test
[] == false // true — coercion chaos
Always use ===. The one exception is x == null, which deliberately catches both null and undefined.
// Exactly 8 falsy values:
false, 0, -0, 0n, "", null, undefined, NaN
Boolean([]) // true — empty array is TRUTHY
Boolean({}) // true — empty object is TRUTHY
Boolean("0") // true — non-empty string
0 || 'fallback' // 'fallback' — || catches all falsy
0 ?? 'fallback' // 0 — ?? only null/undefined
A closure is a function that remembers the variables from where it was defined, even after that outer function has finished.
function makeCounter() {
let count = 0; // stays alive — inner fn uses it
return () => ++count;
}
const c = makeCounter();
c(); c(); c(); // 1, 2, 3
makeCounter()(); // 1 — counters are independent
var plus setTimeout prints 3 3 3; with let it prints 0 1 2. var has one shared binding for the whole function; let creates a new binding each iteration.Express middleware factories are closures: requireRole(...roles) returns a middleware function that remembers roles. A good example to give when asked where you have used one.
thisconst team = {
name: 'Acme',
regular() { return this.name; }, // 'Acme'
arrow: () => this.name, // NOT the object
};
Arrows have no this of their own — they take it from the surrounding scope. Never use an arrow for an object method that needs this. Do use arrows for callbacks inside a method.
console.log('1');
setTimeout(() => console.log('2'), 0);
Promise.resolve().then(() => console.log('3'));
console.log('4');
// prints 1, 4, 3, 2
Synchronous code runs first (1, 4). Then microtasks — promise callbacks (3). Then macrotasks — setTimeout (2). Microtasks always beat setTimeout, even setTimeout(fn, 0).
async function f() { return 'data'; } // always returns a Promise
try { await somethingThatRejects(); }
catch (err) { /* await + try/catch is how you handle it */ }
Promise.all() // waits for ALL, rejects if ANY fails
Promise.allSettled() // waits for all, never rejects
Promise.race() // first one to settle wins
const copy = { ...original }; // ONE level only
copy.nested.x = 999; // leaks into original!
structuredClone(original) // true deep copy
function debounce(fn, delay) {
let timer; // closure keeps this alive
return function (...args) {
clearTimeout(timer); // cancel the previous call
timer = setTimeout(() => fn.apply(this, args), delay);
};
}
Debounce waits until the user stops typing, then fires once. Throttle fires at most once per interval while typing. The everyday use is a search box: debounce it so you make one API call instead of one per keystroke.
Round 1 material. Read the approach, then close the page and write it from a blank file — retyping from memory is where the learning happens.
As you walk the array, ask: have I already seen target - current?
function twoSum(nums, target) {
const seen = new Map(); // value -> index
for (let i = 0; i < nums.length; i++) {
const need = target - nums[i];
if (seen.has(need)) return [seen.get(need), i];
seen.set(nums[i], i);
}
return [];
}
// O(n) time, O(n) space. Brute force is two nested loops, O(n²).
const reverse = s => s.split('').reverse().join('');
// if they ban built-ins:
function reverseLoop(s) {
let out = '';
for (let i = s.length - 1; i >= 0; i--) out += s[i];
return out;
}
function isPalindrome(s) {
const c = s.toLowerCase().replace(/[^a-z0-9]/g, '');
let i = 0, j = c.length - 1;
while (i < j) {
if (c[i] !== c[j]) return false;
i++; j--;
}
return true;
}
// Two pointers = O(1) extra space. Say that — the
// reverse-and-compare version is O(n) space.
function secondLargest(arr) {
let first = -Infinity, second = -Infinity;
for (const n of arr) {
if (n > first) { second = first; first = n; }
else if (n > second && n !== first) second = n;
}
return second === -Infinity ? null : second;
}
[1]. Duplicates break it — [5,5,3] would give 5 instead of 3.// O(n log n) — one line
const norm = s => s.toLowerCase().split('').sort().join('');
const isAnagram = (a, b) => norm(a) === norm(b);
// O(n) — the better answer
function isAnagramCount(a, b) {
if (a.length !== b.length) return false;
const freq = {};
for (const ch of a) freq[ch] = (freq[ch] || 0) + 1;
for (const ch of b) {
if (!freq[ch]) return false;
freq[ch]--;
}
return true;
}
function moveZeroes(nums) {
let write = 0;
for (let read = 0; read < nums.length; read++) {
if (nums[read] !== 0) {
[nums[write], nums[read]] = [nums[read], nums[write]];
write++;
}
}
return nums;
}
// O(n) time, O(1) space — "in place" is the point.
for (let i = 1; i <= n; i++) {
if (i % 15 === 0) out.push('FizzBuzz'); // check 15 FIRST
else if (i % 3 === 0) out.push('Fizz');
else if (i % 5 === 0) out.push('Buzz');
else out.push(String(i));
}
function isPrime(n) {
if (n < 2) return false;
for (let i = 2; i * i <= n; i++) if (n % i === 0) return false;
return true;
}
“I only loop to the square root, because if n has a factor above its square root it must also have one below it.” O(√n).
Set of vowels, O(1) lookup per char.[...new Set(arr)].reduce, and guard length === 0 or you get NaN.n*(n+1)/2, subtract the actual. O(1) space.s.trim().split(/\s+/).reverse().join(' '); the regex handles double spaces.Almost every easy and medium problem is one of these. Recognising which beats memorising any single solution.
// First non-repeating character — two passes.
// One pass can't work: you don't know if a duplicate is coming.
function firstNonRepeating(s) {
const freq = {};
for (const ch of s) freq[ch] = (freq[ch] || 0) + 1;
for (const ch of s) if (freq[ch] === 1) return ch;
return null;
}
// Group anagrams — the sorted string IS the key
function groupAnagrams(words) {
const groups = new Map();
for (const w of words) {
const key = w.split('').sort().join('');
if (!groups.has(key)) groups.set(key, []);
groups.get(key).push(w);
}
return [...groups.values()];
}
// Pair with a given sum in a SORTED array.
// Too small -> move left up. Too big -> move right down.
function pairSum(arr, target) {
let i = 0, j = arr.length - 1;
while (i < j) {
const sum = arr[i] + arr[j];
if (sum === target) return [arr[i], arr[j]];
if (sum < target) i++; else j--;
}
return [];
}
// O(n) time, O(1) space — better than the hash map version
// precisely because the array is already sorted.
// FIXED window — max sum of size k. Add the new, subtract
// the one that fell out. One pass instead of O(n·k).
function maxSum(arr, k) {
let sum = 0;
for (let i = 0; i < k; i++) sum += arr[i];
let best = sum;
for (let i = k; i < arr.length; i++) {
sum += arr[i] - arr[i - k];
best = Math.max(best, sum);
}
return best;
}
// VARIABLE window — longest substring, no repeats
function longestUnique(s) {
const lastSeen = new Map();
let left = 0, best = 0;
for (let right = 0; right < s.length; right++) {
const ch = s[right];
if (lastSeen.has(ch) && lastSeen.get(ch) >= left) {
left = lastSeen.get(ch) + 1; // jump the window
}
lastSeen.set(ch, right);
best = Math.max(best, right - left + 1);
}
return best;
}
function isValidParens(s) {
const pairs = { ')': '(', ']': '[', '}': '{' };
const stack = [];
for (const ch of s) {
if (ch === '(' || ch === '[' || ch === '{') stack.push(ch);
else if (pairs[ch] && stack.pop() !== pairs[ch]) return false;
}
return stack.length === 0; // leftover openers = invalid
}
function binarySearch(arr, target) {
let lo = 0, hi = arr.length - 1;
while (lo <= hi) {
const mid = Math.floor((lo + hi) / 2);
if (arr[mid] === target) return mid;
if (arr[mid] < target) lo = mid + 1;
else hi = mid - 1;
}
return -1;
}
lo <= hi, not <. And it’s mid + 1 / mid - 1, not mid — otherwise you loop forever.