Lesson 61 of 80
Map and Object don't do magic — they hash a key to a bucket index and chain the collisions. Build one and the O(1) lookup stops feeling like a trick.
18 min read
Every option explains itself — why the right one is right, and why each wrong one is wrong. Miss one and it comes back later on a spaced-repetition schedule.
This topic is about judgment and practical tradeoffs more than a single right answer — a mock interview is the better way to test yourself on it.
Be interviewed on this →Hints without spoilers, and a plain explanation of why a wrong answer was wrong — answered only from this lesson. Free account, no card.
Sign up freeWhat interviewers actually ask about this, and what a strong answer has to cover.
A strong answer covers
Likely follow-ups
A hash map is a lookup table: give it a key, get a value back, in roughly constant time no matter how many entries it holds. The trick is turning an arbitrary key — a string, usually — into an array index. You run the key through a hash function that produces a number, then use that number (mod the array's length) as the slot to store the entry in.
Two different keys can hash to the same slot. That's a collision, and it's not a bug — it's guaranteed to happen once you have more possible keys than slots (pigeonhole principle). The whole design of a hash map is: pick a hash function that spreads keys out evenly, then have a plan for when two keys land in the same bucket anyway. The most common plan is chaining: each bucket holds a small list, and a collision just appends to that list instead of overwriting.
class HashMap {
#buckets;
#size = 0;
constructor(capacity = 16) {
this.#buckets = Array.from({ length: capacity }, () => []);
}
#hash(key) {
let hash = 0;
const str = String(key);
for (let i = 0; i < str.length; i++) {
hash = (hash * 31 + str.charCodeAt(i)) | 0; // keep it a 32-bit int
}
return Math.abs(hash) % this.#buckets.length;
}
set(key, value) {
const bucket = this.#buckets[this.#hash(key)];
const existing = bucket.find((entry) => entry[0] === key);
if (existing) {
existing[1] = value;
return;
}
bucket.push([key, value]);
this.#size++;
if (this.#size / this.#buckets.length > 0.75) this.#resize();
}
get(key) {
const bucket = this.#buckets[this.#hash(key)];
return bucket.find((entry) => entry[0] === key)?.[1];
}
#resize() {
const old = this.#buckets;
this.#buckets = Array.from({ length: old.length * 2 }, () => []);
this.#size = 0;
for (const bucket of old) {
for (const [k, v] of bucket) this.set(k, v);
}
}
}
#hash walks the string and folds each character code into a running total (the * 31 is the classic Java-string-hash trick — a prime multiplier that spreads output well for short strings). set and get both hash the key to find the right bucket, then do a plain linear scan of that bucket's array for a matching key — that scan is the "chain." As long as buckets stay small, that scan is effectively free, which is why average-case lookup is O(1). #resize is what keeps buckets small: once the map gets more than 75% full, it doubles the bucket count and re-hashes everything, because every bucket index depends on #buckets.length.
get would degrade to O(n) — a linear scan through everything. Good distribution and resizing are what keep that from happening in practice; they don't make it impossible in theory.Map and Object (for string keys) are implemented with exactly this idea in the engine — a hash table with some collision strategy — just written in C++ with far more tuning than a teaching example needs.Map in JS actually does preserve insertion order as a language guarantee, but that's a spec promise layered on top, not something hashing gives you for free — a naive hash map like the one above has no defined iteration order at all.Reaching into an object with obj[key] or a Map with map.get(key) is this same hash-then-chain lookup happening beneath the language surface — you're just usually not the one writing the hash function. Understanding the mechanism is what makes "why is this O(1)" stop being something you take on faith.