Hash tables
A Python list finds the value at index i in constant time, however long the list is: the slots of the list sit side by side in memory, so the location of slot i is computed from i with one multiplication and one addition. A Python dictionary does something that looks harder. It finds the value for a key such as "blue" or "Hanover" in constant time too, almost always, even though a string is not an index.
The structure that makes this possible is the hash table. A hash table converts each key into a number, uses that number as an index into an ordinary list, and stores the key’s value there. These notes describe the map and set ADTs that a hash table implements, how keys are turned into indices, what happens when two keys want the same slot, and why the whole scheme runs in constant time on average.
Counting with a list whose indices are the data
Section titled “Counting with a list whose indices are the data”Suppose that a long list digits holds the digits 0 through 9, in some order, and the problem is to count how often each digit appears. A list of ten counters solves the problem in one pass, because each digit can serve as an index:
def count_digits(digits): frequency = [0] * 10 for d in digits: frequency[d] += 1 return frequencyAfter the loop, frequency[0] holds the number of zeros, frequency[1] the number of ones, and so on. Each step of the loop takes constant time, since frequency[d] goes straight to slot d, so the whole count takes time for digits.
The same question about words is more useful. Search engines and plagiarism detectors count how often each word appears in a document: a document in which patient, illness, and blood appear many times is probably about medicine. Counting words with the digit trick would need an index for every word, and a word is not an integer. Python’s dictionary fills the gap, since it accepts a string as its index:
counts = {}for word in words: if word in counts: counts[word] += 1 else: counts[word] = 1The rest of these notes explain how a dictionary manages to behave like the list of digit counters, with strings in place of digits.
The map and set ADTs
Section titled “The map and set ADTs”A map is an abstract data type that stores key-value pairs, with at most one pair for each key. The key is used to look up the value, the way a word is used to look up its count, or a name to look up a phone number. The main operations are:
| Operation | Effect | Python dict |
|---|---|---|
put(key, value) | Associate value with key, replacing any earlier value for key | d[key] = value |
get(key) | Return the value associated with key; an error if there is none | d[key] |
contains(key) | Return whether key has a value | key in d |
remove(key) | Remove key and its value | del d[key] |
size() | Return the number of keys | len(d) |
A map is also called a dictionary or an associative array. Python’s dict implements the map ADT, Java’s HashMap and C++‘s unordered_map do too.
A set ADT is a map with keys and no values: add(key), contains(key), remove(key), and size(). Python’s set is the set ADT, and is built the same way as dict. Everything below about maps applies to sets, by leaving out the values.
A slow map: searching a list of pairs
Section titled “A slow map: searching a list of pairs”The first implementation of a map that comes to mind keeps two lists, keys and values, with the value for keys[j] stored in values[j]. To find the value for a key, loop over keys until the key turns up at some index j, then return values[j]:
class ListMap: def __init__(self): self.keys = [] self.values = []
def get(self, key): for j in range(len(self.keys)): if self.keys[j] == key: return self.values[j] raise KeyError(key)This works, but get is a linear search, which takes time in the worst case for a map holding keys. put and contains are no better, since each must first search for the key. Counting the words of a book with this map would take time proportional to the number of words times the number of distinct words. Python’s dict is not implemented this way.
Hash tables: computing where a key belongs
Section titled “Hash tables: computing where a key belongs”A catalog store once kept the order forms waiting for pickup in 100 pigeonholes behind the counter, numbered 0 through 99. A customer arriving to pick up an order was asked for the last two digits of their phone number, and the clerk looked only in that pigeonhole. Even with several hundred forms in the store, each pigeonhole held a few, which the clerk could search quickly. The last two digits of the phone number split one big collection into 100 small ones, and anyone could compute which small collection a form belonged in. (The first two digits would have worked poorly: most customers shared the same local area code and exchange, and their forms would all have landed in a few pigeonholes.)
A hash table does the same thing inside the computer. It has two parts:
- A list of slots, called the table, holding the keys and their values. The number of slots is the table’s capacity, written .
- A hash function, which converts any key into an integer called the key’s hash code. The remainder of the hash code divided by is an index between and , and the key belongs in the slot at that index.
To look up a key, compute its hash code, take the remainder by , and go to that slot. Both steps take constant time, independent of how many keys the table holds. The figure below follows the key "cat" through both steps, in a table with 8 slots and a hash function named string_hash, described below.
The second step, which squeezes a large hash code into the range of indices, is called compression. In Python, h % m is never negative when m is positive, so it is always a valid index. (In Java and C, the remainder of a negative number is negative, and a hash table must clear the sign first.)
If each key went to a different slot, there would be nothing more to say. That would be like every customer of the catalog store having different last two digits. They do not, and much of the design of a hash table is about what to do when two keys share a slot.
Hash functions for strings
Section titled “Hash functions for strings”Adding character codes: a weak hash function
Section titled “Adding character codes: a weak hash function”Every character has a numeric code, which Python’s built-in ord returns: ord("a") is 97, ord("b") is 98, and so on. A first attempt at a hash function for strings adds up the codes of the characters:
def sum_hash(s): total = 0 for c in s: total += ord(c) return totalThis is a legal hash function, but a poor one, for two reasons:
- It gives the same hash code to many different strings. Two different keys with the same hash code are said to collide. Every rearrangement of a word’s letters has the same sum, so
"stop","pots","tops","spot","post", and"opts"all collide. Strings that are not anagrams collide too:"pat"and"let"both sum to 325. - It does not spread keys evenly over a large table. English words are short, so their sums fall into a narrow range of a few hundred to about two thousand. In a table with a million slots, almost every slot would go unused while a few slots got crowded.
What makes a hash function good
Section titled “What makes a hash function good”A good hash function has three properties:
- It is fast to compute, since it runs on every operation.
- It spreads keys evenly over the table, so that each slot receives about the same number of keys. This is called uniform hashing.
- Small changes to the key change the hash code, such as changing one character of a string or swapping two of them. Real keys are often similar to each other, like
"file1","file2", and"file3", and similar keys must not cluster into the same few slots.
A hash function must also be deterministic: the same key must give the same hash code every time it is asked for during the life of the table. Otherwise a key stored in one slot would be looked for in another.
A polynomial hash with Horner’s rule
Section titled “A polynomial hash with Horner’s rule”The weakness of sum_hash is that it treats each character the same, regardless of its position. A better hash function multiplies each character’s code by a different power of a constant , so that position matters. For a string with character codes , the hash code is the polynomial
Computing each power separately is wasteful. Horner’s rule factors the polynomial into nested products, so that each coefficient is multiplied by only once, one level at a time:
Read from the inside out, the nested form is a loop: start a running total at (or, equivalently, at zero), and for each following character multiply the total by and add the character’s code. For "cat", with and the character codes , , , the running total goes from to , then to , then to . Horner’s rule evaluates the polynomial with one multiplication and one addition per character:
def string_hash(s): h = 0 for c in s: h = 31 * h + ord(c) return hHere , the constant that Java uses for its strings; 33, 37, 39, and 41 also work well in practice. Now string_hash("stop") is 3540994 and string_hash("pots") is 3446974, and "pat" and "let" no longer have the same hash code. The function runs in time proportional to the length of the string, which is constant for keys of bounded length. In a language with fixed-size integers, the total wraps around when it grows too large, which keeps the arithmetic fast and does no harm, since only the remainder by is used in the end.
Python’s built-in hash
Section titled “Python’s built-in hash”Python has a hash function built in, named hash, which works on numbers, strings, tuples, and every other immutable built-in type:
hash(42) # 42: a small integer is its own hash codehash("banana") # a large integer, different each time Python startshash((3, 4)) # tuples combine the hash codes of their elementsFor strings, Python uses a hash function called SipHash, which mixes the bits of the string far more thoroughly than string_hash does. It also mixes in a random number chosen when Python starts, so the hash code of "banana" changes from one run of a program to the next, although it stays fixed during any one run. The reason for the random number is described below, in the section on hash flooding. Because of it, the figures in these notes use string_hash, whose values can be checked by hand, instead of hash.
Collisions are unavoidable: the birthday paradox
Section titled “Collisions are unavoidable: the birthday paradox”With 100 slots, it might seem that close to 100 keys could be stored before two of them were likely to land in the same slot. That intuition is wrong, and a famous puzzle shows why. How many people must be in a room before there is at least a 50% chance that two of them share a birthday? The answer is 23, out of 365 possible birthdays.
To see why, count the ways for everyone’s birthday to be different. The first person may have any birthday, the second must avoid one day, the third must avoid two days, and so on. The probability that people, each with a random birthday out of possible days, all have different birthdays is
Each factor is close to 1, but there are many factors, and the product shrinks fast:
def prob_no_collision(n, m): p = 1.0 for i in range(n): p *= (m - i) / m return p
1 - prob_no_collision(23, 365) # 0.5071 - prob_no_collision(12, 100) # 0.497The number of pairs of people is what matters. With 23 people there are pairs, and each pair has a chance of of sharing a birthday. In general, collisions become likely once the number of keys is around , far below .
The same arithmetic describes keys in a hash table, if the hash function sends each key to a random slot. With 100 slots, a 50% chance of a collision arrives with only 12 keys. Hashing keys into slots leaves about of the slots empty, so many of the others must hold two or more keys. No hash function can prevent collisions, so every hash table needs a plan for them. The next sections describe the two standard plans.
Chaining: a list of entries in each slot
Section titled “Chaining: a list of entries in each slot”In a hash table with chaining, each slot of the table holds a short list, called a bucket or a chain, of all the key-value pairs whose keys hash to that slot. To look up a key, compute its slot and search that slot’s bucket, comparing the key with each key in the bucket.
The figure below shows a table with 8 slots after inserting "cat", "dog", "gnu", "owl", and "bee", in that order; the values stored with the keys are not shown. The table uses string_hash, so each key’s slot can be checked by hand. Two pairs of keys collide, and each pair shares a bucket.
Each bucket may be a linked list or a Python list. A Python list is simplest, and the order of the entries in a bucket does not matter.
A hash map with chaining in Python
Section titled “A hash map with chaining in Python”The class below implements the map ADT with chaining. The table is a Python list of buckets, and each bucket is a Python list of (key, value) tuples. Every method begins by finding the right bucket with _bucket, which hashes the key with Python’s hash and compresses the result with %:
class HashMap: def __init__(self, capacity=8): self.table = [[] for _ in range(capacity)] self.size = 0
def _bucket(self, key): return self.table[hash(key) % len(self.table)]
def get(self, key): for k, v in self._bucket(key): if k == key: return v raise KeyError(key)
def contains(self, key): for k, v in self._bucket(key): if k == key: return True return False
def put(self, key, value): bucket = self._bucket(key) for i in range(len(bucket)): if bucket[i][0] == key: bucket[i] = (key, value) # replace the old value return bucket.append((key, value)) # a new key self.size += 1
def remove(self, key): bucket = self._bucket(key) for i in range(len(bucket)): if bucket[i][0] == key: del bucket[i] self.size -= 1 return raise KeyError(key)The table is built with [[] for _ in range(capacity)], not [[]] * capacity. The second form would make a list of capacity references to one empty list, and every key would end up in that same bucket.
Two different keys may land in the same bucket, so each method compares the key it is given with the keys in the bucket, using ==. The hash code only narrows the search to one bucket.
Renaming get, put, contains, and remove to Python’s special method names __getitem__, __setitem__, __contains__, and __delitem__ lets client code write m["cat"] = 3, m["cat"], "cat" in m, and del m["cat"], as it would with a dict.
Load factor and the cost of a search
Section titled “Load factor and the cost of a search”How long does a search take? Hashing the key takes constant time, and then the search walks one bucket, so the cost depends on how long the buckets are. If the table has slots and holds keys, the average bucket holds
keys. This ratio (the Greek letter lambda) is the load factor of the table.
To say more, assume simple uniform hashing: that every key is equally likely to hash to any of the slots, independently of where the other keys went. Under this assumption, the bucket that a search walks holds keys on average, and:
- An unsuccessful search, for a key that is not in the table, compares the key with everything in its bucket: comparisons on average.
- A successful search stops when it finds the key, about halfway along the bucket on average: about comparisons.
Either way, a search takes time on average. The accounts for computing the hash code and finding the bucket, which takes time even when is small. put and remove begin with a search, so they cost the same.
If the load factor stays below some constant, such as , then every operation takes constant time on average, however many keys the table holds. This is the source of the constant-time dict and set.
Growing a hash table by rehashing
Section titled “Growing a hash table by rehashing”The load factor grows as keys are added. A table with 8 slots and 8000 keys has buckets of 1000 keys on average, and searching it is little better than searching one long list. To keep small, a hash table grows when the load factor passes a threshold: it allocates a new table about twice as large and rehashes every key, computing its slot in the new table. Every key must be rehashed, because the slot of a key depends on the capacity: "cat" belongs in slot 98262 % 8 = 6 of a table with 8 slots, but in slot 98262 % 16 = 6 or 98262 % 32 = 22 of larger tables.
Two changes to HashMap add growth. At the end of put, after a new key has been added:
if self.size > 0.75 * len(self.table): self._grow()and a new method:
def _grow(self): old_table = self.table self.table = [[] for _ in range(2 * len(old_table))] for bucket in old_table: for key, value in bucket: self._bucket(key).append((key, value))_grow takes time, since it touches every key. It runs rarely, though, and it runs less often as the table gets larger. The argument is the one for a growing array whose capacity doubles when it fills: if the table grows from capacity 8 to 16 to 32 and so on up to , then the total number of keys rehashed over all of the growths is less than , which is a constant times the number of keys inserted. Spread over all of the insertions, the cost of rehashing is a constant per insertion. put therefore takes constant time on average, counting the growth.
Real hash tables choose the threshold and starting capacity as engineering decisions. Java’s HashMap uses chaining, starts with 16 slots, and doubles when passes .
Open addressing with linear probing
Section titled “Open addressing with linear probing”Chaining scatters small lists across memory, and each list costs some memory of its own. Open addressing keeps every key in the table itself, at most one key per slot, with no buckets. When a key’s slot is taken by a different key, the key goes into another slot, chosen by a fixed rule.
The simplest rule is linear probing. To insert a key whose slot is , look at, or probe, slot . If it is taken by a different key, probe slot , then , and so on, wrapping around from slot to slot , until reaching an empty slot or the key itself. The figure below inserts the same five keys as before, in the same order, into a table with 8 slots. "gnu" belongs in slot 6, which "cat" already holds, so it goes into slot 7; "owl" belongs in slot 4, which "dog" holds, so it goes into slot 5.
A search follows the same sequence of slots as an insertion. It starts at the key’s slot and moves right until it finds the key, a success, or reaches an empty slot, a failure: had the key been in the table, it would have been placed in that empty slot or before it. In the figure, a search for "emu", whose slot is 5, probes slots 5, 6, 7, and 0, and stops at the empty slot 0, after four probes. A table with linear probing keeps its load factor below 1, so that some slot is always empty and every search stops.
In code, with the table a list of (key, value) tuples and None for an empty slot, the slot for a key is found by:
def _find_slot(self, key): i = hash(key) % len(self.table) while self.table[i] is not None and self.table[i][0] != key: i = (i + 1) % len(self.table) return iget returns the value in the slot that _find_slot returns, if the slot is not empty, and put writes the pair into that slot.
Removing a key under linear probing
Section titled “Removing a key under linear probing”Removing a key from a table with linear probing takes care. Emptying the key’s slot can break the search for some other key. In the figure, remove "cat" from slot 6 by setting the slot to None, and then search for "gnu". The search starts at slot 6, finds it empty, and reports that "gnu" is not in the table, although it sits in slot 7.
The fix is to mark a removed slot with a special value, called a tombstone, rather than emptying it. A search treats a tombstone as taken and probes past it; an insertion may reuse it. A table with many removals fills up with tombstones, which slow down searches, so the table is rehashed from time to time to clear them.
Clustering and the cost of probing
Section titled “Clustering and the cost of probing”Linear probing has a second problem. Keys that land near each other build up into long runs of taken slots, called clusters, and a cluster tends to grow. A key that hashes anywhere into a cluster of slots lands in the empty slot at its end, so the slot after a cluster of slots is filled next with probability , rather than . Two clusters that grow until they meet merge into one longer cluster.
Donald Knuth’s analysis of linear probing, under the assumption that each key hashes to a random slot, gives the average number of probes:
| Load factor | Successful search, | Unsuccessful search, |
|---|---|---|
| 0.5 | 1.5 | 2.5 |
| 0.75 | 2.5 | 8.5 |
| 0.9 | 5.5 | 50.5 |
At a load factor of one-half, linear probing is fast. As the table approaches full, an unsuccessful search slows down sharply, so a table with open addressing grows at a lower load factor than one with chaining.
Other probing rules break up clusters. Double hashing uses a second hash function to choose the step between probes, so that the probes for a key visit slots , , , and so on, all modulo . Two keys that start in the same slot usually take steps of different sizes, and so they follow different paths through the table.
Python’s dict and set use open addressing. Their probe sequence mixes in more and more bits of the hash code at each step, which avoids clustering, and they grow before they are two-thirds full.
Hashing objects: __eq__ and __hash__ must agree
Section titled “Hashing objects: __eq__ and __hash__ must agree”A hash table relies on one rule connecting equality and hash codes: two keys that are equal must have the same hash code. If they did not, a key stored under one hash code could not be found by looking it up with an equal key, since the search would go to a different slot. The converse does not hold: two keys with the same hash code need not be equal, and that is a collision.
Python’s built-in types follow the rule. A class written by a programmer compares its objects with __eq__ and hashes them with __hash__, and the two methods must be written together. A point on a grid is a typical key:
class Point: def __init__(self, x, y): self.x = x self.y = y
def __eq__(self, other): return self.x == other.x and self.y == other.y
def __hash__(self): return hash((self.x, self.y))__hash__ builds a tuple of the attributes that __eq__ compares and returns the tuple’s hash code, so two points with the same coordinates have the same hash code. With both methods in place, Point(3, 4) in {Point(3, 4)} is True. Without __eq__ and __hash__, Python compares and hashes objects by identity, and two different Point(3, 4) objects are unequal. A class that defines __eq__ and not __hash__ cannot be used as a key at all; Python marks its objects as unhashable, so that the rule cannot be broken by accident.
A key must also stay the same while it is in the table. If a key’s attributes changed after it was stored, its hash code would change, and the key would be sitting in the slot for its old hash code, where no search would look for it. That is why Python’s list, set, and dict are unhashable and cannot be keys, while tuple, str, and frozenset, which never change, can. A class like Point should be treated as immutable once its objects are used as keys.
The worst case and hash flooding
Section titled “The worst case and hash flooding”All of the constant-time results above are averages, which rely on the hash function spreading keys out. The worst case is bad. If every key hashes to the same slot, a table with chaining is one long bucket, and a search takes time; a table with open addressing is one long cluster, with the same result. For any fixed hash function and a table with slots, some slot is the hash of at least of all possible keys, so the bad keys exist. With a good hash function, ordinary data almost never runs into them.
Data chosen by an adversary is another matter. In 2011, researchers showed that many web servers stored the parameters of each incoming request in a hash table, with a hash function that anyone could look up. An attacker could compute thousands of parameter names that all collided and send them in one request, and the server would spend minutes inserting them into one long bucket. This attack is called hash flooding. The defense is to make the hash function unpredictable: Python and many other languages now mix a random number, chosen when the program starts, into the hash code of every string, so an attacker cannot know in advance which strings will collide. Choosing the hash function at random from a family of functions, in a way that guarantees few collisions on average for any set of keys, is called universal hashing.
What a hash table gives up
Section titled “What a hash table gives up”A hash table scatters its keys across the table in an order set by the hash function, with no relation to the order of the keys themselves. It therefore offers no efficient way to find the smallest key, to list the keys in sorted order, or to find all keys between "apple" and "banana". Each of these requires examining every key. (Python’s dict returns its keys in the order they were inserted, by keeping a separate list of entries in insertion order, but that is still not sorted order. A set makes no promise about order at all.)
A map built from a balanced binary search tree keeps its keys in sorted order and supports all of these operations, at a cost of time per operation in the worst case, compared with constant time on average for a hash table. When only put, get, contains, and remove are needed, a hash table is the usual choice.
Source material for these notes
Section titled “Source material for these notes”The framing of these notes follows the CS 1 notes by Balkcom et al., chapter 16, “Stacks, queues, and dictionaries”: counting digits with a list as motivation for a dictionary, counting the words of a document, the map built from two lists and searched linearly, the hash function that adds character codes and its flaws, the description of a hash table as a list indexed by a compressed hash code, and the use of lists in table slots to handle collisions. The catalog-store pigeonholes, the birthday paradox and the count of empty slots, the load factor and the analysis of chaining under simple uniform hashing, rehashing, open addressing with linear probing, removal from a probed table, clustering, double hashing, the properties of a good hash function, the polynomial hash with Horner’s rule, and the requirement that equal objects have equal hash codes are adapted from the CS 10 lecture notes developed by Drysdale, Bailey-Kellogg, and Pierson, “Hashing,” for the earlier, Java-based version of CS 10. The analysis of linear probing follows Knuth, The Art of Computer Programming, volume 3, section 6.4. Hash flooding was presented by Alexander Klink and Julian Wälde, “Efficient Denial of Service Attacks on Web Application Platforms” (2011), and universal hashing was introduced by J. Lawrence Carter and Mark Wegman in 1977.
Claude (Anthropic) helped adapt these sources to Python, and drafted the HashMap class and its growth, the _find_slot method, the section on __eq__ and __hash__ in Python, the sections on hash flooding and on what a hash table gives up, the figures, and the practice problems, which Balkcom then edited. The same material is covered in Cormen, Leiserson, Rivest, and Stein, Introduction to Algorithms, chapter 11, “Hash Tables,” and in Goodrich, Tamassia, and Goldwasser, Data Structures and Algorithms in Python, chapter 10.
Further reading
Section titled “Further reading”- Mapping types:
dictand the__hash__method in the Python documentation - PEP 456: Secure and interchangeable hash algorithm: why Python’s strings are hashed with SipHash
- Raymond Hettinger, “Modern Python Dictionaries: A confluence of a dozen great ideas”: a talk on how Python’s
dictis built
Practice problems
Section titled “Practice problems”Try these on your own before checking the solutions.
Problem 1: Hash by hand
Section titled “Problem 1: Hash by hand”Compute string_hash("ab") and string_hash("ba"), using ord("a") = 97 and ord("b") = 98. In which slot of a table with 8 slots does each key belong? Do the two keys collide under sum_hash?
(Solution)
Problem 2: Chaining by hand
Section titled “Problem 2: Chaining by hand”A table with chaining has 5 slots, and the hash function for an integer key is the key itself. Draw the table after inserting the keys 12, 7, 22, 3, 17, and 10, in that order. What is the load factor? How many keys does a search for 27 compare with?
(Solution)
Problem 3: Linear probing by hand
Section titled “Problem 3: Linear probing by hand”Insert the same keys, 12, 7, 22, 3, 17, and 10, in the same order, into a table with 7 slots that uses linear probing, with the key itself as its hash code. Show the table, and list the slots that a search for 24 probes.
(Solution)
Problem 4: Phone numbers as keys
Section titled “Problem 4: Phone numbers as keys”A table with 100 slots stores customers by phone number, with the phone number as the hash code. Explain why compressing with the last two digits of the number spreads customers evenly, while using the first two digits does not. What goes wrong if the table has 1000 slots and the hash code is the whole phone number, for a store whose customers’ numbers all end in 0?
(Solution)
Problem 5: A broken __hash__
Section titled “Problem 5: A broken __hash__”What goes wrong with each of these __hash__ methods for the Point class, whose __eq__ compares x and y?
return 0return hash(self.x)return id(self)(idreturns a number unique to each object)return hash(self.x) + hash(self.y)
(Solution)
Problem 6: Counting collisions
Section titled “Problem 6: Counting collisions”Write a function count_collisions(keys, m) that returns the number of keys in keys that land in a slot of a table with m slots that already holds an earlier key, using string_hash. Run it on the distinct words of a long text, for several values of m, and compare the count with the count for sum_hash.
(Solution)
Problem 7: Rehashing cost
Section titled “Problem 7: Rehashing cost”A HashMap starts with 8 slots and doubles when the load factor passes 0.75. How many times does it grow while 1000 keys are inserted, and how many keys does it rehash in total? Compare the total with 1000.
(Solution)
Problem 8: Removing with chaining
Section titled “Problem 8: Removing with chaining”Removing a key from a table with linear probing needs a tombstone. Explain why removing a key from a table with chaining does not.
(Solution)