Hash Tables Visualized
A visual guide to how hash tables really work: hashing, collisions, probing, resizing, and why performance can collapse if you ignore the details.
Hash tables are one of the most important data structures in software engineering. They power dictionaries, maps, caches, deduplication, indexing, and fast lookups across most programming languages.
At a high level, a hash table stores key-value pairs and uses a hash function to convert a key into an array index. If the table is sized well and the hash function distributes keys evenly, average operations are close to .
But the real story is collisions. Two different keys can map to the same index. How you handle that decides whether your hash table stays fast or turns into something much closer to .
Why Hash Tables Matter
If you store 1,000,000 items, a balanced tree lookup might cost around comparisons. A well-behaved hash table often feels constant time. But a hash table with poor hashing or a high load factor can degrade sharply and cause unexpected latency spikes in production.
Hash tables are often used in performance-critical code paths: API request caching, session stores, symbol tables, counting frequencies, de-duplication, and data pipelines. This is why understanding resizing, load factor, and collision strategies matters just as much as memorizing the definition.
Core Concepts
- Key: the identifier you want to look up (string, number, object).
- Hash function: converts a key into an integer hash value.
- Bucket/index: the array slot where the item should be stored.
- Collision: two different keys map to the same bucket.
- Load factor (α): size / capacity. Higher α usually means more collisions.
- Resizing (rehashing): when capacity grows, items must be redistributed.
Collision Handling Strategies
All hash tables must handle collisions. Below are the two most common strategies and how they behave.
Separate Chaining
Separate chaining stores multiple entries in the same bucket, usually via a linked list or dynamic array. The array index points to a small container of entries that share the bucket.
How it works:
- Compute index = hash(key) % capacity
- Go to that bucket
- Search the bucket for the key
- Insert or update within the bucket
Separate chaining is robust and simple, but memory overhead can be higher. Many real implementations use arrays of small vectors for better cache locality.
Open Addressing
Open addressing stores all entries directly in the table array. If a collision happens, the table searches for another empty slot using a probing strategy.
How it works:
- Compute start index
- If occupied, probe next index based on a probe rule
- Continue until key is found or an empty slot appears
Open addressing can be very fast due to cache locality, but performance drops quickly when the load factor gets too high.
Probing Strategies (Open Addressing)
| Strategy | Probe Sequence | Pros | Cons |
|---|---|---|---|
| Linear Probing | i, i+1, i+2, ... | Simple, fast | Primary clustering |
| Quadratic Probing | i+12, i+22, ... | Reduces clustering | Can fail to visit all slots |
| Double Hashing | i + k × hash2(key) | Best distribution | Needs second hash function |
Load Factor and Resizing
As a hash table fills up, collisions become more frequent. Most implementations resize when the load factor crosses a threshold (commonly around 0.7 to 0.8 for open addressing).
When resizing happens, the table allocates a larger array and reinserts items. This is called rehashing. It can be expensive, but amortized over many inserts it stays efficient.
Resizing is not just copying memory. Every entry must be re-indexed because hash(key) % capacity changes when capacity changes.
Strategy Comparison
| Strategy | Collision Handling | Average | Worst | Memory | Notes |
|---|---|---|---|---|---|
| Separate Chaining | bucket list/vector | higher | stable performance under higher load | ||
| Linear Probing | next slot | lower | fastest but clustering risk | ||
| Quadratic Probing | squared step | lower | less clustering, careful sizing | ||
| Double Hashing | second hash step | lower | best distribution, more compute |
Code Examples
Hash Table with Linear Probing
#include <string>
#include <vector>
#include <optional>
#include <functional>
class HashTable {
struct Entry {
std::string key;
int value;
bool occupied = false;
bool deleted = false;
};
std::vector<Entry> table;
size_t count = 0;
size_t indexFor(const std::string& key) const {
return std::hash<std::string>{}(key) % table.size();
}
void rehash() {
std::vector<Entry> old = table;
table.assign(table.size() * 2, Entry{});
count = 0;
for (const auto& e : old) {
if (e.occupied && !e.deleted) put(e.key, e.value);
}
}
public:
HashTable(size_t cap = 16) : table(cap) {}
void put(const std::string& key, int value) {
if ((double)count / table.size() > 0.7) rehash();
size_t i = indexFor(key);
while (table[i].occupied && !table[i].deleted && table[i].key != key) {
i = (i + 1) % table.size();
}
if (!table[i].occupied || table[i].deleted) count++;
table[i] = {key, value, true, false};
}
std::optional<int> get(const std::string& key) const {
size_t i = indexFor(key);
size_t start = i;
while (table[i].occupied) {
if (!table[i].deleted && table[i].key == key) {
return table[i].value;
}
i = (i + 1) % table.size();
if (i == start) break;
}
return std::nullopt;
}
};The C++ example shows a simple hash table with linear probing, tombstones for deletions, and automatic resizing at 0.7 load factor. Real-world standard libraries add more optimizations.
Common Pitfalls in Real Systems
- Bad hash functions cause clustering: If many keys hash to similar values, performance degrades to .
- Too high load factor causes latency spikes: Always resize before the table gets too full.
- Using mutable objects as keys breaks lookups: If the key changes after insertion, it's effectively lost.
- Hash DoS attacks: Attacker-controlled keys can force collisions. Modern languages use randomized hashing.
Try It Yourself
Use the interactive visualizer below to see how different collision strategies work. Insert keys, watch collisions happen, and observe how probing finds empty slots!
Interactive Visualizer
Related Tutorials
What Big O actually measures, how each growth class behaves as input grows, and when complexity decides your architecture versus when it is the wrong thing to optimize.
A step-by-step guide to binary search, including the core invariant, common implementation bugs, and why halving the search interval changes performance completely.
How the organization of linear data makes indexing, insertion, removal, and traversal cheap or expensive.