
Part 5 ended with the engine being "naive" — it still scans the whole table every time. The classic case that stings the most: "I know the exact id; give me that row."
In real life you'd write SELECT * FROM books WHERE id = 42. Our current engine does it by scanning from the top:
function findById(rows, targetId) {
for (const row of rows) {
if (row.id === targetId) return row;
}
return null;
}100k rows, and it may check 100k of them. It's as if you knew the book's exact shelf number, yet you swept every shelf anyway. That's absurd — and a hash table is exactly "mapping a key straight to a location."
Recall: an array is O(1) if you know the index — you compute the address and jump. So a hash table's trick is: turn an arbitrary key (id, string) into an array index.
That conversion is the hash function. Two steps:
// Step A: hash the key into a number
function hash(key) {
if (typeof key === "number") return key;
// for strings, fold chars into a number (simplified on purpose)
let h = 0;
for (const ch of String(key)) h = (h * 31 + ch.charCodeAt(0)) | 0;
return h;
}
// Step B: compress into the array's range by modulo
function indexFor(key, capacity) {
return hash(key) % capacity; // 0 .. capacity-1
}So hash("apple") gives a big number; % capacity gives a slot. Now "find id = 42" is:
hash(42) // 42
42 % capacity // some slot, e.g. 2
slots[2] // jump straight thereThree math operations, no sweeping. That's where O(1) comes from: you didn't "find" — you computed.
Here's the catch: two different keys can land on the same slot.
hash("apple") % 8 === 1
hash("banana") % 8 === 1 // collide: both want slot 1If you ignored it, you'd overwrite. The textbook fix is chaining: each slot holds a list (a bucket), and colliding entries are strung on that bucket:
// a minimal hash table with chaining
class HashTable {
constructor(capacity = 8) {
this.capacity = capacity;
this.buckets = new Array(capacity).fill(null).map(() => []);
this.size = 0;
}
// insert (key, value)
set(key, value) {
const idx = indexFor(key, this.capacity);
const bucket = this.buckets[idx];
for (const entry of bucket) {
if (entry.key === key) { // key already there → update
entry.value = value;
return;
}
}
bucket.push({ key, value });
this.size++;
if (this.size / this.capacity > 0.7) this.resize(); // see Step 3
}
// find by key
get(key) {
const idx = indexFor(key, this.capacity);
for (const entry of this.buckets[idx]) {
if (entry.key === key) return entry.value;
}
return null;
}
}Now findById becomes:
const byId = new HashTable();
books.forEach((b, i) => byId.set(b.id, i)); // build index once
// "SELECT * FROM books WHERE id = 42"
const found = books[byId.get(42)];Go from "sweep 100k" to "hop straight." That's the whole point of an index.
Is it now really O(1)? Only on average, and only if the hash spreads keys evenly. Two knobs control that:
Load factor — size / capacity. The fuller the buckets, the longer the chains, the closer to O(n). We keep it under ~0.7.
Resize — when the load factor passes the threshold, double the array and rehash everything into the new one. That single resize is O(n), but it happens rarely, so amortized over all operations it stays O(1) on average.
resize() {
const old = this.buckets;
this.capacity *= 2;
this.buckets = new Array(this.capacity).fill(null).map(() => []);
this.size = 0;
for (const bucket of old) {
for (const entry of bucket) this.set(entry.key, entry.value);
}
}So the truthful statement: a hash table is average O(1) — given a good hash function and a table that grows.
You've built something genuinely fast — for exact equality. Now try the query from Part 4:
// "SELECT * FROM books WHERE price < 50"With a hash table, the only thing you can ask is "give me price === 50." You cannot ask "all prices below 50," because the hash table never ordered the keys.
Think about why: the hash function scrambles the key into a pseudo-random slot. 10 and 11 and 12 — their slots have nothing to do with each other. There's no "a region holds all small values."
Here's the moment to remember: a hash table wins at "exactly this key," and loses at "any range." Real databases hit this same wall — it's why nearly all primary indexes aren't hash but a sorted tree. And that's exactly where Part 7 turns.
HashTable, index 100k books by id, then time byId.get(50000) vs the loop findById. Watch the gap widen as rows grow."apple" and "banana" (make a small capacity), print the bucket chain, and see them shared as intended.resize() fires and how the total grew.Keywords: hash table、hash function、modulo、collision、chaining、load factor、resize、average O(1)
You've felt both sides: the naive loop is slow, and the hash table is fast but only for equality. So the question becomes, almost by itself: is there a structure that finds "a specific key" fast and can sweep a range cheaply? A structure that keeps data sorted.
That's Part 7 — a sorted array with binary search. You'll see ranges work, but hit a new pain: insertion must shift. That pain is what will drive you toward a tree.