Skip to content

Hash tables — solutions

Solutions to the practice problems at the end of Hash tables.

string_hash("ab") is 31⋅97+98=310531 \cdot 97 + 98 = 3105, and string_hash("ba") is 31⋅98+97=313531 \cdot 98 + 97 = 3135. In a table with 8 slots, "ab" belongs in slot 3105 mod 8=13105 \bmod 8 = 1 and "ba" in slot 3135 mod 8=73135 \bmod 8 = 7. Under sum_hash, both strings have the hash code 97+98=19597 + 98 = 195, so they collide: every rearrangement of a string’s characters has the same sum.

The slots are 12 mod 5=212 \bmod 5 = 2, 7 mod 5=27 \bmod 5 = 2, 22 mod 5=222 \bmod 5 = 2, 3 mod 5=33 \bmod 5 = 3, 17 mod 5=217 \bmod 5 = 2, and 10 mod 5=010 \bmod 5 = 0:

SlotBucket
010
1
212, 7, 22, 17
33
4

The load factor is 6/5=1.26 / 5 = 1.2. The key 27 belongs in slot 2, so an unsuccessful search for 27 compares it with all four keys in that bucket. The keys happen to share a remainder by 5, so the table works much worse than its load factor suggests; a hash function that ignored this pattern in the keys would spread them more evenly.

12 goes into slot 5, 7 into slot 0, 22 into slot 1, and 3 into slot 3. 17 belongs in slot 3, which is taken, so it goes into slot 4. 10 belongs in slot 3; slots 3, 4, and 5 are taken, so it goes into slot 6:

Slot0123456
Key7223171210

A search for 24 starts at slot 24 mod 7=324 \bmod 7 = 3 and probes slots 3, 4, 5, 6, 0, 1, and 2, stopping at the empty slot 2: seven probes, to learn that 24 is not in the table. The six keys form one long cluster, from slot 3 around to slot 1.

The last two digits of a phone number vary almost at random from customer to customer, so each of the 100 pigeonholes gets about the same share. The first two digits are part of the area code, which most customers of one store share, so nearly every customer would land in one or two slots.

With 1000 slots and the whole phone number as the hash code, the slot is the last three digits of the number. If every number ends in 0, only the slots 0, 10, 20, …, 990 are ever used: 100 slots out of 1000. The table behaves like a table with 100 slots, and its buckets are ten times longer than the load factor predicts.

  1. return 0 is correct, since equal points have equal hash codes, but every point collides with every other point. The table becomes one long bucket, and every operation takes Θ(n)\Theta(n) time.
  2. return hash(self.x) is correct, but all points with the same x collide. Points on a vertical line, which are common in grid problems, all land in one bucket.
  3. return id(self) breaks the rule. Two different Point(3, 4) objects are equal but have different ids, so after storing one, a search with the other looks in the wrong slot and reports that the point is missing.
  4. return hash(self.x) + hash(self.y) is correct, but symmetric: Point(1, 2) and Point(2, 1) collide, and so do all the points on each line x+y=cx + y = c. Hashing the tuple (self.x, self.y) avoids this, since a tuple’s hash code depends on the order of its elements.
def count_collisions(keys, m, hash_function=string_hash):
used = set()
collisions = 0
for key in keys:
i = hash_function(key) % m
if i in used:
collisions += 1
used.add(i)
return collisions

On the 1138 distinct words of the United States Constitution, the counts are:

mstring_hashsum_hash
10010381038
1000441645
1000061619
1000004619

With 100 slots, both functions must collide at least 1138−100=10381138 - 100 = 1038 times. As m grows, the collisions under string_hash fall off roughly as the birthday paradox predicts for random slots. The collisions under sum_hash stop falling: the largest sum for any word in the text is 1765, so no table larger than that helps, and anagrams collide whatever the size.

The table grows when the number of keys passes three-quarters of the capacity: on the 7th key (from 8 to 16 slots), the 13th (to 32), the 25th (to 64), the 49th (to 128), the 97th (to 256), the 193rd (to 512), the 385th (to 1024), and the 769th (to 2048). That is 8 growths, which rehash

7+13+25+49+97+193+385+769=15387 + 13 + 25 + 49 + 97 + 193 + 385 + 769 = 1538

keys in total, about 1.5 rehashes per key inserted. Each growth rehashes about twice as many keys as the one before, so the last growth accounts for about half of the total, and the total stays below a constant times the number of keys.

A search in a table with chaining looks only in the bucket for its key’s slot, and a bucket holds exactly the keys that hash to that slot. Removing a key from its bucket leaves every other key in its own bucket, where a search for it will look. Under linear probing, a key may sit in a slot other than its own, and a search reaches it by walking through the slots in between. Emptying one of those slots cuts the walk short, which is the problem that a tombstone solves.