Hash Functions
How do you turn "a name" into "a shelf number" so you always know where to look? That mapping is a hash function.
The problem
You're building a coat check at a busy theater. A thousand guests arrive, hand you a coat, and later come back for it. If you just pile the coats up, finding one guest's coat means digging through the whole heap — a fresh search every single time.
So you buy a wall of numbered hooks. Now the real question is the hard one: when a guest
named ria_codes hands you a coat, which hook do you hang it on? You need a rule that
turns a name into a hook number — and gives you back the same number when she returns.
A first attempt
The easy rule: keep a running list, and give each new guest the next free hook. ria_codes
gets hook 0, fahim gets hook 1, and so on.
But now retrieval is broken again. When ria_codes comes back, which hook was hers? You
don't know — so you scan the list to find her name, then read off the hook. That scan is
O(n), exactly the pile-of-coats problem you were trying to escape. Assigning numbers in
arrival order tells you nothing about where to look later.
The fix has to come from the name itself. The number must be computed from the key, not handed out in sequence.
The insight
What if the name is the address? Take the characters of ria_codes, mash them into a
single number with arithmetic, then fold that number down to a valid hook. Same name in,
same number out — every time, without any list to consult.
That rule is a hash function: a deterministic function that maps a key of any size to a fixed-size number, its hash code. Reduce that code modulo the number of hooks and you get a slot. The name computes its own address.
Determinism is the whole game
A hash function must return the same output for the same input, forever. If hash("ria")
could drift between calls, you'd store a coat on one hook and look for it on another. Given
that, everything else is about spreading keys evenly so few of them collide.
How it works
Start from a seed
Begin an accumulator at some non-zero value. It will absorb every character of the key.
Mix in each character
Walk the string. For each character, multiply the accumulator by a small prime (a common
choice is 31) and add the character's code. Multiplying before adding makes position
matter — so "abc" and "cba" land on different codes.
Keep it bounded
The running number can grow huge, so wrap it into a fixed integer range as you go. This is the raw hash code.
Fold to a slot
Take the hash code modulo the table size to get an index in range. A prime table size and a good mix keep those indices spread out.
Here's the mixing, character by character, for a tiny 8-hook wall:
key = "cat", acc starts at 7
'c' (99): acc = 7*31 + 99 = 316
'a' (97): acc = 316*31 + 97 = 9893
't'(116): acc = 9893*31 +116 = 306799
slot = 306799 % 8 = 7 ← "cat" hangs on hook 7Change one letter and the whole number swerves — that avalanche is what scatters keys across the hooks instead of clumping them.
The code
A classic string hash: fold each character into an accumulator, then reduce to a slot.
def hash_code(key: str) -> int:
h = 7
for ch in key:
h = (h * 31 + ord(ch)) & 0xFFFFFFFF # keep it 32-bit
return h
def slot(key: str, table_size: int) -> int:
return hash_code(key) % table_sizefunction hashCode(key: string): number {
let h = 7;
for (const ch of key) {
h = (Math.imul(h, 31) + ch.charCodeAt(0)) | 0; // 32-bit wrap
}
return h >>> 0; // treat as unsigned
}
function slot(key: string, tableSize: number): number {
return hashCode(key) % tableSize;
}int hashCode(String key) {
int h = 7;
for (int i = 0; i < key.length(); i++) {
h = h * 31 + key.charAt(i); // int overflow wraps naturally
}
return h;
}
int slot(String key, int tableSize) {
return Math.floorMod(hashCode(key), tableSize);
}#include <stddef.h>
unsigned int hash_code(const char *key) {
unsigned int h = 7;
for (const char *p = key; *p; p++) {
h = h * 31u + (unsigned char)*p; // unsigned wraps by spec
}
return h;
}
size_t slot(const char *key, size_t table_size) {
return hash_code(key) % table_size;
}#include <string>
#include <cstddef>
unsigned int hash_code(const std::string &key) {
unsigned int h = 7;
for (unsigned char ch : key) {
h = h * 31u + ch; // unsigned wraps by spec
}
return h;
}
std::size_t slot(const std::string &key, std::size_t table_size) {
return hash_code(key) % table_size;
}Computing the code touches every character once, so hashing a key of length k is O(k).
For short keys we treat that as effectively constant — which is why a hash-table lookup is
called O(1).
Complexity
| Operation | Time | Space | Note |
|---|---|---|---|
Hash a key length k | O(k) | O(1) | one pass over the characters |
| Fold to a slot | O(1) | O(1) | a single modulo |
A good hash isn't just fast
Speed is only half the job. A hash function that returns 0 for everything is instant and
useless — every key collides. What you actually want is uniform spread: keys scattered
evenly so each hook holds about the same number of coats. A weak hash silently turns your
O(1) table into an O(n) list.
When to use it
Reach for it — and when not to
You need a hash function any time you build or use a hash table, deduplicate items, or shard data across buckets. Use your language's built-in hash for everyday keys; it's tuned and tested. Write your own only for learning, or for custom key types. And never use a plain hash like this one for security — passwords and signatures need cryptographic hashes, which are a different tool with different guarantees.
Practice
Recap
- A hash function deterministically turns a key into a fixed-size number, which you fold modulo the table size into a slot — the key computes its own address.
- Good hashes are fast and spread keys uniformly; multiplying by a prime makes position matter so anagrams don't collide.
- Collisions are mathematically inevitable; the hash's job is to make them rare, and the table's job is to resolve them.
How is this guide?
Last updated on