Hashing and Hash Tables
A hash function takes an input of arbitrary size (a string, an object, anything) and deterministically produces a fixed-size output — usually an integer — called a hash code. The same input always produces the same hash code.
🎯 What you'll learn
- What a Hash Function Does
- What a Hash Table Is
- Hash Collisions
- Collision Resolution Strategies
- Load Factor and Resizing
1What a Hash Function Does
A hash function takes an input of arbitrary size (a string, an object, anything) and deterministically produces a fixed-size output — usually an integer — called a hash code. The same input always produces the same hash code.
function simpleHash(str) {
let hash = 0;
for (let i = 0; i < str.length; i++) {
hash = (hash * 31 + str.charCodeAt(i)) | 0; // |0 keeps it a 32-bit integer
}
return hash;
}
simpleHash("hello"); // always the same number for "hello"
simpleHash("world"); // a different number (almost always)A good hash function is fast, deterministic, and spreads outputs evenly across the possible output range, minimizing how often two different inputs produce the same output.
2What a Hash Table Is
A hash table is a data structure that uses a hash function to map keys to array indices (called "buckets"), giving average O(1) lookup, insertion, and deletion — dramatically faster than scanning a list (O(n)) for large datasets.
key "name" -> hash("name") -> 47 -> bucket[47 % tableSize] -> stores { key: "name", value: "Ana" }JavaScript's Object, Map, and Set are all implemented internally using hash tables — this is why property/key lookup on them is fast regardless of how many entries exist.
3Hash Collisions
Two different inputs can produce the same hash code (or map to the same bucket after modulo) — this is called a collision, and it's mathematically unavoidable once you have more possible inputs than output slots (the pigeonhole principle).
hash("Alice") % 10 = 3
hash("Bob") % 10 = 3 <- collision, both land in bucket 34Collision Resolution Strategies
| Strategy | How it works |
|---|---|
| Separate chaining | each bucket holds a list (or small array) of all entries that hashed there; on collision, just append |
| Open addressing (linear probing) | on collision, keep checking the next slot (+1, +1, ...) until an empty one is found |
| Open addressing (double hashing) | use a second hash function to compute the probing step size, spreading collisions more evenly than linear probing |
Separate chaining, bucket 3: [ {key:"Alice",...}, {key:"Bob",...} ]
— lookup for "Bob" hashes to bucket 3, then linearly scans that
small list to find the matching keyReal-world hash table implementations (including JS engines' internal Map/Object implementations) use variations of these strategies, often switching approach based on how full the table is.
5Load Factor and Resizing
Load factor = number of entries ÷ number of buckets. As load factor rises, collisions become more frequent and performance degrades toward O(n). Hash tables automatically resize (allocate a bigger underlying array and rehash every entry) once load factor crosses a threshold (commonly ~0.75), to keep operations close to O(1) on average.
1A Minimal Hash Table with Separate Chaining
class HashTable {
constructor(size = 16) {
this.buckets = new Array(size).fill(null).map(() => []);
this.size = size;
this.count = 0;
}
_hash(key) {
let hash = 0;
const str = String(key);
for (let i = 0; i < str.length; i++) {
hash = (hash * 31 + str.charCodeAt(i)) % this.size;
}
return hash;
}
set(key, value) {
const index = this._hash(key);
const bucket = this.buckets[index];
const existing = bucket.find(entry => entry[0] === key);
if (existing) {
existing[1] = value; // update
} else {
bucket.push([key, value]);
this.count++;
}
}
get(key) {
const bucket = this.buckets[this._hash(key)];
const entry = bucket.find(entry => entry[0] === key);
return entry ? entry[1] : undefined;
}
has(key) {
return this.buckets[this._hash(key)].some(entry => entry[0] === key);
}
delete(key) {
const bucket = this.buckets[this._hash(key)];
const index = bucket.findIndex(entry => entry[0] === key);
if (index === -1) return false;
bucket.splice(index, 1);
this.count--;
return true;
}
}2Why the Modulo Matters
hash % this.size compresses a potentially huge hash number down into a valid array index (0 to size - 1). This is the step that actually creates collisions in practice — even a perfect, evenly-distributed hash function will produce colliding bucket indices once you have more keys than buckets.
3Why This Beats a Plain Array of [key, value] Pairs
// Naive linear search — O(n) every lookup
function linearGet(pairs, key) {
for (const [k, v] of pairs) if (k === key) return v;
return undefined;
}
// Hash table — O(1) average: jump straight to the right bucket,
// then scan only the (typically very short) list of collisions within itWith 1,000,000 entries, a linear search checks up to 1,000,000 pairs in the worst case. A well-sized hash table checks roughly 1,000,000 / bucketCount entries — a handful, not a million.
4Worst-Case vs Average-Case
| Operation | Average case | Worst case |
|---|---|---|
get/set/delete | O(1) | O(n) — if every key collides into the same bucket |
The worst case happens with a poor hash function (everything collides) or a deliberately crafted "hash flooding" attack against a predictable hash function — one reason production hash table implementations use randomized hash seeds.
5Resizing (Rehashing) Sketch
_resize(newSize) {
const oldEntries = this.buckets.flat();
this.size = newSize;
this.buckets = new Array(newSize).fill(null).map(() => []);
this.count = 0;
for (const [key, value] of oldEntries) this.set(key, value); // re-hash every entry into the new size
}Every entry must be rehashed because hash % size depends on size — changing the table size invalidates every existing bucket assignment.
Under the HoodWhat Real Engines Do Differently From This Sketch▾
The HashTable class built above teaches the correct mental model, but production engines refine it in a few concrete ways worth knowing.
String hash codes are computed once and cached
Recomputing a string's hash on every lookup would waste work for strings used repeatedly as keys (property names, Map/Set keys). V8 computes a string's hash lazily on first use, then caches it directly inside the string object's own header — a spare bit-field reserved for exactly this. Every subsequent hash of that same string instance is a cheap field read, not a recomputation character-by-character. This is part of why using the same repeated string literal as a key across many lookups is cheaper than it looks.
Randomized seeds defeat "hash flooding" attacks
Section 4 mentions hash-flooding: an attacker who knows your exact hash function can craft many different inputs that all collide into the same bucket, degrading every lookup to O(n) and creating a denial-of-service vector (this is a documented, real category of vulnerability that affected multiple language runtimes historically). Production hash table implementations mix in a random seed, generated once per process, into the hash computation — so the mapping from input to bucket differs between runs and can't be predicted by an external attacker who only knows the hash algorithm itself, not the runtime's private seed.
Real hash tables in engines are exactly this + the OrderedHashTable layer
The Map and Set topics' "Under the Hood" sections describe V8's actual OrderedHashMap/OrderedHashSet — which is precisely this chapter's chaining-based hash table, with one additional layer bolted on: a separate insertion-ordered entries array, so that iteration order matches insertion order (something a raw hash table, as built in this topic, does not guarantee at all — see the last Common Pitfall below).
⚠️ Common Pitfalls
- Using a hash function that only depends on length or a single character — produces massive collision rates; always fold in every character's contribution (as
_hashabove does). - Forgetting to update, not duplicate, an existing key —
setmust check for an existing entry in the bucket first, or you'll get duplicate[key, value]pairs for the same key. - Never resizing — a fixed-size table degrades to O(n) performance as more entries are added and load factor climbs.
- Assuming hash tables preserve insertion order — a raw hash table (unlike JS's actual
Map) generally does not;Map's insertion-order guarantee is an additional design choice on top of hashing, not an inherent property of hash tables.
📋 Quick Reference
hash(key) -> number -> index = hash % bucketCount
collision -> chaining (list per bucket) or open addressing (probe next slot)
load factor = entries / buckets -> resize when too high
average case O(1), worst case O(n)🎤 Interview Q&A
Q1How does a hash table work?core▾
A hash function maps keys to bucket indexes in an array; lookups compute the hash and check that bucket. Collisions are handled by chaining (lists per bucket) or open addressing (probing). When the load factor is high, the table resizes and rehashes.
- Average vs worst-case lookup? O(1) average, O(n) worst case with many collisions.
Q2What makes a good hash function?core▾
Deterministic, fast, distributing keys uniformly, and (for adversarial inputs) resistant to collision attacks. Language runtimes use randomized hashing to prevent hash-flooding DoS.
- What's hash flooding? Crafting many colliding keys to degrade a server's hash tables.
Q3Which JavaScript structures are hash tables?senior▾
Map and Set (hash-based, insertion-ordered), and objects in dictionary mode. Engines also use hidden classes for objects with fixed shapes.
- When does an object become dictionary-mode? Many dynamic additions/deletions.
Q4What interview problems use hash maps?core▾
Two Sum, group anagrams, frequency counting, first non-repeating character, subarray sum equals K with prefix sums, detecting duplicates, LRU caches.
- Typical complexity improvement? O(n²) nested loops to O(n) with a map.
Q5Hashing for data structures vs cryptographic hashing?core▾
Data-structure hashes prioritize speed and distribution; cryptographic hashes (SHA-256) prioritize collision and preimage resistance; password hashes are deliberately slow (Argon2, bcrypt).
- Why not SHA-256 for hash tables? Too slow for the purpose.
🛠️ Practice exercises
Each exercise is a file in this topic folder. Code exercises are self-checking: run them and make the checks pass.