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

  1. Count once, answer many times
  2. Counting with an array
  3. Traced: filling a counting array
  4. The 26-letter array
  5. Counting with a Map
  6. Highest and lowest frequency
  7. Inside a hash table
  8. Collisions
  9. Keys in JavaScript: what counts as “the same”
  10. Practice questions (7)
  11. Make it stick
  12. Part 2 complete — what's next

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:

JavaScript
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

step 1 / 15
Set up
nums

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]);
Variables
nums[2, 3, 2, 5, 3, 2]
Console
(nothing printed yet)
One pass to fill the counters; after that, every question is answered by reading one box. · interactive
count =
0
0
0
1
3
2
2
3
0
4
1
5
↑ index (position), always starting at 0
The finished counting array for [2, 3, 2, 5, 3, 2]: the index is the value, the box holds its count.

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:

JavaScript
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:

JavaScript
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 arrayMap
Works forSmall non-negative whole numbers, lettersAny value
MemoryOne box per possible valueOne entry per value that appears
SpeedO(1), very fast in practiceO(1) on average
Missing valueReads as 0Reads 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:

JavaScript
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:

  1. To store a key, compute hash(key) (a number) and put the entry in that bucket.
  2. 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.

Dry run · hash(key) = (sum of character codes) % 5
KeySum of codesBucket
cat99 + 97 + 116 = 3122
dog100 + 111 + 103 = 3144
fish102 + 105 + 115 + 104 = 4261
act97 + 99 + 116 = 3122 ← 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.

JavaScript
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 undefined

Real 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).
Why we say “O(1) on average”

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:

JavaScript
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] and obj["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).

Q1Frequency queriesEasy

For each value in queries, return how many times it appears in nums.

Example 1
Input: nums = [2, 3, 2, 5, 3, 2], queries = [2, 4, 3]
Output: [3, 0, 2]
Explanation: 2 appears 3 times, 4 never, 3 twice.
Stuck? Open one hint at a time
Hint 1
Do not scan nums again for every query. Count everything once first.
Show all 2 solutions with explanations
Approach 1Scan for every query
The idea

For each query, count matches with a loop.

JavaScript
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 ]
How it works

O(n × q) time. filter is a full pass over the array, and it runs inside map once for every query.

Approach 2Count once with a Map
The idea

Build a frequency map, then answer each query with one lookup.

JavaScript
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 ]
How it works

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.

Which approach should you use?

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

Q2How many numbers are smaller than each oneEasy

For each nums[i], count how many numbers in the array are strictly smaller than it. Values are between 0 and 100.

Example 1
Input: [8, 1, 2, 2, 3]
Output: [4, 0, 1, 1, 3]
Explanation: For 8: four numbers (1, 2, 2, 3) are smaller. For 1: none. For each 2: only 1. For 3: 1, 2, 2.
Example 2
Input: [7, 7, 7]
Output: [0, 0, 0]
Explanation: Equal values are not smaller.
Stuck? Open one hint at a time
Hint 1
Brute force means trying everything: for each number, count the others that are smaller. This is O(n²).
Hint 2
Values are between 0 and 100. Count each value. Then ask: how many numbers are smaller than v? The answer is the sum of the counts of 0 … v − 1.
Show all 2 solutions with explanations
Approach 1Compare with every other number
The idea

Use two loops, one inside the other. (Here map and filter are the two loops.)

JavaScript
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 ]
How it works

O(n²) time. This is fine for the LeetCode limit of 500 values, but too slow for much more.

Approach 2Counting array + running total
The idea
  1. Count each value in an array of size 101.
  2. 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.
  3. Answer each number with smaller[x].
JavaScript
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 ]
How it works
Dry run · nums = [8, 1, 2, 2, 3] (only the values that matter)
vcount[v]smaller[v]
110
221
313
814
Which approach should you use?

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

Q3First letter to appear twiceEasy

Return the first letter to appear twice in a string of lowercase letters (one always exists).

Example 1
Input: "abccbaacz"
Output: "c"
Explanation: c is the first letter whose second copy appears (index 3). a and b repeat later.
Example 2
Input: "abcdd"
Output: "d"
Explanation: Only d repeats.
Stuck? Open one hint at a time
Hint 1
Walk along the string and remember what you have seen. The first character that you have already seen is the answer.
Show all 2 solutions with explanations
Approach 1Set
The idea

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.

JavaScript
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"));     // d
How it works

O(n) time. The Set holds at most 26 letters, so the space is O(1).

Approach 226 booleans
The idea

Use the same idea with a fixed array of 26 flags (each one is true or false).

JavaScript
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")); // c
How it works

It behaves the same way. The array version is what you would write in a language that has no built-in Set.

Which approach should you use?

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

Q4All characters occur equally oftenEasy

Return true if every character that appears in s appears the same number of times.

Example 1
Input: "abacbc"
Output: true
Explanation: a, b and c each appear twice.
Example 2
Input: "aaabb"
Output: false
Explanation: a appears 3 times, b only 2.
Stuck? Open one hint at a time
Hint 1
Count every letter, then check that all the counts above zero are equal.
Hint 2
Put the counts into a Set. How big should the Set be if all counts are equal?
Show the solution with explanations
Approach 1Count, then compare
The idea

Build the counts. Then check whether all the counts are the same number.

JavaScript
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"));  // false
How it works

If all counts are the same, the Set of counts has exactly one item. O(n) time.

Which approach should you use?

Changing “are they all equal?” into “does the Set of them have size 1?” is a small trick that appears often. (LeetCode 1941.)

Q5Most and least frequentEasy

Return [most frequent value, least frequent value]. On a tie, choose the smaller value.

Example 1
Input: [10, 5, 10, 15, 10, 5]
Output: [10, 15]
Explanation: 10 appears 3 times (most); 15 once (least).
Example 2
Input: [2, 2, 1, 1]
Output: [1, 1]
Explanation: Tie: both values appear twice. The question says to choose the smaller value.
Stuck? Open one hint at a time
Hint 1
Count first. Then scan the map and update the best values. Use a tie rule for equal counts.
Show the solution with explanations
Approach 1Map, then one scan with a tie rule
The idea

Count first, then scan once. When two counts are equal, prefer the smaller value.

JavaScript
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 ]
How it works

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.

Which approach should you use?

Whenever a problem says “most frequent”, ask the clarifying question from Lesson 11: what should happen on a tie?

Q6Sort characters by frequencyMedium

Rearrange the characters of s so that characters that appear more often come first. Any order is accepted among characters with equal counts.

Example 1
Input: "tree"
Output: "eetr"
Explanation: e appears twice, then t and r once each. "eert" is also accepted.
Example 2
Input: "Aabb"
Output: "bbAa"
Explanation: Upper and lower case are different characters.
Stuck? Open one hint at a time
Hint 1
Count each character with a Map.
Hint 2
Sort the different characters by their count, largest first. Then write each character as many times as its count.
Hint 3
Faster idea: put the characters into buckets by count (a count is at most n).
Show all 2 solutions with explanations
Approach 1Count, then sort the entries
The idea

Sort the [char, count] pairs by count, largest first (descending), and build the string.

JavaScript
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")); // bbAa
How it works

O(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).

Approach 2Buckets by count
The idea

Make an array where buckets[c] is the list of characters that appear c times. Read it from the highest count down to 1.

JavaScript
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")); // bbAa
How it works

O(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”.

Which approach should you use?

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

Q7Design a hash mapEasy

Build a hash map for whole-number keys without using Map or objects as dictionaries. Support put, get (−1 if missing) and remove.

Example 1
Input: put(1, 1), put(2, 2), get(1), get(3), put(2, 1), get(2), remove(2), get(2)
Output: 1, -1, 1, -1
Explanation: get returns the stored value or -1 when the key is missing. put on an existing key replaces its value.
Stuck? Open one hint at a time
Hint 1
Use an array of buckets and a hash function such as key % size (the remainder of the key divided by the number of buckets).
Hint 2
Each bucket is a small array of [key, value] pairs. This is called chaining.
Show the solution with explanations
Approach 1Buckets with chaining
The idea

Use the hash table from the lesson, for whole-number keys, and add remove.

JavaScript
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)); // -1
How it works

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

Which approach should you use?

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

Make it stick — close this page, then
  1. Explain why answering q frequency questions costs O(n × q) without pre-computation and O(n + q) with it.
  2. Write the 26-letter counting loop from memory.
  3. Describe what a hash function, a bucket and a collision are, in one sentence each.
  4. Say why map.get([1, 2]) returns undefined even after map.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