Engineering Notes
Home/Languages/JavaScript/Hashing and Hash Tables
Languages · JavaScript

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.

💻 Languages● Intermediate⏱ 7 min read🛠️ 4 exercises🎤 5 interview Q&As

🎯 What you'll learn

  • What a Hash Function Does
  • What a Hash Table Is
  • Hash Collisions
  • Collision Resolution Strategies
  • Load Factor and Resizing
Part 1The Concept

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.

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

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

text
hash("Alice") % 10 = 3
hash("Bob")   % 10 = 3   <- collision, both land in bucket 3

4Collision Resolution Strategies

Alice and Bob both hash to bucket 3; separate chaining stores both as a short list in that one bucket
Alice and Bob both hash to bucket 3; separate chaining stores both as a short list in that one bucket
StrategyHow it works
Separate chainingeach 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
text
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 key

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

Part 2Building One

1A Minimal Hash Table with Separate Chaining

js
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

js
// 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 it

With 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

OperationAverage caseWorst case
get/set/deleteO(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

js
_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

  1. 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 _hash above does).
  2. Forgetting to update, not duplicate, an existing key — set must check for an existing entry in the bucket first, or you'll get duplicate [key, value] pairs for the same key.
  3. Never resizing — a fixed-size table degrades to O(n) performance as more entries are added and load factor climbs.
  4. 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

text
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

Questions interviewers ask about this topic. Answer out loud first, then open a card to compare.

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.

Follow-ups interviewers ask
  • 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.

Follow-ups interviewers ask
  • 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.

Follow-ups interviewers ask
  • 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.

Follow-ups interviewers ask
  • 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).

Follow-ups interviewers ask
  • 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.