AlgoScope

Rehashing

algorithmintermediateTime O(1) amortizedSpace O(n)

A hash table is fast only while it is mostly empty. The load factor, keys over slots, says how full it is, and past a limit probe chains get long. So the table doubles and every key is placed again, because key mod capacity changes with the capacity. That costs a full pass, but doubling makes it rare enough that inserts stay O(1) on average.

01234

An empty table of 5 slots. Keys land at key mod 5 and probe forward on a collision. When the load factor, keys over slots, passes 0.70 the table doubles.

Check your understanding

The player pauses before each decision in this run and asks what happens next. Here are all 4, with their answers.

  1. After 5 the table holds 1 of 5 slots. Rehash?

    • Under the limit, keep going
    • Over the limit, rehash

    Answer: Under the limit, keep going. 1 / 5 = 0.20 is still within the limit.

  2. After 10 the table holds 2 of 5 slots. Rehash?

    • Under the limit, keep going
    • Over the limit, rehash

    Answer: Under the limit, keep going. 2 / 5 = 0.40 is still within the limit.

  3. After 15 the table holds 3 of 5 slots. Rehash?

    • Under the limit, keep going
    • Over the limit, rehash

    Answer: Under the limit, keep going. 3 / 5 = 0.60 is still within the limit.

  4. After 20 the table holds 4 of 5 slots. Rehash?

    • Under the limit, keep going
    • Over the limit, rehash

    Answer: Over the limit, rehash. 4 / 5 = 0.80 is past 0.70. Probe chains would only get longer from here.

How it runs, step by step

  1. An empty table of 5 slots. Keys land at key mod 5 and probe forward on a collision. When the load factor, keys over slots, passes 0.70 the table doubles.

    Empty hash table with 5 slots and a load limit of 0.70. Inserting 4 keys: 5, 10, 15, 20.

  2. insert 5. 5 mod 5 = 0 and slot 0 is free. Load 1 / 5 = 0.20, under 0.70.

    Insert 5 at slot 0. The load factor is 0.20, under the limit.

  3. insert 10. 10 mod 5 = 0, taken by 5, so probe 1 slot forward to slot 1. Load 2 / 5 = 0.40, under 0.70.

    Insert 10 at slot 1 after 1 collision probe. The load factor is 0.40, under the limit.

  4. insert 15. 15 mod 5 = 0, taken by 5, so probe 2 slots forward to slot 2. Load 3 / 5 = 0.60, under 0.70.

    Insert 15 at slot 2 after 2 collision probes. The load factor is 0.60, under the limit.

  5. insert 20. 20 mod 5 = 0, taken by 5, so probe 3 slots forward to slot 3. Load 4 / 5 = 0.80, over 0.70. Time to rehash.

    Insert 20 at slot 3 after 3 collision probes. The load factor is 0.80, over the limit.

  6. Double the table from 5 to 10 slots. Every key must be placed again, because key mod 10 is not key mod 5. 4 keys to move: 5, 10, 15, 20.

    The table grows from 5 to 10 slots. All 4 keys are re-inserted.

  7. Move 5. 5 mod 10 = 5 and slot 5 is free.

    5 is re-inserted at slot 5.

  8. Move 10. 10 mod 10 = 0 and slot 0 is free.

    10 is re-inserted at slot 0.

  9. Move 15. 15 mod 10 = 5, taken by 5, so probe 1 slot forward to slot 6.

    15 is re-inserted at slot 6.

  10. Move 20. 20 mod 10 = 0, taken by 10, so probe 1 slot forward to slot 1.

    20 is re-inserted at slot 1.

  11. 4 keys in 10 slots, load 4 / 10 = 0.40. 1 rehash moved 4 keys in total. Doubling keeps that under one extra move per insert on average, so insert stays O(1) amortized.

    Done. 4 keys in 10 slots with a load factor of 0.40. 1 rehash moved 4 keys in total.

Remember

  • Load factor is keys divided by slots. Open addressing keeps it under about 0.7, chaining tolerates more.
  • Rehashing moves every key, because key mod capacity is a different slot once the capacity changes.
  • Doubling means each key moves O(1) times on average, so insert is O(1) amortized despite the occasional full pass.

Topics covered

Where this is used

DatabasesRedis dictionaries

Redis keeps two hash tables per dict and rehashes incrementally: a step moves one bucket from the old table to the new one, and while the move is in progress every lookup checks both tables. A single-threaded server that stopped to move a million keys at once would freeze every connected client, so the pass is deliberately smeared over thousands of commands.

Language runtimesjava.util.HashMap resize

HashMap keeps its capacity a power of two and resizes at a load factor of 0.75. Because the capacity exactly doubles, a key's new index is either its old index or that index plus the old capacity, decided by the single new bit of the stored hash. Resize therefore splits each bucket's list in two by testing one bit, without recomputing any hash codes.

Distributed systemsConsistent hashing in sharded caches

Sharding by key mod N is a hash table whose slots are servers, so adding one machine is a rehash that relocates nearly every key, and a cache that loses nearly every entry at once dumps its whole load onto the database. Amazon Dynamo and Apache Cassandra place nodes on a hash ring instead, so joining a node moves only the keys in its arc, about 1/N of them, and the rest of the cluster is untouched.

InterpretersCPython dict growth

A Python dict grows once it is two thirds full, and it sizes the replacement from the number of live entries rather than the number of slots ever used, so a dict that has had many deletions can be rebuilt smaller than it was. The rebuild also compacts the dense entry array behind the index table, which is what keeps iteration proportional to the keys present instead of to the capacity.

Why it works this way

Why double the capacity instead of adding a fixed block of slots?

Growing by a constant c means a rehash every c inserts, and each pass touches every key already stored, so n inserts do roughly n squared over 2c work and a single insert costs O(n) on average. Doubling makes the passes exponentially rarer as the table grows: their sizes are n, n/2, n/4 and so on, under 2n moves in total. That total divided over n inserts is where the amortized O(1) comes from.

Why 0.7, and why chaining can run past 1.0

With linear probing an unsuccessful lookup costs about (1 + 1/(1-a)^2)/2 probes for load factor a: roughly 6 probes at 0.7 but roughly 50 at 0.9. The cost is not proportional to how full the table is, it blows up as the last slots disappear, so open addressing has to resize well before the table fills. Chaining degrades gently, since the average chain length is just a, which is why a chained table can sit at 1.0 or higher and only pay a slightly longer walk.

Deleting a key does not free its slot

Under open addressing you cannot blank a slot on delete, because a probe that walked past that slot to place a later key would now stop at the gap and report the key missing. The usual fix is a tombstone, a marker meaning keep probing, but tombstones lengthen probes while counting as neither live keys nor free space, so a table churned by inserts and deletes gets slow at a load factor that still looks small. The cure is a rehash into a table of the same capacity, which is the reminder that not every rehash is a growth.

Amortized O(1) is not O(1) on the insert that triggers it

One insert in every n pays for the whole pass, so a table holding a million keys has one insert that moves a million keys. That shows up as a latency spike, and averaging it away is exactly what a request budget, a game frame or an audio callback cannot do. Code with a deadline either sizes the table up front, with HashMap.newHashMap(n) or reserve or make(map[K]V, n), or spreads the move across many operations instead of doing it in one. Watch the Java form: the HashMap(n) constructor takes a bucket count, not a key count, so passing the number of keys you expect still resizes once you pass 0.75 of it, which is why newHashMap was added.

Read more

Next up