Hash-map patterns: count, group, index
Pick the shape (count, group or index), choose the key, and use a Map unless the keys are plain strings.
Predict first
Before you read: predict
ExamHall counts answer events per attempt. One service sends attempt IDs as numbers, another as strings:
What is line?
const events = [{ attemptId: 205 }, { attemptId: '205' }, { attemptId: 31 }, { attemptId: 205 }, { attemptId: 'x9' }];const counts = {};for (const e of events) counts[e.attemptId] = (counts[e.attemptId] ?? 0) + 1;export const line = Object.entries(counts).map(([k, n]) => `${k}:${n}`).join(' ');The 30-second version
Many list problems are one pass with a hash map: count (key to number), group (key to list) or index (key to item or position). Objects turn keys into strings and list integer-like keys first; a Map keeps keys as they are.
Pick the shape (count, group or index), choose the key, and use a Map unless the keys are plain strings.
How it works
Step 1 of 10
Analogy: A mushroom club's sorting table after a foray (foraging)
After the morning walk, members tip their baskets onto one long table and the club sorts the finds.
| At the sorting table | In the code |
|---|---|
| one tally mark per find on the species sheet | count: key to a number (step 2) |
| the card box filed by basket number, so "whose basket was this?" is one pull | index: key to an item (step 4) |
| the old label printer writes every tray label as text, so basket 7 and basket "7" share a tray | object keys are strings (step 7) |
| the shelf puts numbered trays first, lowest number first, then named trays in arrival order | objects reorder integer-like keys (step 8) |
| the new label printer prints each label exactly as written on the basket, number or word | a Map (step 9) |
In real terms: a hash map turns "find everything with this key" from a walk along the list into one lookup, if the key says exactly what belongs together.
Breaks down when: a tray is a physical place, while a key is a value the code computes: two equal-looking keys can still differ (a number and a string in a Map).
Contrasting case: five finds on a plate. Sorting by eye is fine; the pattern earns its keep as the list grows.
Examples, run for real
4.1 ExamHall EXH-4250: answer sheets by filtering per attempt
ExamHall built each candidate's answer sheet by filtering the whole event list once per attempt:
const sheets = attemptIds.map((id) => events.filter((e) => e.attemptId === id));
At exam size, 50,000 events from 2,500 attempts, the job crawled (EXH-4250). MR !4550 replaced it with one pass that files each event under its attempt in a Map (step 3). Both were counted at two smaller sizes, 20 events per attempt:
4.1 EXH-4250 filter per attempt, 4,000 events, 200 attempts: 800,000 event reads
4.1 MR !4550 one pass with a Map, same input: 12,200 counted operations (4,000 event reads, 4,200 Map calls, 4,000 list writes); the same sheets
4.1 EXH-4250 filter per attempt, 8,000 events, 400 attempts: 3,200,000 event reads
4.1 MR !4550 one pass with a Map, same input: 24,400 counted operations (8,000 event reads, 8,400 Map calls, 8,000 list writes); the same sheets
4.1 doubling events and attempts: filter ×4.00 · one pass ×2.00; reads = events × attempts, so 50,000 × 2,500 = 125,000,000 (arithmetic, not run)
Filtering reads every event once per attempt, so doubling both quadruples the reads (dsa-big-o-js-costs step 4). The one pass reads each event once and makes one or two Map calls per event, so doubling the events doubles the count.
4.2 Ticketing: seat sections as object keys and as Map keys
A box office counts sales per section; the sections arrive as numbers and as strings:
4.2 sales, in order: 101, 'VIP', '101', 7
4.2 plain object: 3 keys · '7':1 '101':2 'VIP':1
4.2 Map: 4 keys · 101:1 'VIP':1 '101':1 7:1
The object merged 101 and '101' (step 7) and moved the integer-like keys to the front, smallest first (step 8); the Map kept four keys, each with the type it was set with (step 9).
Try it
Answer first, then check. Nothing is saved.
Question 1 of 8
A stadium groups ticket scans by gate:
The report built from it lists no gates at all.
Which one change makes it list every ticket under its gate?
export function ticketsByGate(scans) { const groups = new Map(); for (const s of scans) { const list = groups.get(s.gate) || []; list.push(s.ticket); } return groups;}Question 2 of 8
ExamHall builds each attempt's final answer sheet in one pass: one Map per attempt, keyed by question, keeping the answer with the latest at (events can arrive out of order). The loop's five lines are shuffled (the app splits an order on >, so the comparison reads <):
In what order does the loop run them, top first?
export function answerSheets(events) { const sheets = new Map(); for (const e of events) { /* the five lines, in order */ } return sheets;}Your order
- 1empty
- 2empty
- 3empty
- 4empty
- 5empty
Tap the lines in the order you expect.
Question 3 of 8
A payments ledger looks for two refunds that add up to a disputed amount. The draft stores each amount before it looks for the partner:
What does findPair([25, 30, 25, 20], 50) return?
export function findPair(amounts, target) { const seen = new Map(); for (let i = 0; i < amounts.length; i++) { seen.set(amounts[i], i); const j = seen.get(target - amounts[i]); if (j !== undefined) return [j, i]; } return null;}Question 4 of 8
A word game in a chat app groups the words players typed into anagram sets, keyed on an object that holds the sorted letters:
How many groups does groups hold?
const words = ['listen', 'enlist', 'google', 'silent', 'gogole'];const groups = {};for (const w of words) { const key = { letters: [...w].sort().join('') }; if (!groups[key]) groups[key] = []; groups[key].push(w);}export const count = Object.keys(groups).length;Question 5 of 8
the interviewer says: "Given an array of numbers and a target, return how many pairs of positions hold two values that sum to the target. Do it in one pass." You have 15 minutes and a plain editor, and ExamHall's score-pairing check is the use case.
Before you type: say your test list out loud.
Name at least five tests, each with its input and what it expects.
Question 6 of 8
the interviewer says: "Given an array of numbers and a target, return how many pairs of positions hold two values that sum to the target. Do it in one pass." You have 15 minutes and a plain editor, and ExamHall's score-pairing check is the use case.
Now write it, in 15 minutes, from a blank file that exports countPairs.
Question 7 of 8
A ledger counts today's payments per currency:
What happens when this module runs?
const payments = [{ currency: 'EUR' }, { currency: 'USD' }, { currency: 'EUR' }];const counts = {};for (const p of payments) counts[p.currency]++;export const line = Object.entries(counts).map(([k, n]) => `${k}:${n}`).join(' ');Question 8 of 8
A chat moderation tool lists, in one pass, the senders in a busy room who posted only once:
The room's senders, in posting order, are [42, 7, 42, 19, 42, 3].
How many senders are in the list postedOnce returns?
export function postedOnce(ids) { const counts = new Map(); const once = []; for (const id of ids) { counts.set(id, (counts.get(id) ?? 0) + 1); if (counts.get(id) === 1) once.push(id); } return once;}Go deeper
Pro tips
- Use a
Mapwhen keys are not plain strings, or when order and size matter. When: choosing the dictionary for a count, group or index (steps 7–9). Verify (RAN,tips.mjs): (The curriculum row's tip, [SELF].)tip 1 keys 12, '12', 7 · object: 2 keys (string, string) · Map: 3 keys (number, string, number) - Say the shape out loud before you code: "this is a count", "a group" or "an index", then name the key. When: the first minute of a list question in an interview (steps 1, 6). Node 24 also ships
Map.groupByfor the group shape. Verify (RAN,tips.mjs):tip 2 typeof Map.groupBy on Node 24.21.0: function · gates E: e1 e2 · W: w1
Source
Vendor pages change, so their claims are labelled as of 2026-10-15; every behaviour this lesson grades is executed on Node 24.
- MDN, Map (the curriculum row's first source), https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map (
mdn-map.txt), "Objects vs. Maps": "A Map's keys can be any value (including functions, objects, or any primitive)." and "The keys of an Object must be either a String or a Symbol." (steps 7, 9). As of 2026-10-15. - MDN, Property accessors, https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Property_accessors (
mdn-property-accessors.txt): "Each property name is a string or a Symbol. Any other value, including a number, is coerced to a string." and "both foo and bar are converted to the same string ("[object Object]")." (step 7). As of 2026-10-15. - ECMAScript 2026, OrdinaryOwnPropertyKeys, https://tc39.es/ecma262/multipage/ordinary-and-exotic-objects-behaviours.html (
ecma-ordinary-objects.txt), §10.1.11.1: "For each own property key propertyKey of obj such that propertyKey is an array index, in ascending numeric index order, do" and "propertyKey is a String and propertyKey is not an array index, in ascending chronological order of property creation" (step 8). - Tech Interview Handbook, Hash table (the curriculum row's second source), https://www.techinterviewhandbook.org/algorithms/hash-table/ (
tih-hash-table.txt): "we can traverse the array once and hash all the elements into a hash table" and "which is O(1) on average." (steps 1, 4); its practice list names Two Sum and Group Anagrams (steps 5, 6). As of 2026-10-15.
Related lessons
Checked against the tools and docs as of 2026-10-15.