Revision notes

JavaScript & DSA Notes

Round 1 — easy DSA in JavaScript
Round 2 — harder DSA + React / Node

00On the day

Read only this page on the day. No new problems — new material on the morning of an interview adds panic, not knowledge.

If your mind goes blank

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.

01In the room

For a trainee role, how you reason is weighted more heavily than whether you reach the optimal solution.

  1. Think out loud, always. Silence reads as stuck. Narration reads as engineer. Even “I’m thinking a hash map here, because lookups are O(1)” earns marks.
  2. Clarify before coding. Can the array be empty? Negative numbers? Duplicates? Asking is scored behaviour.
  3. Brute force first, then optimise. Say it: “Let me get it working, then improve it.” A working O(n²) beats a broken O(n).
  4. State complexity unprompted. End every problem with “that’s O(n) time, O(n) space.” Costs nothing, sounds senior.
  5. When stuck, say what you know. “I need to remember what I’ve already seen, so a Set is probably involved — let me build from there.”
  6. Never bluff. “I haven’t used that, but I’ve used X for the same problem” survives follow-ups. Bluffing collapses on question two.
Skip entirely: dynamic programming, graphs, trees, backtracking. If one appears, talk through a brute force and move on. That is an acceptable outcome at trainee level.

02Pattern triggers

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 hearReach for
count · frequency · duplicate · seen before · group byhash map / Set
sorted · in place · pair that sums to · palindrometwo pointers
subarray · substring · consecutive · window · longestsliding window
brackets · matching · valid · most recentstack
sorted + find somethingbinary search
Say this before you write code

“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.

03Complexity

NotationNameWhere it shows up
O(1)constantarray index, Map/Set get·has·set, push·pop
O(log n)logarithmicbinary search — halving each step
O(n)linearone pass, includes, indexOf
O(n log n)linearithmicsorting
O(n²)quadraticnested 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).

04JS essentials

The syntax you need to write DSA at all. Without it the problems feel harder than they are.

The sort trap they test this

// 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)

Frequency counting memorise this line

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);

Set and Map

const seen = new Set();
seen.add(1); seen.has(1);      // O(1) both
[...new Set(arr)]              // dedupe in one line

Strings are immutable

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)

Mutating vs returning new

// MUTATES:  push pop shift unshift splice sort reverse fill
// RETURNS NEW: map filter slice concat reduce flat join

Array cost

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

Small things that come up

[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
You cannot break out of .forEach(). If you might need to stop early, use for or for...of.

05JS concepts

Asked as “what does this print?”. Guess first, then read the why — the why is what’s scored.

var · let · const

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
Say it like this

const prevents reassignment, not mutation.

Hoisting

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() {}

== vs ===

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
Say it like this

Always use ===. The one exception is x == null, which deliberately catches both null and undefined.

Truthy and falsy

// 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

Closures they will ask

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
The classic trap. A loop with 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.
Where you meet it in real code

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.

Arrow functions and this

const 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.

The event loop very common

console.log('1');
setTimeout(() => console.log('2'), 0);
Promise.resolve().then(() => console.log('3'));
console.log('4');

// prints 1, 4, 3, 2
The why

Synchronous code runs first (1, 4). Then microtasks — promise callbacks (3). Then macrotasks — setTimeout (2). Microtasks always beat setTimeout, even setTimeout(fn, 0).

Promises

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

Shallow vs deep copy

const copy = { ...original };    // ONE level only
copy.nested.x = 999;             // leaks into original!

structuredClone(original)        // true deep copy

Debounce search boxes

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.

06Easy problems

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.

Two Sum know this cold

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²).

Reverse a string

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;
}

Palindrome

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.

Second largest

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;
}
Don’t sort and take [1]. Duplicates break it — [5,5,3] would give 5 instead of 3.

Anagram

// 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;
}

Move zeroes (in place)

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.

FizzBuzz

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));
}

Prime check

function isPrime(n) {
  if (n < 2) return false;
  for (let i = 2; i * i <= n; i++) if (n % i === 0) return false;
  return true;
}
Say this

“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).

Also know

07The four patterns

Almost every easy and medium problem is one of these. Recognising which beats memorising any single solution.

Hash map count · seen before

// 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()];
}

Two pointers sorted · in place

// 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.

Sliding window subarray · longest

// 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;
}

Stack brackets · matching

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
}

Binary search sorted + find

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;
}
Two bugs to avoid: it’s lo <= hi, not <. And it’s mid + 1 / mid - 1, not mid — otherwise you loop forever.