Free Handbook · Runs in your browser

Data Structures & Algorithms

Arrays, hash maps, stacks, queues, linked lists, trees, heaps and graphs — each built by hand in JavaScript and then as the built-in you would use — plus searching, sorting, recursion and dynamic programming, traced step by step so they are easy, not scary.

0 / 111 lessons🔥 0 day streak
ShareXLinkedIn

Module 13 · what you'll be able to do

  • Explain what a data structure is and pick the right one by its cost table
  • Build a stack, a queue, a linked list, a binary search tree and a heap from scratch
  • Use Array, Map and Set the way a working engineer does — and know what JavaScript lacks
  • Trace binary search, BFS and merge sort by hand
  • Turn a slow recursive solution into a fast one with memoisation
  • Recognise the structure an interview problem is secretly about
01

What a data structure is (and why you already use them)

A data structure is a way of arranging data so that some operation is fast. An array is fast to index and slow to search; a Map is fast to search and has no positions. An algorithm is a recipe that uses those operations. You have used both since Module 03 — this module makes the trade-offs explicit, because choosing the structure is most of solving the problem.

javascriptEdit it. ⌘/Ctrl + Enter runs.
You should see
array search: much slower than the Set
set search: instant
Your turn
Change "user199999" to "user0" (the first item). Why is the array suddenly fast too? That is why we talk about the worst case.
The whole module on one card. JavaScript ships fewer structures than Python (no deque, no heapq) — so you will write a queue and a heap here, and they are short.
StructureJavaScriptIndex by positionSearch by valueAdd / remove at endAdd / remove at frontOrdered?
Dynamic arrayArrayO(1)O(n)O(1)O(n)yes, by insertion
Hash mapMap (or object)O(1) by keyO(1)insertion order kept
Hash setSetO(1)O(1)insertion order kept
StackArray (push/pop)O(n)O(1)LIFO
Queuebuild it (no built-in!)O(n)O(n)O(1)O(1)FIFO
Linked listbuild itO(n)O(n)O(1) with tailO(1)yes
Binary search treebuild itO(log n) if balancedO(log n)sorted
Heapbuild it (no built-in!)O(n)O(log n)min in O(1)partial
GraphMap of arraysBFS / DFS O(V+E)O(1)no
How to use this module
Each lesson builds the structure by hand in ten to twenty lines, runs it, then shows the built-in (or the idiom) you would use at work. Do not memorise the code. Memorise the cost table and the one sentence under each heading that says what the structure is for.
02

Arrays: what an Array really is

A JavaScript Array is a dynamic array: engines store dense arrays as one block of memory, so items[i] is a single jump and push is cheap (spare capacity is kept ready). unshift and shift are expensive because every other item has to move. That single fact explains most array performance — and why shift() as a queue is a trap.

javascriptEdit it. ⌘/Ctrl + Enter runs.
You should see
push 50k at the END: fast
unshift 50k at the FRONT: much slower — every item shuffles right
10 50 [ 20, 30 ] [ 50, 40, 30, 20, 10 ]
[ 10, 20, 99, 40, 50 ]
[ [ 0, 7, 0 ], [ 0, 0, 0 ] ]
6 [ <empty>, <empty>, <empty>, <empty>, <empty>, 1 ]
Your turn
Write rotateLeft(items, k) that returns the array rotated left by k places using slices. rotateLeft([1,2,3,4,5], 2)[3,4,5,1,2].
Error you will hit

The grid bug: Array(2).fill([0, 0, 0]) makes two names for ONE row

javascript
const grid = Array(2).fill([0, 0, 0])
grid[0][1] = 7
console.log(grid)
[ [ 0, 7, 0 ], [ 0, 7, 0 ] ]
Why the engine said that

No exception — worse, a silent wrong answer. fill puts the same inner array reference in both slots, so both rows are one object. Changing "row 0" changes "row 1" too.

The fix

Build each row separately: Array.from({ length: 2 }, () => Array(3).fill(0)). Filling with a primitive (0) is fine because primitives are copied.

javascript
const grid = Array.from({ length: 2 }, () => Array(3).fill(0))
grid[0][1] = 7
console.log(grid)   // [ [ 0, 7, 0 ], [ 0, 0, 0 ] ]
Quick check

Which array operation is O(n) — grows with the size of the array?

03

Hash maps and sets: O(1) lookup

A hash map (Map) turns a key into a number with a hash function and uses that number as the slot to store the value. Looking something up is one calculation and one jump — no scanning. A Set is a hash map that stores only keys. This is why "put it in a Map" fixes most slow code. Keys are compared by identity for objects (SameValueZero), which is why two equal-looking arrays are two different keys.

javascriptEdit it. ⌘/Ctrl + Enter runs.
You should see
37 28 undefined
Map(3) { 'a' => 3, 'b' => 2, 'c' => 1 }
Map(2) { 'data' => [ 'ada', 'cy' ], 'infra' => [ 'bob' ] }
{ '2': [ 'cy' ], '3': [ 'ada', 'bob' ] }
true false [ 3, 1, 2 ]
[ 2, 3 ] [ 1, 2, 3, 4 ] [ 1 ]
undefined undefined
Your turn
Using a Map, write firstRepeat(items) that returns the first value that appears twice, or null. One pass, O(n).
Error you will hit

Array keys silently miss: visited.has([0, 1]) is always false

javascript
const visited = new Set()
visited.add([0, 1])
console.log(visited.has([0, 1]), visited.size)
false 1
Why the engine said that

No exception, wrong answer — and the Set keeps growing. Two array literals are two different objects; Map and Set compare object keys by identity. Grid problems hit this on the first line.

The fix

Use a primitive key that encodes the coordinate: a string `${r},${c}`, or a number r * cols + c.

javascript
const visited = new Set()
visited.add("0,1")
console.log(visited.has("0,1"))   // true
Hash function
Turns a key into an integer, fast and deterministically. Engines hash strings by content and objects by identity.
Collision
Two keys landing in the same slot. Handled by chaining (a small list per slot, as above) or probing. Rare enough that lookups stay O(1) on average.
SameValueZero
Map/Set key equality: like === except NaN equals NaN. Objects are equal only to themselves.
04

Stacks: last in, first out

A stack is a pile of plates: add to the top, take from the top. JavaScript needs no special type — an array with push and pop is a perfect stack. Stacks are how the engine tracks function calls (the call stack in every stack trace), how undo works, how brackets are matched, and how you turn recursion into a loop.

javascriptEdit it. ⌘/Ctrl + Enter runs.
You should see
3 2 1 1
kcats
true false false
14
hel
Your turn
Write MinStack: a stack that also returns the current minimum in O(1). Hint: keep a second stack of "minimum so far".
VisualizeMatching brackets with a stackStep 1 / 7
const stack = []
for (const ch of "([)]") {
if ("([{".includes(ch)) stack.push(ch)
else if (stack.pop() !== { ")": "(", "]": "[" }[ch]) {
console.log("unbalanced"); break
}
}
Line 2

First character (.

Variables now
ch'('
stack[]
All 7 steps as a table
StepLineWhat happenedVariables now
12First character (.ch = '(' stack = []
23It is an opener — push it.stack = ['(']
32Next [.ch = '['
43Opener — push.stack = ['(', '[']
52Next ).ch = ')'
64A closer. Pop the top: [. The matching opener for ) is (. [(.stack = ['(']
75Mismatch — the brackets interleave, which is not allowed. Print and stop.
Error you will hit

pop() on an empty array returns undefined — silently

javascript
const stack = []
const top = stack.pop()
console.log(top + 1)
NaN
Why the engine said that

Unlike Python, JavaScript does not throw on an empty pop: you get undefined, which turns into NaN or "Cannot read properties of undefined" three lines later. In bracket matching this is the ")(" case — a closer before any opener.

The fix

Check stack.length before popping, or wrap the array in a class that throws (as above). Every stack algorithm has this guard.

javascript
const stack = []
if (!stack.length) console.log("nothing to pop")
else console.log(stack.pop() + 1)
05

Queues: first in, first out (and why shift() is a trap)

A queue is a line at a shop: join at the back, served from the front. Queues drive breadth-first search, task schedulers, and message systems. ⛔ JavaScript has no built-in queue, and array.shift() is the wrong one: it moves every element, O(n). Two fixes: a head index into an array (simplest), or a linked list (next lesson). Twenty lines gets you a real O(1) queue.

javascriptEdit it. ⌘/Ctrl + Enter runs.
You should see
ada bob 1 cy
[ 'B', 'C', 'A' ]
[ 4, 5, 6 ]
Your turn
Implement a queue using two stacks: push onto stack A; to dequeue, if stack B is empty pour A into B first. Amortised O(1) — a favourite interview question.
bash
# Measure it yourself in Node (native speed — the sandbox on this page interprets, which hides the gap)
node -e '
for (const n of [60000, 200000]) {
  let a = Array.from({ length: n }, (_, i) => i), t = Date.now()
  while (a.length) a.shift()
  const shiftMs = Date.now() - t
  a = Array.from({ length: n }, (_, i) => i); t = Date.now()
  let head = 0; while (head < a.length) a[head++]
  console.log(n, "shift:", shiftMs, "ms | head index:", Date.now() - t, "ms")
}'

# 60000  shift: 449 ms  | head index: 1 ms
# 200000 shift: 5031 ms | head index: 1 ms      ← 3.3× the items, 11× the time: quadratic

Measured on Node 22. Doubling the queue quadruples shift()'s cost; the head-index queue stays flat.

The one rule
For a handful of items, shift() is fine. For a BFS over a big graph or a job queue, use a head index or a linked list. If you see while (queue.length) { const x = queue.shift() … } in a hot path, that is the bug.
06

Linked lists: nodes and pointers

A linked list is a chain of nodes, each holding a value and a reference to the next node. Nothing is side by side in memory, so there is no indexing — to reach item 5 you walk five links. What you gain: inserting or removing at a known node is O(1), with no shuffling. You will rarely build one at work, but interviews ask, and it is the simplest way to learn to think in references.

javascriptEdit it. ⌘/Ctrl + Enter runs.
You should see
1 -> 2 -> 3 -> 4 -> null
true false 1 -> 3 -> 4 -> null
4 -> 3 -> 1 -> null
3
Your turn
Add a tail pointer so append is O(1), and a DoublyLinked variant with prev links that can remove from the end in O(1) — that is a deque.
VisualizeReversing 1 → 2 → 3 with three pointersStep 1 / 11
let prev = null, cur = head // head is node 1
while (cur) {
const next = cur.next
cur.next = prev
prev = cur; cur = next
}
head = prev
Line 1

Start: nothing reversed yet; cur is node 1.

Variables now
prevnull
cur1
list1→2→3
All 11 steps as a table
StepLineWhat happenedVariables now
11Start: nothing reversed yet; cur is node 1.prev = null cur = 1 list = 1→2→3
23Remember node 2 before we break the link.next = 2
34Point node 1 backwards (at null). Node 1 is now the tail of the reversed part.list = 1→null, 2→3
45Advance: the reversed part ends at 1; continue from 2.prev = 1 cur = 2
53Remember node 3.next = 3
64Node 2 now points back at node 1.list = 2→1→null, 3
75Advance.prev = 2 cur = 3
83Remember null — nothing after 3.next = null
94Node 3 points back at node 2.list = 3→2→1→null
105Advance: cur becomes null, so the loop ends.prev = 3 cur = null
117The new head is the last node we processed.head = 3
Error you will hit

TypeError: Cannot read properties of null (reading 'next')

javascript
let cur = head
while (cur.next.value !== 99) {     // walks off the end
  cur = cur.next
}
Uncaught TypeError: Cannot read properties of null (reading 'value')
    at your code:2
Why the engine said that

Every linked-list bug is this one: a pointer reached null (the end) and you asked it for .next or .value. Here 99 was not in the list, so the walk ran off the end.

The fix

Loop while (cur) (or while (cur && cur.next)) and check the value inside the body. Draw the boxes and arrows on paper before you code — everyone does.

07

Recursion, revisited: the call stack you can see

Module 04 introduced recursion. Trees, graphs and divide-and-conquer sorting all lean on it, so here is the mental model that makes it easy: a recursive function is a base case (the answer for the smallest input, no recursion) plus a step that shrinks the problem and trusts the function to solve the smaller one. Each call is a frame on the call stack; the answers assemble on the way back up.

javascriptEdit it. ⌘/Ctrl + Enter runs.
You should see
120
factorial(4)
  factorial(3)
    factorial(2)
      factorial(1)
      → 1
    → 2
  → 6
→ 24
15
15
1024 243
Your turn
Write flattenKeys(obj) that turns { a: { b: 1, c: { d: 2 } } } into { "a.b": 1, "a.c.d": 2 } recursively.
Visualizefactorial(3): down the stack, then back upStep 1 / 9
function factorial(n) {
if (n <= 1) return 1
return n * factorial(n - 1)
}
console.log(factorial(3))
Line 5

Call factorial(3). A frame is pushed.

Variables now
stackf(3)
All 9 steps as a table
StepLineWhat happenedVariables now
15Call factorial(3). A frame is pushed.stack = f(3)
223 ≤ 1? No.n = 3
33Needs factorial(2) first — push another frame and wait.stack = f(3) f(2)
422 ≤ 1? No.n = 2
53Needs factorial(1) — push.stack = f(3) f(2) f(1)
621 ≤ 1? Yes — base case. Return 1. Frame popped.n = 1 stack = f(3) f(2)
73f(2) resumes: 2 × 1 = 2. Return. Frame popped.stack = f(3)
83f(3) resumes: 3 × 2 = 6. Return.stack = (empty)
95Print the result.
Error you will hit

RangeError: Maximum call stack size exceeded

javascript
function countDown(n) {
  console.log(n)
  countDown(n - 1)      // no base case
}
countDown(3)
3
2
1
0
-1
…
Uncaught RangeError: Maximum call stack size exceeded
Why the engine said that

Nothing ever stops the calls, so frames pile up until the engine refuses (around 10,000 frames in V8). The same error appears when the base case exists but the step does not move toward it.

The fix

Every recursive function needs (1) a base case that returns without recursing and (2) a step that makes the input strictly smaller. Write the base case first. JavaScript has no tail-call optimisation in practice, so very deep recursion (a 100,000-node list) needs the loop-with-a-stack form.

javascript
function countDown(n) {
  if (n < 0) return
  console.log(n)
  countDown(n - 1)
}
countDown(3)
08

Trees and binary search trees

A tree is nodes with children instead of a single next: folders on a disk, the DOM, a company org chart, the JSON you parsed in Module 03. A binary search tree (BST) is a tree with a rule — everything left is smaller, everything right is bigger — which turns searching into "go left or go right" and halves the work at every step: O(log n) when the tree is balanced.

javascriptEdit it. ⌘/Ctrl + Enter runs.
You should see
true false
[ 1, 3, 4, 6, 7, 8, 10, 13, 14 ]
[ 8, 3, 1, 6, 4, 7, 10, 14, 13 ]
[ 1, 4, 7, 6, 3, 13, 14, 10, 8 ]
4
[ [ 8 ], [ 3, 10 ], [ 1, 6, 14 ], [ 4, 7, 13 ] ]
[ '/root', '/root/a', '/root/b', '/root/b/c' ]
Your turn
Write minValue(root) (keep going left) and maxValue(root). Then isBst(root): check that the in-order traversal is strictly increasing.
A BST is only fast if it is balanced
Insert 1, 2, 3, 4, 5 in that order and every node goes right: you have built a linked list with extra steps, and search is O(n) again. Real systems use self-balancing trees (AVL, red-black — what a database index is). You will not implement one; you should know why they exist.
Quick check

Which traversal of a binary search tree visits the values in sorted order?

09

Heaps: always know the smallest

A heap is a tree stored in a flat array with one promise: the smallest item is always at index 0. Adding or removing the smallest costs O(log n) — far better than re-sorting. That makes it a priority queue: "give me the most urgent thing next". JavaScript has no built-in heap, so here is a 30-line one you can keep. Use it for top-k, merging sorted streams, scheduling and Dijkstra.

javascriptEdit it. ⌘/Ctrl + Enter runs.
You should see
1 5
[ 1, 2, 3, 5, 8 ]
[ 99, 95, 90 ]
fix prod
review PR
write tests
9
[ 1, 2, 3, 4, 5, 6, 7, 9 ]
Your turn
Given a stream of numbers, print the running median after each one using two heaps (a max-heap of the lower half, a min-heap of the upper half).
Error you will hit

The heap array is not a sorted array

javascript
const h = new MinHeap()
for (const x of [5, 1, 8, 3, 2]) h.push(x)
// peeking at the internal array: [1, 2, 8, 5, 3]
// "second smallest" is NOT at index 1
[ 1, 2, 8, 5, 3 ]  — only index 0 is guaranteed
Why the engine said that

Only the top is guaranteed to be the minimum. The heap keeps a looser order (each parent ≤ its children) precisely so that pushes and pops stay O(log n).

The fix

Pop to get items in order. For "the k smallest", pop k times; for a one-off, toSorted is fine.

10

Graphs: BFS and DFS

A graph is nodes (vertices) joined by edges: cities and roads, users and follows, tasks and their dependencies, modules and imports. A tree is just a graph with no cycles. The universal representation is an adjacency list — a Map (or object) from each node to the array of its neighbours. Two algorithms cover most graph questions: BFS (a queue, explores by distance — shortest path in unweighted graphs) and DFS (a stack or recursion, explores by depth — cycles, components, ordering).

javascriptEdit it. ⌘/Ctrl + Enter runs.
You should see
bfs  [ 'A', 'B', 'C', 'D', 'E' ]
dfs  [ 'A', 'B', 'D', 'C', 'E' ]
[ 'A', 'B', 'D', 'E' ] null
[ [ 'A', 'B', 'C', 'D', 'E' ], [ 'F' ] ]
[ 'lint', 'build', 'test', 'deploy' ]
Your turn
Add hasCycle(graph) for the directed deps graph: DFS with three colours (unvisited / in progress / done) — reaching an "in progress" node means a cycle. Then try it on { a: ["b"], b: ["a"] }.
VisualizeBFS from A: the queue is the frontierStep 1 / 9
const visited = new Set(["A"]), queue = ["A"]
let head = 0
while (head < queue.length) {
const node = queue[head++]
order.push(node)
for (const nb of graph[node])
if (!visited.has(nb)) { visited.add(nb); queue.push(nb) }
}
Line 1

Start with A marked and queued.

Variables now
queue[A]
visited{A}
order[]
All 9 steps as a table
StepLineWhat happenedVariables now
11Start with A marked and queued.queue = [A] visited = {A} order = []
24Take A from the front (head moves; nothing is shifted).node = A head = 1
35Record it.order = [A]
47A's neighbours B and C are new — mark and queue both.queue = [A, B, C] visited = {A, B, C}
54Take B.node = B head = 2
67B's neighbours: A (seen), D (new).queue = [A, B, C, D] visited = {A, B, C, D} order = [A, B]
74Take C. Its neighbours A and D are already seen — nothing queued.node = C head = 3 order = [A, B, C]
84Take D. New neighbour: E.node = D head = 4 order = [A, B, C, D] visited = {A, B, C, D, E}
94Take E. No new neighbours. head reaches the end — done. F was never reachable.head = 5 order = [A, B, C, D, E]
Which one?
Shortest path / fewest steps / nearest → BFS. Does a path exist / count components / detect a cycle / order dependencies → DFS. Weighted shortest path → Dijkstra, which is BFS with the heap from the last lesson instead of a queue. Grid problems ("number of islands") are graphs where each cell's neighbours are up/down/left/right — Module 12 has one.
11

Searching: linear vs binary

Linear search checks every item — O(n), and the only option for unsorted data (find, indexOf, includes). Binary search works on sorted data: look at the middle, and the half the target cannot be in is thrown away. A million items take at most 20 steps. The idea generalises far beyond arrays — "find the smallest X that satisfies a condition" is binary search on the answer.

javascriptEdit it. ⌘/Ctrl + Enter runs.
You should see
999999
[ 999999, 20 ]
[ -1, 20 ]
2 2 4
15
Your turn
Write firstTrue(lo, hi, pred): given that pred is false then true across the range, return the first index where it is true. Then use it to find the integer square root of 1,000,000.
VisualizeBinary search for 7 in [1, 3, 5, 7, 9, 11]Step 1 / 7
let lo = 0, hi = 5
while (lo <= hi) {
const mid = (lo + hi) >> 1
if (items[mid] === 7) break
if (items[mid] < 7) lo = mid + 1
else hi = mid - 1
}
Line 1

Search the whole array.

Variables now
lo0
hi5
All 7 steps as a table
StepLineWhat happenedVariables now
11Search the whole array.lo = 0 hi = 5
23Middle index (0 + 5) >> 1 = 2 → items[2] is 5.mid = 2
355 < 7, so 7 must be to the right. Discard indexes 0–2.lo = 3
43Middle of 3..5 is 4 → items[4] is 9.mid = 4
569 > 7, so look left. Discard 4–5.hi = 3
63lo == hi == 3 → items[3] is 7.mid = 3
74Found in 3 steps. Linear search would have taken 4 — and for a million items, 20 versus a million.
Error you will hit

Binary search on unsorted data returns nonsense

javascript
const items = [9, 1, 7, 3]
console.log(binarySearch(items, 7))   // from the example above
[ -1, 2 ]
Why the engine said that

No exception, wrong answer: 7 is in the array. Binary search assumes sorted order; the first comparison threw away the half that contained the target. The other classic bug is mixing an exclusive hi = items.length with hi = mid - 1 — an off-by-one that skips the last item.

The fix

Sort first (O(n log n), worth it if you will search many times), or use linear search once. Pick one convention for hi — inclusive with lo <= hi, or exclusive with lo < hi — and never mix them.

12

Sorting: from bubble sort to toSorted()

You will call sort a thousand times before you write one, so the point of this lesson is not to reimplement it — it is to understand why the built-in is O(n log n), what "stable" means (guaranteed since ES2019), and to recognise merge sort's divide-and-conquer shape, which reappears everywhere. Bubble sort is here only because seeing an O(n²) algorithm is the fastest way to appreciate the difference.

javascriptEdit it. ⌘/Ctrl + Enter runs.
You should see
[ 1, 2, 5, 5, 6, 9 ]
[ 1, 2, 5, 5, 6, 9 ]
bubbleSort: much slower than mergeSort
mergeSort vs toSorted: the built-in wins — it is native code
[ [ 'bob', 28 ], [ 'dee', 28 ], [ 'ada', 36 ], [ 'cy', 36 ] ]
[ [ 'ada', 36 ], [ 'cy', 36 ], [ 'bob', 28 ], [ 'dee', 28 ] ]
[ 'Apple', 'banana', 'cherry' ] [ 'Apple', 'banana', 'cherry' ]
Your turn
Implement quickSort(a): pick a pivot, build less, equal, greater with filter, recurse on the outer two. Three lines of real work.
Know the Timsort row by heart; know the others exist and why. And always pass a comparator — the default sorts as strings.
AlgorithmTimeSpaceStableWhen you meet it
Bubble / insertionO(n²)O(1)yesTiny inputs; insertion sort is what Timsort uses on small runs
Merge sortO(n log n)O(n)yesExternal sorting, linked lists
Quick sortO(n log n) avg, O(n²) worstO(log n)noC's qsort; in-memory arrays
Heap sortO(n log n)O(1)noGuaranteed bound, no extra memory
Timsort (V8 sort)O(n log n), O(n) if nearly sortedO(n)yesEvery day. Stable by spec since 2019.
Counting / radixO(n + k)O(k)yesIntegers in a small range — beats n log n
Quick check

What does it mean that a sort is stable?

13

Dynamic programming, gently

Dynamic programming (DP) has a scary name for a simple idea: if you will need the answer to a sub-problem more than once, save it. Start with the recursive solution that is obviously correct, notice it recomputes the same calls, add a cache (memoisation), and — if you like — flip it into a table filled bottom-up. Fibonacci is the "hello world"; the shape is the same for coin change, edit distance and knapsack.

javascriptEdit it. ⌘/Ctrl + Enter runs.
You should see
6765 took 21891 calls
2880067194370816000
2880067194370816000
89 165580141
6 -1
4
6

fib(90) is 2880067194370816120 — beyond 2⁵³, so the number prints rounded. That is the Number.MAX_SAFE_INTEGER lesson from Module 01 in the wild; use BigInt for exact big integers.

  1. 1
    Write the brute-force recursion

    Define the answer for size n in terms of smaller sizes. Do not optimise. Test it on tiny inputs.

  2. 2
    Spot the repeated sub-problems

    Log the arguments; if the same n or (i, j) appears many times, memoisation will help. If every call is unique, DP is not the tool.

  3. 3
    Add a cache

    A Map keyed by the arguments (stringify several). This alone turns exponential into polynomial.

  4. 4
    Optionally, go bottom-up

    Replace recursion with a table filled from the base case upwards. Same answers, no call-stack limit, often less memory (keep only the last row).

How to recognise a DP problem
The words minimum / maximum / how many ways / longest / is it possible, an input that shrinks by one step each time, and a choice at each step (take it or not, one step or two). If the greedy "take the best now" answer can be wrong, it is probably DP.
14

Cheat sheet and what to practise

If the problem says…Reach forBecause
"count", "group", "have we seen", "pairs that sum to"Map / SetO(1) lookup turns n² into n
"nested", "matching", "undo", "most recent", "next greater"stack (Array)LIFO mirrors the structure
"shortest path", "fewest steps", "level by level", "nearest"BFS with a head-index queueexplores by distance
"connected", "reachable", "cycle", "dependency order"DFS (recursion or stack)explores exhaustively
"top k", "k smallest", "merge sorted", "schedule by priority"the MinHeap classmin in O(1), push/pop in O(log n)
"sorted array", "find the smallest X such that"binary search / lower boundhalves the space each step
"subarray", "substring", "window of size k"sliding window (two pointers)each element enters and leaves once
"how many ways", "minimum cost", "longest …", "can we make"DP (memoise, then table)overlapping sub-problems
"tree", "hierarchy", "folder", "DOM", "menu"recursion on nodesa tree is defined recursively

JavaScript ships Array, Map and Set and leaves the rest to you — which is why the queue, heap, linked list and BST in this module are worth keeping in a lib/ds.js you can paste into any interview or project. Module 15 has the interview questions, where the senior tier asks you to explain these choices out loud; the practice platform has graded problems in SQL and Python that exercise the same patterns.

JuniorWhen would you use a Set instead of an array?

When I need to check membership or remove duplicates and do not care about position. set.has(x) is O(1); array.includes(x) scans. The trade-offs: no indexing, and object elements are compared by identity, so I would key a Set of coordinates by a string.

What they are really testing: Whether you know the cost table, not the syntax.

Mid-levelWhy is using array.shift() as a queue a problem, and what would you do instead?

shift removes index 0 and moves every remaining element down — O(n) per call, so a BFS over n nodes becomes O(n²). Use a head index into the array (dequeue = read items[head++], compact occasionally), a linked list, or a ring buffer. For small queues it does not matter; for a graph search or a job queue it does.

What they are really testing: That you know what the built-ins cost, and that "JavaScript has no queue" has a twenty-line answer.

SeniorA Map lookup is O(1) — when is that not true, and what would you do about it?

Average O(1) relies on the hash spreading keys across buckets. With many collisions — adversarial string keys against a predictable hash, for example — it degrades toward O(n). V8 uses a randomised seed per process to blunt hash-flooding. If keys are user-controlled and the map is a hot path, I would cap the map size, bound the key length, or move the lookup into a database index that handles it. I would also check whether the "map" is actually a plain object with integer-like keys, which V8 stores as an array — a different cost model entirely.

What they are really testing: Depth — whether "O(1)" is a fact you memorised or a mechanism you understand.

Frequently asked questions

Do I need to learn data structures and algorithms to get a JavaScript job?
For most front-end and Node roles you need the cost table (which structure is fast for what), fluency with Array, Map and Set, and the ability to reason about Big-O out loud. Implementing a red-black tree is not required; recognising that a problem is "really a BFS" is — and JavaScript interviews do ask you to write a queue or a heap because the language has none.
Is this enough for coding interviews?
It is the foundation every interview assumes: arrays, hash maps, stacks, queues, linked lists, trees, heaps, graphs, binary search, sorting, recursion and dynamic programming, each with the canonical example in JavaScript. Pair it with Module 12 (patterns) and Module 15 (questions), then practise timed problems.

Finish the JavaScript handbook, then get hired

Sit the exam for your certificate, run your resume through the ATS checker, and see the jobs that ask for exactly this.

Check my resume
Found this course useful? Share it.
ShareXLinkedIn

Comments

0

Join the conversation. Sign in to leave a comment — we'd love to hear your thoughts.