The Hash Table
A hash function turns a key into an array index, and the table jumps straight to it. A lookup therefore does not scan the stored data. The price is that two different keys can produce the same index, and handling that is most of what the rest of this chapter is about.
Intuition: compute the position instead of searching for it
A hash table is an array plus a rule that turns any key into an index.
To find a word in a paper dictionary you can start at page one and turn pages until you reach it. That is a linear search, O(n), and it gets slower as the dictionary gets thicker. Or you can use the index at the front: the spelling of the word tells you which page to open, and you go there directly. The size of the dictionary barely matters. The second method works because the word itself, put through a fixed rule, produces a position.
An array already has this ability. Give it an index and it reaches the element in O(1), because the address is computed from the index rather than searched for. But the index has to be a small non-negative integer. A hash table extends the same ability to any kind of key: a hash function turns the key — a string, an object, a pair of coordinates — into an integer, and that integer is used as the index into an array. Nothing is scanned. That is the whole idea. A hash table is not a new way to store data; it is an array, a rule for computing indexes, and a plan for when two keys compute the same one.
It turns any key into an integer inside a fixed range. The same key must always produce the same integer. If it does not, a value you stored can never be found again.
The place where the data actually lives: an ordinary array whose slots are called buckets. The output of the hash function is the bucket index, so the O(1) jump comes from the array underneath.
There are more possible keys than buckets, so two different keys landing in the same bucket is certain, not a bug. Separate chaining and open addressing are the two standard answers.
Where you already rely on hash tables
Python resolves a variable name in a namespace, and a namespace is a dict. A browser cache finds a file by its URL. A database index answers "which rows hold this value". Redis is close to a hash table with a network interface. Wherever a name leads directly to content, there is usually a hash table behind it. One case is related but different: Git names each commit by a cryptographic hash of its content. That is a different kind of hash function with a different goal — making it infeasible to find two contents with the same name — not making lookups fast.
Hash functions: turning a key into an index
One fixed mapping: any key goes in, an integer in a fixed range comes out.
A hash function takes a key and returns an integer. It could compute almost anything, but three requirements decide whether the table works at all. Each one exists because of what goes wrong without it.
| Requirement | What it means | What happens without it |
|---|---|---|
| Deterministic | The same key always produces the same value | You store an entry in bucket 3 and later compute bucket 5 for the same key. The entry is still there, but nothing can reach it. |
| Well spread | Different keys are spread over all the buckets | Most keys pile into a few buckets. Those buckets hold long lists, and lookup degrades from O(1) toward O(n). |
| Fast | Computing the hash is itself close to O(1) | The time saved on the search is spent computing the hash instead. |
How does a string become a number? The classic method is a polynomial hash: multiply each character code by a power of 31 and add the results. For "cat" (c = 99, a = 97, t = 116):
hash("cat") = 99 × 31² + 97 × 31 + 116 = 95139 + 3007 + 116 = 98262
Why not simply add the character codes? Because then "cat" and "act" would produce the same value: same letters, different order. Multiplying by powers of 31 makes the position take part in the result — a character further to the left is multiplied by 31 more times. Written as a loop it is one line: h = h × 31 + code(c). The last step takes this large number modulo the bucket count, which pushes it into the range of legal array indexes. Try the machine yourself:
Why 31? The choice made in Java's String.hashCode
31 is an odd prime. An odd multiplier keeps the low bits of the result meaningful, which matters because the bucket count is usually a power of two and the index is taken from the low bits. It is also cheap: 31 × i equals (i << 5) − i, one shift and one subtraction. The collision between Aa and BB that you can produce in the lab above is not a bug — both have hashCode 2112 in Java. There are infinitely many strings and only 2³² int values, so by the pigeonhole principle some pairs of keys must share a hash. Collisions cannot be designed away. They can only be handled.
Collisions: two keys, one bucket
Collisions cannot be avoided. What can be avoided is letting them ruin the performance.
Option one: separate chaining. A bucket holds a list instead of a single entry, and a colliding entry is appended to that list. A lookup hashes to the bucket and then compares the keys in the list one by one. Java's HashMap works this way, and so does the implementation in §04.
Option two: open addressing. A bucket holds at most one entry. On a collision the table follows a fixed rule to find another free bucket. The simplest rule is to try the next bucket to the right, which is called linear probing. CPython's dict and Rust's standard HashMap use open addressing. It is friendlier to the CPU cache, because the next bucket probed is usually in the same cache line. Here are the same six keys under both strategies:
A single list can become very long if the hash is poor or if the input was chosen by an attacker. Since Java 8, once a bucket holds about 8 entries and the table has at least 64 buckets, that bucket is converted from a linked list to a red-black tree, so a lookup in it costs O(log n) instead of O(n). Why 8? With a hash that spreads well, the number of entries per bucket follows a Poisson distribution, and reaching 8 has a probability of roughly six in one hundred million. The conversion is not meant for ordinary data. It is a safety limit for deliberately crafted input, a HashDoS attack.
In a probing table you cannot simply set a slot back to empty. A search stops at the first empty slot, so any entry that was pushed further along the probe path would become unreachable. The usual fix is to write a tombstone: a marker meaning "something used to be here". A search continues past it, and an insert may reuse it. So deletion under open addressing does not really free the slot, and once there are many tombstones the whole table has to be rebuilt.
The fuller the buckets, the more often keys collide. The measure of "full" is the load factor: the number of stored entries divided by the number of buckets. A Java HashMap grows once the load factor passes 0.75 by default. Growing means allocating a bigger bucket array — the capacity doubles — and then placing every existing key again. That second part is called rehashing, and it is not optional. The index comes from hash % bucketCount, so changing the bucket count changes where a key belongs. For example, 98262 % 8 = 6 and 98262 % 16 = 6, so that key happens to stay; but 98270 % 8 = 6 while 98270 % 16 = 14, so that key moves. With a power-of-two capacity an entry in bucket i either stays at i or moves to i + oldCapacity, and every entry still has to be visited to find out which.
Rehashing means a single insert can cost O(n). Because the capacity doubles, that cost is spread over the insertions that follow, so insertion is O(1) amortized rather than O(1) every time. Why 0.75? Higher, and each bucket holds more entries on average, so the lists grow and the average lookup stops being constant. Lower, and many buckets stay empty and waste memory.
| Operation | Average | Worst case | Why |
|---|---|---|---|
| put | O(1) amortized | O(n) | Average: compute the index, then compare a constant number of keys. The amortized part covers the rehash after the table grows. Worst: every key lands in one bucket. |
| get / contains | O(1) | O(n) | Same as above. The average holds only while the hash spreads the keys and the load factor stays bounded — both conditions are maintained on purpose. |
| remove | O(1) | O(n) | Finding the entry costs the same as a lookup. Unlinking it from a list, or writing a tombstone, is O(1). |
| Iterate | O(n + buckets) | Every bucket has to be visited, including the empty ones. A hash table keeps no order of its own. | |
State the complexity together with its conditions
Insert, lookup, and delete in a hash table are O(1) on average, under two conditions: the hash function spreads the keys well, and the load factor stays bounded, which is what growing the table maintains. The worst case is O(n), and it happens when a large number of keys land in the same bucket. Saying only "a hash table is O(1)" leaves out both the average and the conditions, and that is the most common mistake in an interview answer.
Build one: separate chaining from scratch
Locate the bucket, compare along the list, and double the table when it gets too full.
A bucket array with one list per bucket, and five methods: hash (locate), put, get, remove, and resize. Watch the line inside resize that computes the index again. That single line is the rehash described in §03.
hash() for str is randomized with a per-process seed by default (a defence against HashDoS), so the numeric value of hash("abc") differs between runs and must never be relied on.What these five methods already answer
"What happens during put?" "What happens when the table grows?" "Why is get O(1) on average?" Each answer is a line above, and you can point at it. What is left out — treeified buckets, thread safety, ConcurrentHashMap — is added on top of this same skeleton.
Three languages: Map and Set
One abstraction in two shapes: a dictionary from key to value, and a set that stores keys only.
Every language ships an industrial-strength hash table, and each ships it in two shapes. A Map (dictionary) stores key-value pairs. A Set stores keys only — it is the same hash table with no value attached, which is why a membership test is as fast as a map lookup. The APIs are similar. The mistakes are language-specific:
list, dict, or set has no __hash__, so it cannot be a key. If its contents changed, its hash would change and the entry would be lost. A tuple is immutable and can be a key, provided everything inside it is hashable too. And the difference between d[k] and d.get(k) — raising KeyError versus returning None — catches most beginners once.| Operation | Java (HashMap) | Python (dict) | JavaScript (Map) | Complexity |
|---|---|---|---|---|
| Create | new HashMap<>() | {} | new Map() | O(1) |
| Insert or update | m.put(k, v) | m[k] = v | m.set(k, v) | O(1) amortized |
| Read | m.get(k) | m[k] (raises if absent) | m.get(k) | O(1) avg |
| Read with a default | m.getOrDefault(k, d) | m.get(k, d) | m.get(k) ?? d | O(1) avg |
| Contains key | m.containsKey(k) | k in m | m.has(k) | O(1) avg |
| Delete | m.remove(k) | del m[k] / m.pop(k, None) | m.delete(k) | O(1) avg |
| Size | m.size() | len(m) | m.size | O(1) |
| Iterate | for (var e : m.entrySet()) | for k, v in m.items(): | for (const [k, v] of m) | O(n) |
| Iteration order | No guarantee (LinkedHashMap keeps insertion order) | Insertion order (guaranteed since 3.7) | Insertion order (guaranteed) | — |
Why an IDE always generates equals and hashCode together
IntelliJ and Eclipse generate the two methods in one action, Lombok's annotation is called @EqualsAndHashCode, and a Java record generates both automatically. The whole ecosystem enforces one rule: objects that are equal must have equal hash codes. A HashMap lookup has two steps — hashCode picks the bucket, equals confirms the match inside it. Break the rule and the first step already goes to the wrong bucket.
Three signals: seen before, pairing, grouping
★ Interview coreWhen a problem sounds like one of these three, reach for a hash table.
Every hash-table solution in this chapter does the same thing: it spends O(n) memory so that a search that would cost O(n) becomes one lookup that costs O(1) on average. Three signals tell you when that applies:
Detecting duplicates, detecting a cycle, computing an intersection. You only care whether a value exists, not what is attached to it. → LC 217, 202, 349, 128.
Looking for "the other number that sums to k", or for where a value appeared. The key is the value you want to be found by; the value is the index or the count. → LC 1, 454, 560.
Put items that are the same in some sense under one key. Design a signature function so that items of the same kind always produce the same key. → LC 49, 383, 299.
LC 1 · Two Sum
EASYThe first problem on LeetCode, and the prototype of the pairing signal. The task: find two numbers in the array that add up to target, and return their indexes. Brute force: two nested loops over every pair, O(n²). Where the improvement comes from: look at what the inner loop is doing. For each i, it searches the part of the array before i for target − nums[i]. That is a linear search. Put the numbers you have already passed into a hash table, and that search becomes one O(1) lookup:
Complexity and follow-up questions
Time O(n), space O(n) — memory traded for time. Follow-up one: why look up before recording? It stops a number from pairing with itself. With target = 8 and nums[i] = 4, recording first would let 4 match its own entry. Follow-up two: what if the array is sorted? Then two pointers moving toward each other solve it in O(n) time and O(1) space (LC 167, covered in the array chapter). Sorted input suggests two pointers; unsorted input suggests a hash table.
LC 49 · Group Anagrams
MEDIUMThe task: group the words that contain the same letters in a different order. Brute force: compare every pair of words, O(n² · k). Where the improvement comes from: instead of comparing words with each other, give every word a signature — a value that is identical for anagrams and different for everything else. Then a Map from signature to list collects the groups on its own:
Two signatures, and when to switch
Sorted signature: O(k log k) per word, and the shortest to write. Counting signature: count the 26 letters and join the counts into a string such as "a1e1t1", which is O(k) per word and better when the words are long. Both follow the same rule: the signature must capture exactly what makes two items belong together. The same idea returns in LC 249 (Group Shifted Strings) and LC 205 (Isomorphic Strings).
LC 128 · Longest Consecutive Sequence
MEDIUMThe task: in an unsorted array, find the length of the longest run of consecutive values. Their positions do not matter, and the solution must be O(n). First idea: sort and scan — but sorting is O(n log n), which the problem rules out. Where the improvement comes from: put every value into a set, and "is x + 1 present" becomes O(1), so a run can be measured by walking right. Doing that from every value is still O(n²): in the run 1..100 you would count from 1, then from 2, then from 3. One rule fixes it: only start counting at the beginning of a run, that is at a value x for which x − 1 is not in the set:
Why this is O(n)
There is a nested while loop, but it only runs from the start of a run, and each run has exactly one start. Adding up all its iterations, every value is counted through once. n start checks on the outside, plus at most n steps on the inside, gives O(n). This is the same amortized argument as the 2n bound for the sliding window in the array chapter. Follow-up: can you iterate over nums instead of the set? Yes, but duplicates would repeat the start check for no reason. Iterating the set is cleaner.
Problem set: 10 hash table problems
Hot 100 selectionGrouped by signal, easiest first. LC 560, prefix sums plus a map, is the one to understand completely.
Quiz
✎ QuizEight questions. Get them all right to mark this chapter as complete.
Which of these must a usable hash function satisfy? (select all)
A string hash can be a number in the hundreds of thousands. Why take it modulo the bucket count at the end?
The load factor is the number of stored entries divided by the number of buckets. Above which load factor does a Java HashMap grow by default? (write a decimal)
Lookup in a hash table is O(1) on average but O(n) in the worst case. What causes the worst case?
In Java you override equals() but forget hashCode(), then use the object as a HashMap key. What happens?
In Python a list cannot be a dict key but a tuple can. What is the underlying reason?
In JavaScript, what is the most important difference between using a plain object as a dictionary and using a Map?
In Python 3.7 and later, in what order does iterating a dict return the keys?
- A hash table is an array + a hash function + a plan for collisions. The hash function turns a key into an index, so the O(1) jump that an array offers to integer indexes becomes available to any kind of key.
- A hash function must be deterministic, well spread, and fast. There are more possible keys than buckets, so collisions are certain. Separate chaining stores a list per bucket (Java); open addressing probes for another slot (Python), which costs more at a high load factor and makes deletion need tombstones.
- Past the load factor (0.75 by default in Java) the table allocates a larger array and rehashes every key, because the index depends on the bucket count. One insert can therefore cost O(n), which is why insertion is O(1) amortized. Stated precisely: average O(1) with a good hash and a bounded load factor, worst case O(n) when many keys share a bucket.
- One trap per language. Java: overriding equals means overriding hashCode, and never modify a key after inserting it. Python: mutable objects cannot be keys (a list cannot, a tuple of hashables can), and dict preserves insertion order since 3.7. JavaScript: use Map, not a plain object — object keys are converted to strings and the prototype contributes keys of its own.
- Three signals: "seen before" → Set; "find the partner" → Map from value to index or count; "group and count" → Map from signature to list. A Set is a hash table that stores keys only. Prefix sums plus a map (LC 560) is Two Sum applied to prefix sums.