Lesson 1 of 6
How Hashing Works
"There are only two hard things in Computer Science: cache invalidation and naming things." — Phil Karlton
There is a third hard thing hiding behind the joke, and you use it a hundred times a day without noticing: turning a name into a place. When you write user["email"] or seen.has(x), something has to translate that key — a string, a number, an object — into an address it can jump to directly. Do it well and lookups are instant no matter how much data you have. Do it badly and your "fast" map quietly degrades into a linear scan. That machinery is hashing, and it is the single most useful idea you can carry into a coding interview: a startling number of "make this faster" problems are solved by the same move — remember it in a hash map instead of searching for it again.
How can a computer find the value for a key — out of a million keys — in roughly one step, without ever scanning the list? And what is the catch that turns that one step back into a million?
Core Concepts
Start with the problem. You have a pile of key–value pairs and you want to look one up by key. In a plain array, finding a key means checking entries one by one — O(n). Keep the array sorted and binary search gets you to O(log n), but now every insert has to shuffle elements to stay sorted. Both scale with n. We want something that does not care how big n is: given a key, go straight to its value.
The trick is to make the key compute its own address. If keys were small integers — say, always 0 to 9 — this is trivial: use an array of size 10 and store the value for key k at index k. Reading it back is arr[k], one step, O(1). Hashing is what makes that trick work for any key: strings, big numbers, objects.
The mechanism: key → integer → bucket
Two stages turn an arbitrary key into an array index:
- A hash function maps the key to an integer — the hash code. This is a deterministic scramble: same key in, same number out, every time.
- That integer is squeezed into the range of a fixed-size array (the bucket array) with modulo:
index = hashCode % numberOfBuckets.
Store the pair at that index; to read it back, recompute the index the same way and look. Here is a tiny string hash so you can watch keys land in buckets:
Five names, five different buckets, computed directly from the letters — no searching. The multiplier 31 mixes the characters so that "Ada" and "Ana" land far apart despite sharing letters; that mixing is the whole art of a hash function.
A giant coat-check room with numbered racks. Instead of writing down where each coat went and searching the log later, the attendant computes the rack from the coat itself — "wool, size L, blue → rack 7." Every wool-L-blue coat goes to rack 7, and to retrieve yours she recomputes "rack 7" and walks straight there. She never scans the room. The rule that turns coat features into a rack number is the hash function; the racks are the buckets.
What makes a hash function good
Three properties, and each one is load-bearing:
- Deterministic — the same key must always produce the same code, or you could never find what you stored.
- Uniform — codes should spread evenly across the buckets. This is the property people underestimate. If a hash sends most keys to the same few buckets (clustering), those buckets grow long and lookups slow down — you have quietly rebuilt the
O(n)scan you were trying to escape. - Fast — hashing happens on every single operation. A brilliant distribution that takes a millisecond to compute is worthless for a hash map (cryptographic hashes like SHA-256 are uniform but deliberately slow, so they are the wrong tool here).
Uniformity is why we multiply by a prime like 31 and why table sizes are chosen with care. A lazy hash — say, "use the first letter" — clusters every "S" name into one bucket. Distribution, not cleverness, is the goal.
Hashing was born from a memory shortage. In 1953, Hans Peter Luhn at IBM, wrestling with how to look things up quickly without enough storage to keep everything sorted, sketched the idea of "chaining" keys in buckets in an internal memo. Around the same time Arnold Dumey described using modulo of a number as an address. The technique had no agreed name until Robert Morris's 1968 survey popularized "hashing" — from the everyday sense of *to hash*, to chop and mix. Seventy years on, that chop-and-mix underlies every dictionary, database index, and language runtime you touch.
Collisions are not a bug — they are a certainty
Squeeze many possible keys into a fixed number of buckets and, sooner or later, two different keys produce the same index. This is the pigeonhole principle: if you have more pigeons than holes, some hole holds two pigeons. There are billions of possible strings and only a handful of buckets in our example above — collisions are guaranteed, not unlucky. A hash map is not defined by avoiding collisions; it is defined by handling them gracefully. Two strategies dominate:
Separate chaining — each bucket holds a small list. On collision, you append to that bucket's list; on lookup, you hash to the bucket and scan its (short) list. Simple, forgiving, and the model most people picture. Let us build a minimal one and watch it work:
Notice get still scans a bucket — but a bucket, not the whole map. As long as buckets stay short, that scan is effectively constant.
Open addressing — keep one flat array and, on collision, probe for the next free slot by a fixed rule. Linear probing tries index+1, index+2, …; quadratic probing tries index+1, index+4, index+9, … to avoid long runs clumping together; double hashing uses a second hash function to decide the step size. Lookup replays the same probe sequence until it finds the key or an empty slot. Open addressing packs everything into one contiguous array — friendlier to the CPU cache and memory — but it is fussier: deletions need "tombstone" markers, and it degrades sharply as the table fills.
What this looks like in real engineering, not just theory:
• You almost never write the hash function. Your language ships a good one. But you do pick keys — and keys must be hashable and stable, which is why immutable strings and numbers are ideal keys and mutable objects are traps.
• "Can I remember this instead of re-finding it?" is the reflex hashing gives you — the move that collapses an O(n²) nested scan into O(n).
• Real runtimes mix strategies. Java's HashMap uses chaining but upgrades a bucket's list to a balanced tree once it grows long, capping the worst case at O(log n). Production hashing is chaining plus a dozen pragmatic patches.
Load factor and resizing — how O(1) is kept honest
The load factor is n / numberOfBuckets — average items per bucket. As you insert more keys into a fixed number of buckets, chains lengthen and every operation creeps toward O(n). The fix is to grow the table: when the load factor crosses a threshold (commonly 0.75), allocate a larger bucket array — usually double — and rehash every existing key into it, because the bucket index depends on numberOfBuckets and that number just changed.
Rehashing is an O(n) event. But it happens rarely — each resize doubles capacity, so the cost is spread thin across all the cheap inserts between resizes. This is amortized analysis (the same reasoning that made dynamic-array append O(1) amortized): occasional expensive operations, averaged over the many cheap ones, give an O(1) amortized cost per insert. Keep the load factor bounded and hashing stays fast; let it climb and it does not.
When the Academy renders a lesson page, it needs each lesson's completion status for the current student. The naive path is, for every lesson, scan the student's progress rows — an O(lessons × rows) nested loop. Instead the code loads the progress rows once and builds a plain JavaScript object keyed by lesson id — a hash map — then each lesson does an O(1) lookup by id. The page goes from quadratic to linear with one {}. The database's primary-key and unique indexes are the same idea one layer down: hash-backed (or tree-backed) structures so a row lookup never scans the table.
The honest complexity: average O(1), worst case O(n)
Say it plainly, because interviewers listen for exactly this nuance: hash-map get, put, and delete are O(1) on average and O(n) in the worst case. The worst case is when every key collides into one bucket — then the structure is just a list wearing a map's clothing, and every operation scans it. In practice a good hash function plus a bounded load factor makes that case astronomically unlikely, which is why we rely on O(1) daily. But "unlikely" is not "impossible": adversaries have crashed real servers by deliberately sending keys that all collide (a hash-flooding denial-of-service attack), which is why serious runtimes now randomize their hash seed. The average case is the promise; the worst case is the fine print — and knowing both is the difference between reciting "hash maps are O(1)" and actually understanding them.
Common Mistakes
- Claiming hash maps are "always O(1)." They are
O(1)average,O(n)worst case. In an interview, say both — the qualifier is the point being tested. - Forgetting to re-index on modulo change. The bucket is
hashCode % numBuckets. After resizing you must rehash every key, because the index depended on the old bucket count. Skip it and lookups silently miss. - Using mutable objects as keys. If a key's contents (and thus its hash) change after insertion, you can never find it again — it now hashes to a different bucket than where it lives. Keys must be effectively immutable.
- Ignoring the load factor. A map that never resizes turns into a linked list as it fills; a map that resizes too eagerly wastes memory. The threshold (~0.75) is a deliberate time-vs-space trade.
- Assuming iteration order. A hash map's storage order follows bucket indices, not insertion — treat ordering as unspecified unless the structure explicitly guarantees it (some, like JavaScript's
Map, do; most classic hash tables do not). - Reinventing the hash function. "First character" or "string length" cluster horribly. Use the language's built-in keying (or a proven polynomial hash); do not hand-roll a weak one.
You have 1,000 keys and a bucket array of size 1. What is the load factor, and what is a lookup? The load factor is 1000 / 1 = 1000; every key lands in the one bucket, so lookup scans a 1,000-element list — pure O(n). Now grow to 1,000 buckets with a good hash: load factor 1.0, roughly one key per bucket, lookup O(1). Same keys, same hash mechanism — only the bucket count changed. That single number, tuned by resizing, is what stands between constant time and linear time. Hashing is not magic; it is a load factor you keep small on purpose.
Interview Corner
O(1)?"n / buckets; when it crosses about 0.75 you double the array and rehash everything so chains stay short. Rehashing is O(n) but rare, so inserts are O(1) amortized."hash % buckets; changing the bucket count changes every key's index, so all keys must be re-placed.O(log n). Three: resize pauses — a big rehash on the request path shows up as latency; incremental or background resizing smooths it. The theme is that O(1) is an average built on assumptions, and production means defending those assumptions."O(n²) scan into O(n), and write it cleanly under time pressure.- In the editor above, add a
delete(key)method to the chainingHashMapthat removes the pair from its bucket. Then confirmgetreturnsundefinedfor a deleted key. - Instrument the map: after inserting 20 keys into 8 buckets, log the length of each bucket. Compute the load factor and say out loud whether it is time to resize.
- Write a
resize()that doubles the bucket count and rehashes every existing pair. State its time complexity, and why it keeps per-insert costO(1)amortized. - Out loud (as in a micro1 screen), explain why a hash map is
O(1)average butO(n)worst case, and name one thing production systems do to defend the average.
Chapter Summary
- Hashing gives
O(1)average lookup, insert, and delete by key — the mechanism behind everyMap,Set, and object. - The pipeline is key → hash function → integer →
% buckets→ bucket index, letting you jump to a value instead of scanning for it. - A good hash function is deterministic, uniform, and fast; poor uniformity causes clustering, which drags operations back toward
O(n). - Collisions are inevitable (pigeonhole principle) and are resolved by separate chaining (a list per bucket) or open addressing (probe for a free slot: linear, quadratic, double hashing).
- Load factor (
n / buckets) governs speed; crossing the threshold triggers a resize + rehash, which isO(n)but rare — givingO(1)amortized inserts. - Be honest about complexity: hash operations are
O(1)average,O(n)worst case (all keys collide) — stating both is an interview signal.
You now own the machine — you can build a hash table from a bare array and defend its complexity out loud. But almost no working engineer implements one; they wield the ones the language ships. Next, in "Hash Maps & Sets in Practice," we put the theory to work with JavaScript's real Map, Set, and plain objects — when to reach for each, their gotchas, and the handful of interview patterns (frequency counts, deduplication, seen-sets, grouping) that this one data structure quietly solves.
Editorial reflection — it is tempting to treat hashing as a solved black box — and most days you should, because the runtime's version is better than the one you'd write. But the engineers who understand what is inside the box are the ones who reach for it at the right moment, choose keys that stay findable, and know why the "instant" lookup occasionally isn't. Learn the mechanism not to reimplement it, but so that when a profiler points at a slow map, you already know which of the machine's assumptions just broke.
Q&A · 0
Enrol to ask questions and join the discussion.
No questions yet — be the first to ask.