A hash table as a counter
In short: A map used as a counter (Map in JavaScript) counts how often each element occurs in a single pass: O(n) instead of O(n²) with searching a list.
Problem
The best-selling products of the day
The shop dashboard shows the three best-selling products of the day. The log is a list of orders, one per line. The code keeps two lists, the names and the counts, and for every order it looks for the product from the start of the names:
const log = logOf(1000);const names = [];const counts = [];let comparisons = 0;for (const item of log) { let i = 0; while (i < names.length) { comparisons++; if (names[i] === item) break; i++; } if (i === names.length) { names.push(item); counts.push(1); } else { counts[i]++; }}console.log(`orders: ${log.length}`);console.log(`products: ${names.length}`);console.log(`comparisons: ${comparisons}`);orders: 1000products: 251comparisons: 125255A thousand orders and 251 different products: 125,255 comparisons. The catalog grows with the shop, and so does the list every order walks through. On a big day with 100,000 orders the dashboard takes minutes to load.
Where the log comes from
The log follows a formula, so that the programs in all four languages and the comparison in the browser below build exactly the same one. The catalog has n / 4 + 1 products:
function logOf(n) { const catalog = Math.floor(n / 4) + 1; return Array.from({ length: n }, (_, i) => `SKU-${(i * 7919) % catalog}`);}The idea
The map counts
In the lesson “Map and Set” you counted orders with a map. Here it becomes a technique: a hash table as a counter (a frequency map). The key is what you count, the value is how many times you have seen it.
- One pass over the data. For every element: find its counter (or 0) and add 1.
- The lookup is in a hash table: O(1) on average, however many products there are. O(n) in total instead of O(n · m).
- After that you answer the questions from the counts, not from the data: which one is most common, which one appears once, whether two things have the same counts.
The memory is O(m): one pair for every different product.
See it
The counter fills up
const log = [ "mouse", "headset", "mouse", "monitor", "keyboard", "mouse", "headset", "monitor", "mouse", "headset",];const counts = new Map();for (const item of log) { counts.set(item, (counts.get(item) ?? 0) + 1);}const top = [...counts].sort((x, y) => y[1] - x[1]);for (const [item, n] of top.slice(0, 3)) { console.log(item, n);}mouse 4headset 3monitor 2const counts = new Map();for (const item of log) { counts.set(item, (counts.get(item) ?? 0) + 1);}const top = [...counts].sort((x, y) => y[1] - x[1]);for (const [item, n] of top.slice(0, 3)) { console.log(item, n);}Step by step, in words
- An empty map: product → how many times it was sold. The log has 10 orders.
- Order 1: mouse. It is not in the map yet, so it gets a count of 1.
- Order 2: headset. It is not in the map yet, so it gets a count of 1.
- Order 3: mouse, 1 → 2. One lookup in the hash table, without walking the other products.
- Order 4: monitor. It is not in the map yet, so it gets a count of 1.
- Order 5: keyboard. It is not in the map yet, so it gets a count of 1.
- Order 6: mouse, 2 → 3. One lookup in the hash table, without walking the other products.
- Order 7: headset, 1 → 2. One lookup in the hash table, without walking the other products.
- Order 8: monitor, 1 → 2. One lookup in the hash table, without walking the other products.
- Order 9: mouse, 3 → 4. One lookup in the hash table, without walking the other products.
- Order 10: headset, 2 → 3. One lookup in the hash table, without walking the other products.
- 10 orders, 10 counter updates. Only the 4 distinct products get sorted by count, not the orders.
- The top three: mouse 4, headset 3, monitor 2.
counts.get(item) ?? 0 gives 0 for a product that is not there yet. The parentheses around it are needed: without them + 1 is computed before ??. The top three come from sorting the pairs by count: only the different products are sorted, not the orders.
Try it
Fill in
The line that counts:
counts.set(item, (counts.get(item) 0) + 1);?? gives 0 when get returns undefined for a product that is not there yet.See it
The tools of your language
JavaScript has no ready-made counter. A Map with get, ?? 0 and set is the usual way. A plain object also works, but it has inherited keys ("constructor", "toString") and turns every key into text. A Map walks its keys in insertion order:
const log = [ "mouse", "headset", "mouse", "monitor", "keyboard", "mouse", "headset", "monitor", "mouse", "headset",];const counts = new Map();for (const item of log) counts.set(item, (counts.get(item) ?? 0) + 1);console.log(counts.get("mouse"), counts.get("tablet") ?? 0);for (const [item, n] of counts) console.log(item, n);4 0mouse 4headset 3monitor 2keyboard 1See it
Measure: a list of pairs against a map
The same thousand orders with the map:
const log = logOf(1000);const counts = new Map();let updates = 0;for (const item of log) { counts.set(item, (counts.get(item) ?? 0) + 1); updates++;}console.log(`orders: ${log.length}`);console.log(`products: ${counts.size}`);console.log(`counter updates: ${updates}`);orders: 1000products: 251counter updates: 10001,000 updates instead of 125,255 comparisons. Run both in the browser. n is the number of orders, and the catalog grows with it:
- A list of product and count pairs
O(n²) - A map as a counter
O(n)
| Algorithm | n = 10 | n = 1,000 | n = 100,000 |
|---|---|---|---|
| A list of product and count pairs | 16 | 125,255 | ≈ 1,252,550,000 |
| A map as a counter | 10 | 1,000 | 100,000 |
const counts = new Map();for (const item of log) { counts.set(item, (counts.get(item) ?? 0) + 1);}const top = [...counts].sort((x, y) => y[1] - x[1]);for (const [item, n] of top.slice(0, 3)) { console.log(item, n);}More precisely, if you are curious
The top k without sorting everything. Sorting m different products is O(m log m). If there are millions of products and you need the top 10, you keep a heap with at most k pairs: every pair goes in, and when there are k + 1, the smallest goes out. That is O(m log k).
A key with several parts. You count sales by city and product: the key is the pair. In JavaScript a Map compares objects by reference, so the key is text: `${city}|${item}`.
The idea
The basket and the kit
The “home office” kit is a monitor, a keyboard, a mouse and two cables. The warehouse checks that the basket to be shipped is exactly the kit: nothing extra, nothing missing. The order does not matter, the counts do.
One counter: +1 for every product of the kit, −1 for every product in the basket. The basket matches if all counts end up at 0.
function matches(kit, basket) { const need = new Map(); for (const item of kit) need.set(item, (need.get(item) ?? 0) + 1); for (const item of basket) need.set(item, (need.get(item) ?? 0) - 1); return [...need.values()].every((n) => n === 0);}The same check finds anagrams: “listen” and “silent” have the same letters with the same counts. The counter is over letters instead of products.
Try it
Predict the check
const kit = [ "monitor", "keyboard", "mouse", "cable", "cable",];const basket1 = [ "cable", "mouse", "monitor", "cable", "keyboard",];const basket2 = [ "monitor", "keyboard", "mouse", "mouse", "cable",];console.log(`basket 1: ${matches(kit, basket1) ? "complete kit" : "does not match"}`);console.log(`basket 2: ${matches(kit, basket2) ? "complete kit" : "does not match"}`);The first basket has the same five products in a different order: every count becomes 0. The second has two mice and one cable: the mouse stays at −1, the cable at +1, so it does not match, even though there are five products.
The idea
The first customer with a single order
Marketing sends a “come back” voucher to the customers who ordered only once. It wants the first one in time. The counter gives the counts, but not the order:
function firstSingle(customers) { const counts = new Map(); for (const c of customers) counts.set(c, (counts.get(c) ?? 0) + 1); for (const c of customers) { if (counts.get(c) === 1) return c; } return null;}const customers = [ "C-17", "C-4", "C-17", "C-9", "C-4", "C-21", "C-9", "C-30",];console.log(`first customer with one order: ${firstSingle(customers)}`);first customer with one order: C-21Two passes: the first one counts, the second walks the log, in the order of the orders, and stops at the first one with a count of 1. O(n), with no nested loop.
More precisely, if you are curious
Why does the second pass walk the log and not the map? A Map walks in insertion order (promised by the specification), so here walking counts would give the same answer. The log is still clearer: the order comes from it.
The idea
Two products for exactly €50
The customer has a voucher for exactly €50 and wants two products that use it up. The prices in the catalog are not sorted.
You walk through the prices and for each one you ask: have I already seen the price that tops this one up to 50? The map keeps the prices seen so far and their positions:
function voucherPair(prices, voucher) { const seen = new Map(); for (let i = 0; i < prices.length; i++) { const j = seen.get(voucher - prices[i]); if (j !== undefined) return [j, i]; seen.set(prices[i], i); } return null;}const prices = [27, 14, 33, 25, 8, 36, 19];const [a, b] = voucherPair(prices, 50);console.log(`products ${a} and ${b}: €${prices[a]} + €${prices[b]}`);products 1 and 5: €14 + €36One pass: O(n) time and O(n) memory. With two pointers it works without a map, with O(1) memory, but the prices must be sorted. If they are not, sorting costs O(n log n) and loses the position of every product in the catalog. So: sorted data, two pointers; unsorted data, a map.
Try it
Find the bug
Someone swapped two lines. Now for a €50 voucher the program offers one product twice:
const prices = [27, 14, 33, 25, 8, 36, 19];const [a, b] = voucherPair(prices, 50);console.log(`products ${a} and ${b}: €${prices[a]} + €${prices[b]}`);products 3 and 3: €25 + €25Pick the line with the bug.
The price goes into the map before its pair is looked up. The €25 product looks for 50 − 25 = 25 and finds itself. The write must come after the lookup: then the map holds only the products before the current one, and the program finds €14 + €36.
Exercise
Who spends the most
Solve the task in your own editor.
Revenue per customer. The log has pairs of customer and amount: C-4 for €30, C-17 for €55, C-4 for €20, C-9 for €80, C-17 for €10 and C-4 for €45. Add up the amounts per customer with a map (the value is a sum, not a count) and print the two who spent the most:
C-4: €95C-9: €80More tasks
- Anagrams. Write a check whether two words are anagrams (“listen” and “silent”) by counting the letters with a map. How many operations does it take for words of length n?
- The first repeat. Find the first order whose customer has already ordered before. Do you need a counter, or is a set enough?
- Periods with equal counts. From prefix sums you know how to count periods with a given sum. Count the periods of consecutive days in which the number of returned orders equals the number of delivered ones.
Test
Check yourself
Related lessons
Section titled "Related lessons"- Builds on: Hash table · Priority queue · Two pointers · Prefix sums
- Tasks on this topic: Problem-solving techniques