Skip to content

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 frequency

After 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 Θ(n)\Theta(n) time for nn 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] = 1

The rest of these notes explain how a dictionary manages to behave like the list of digit counters, with strings in place of digits.

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:

OperationEffectPython dict
put(key, value)Associate value with key, replacing any earlier value for keyd[key] = value
get(key)Return the value associated with key; an error if there is noned[key]
contains(key)Return whether key has a valuekey in d
remove(key)Remove key and its valuedel d[key]
size()Return the number of keyslen(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.

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 Θ(n)\Theta(n) time in the worst case for a map holding nn 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:

  1. A list of slots, called the table, holding the keys and their values. The number of slots is the table’s capacity, written mm.
  2. A hash function, which converts any key into an integer called the key’s hash code. The remainder of the hash code divided by mm is an index between 00 and m−1m - 1, and the key belongs in the slot at that index.

To look up a key, compute its hash code, take the remainder by mm, 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.

"cat" 98262 6 key hash code index string_hash % 8 0 1 2 3 4 5 "cat" 6 7

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.

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 total

This 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.

A good hash function has three properties:

  1. It is fast to compute, since it runs on every operation.
  2. It spreads keys evenly over the table, so that each slot receives about the same number of keys. This is called uniform hashing.
  3. 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.

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 aa, so that position matters. For a string with character codes x0,x1,…,xk−1x_0, x_1, \ldots, x_{k-1}, the hash code is the polynomial

x0ak−1+x1ak−2+⋯+xk−2a+xk−1.x_0 a^{k-1} + x_1 a^{k-2} + \cdots + x_{k-2} a + x_{k-1}.

Computing each power separately is wasteful. Horner’s rule factors the polynomial into nested products, so that each coefficient is multiplied by aa only once, one level at a time:

(⋯((x0a+x1) a+x2) a+⋯+xk−2) a+xk−1.\Bigl(\cdots\bigl((x_0 a + x_1)\,a + x_2\bigr)\,a + \cdots + x_{k-2}\Bigr)\,a + x_{k-1}.

Read from the inside out, the nested form is a loop: start a running total at x0x_0 (or, equivalently, at zero), and for each following character multiply the total by aa and add the character’s code. For "cat", with a=31a = 31 and the character codes 9999, 9797, 116116, the running total goes from 00 to 9999, then to 99⋅31+97=316699 \cdot 31 + 97 = 3166, then to 3166⋅31+116=982623166 \cdot 31 + 116 = 98262. 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 h

Here a=31a = 31, 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 mm is used in the end.

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 code
hash("banana") # a large integer, different each time Python starts
hash((3, 4)) # tuples combine the hash codes of their elements

For 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 nn people, each with a random birthday out of mm possible days, all have different birthdays is

mm⋅m−1m⋅m−2m⋯m−n+1m.\frac{m}{m} \cdot \frac{m-1}{m} \cdot \frac{m-2}{m} \cdots \frac{m-n+1}{m}.

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.507
1 - prob_no_collision(12, 100) # 0.497

The number of pairs of people is what matters. With 23 people there are 23⋅22/2=25323 \cdot 22 / 2 = 253 pairs, and each pair has a chance of 1/3651/365 of sharing a birthday. In general, collisions become likely once the number of keys is around m\sqrt{m}, far below mm.

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 nn keys into nn slots leaves about n/e≈0.37nn/e \approx 0.37n 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.

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.

0 1 2 "bee" 3 4 "dog" "owl" 5 6 "cat" "gnu" 7

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.

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.

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 mm slots and holds nn keys, the average bucket holds

λ=nm\lambda = \frac{n}{m}

keys. This ratio λ\lambda (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 mm slots, independently of where the other keys went. Under this assumption, the bucket that a search walks holds λ\lambda keys on average, and:

  • An unsuccessful search, for a key that is not in the table, compares the key with everything in its bucket: λ\lambda comparisons on average.
  • A successful search stops when it finds the key, about halfway along the bucket on average: about 1+λ/21 + \lambda / 2 comparisons.

Either way, a search takes Θ(1+λ)\Theta(1 + \lambda) time on average. The 11 accounts for computing the hash code and finding the bucket, which takes time even when λ\lambda is small. put and remove begin with a search, so they cost the same.

If the load factor stays below some constant, such as 0.750.75, 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.

The load factor λ=n/m\lambda = n/m 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 λ\lambda 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 Θ(n)\Theta(n) 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 mm, then the total number of keys rehashed over all of the growths is less than 0.75⋅(8+16+⋯+m)<1.5 m0.75 \cdot (8 + 16 + \cdots + m) < 1.5\,m, 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 λ\lambda passes 0.750.75.

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 ii, look at, or probe, slot ii. If it is taken by a different key, probe slot i+1i + 1, then i+2i + 2, and so on, wrapping around from slot m−1m - 1 to slot 00, 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.

0 1 "bee" 2 3 "dog" 4 "owl" 5 "cat" 6 "gnu" 7 owl: slot 4 taken gnu: slot 6 taken

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 i

get 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 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.

emptied 0 1 "bee" 2 3 "dog" 4 "owl" 5 6 "gnu" 7 search "gnu" not found ✗ tombstone 0 1 "bee" 2 3 "dog" 4 "owl" 5 ✝ 6 "gnu" 7 search "gnu" found ✓

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.

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 tt slots lands in the empty slot at its end, so the slot after a cluster of tt slots is filled next with probability (t+1)/m(t + 1)/m, rather than 1/m1/m. 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 λ\lambdaSuccessful search, 12(1+11−λ)\frac{1}{2}\left(1 + \frac{1}{1 - \lambda}\right)Unsuccessful search, 12(1+1(1−λ)2)\frac{1}{2}\left(1 + \frac{1}{(1 - \lambda)^2}\right)
0.51.52.5
0.752.58.5
0.95.550.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 h2h_2 to choose the step between probes, so that the probes for a key visit slots h1(k)h_1(k), h1(k)+h2(k)h_1(k) + h_2(k), h1(k)+2h2(k)h_1(k) + 2h_2(k), and so on, all modulo mm. 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.

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 Θ(n)\Theta(n) time; a table with open addressing is one long cluster, with the same result. For any fixed hash function and a table with mm slots, some slot is the hash of at least 1/m1/m 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.

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 O(log⁡n)O(\log n) 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.

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.

Try these on your own before checking the solutions.

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)

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)

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)

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)

What goes wrong with each of these __hash__ methods for the Point class, whose __eq__ compares x and y?

  1. return 0
  2. return hash(self.x)
  3. return id(self) (id returns a number unique to each object)
  4. return hash(self.x) + hash(self.y)

(Solution)

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)

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)

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)