Part 2 · Lesson 15 · 24 min read
Basic Hashing
Counting with arrays and maps, how a hash table works inside, and why lookup becomes instant.
On this page
Count once, answer many times
Suppose you are given an array and then many questions: “How many times does 2 appear? And 4? And 3?” The obvious way is to scan the whole array again for every question:
const nums = [2, 3, 2, 5, 3, 2];const queries = [2, 4, 3];// For every query, scan the whole array again: O(n × q)for (const q of queries) { let c = 0; for (const x of nums) if (x === q) c++; console.log(q, "appears", c, "times");}With n = 100,000 values and q = 100,000 questions, that is 1010 steps. The better idea is pre-computation. Pre-computation means doing some work once, before the questions come, and saving the results. Here you walk the array once and record every count. Then you answer each question with a single lookup (reading one saved value). The total work is O(n + q). Storing values so that you can find them directly, without searching, is called hashing.
Counting with an array
A counting array is an array where the position (index) is the value you count, and the number stored there is how many times that value appears. It works when the values are small whole numbers in a known range, say 0 to 1,000. count[7] holds how many 7s there are. Reading or updating it is O(1) (it takes the same short time however big the array is), because an array can jump straight to any index.
Traced: filling a counting array
The values are between 0 and 5, so one counter for each possible value is enough.
▶const nums = [2, 3, 2, 5, 3, 2];2const count = new Array(6).fill(0);3for (const x of nums) {4 count[x]++;5}6console.log(count[2], count[4]);| nums | [2, 3, 2, 5, 3, 2] |
The cost is memory. The array needs one box for every possible value, even the unused ones. That is fine for 0 to 1,000. But values up to 109, or negative values, do not fit. Then you need a Map.
The 26-letter array
The most common fixed range in interviews is “lowercase English letters”. There are 26 of them. Turn each letter into an index from 0 to 25 with its character code (Lesson 9). The character code is the number that stands for a letter: "a".charCodeAt(0) is 97, so subtracting 97 gives 0 for “a”, 1 for “b”, and so on:
function letterCounts(s) { const count = new Array(26).fill(0); for (const ch of s) { count[ch.charCodeAt(0) - 97]++; // "a" → 0, "b" → 1, … "z" → 25 } return count;}const c = letterCounts("hello");console.log(c[4], c[11], c[25]); // 1 2 0 (e, l, z)Two strings are anagrams (they use exactly the same letters, such as “listen” and “silent”) exactly when their 26 counts are equal. That is a fixed 26-step comparison, however long the strings are.
Counting with a Map
For anything else, such as large numbers, negative numbers or words, use the frequency map from Lesson 10. A Map is a JavaScript collection that stores key and value pairs, and it finds a value quickly from its key. A frequency map uses each item as the key and its count as the value. It stores only the values that actually appear:
function counts(items) { const freq = new Map(); for (const x of items) freq.set(x, (freq.get(x) ?? 0) + 1); return freq;}const freq = counts([10, -4, 10, 1000000, -4, 10]);console.log(freq.get(10)); // 3console.log(freq.get(-4)); // 2console.log(freq.get(7) ?? 0); // 0 never seen| Counting array | Map | |
|---|---|---|
| Works for | Small non-negative whole numbers, letters | Any value |
| Memory | One box per possible value | One entry per value that appears |
| Speed | O(1), very fast in practice | O(1) on average |
| Missing value | Reads as 0 | Reads as undefined — use ?? 0 |
Highest and lowest frequency
Once the counts exist, many questions become one simple pass over the map. It is like finding the largest value in an array, but you look at the counts instead:
function mostAndLeast(nums) { const freq = new Map(); for (const x of nums) freq.set(x, (freq.get(x) ?? 0) + 1); let most = null, least = null; for (const [value, c] of freq) { if (most === null || c > freq.get(most)) most = value; if (least === null || c < freq.get(least)) least = value; } return [most, least];}console.log(mostAndLeast([4, 1, 4, 2, 4, 2])); // [ 4, 1 ]There are two passes: O(n) to count, and O(k) to scan the k distinct values (the different values). The comparisons use strict > and <, so when two values have the same count, the one that was seen first wins.
Inside a hash table
How can a Map find a key without searching? A Map is built on a hash table. A hash table is a data structure that stores key and value pairs in an array, and it finds a key fast. Inside, it has an ordinary array of slots called buckets. It also has a hash function, which is a function that turns any key into a bucket number:
- To store a key, compute
hash(key)(a number) and put the entry in that bucket. - To look a key up, compute the same
hash(key)and look only in that bucket.
The hash function does the same small amount of work however many entries the table holds. So storing and finding take O(1). Here is a very simple hash for text with 5 buckets. Add up the character codes, then take the remainder after dividing by 5.
| Key | Sum of codes | Bucket |
|---|---|---|
| cat | 99 + 97 + 116 = 312 | 2 |
| dog | 100 + 111 + 103 = 314 | 4 |
| fish | 102 + 105 + 115 + 104 = 426 | 1 |
| act | 97 + 99 + 116 = 312 | 2 ← same as cat |
Collisions
“cat” and “act” have the same letters, so they have the same sum and the same bucket. When two keys land in one bucket, it is called a collision. You cannot avoid collisions completely, because there are far more possible keys than buckets. The usual fix is chaining: each bucket holds a short list of entries. A lookup goes to the right bucket and checks the few entries there.
class HashTable { constructor(size = 5) { this.buckets = Array.from({ length: size }, () => []); } hash(key) { // text → bucket number let h = 0; for (const ch of key) h += ch.charCodeAt(0); return h % this.buckets.length; } set(key, value) { const bucket = this.buckets[this.hash(key)]; for (const pair of bucket) { if (pair[0] === key) { pair[1] = value; return; } // update } bucket.push([key, value]); // add } get(key) { const bucket = this.buckets[this.hash(key)]; for (const [k, v] of bucket) if (k === key) return v; return undefined; }}const t = new HashTable();t.set("cat", 1);t.set("dog", 2);t.set("act", 3);console.log(t.get("act"), t.get("dog"), t.get("cow")); // 3 2 undefinedReal hash tables keep collisions rare in two ways:
- A good hash function spreads keys evenly over the buckets. Real ones mix the characters much more than a plain sum does, so anagrams do not collide.
- Resizing. When the table gets too full (for example, more entries than buckets), it creates a bigger bucket array and moves every entry into it. This O(n) step is rare. So the average cost per operation stays O(1).
If every key landed in the same bucket, a lookup would check all n entries, which is O(n). With a good hash function this almost never happens. So Map and Set operations are quoted as O(1) on average. This is the exception to the worst-case rule from Lesson 12.
Keys in JavaScript: what counts as “the same”
Three details about keys surprise many people in interviews. The code shows each one:
const m = new Map();m.set(1, "number one");m.set("1", "text one");console.log(m.size); // 2 — 1 and "1" are different keys in a Mapconst obj = {};obj[1] = "number one";obj["1"] = "text one";console.log(Object.keys(obj)); // [ '1' ] — object keys are always textconst pairs = new Map();pairs.set([1, 2], "a");console.log(pairs.get([1, 2])); // undefined — a new array is a different objectpairs.set("1,2", "a");console.log(pairs.get("1,2")); // a — turn the pair into a text key- Objects turn keys into text.
obj[1]andobj["1"]are the same property. A Map keeps them apart. - Arrays and objects are compared by identity, not by contents. Identity means “is it the very same object in memory?”. Two separate
[1, 2]arrays are different keys. - To use a pair or a list as a key, turn it into a string first. Use
`${r},${c}`for a grid cell (row r, column c). Use the sorted letters of a word to group anagrams (Lesson 27).
Practice questions
In each question, decide first whether the range of values is small and fixed (use an array) or not (use a Map).
For each value in queries, return how many times it appears in nums.
Hint 1
nums again for every query. Count everything once first.Show all 2 solutions with explanations
For each query, count matches with a loop.
function answer(nums, queries) { return queries.map((q) => nums.filter((x) => x === q).length);}console.log(answer([2, 3, 2, 5, 3, 2], [2, 4, 3])); // [ 3, 0, 2 ]O(n × q) time. filter is a full pass over the array, and it runs inside map once for every query.
Build a frequency map, then answer each query with one lookup.
function answer(nums, queries) { const freq = new Map(); for (const x of nums) freq.set(x, (freq.get(x) ?? 0) + 1); return queries.map((q) => freq.get(q) ?? 0);}console.log(answer([2, 3, 2, 5, 3, 2], [2, 4, 3])); // [ 3, 0, 2 ]O(n + q) time, and O(k) space for k distinct values (k different values). If the values were known to be small (say 0 to 1,000), a counting array would work the same way.
This is the main idea of the lesson. Pay once to pre-compute (do the work in advance), and then every question is O(1).
For each nums[i], count how many numbers in the array are strictly smaller than it. Values are between 0 and 100.
Hint 1
Hint 2
Show all 2 solutions with explanations
Use two loops, one inside the other. (Here map and filter are the two loops.)
function smallerNumbersThanCurrent(nums) { return nums.map((x) => nums.filter((y) => y < x).length);}console.log(smallerNumbersThanCurrent([8, 1, 2, 2, 3])); // [ 4, 0, 1, 1, 3 ]O(n²) time. This is fine for the LeetCode limit of 500 values, but too slow for much more.
- Count each value in an array of size 101.
- Walk the counts from 0 upwards and keep a running total (a sum that grows as you go). Then
smaller[v]is how many values are below v. - Answer each number with
smaller[x].
function smallerNumbersThanCurrent(nums) { const count = new Array(101).fill(0); for (const x of nums) count[x]++; const smaller = new Array(101).fill(0); let running = 0; for (let v = 0; v <= 100; v++) { smaller[v] = running; // values strictly below v running += count[v]; } return nums.map((x) => smaller[x]);}console.log(smallerNumbersThanCurrent([8, 1, 2, 2, 3])); // [ 4, 0, 1, 1, 3 ]console.log(smallerNumbersThanCurrent([7, 7, 7])); // [ 0, 0, 0 ]| v | count[v] | smaller[v] |
|---|---|---|
| 1 | 1 | 0 |
| 2 | 2 | 1 |
| 3 | 1 | 3 |
| 8 | 1 | 4 |
Approach 2 is O(n + 101) = O(n) time. The small, fixed range of values (0 to 100) is what makes a counting array possible. (LeetCode 1365.)
Return the first letter to appear twice in a string of lowercase letters (one always exists).
Hint 1
Show all 2 solutions with explanations
A Set is a JavaScript collection that keeps each value only once. If the character is already in the Set, return it. Otherwise add it.
function repeatedCharacter(s) { const seen = new Set(); for (const ch of s) { if (seen.has(ch)) return ch; seen.add(ch); }}console.log(repeatedCharacter("abccbaacz")); // cconsole.log(repeatedCharacter("abcdd")); // dO(n) time. The Set holds at most 26 letters, so the space is O(1).
Use the same idea with a fixed array of 26 flags (each one is true or false).
function repeatedCharacter(s) { const seen = new Array(26).fill(false); for (const ch of s) { const i = ch.charCodeAt(0) - 97; if (seen[i]) return ch; seen[i] = true; }}console.log(repeatedCharacter("abccbaacz")); // cIt behaves the same way. The array version is what you would write in a language that has no built-in Set.
Either is fine. The question asks for the letter whose second copy comes first. That is exactly the first time seen.has is true. (LeetCode 2351.)
Return true if every character that appears in s appears the same number of times.
Hint 1
Hint 2
Show the solution with explanations
Build the counts. Then check whether all the counts are the same number.
function areOccurrencesEqual(s) { const freq = new Map(); for (const ch of s) freq.set(ch, (freq.get(ch) ?? 0) + 1); return new Set(freq.values()).size === 1;}console.log(areOccurrencesEqual("abacbc")); // trueconsole.log(areOccurrencesEqual("aaabb")); // falseIf all counts are the same, the Set of counts has exactly one item. O(n) time.
Changing “are they all equal?” into “does the Set of them have size 1?” is a small trick that appears often. (LeetCode 1941.)
Return [most frequent value, least frequent value]. On a tie, choose the smaller value.
Hint 1
Show the solution with explanations
Count first, then scan once. When two counts are equal, prefer the smaller value.
function mostAndLeast(nums) { const freq = new Map(); for (const x of nums) freq.set(x, (freq.get(x) ?? 0) + 1); let most = null, least = null; for (const [v, c] of freq) { const cm = most === null ? -1 : freq.get(most); const cl = least === null ? Infinity : freq.get(least); if (c > cm || (c === cm && v < most)) most = v; if (c < cl || (c === cl && v < least)) least = v; } return [most, least];}console.log(mostAndLeast([10, 5, 10, 15, 10, 5])); // [ 10, 15 ]console.log(mostAndLeast([2, 2, 1, 1])); // [ 1, 1 ]O(n) time, O(k) space. Tie rules often cause wrong answers. Read the question for them and test them with an example like the second one.
Whenever a problem says “most frequent”, ask the clarifying question from Lesson 11: what should happen on a tie?
Rearrange the characters of s so that characters that appear more often come first. Any order is accepted among characters with equal counts.
Hint 1
Hint 2
Hint 3
Show all 2 solutions with explanations
Sort the [char, count] pairs by count, largest first (descending), and build the string.
function frequencySort(s) { const freq = new Map(); for (const ch of s) freq.set(ch, (freq.get(ch) ?? 0) + 1); return [...freq] .sort((a, b) => b[1] - a[1]) .map(([ch, c]) => ch.repeat(c)) .join("");}console.log(frequencySort("tree")); // eetrconsole.log(frequencySort("Aabb")); // bbAaO(n + k log k) for k different characters. The value k is small (at most 62 letters and digits), so this is really O(n).
Make an array where buckets[c] is the list of characters that appear c times. Read it from the highest count down to 1.
function frequencySort(s) { const freq = new Map(); for (const ch of s) freq.set(ch, (freq.get(ch) ?? 0) + 1); const buckets = Array.from({ length: s.length + 1 }, () => []); for (const [ch, c] of freq) buckets[c].push(ch); let out = ""; for (let c = s.length; c >= 1; c--) { for (const ch of buckets[c]) out += ch.repeat(c); } return out;}console.log(frequencySort("tree")); // eetrconsole.log(frequencySort("Aabb")); // bbAaO(n) with no sorting at all. The count is used as an array index, like the counting array in this lesson. Lesson 27 uses the same bucket idea for “top k frequent”.
Approach 1 is shorter and is the usual answer. Approach 2 is worth knowing, because the “bucket by count” idea comes back several times. (LeetCode 451.)
Build a hash map for whole-number keys without using Map or objects as dictionaries. Support put, get (−1 if missing) and remove.
Hint 1
key % size (the remainder of the key divided by the number of buckets).Hint 2
[key, value] pairs. This is called chaining.Show the solution with explanations
Use the hash table from the lesson, for whole-number keys, and add remove.
class MyHashMap { constructor() { this.size = 1009; // a prime spreads keys more evenly this.buckets = Array.from({ length: this.size }, () => []); } bucket(key) { return this.buckets[key % this.size]; } put(key, value) { const b = this.bucket(key); for (const pair of b) { if (pair[0] === key) { pair[1] = value; return; } } b.push([key, value]); } get(key) { for (const [k, v] of this.bucket(key)) if (k === key) return v; return -1; } remove(key) { const b = this.bucket(key); const i = b.findIndex((pair) => pair[0] === key); if (i !== -1) b.splice(i, 1); }}const m = new MyHashMap();m.put(1, 1);m.put(2, 2);console.log(m.get(1)); // 1console.log(m.get(3)); // -1m.put(2, 1);console.log(m.get(2)); // 1m.remove(2);console.log(m.get(2)); // -1With up to 104 operations spread over 1,009 buckets, each bucket stays short, so every operation is O(1) on average. A prime number of buckets causes fewer collisions when the keys follow a pattern (for example, all even, or all multiples of 10).
In real code you would use Map. This question checks that you know how Map works inside: hash the key, go to its bucket, and compare keys in that bucket only. (LeetCode 706.)
Make it stick
- Explain why answering q frequency questions costs O(n × q) without pre-computation and O(n + q) with it.
- Write the 26-letter counting loop from memory.
- Describe what a hash function, a bucket and a collision are, in one sentence each.
- Say why
map.get([1, 2])returnsundefinedeven aftermap.set([1, 2], "a").
Revisit schedule: solve two of the questions again tomorrow, again in 3 days, and once more in a week — without looking at the answers. Each time it will feel easier. That is the memory forming.
Part 2 complete — what's next
You can now read a problem, estimate its cost with Big-O, use basic maths, write recursion, and count with arrays and Maps. These are the thinking tools for everything that follows.
Part 3 — Sorting starts with three simple sorting algorithms. Sorting means putting items in order. You will rarely write these three in an interview. But they show exactly what sorting does, and they explain why the faster ones in Lessons 17 and 18 are faster.
Practise on LeetCode
Real interview problems that use exactly what this lesson teaches. Solve them in JavaScript after the practice questions above.
Published by Jainik · Last updated · How these lessons are made
Keep learning
Part of the study guide
Related series