No chains this time: every key goes in the array itself. Watch what a key does when its slot is taken, and how one collision makes the next one more likely.
Finding the next free slot
Eight keys in eleven slots, k mod 11, stepping right on a collision
#define M 11 #ifndef KEYS#define KEYS 8#endif enum state { EMPTY, FULL, DELETED }; struct slot { int key; enum state state;}; static struct slot table[M];static int probes = 0; bool put(int key){ int target = -1; for (int i = 0; i < M; i++) { int s = (key % M + i) % M; probes++; if (table[s].state == FULL && table[s].key == key) return true; if (table[s].state == DELETED && target < 0) target = s; if (table[s].state == EMPTY) { if (target < 0) target = s; break; } } if (target < 0) return false; table[target].key = key; table[target].state = FULL; return true;} int find(int key){ for (int i = 0; i < M; i++) { int s = (key % M + i) % M; probes++; if (table[s].state == EMPTY) return -1; if (table[s].state == FULL && table[s].key == key) return s; } return -1;} void remove_key(int key){ int s = find(key); if (s >= 0) table[s].state = DELETED;} int main(void){ const int keys[8] = { 26, 37, 5, 59, 16, 72, 81, 9 }; for (int i = 0; i < KEYS; i++) { probes = 0; put(keys[i]); printf("put %d: %d probes\n", keys[i], probes); } probes = 0; int a = find(48); printf("find 48: %d after %d probes\n", a, probes); remove_key(37); probes = 0; int b = find(59); printf("find 59: slot %d after %d probes\n", b, probes); return 0;}main has started: an empty table of 11 slots.
- key
- –
- probes
- 0
- α
- 0.00
Output
Describe this picture
A row of eleven slots, numbered 0 to 10, named struct slot table[11], with the code beside them, the line about to run marked. The clip takes about 37 seconds. The keys 26, 37, 5, 59, 16, 72, 81 and 9 are put in turn, each starting at its home slot, the key mod 11. A marker s stands on each slot probed; a taken slot is marked, and an arc joins it to the next slot tried. 26 lands at its home, slot 4. 37 also has home 4, finds it taken and steps right to 5. 5 then finds slot 5 taken and goes to 6. The full slots from 4 grow into one block, and each new key whose home is in it must cross it: 81, home 4, probes seven slots and lands in slot 10. 9, home 9, steps to 10 and wraps round to slot 0. Then the search for 48 runs from slot 4 along the block to the empty slot 1, which is marked: 9 probes, not found. 37 is removed, and slot 5 shows “del”, a tombstone, greyed. The search for 59 passes it and finds 59 in slot 7 after 4 probes. Under the picture are one sentence on the current step and the readouts “key”, “probes” and “α”, which reaches 0.73. After the clip, the slider “Keys put” sets how many keys are put, from 1 to 8, and replays from the last of them. The transport plays and pauses, steps one beat back or forward, and scrubs; the number of keys and a paused step are kept in the page address as linear-probing.keys and linear-probing.step.
Keys in the array itself
Open addressing keeps every key in the table’s array, with no lists. Each slot is empty, full, or deleted. To put a key, start at its home slot, k mod m. If that slot is taken, try the next one, and the next, wrapping round from the last slot to slot 0:
This is linear probing. A search follows the same sequence and stops at the key, or at an empty slot: had the key been put, it would have gone there or earlier.
The keys must not be negative here: in C, the % of a negative number is negative, which is no slot.
The code and its cost
bool put(int key)
{
int target = -1;
for (int i = 0; i < M; i++) {
int s = (key % M + i) % M;
probes++;
if (table[s].state == FULL && table[s].key == key)
return true;
if (table[s].state == DELETED && target < 0)
target = s;
if (table[s].state == EMPTY) {
if (target < 0)
target = s;
break;
}
}
if (target < 0)
return false;
table[target].key = key;
table[target].state = FULL;
return true;
}
int find(int key)
{
for (int i = 0; i < M; i++) {
int s = (key % M + i) % M;
probes++;
if (table[s].state == EMPTY)
return -1;
if (table[s].state == FULL && table[s].key == key)
return s;
}
return -1;
}
main puts the eight keys into a table of 11 slots and counts each put’s probes:
| Key | 26 | 37 | 5 | 59 | 16 | 72 | 81 | 9 |
|---|---|---|---|---|---|---|---|---|
| Home slot | 4 | 4 | 5 | 4 | 5 | 6 | 4 | 9 |
| Probes | 1 | 2 | 2 | 4 | 4 | 4 | 7 | 3 |
| Slot | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 0 |
That is 27 probes, 3.4 per key.
Clustering
Four of the keys have home 4 and two have home 5, so slots 4 to 10 fill as one run. A run like this is a cluster, and it grows faster the longer it is: any key whose home falls anywhere in it lands at its end, making it longer still, and two clusters that meet merge. That is primary clustering, and it is why 81 needed 7 probes. Keep α, the share of full slots, below about 0.7.
Removal and tombstones
Removing a key cannot simply empty its slot. A search stops at an empty slot, so emptying slot 5 would hide every key that had passed it on the way to its own slot. Removal leaves a tombstone instead: the slot is marked deleted. A search passes a tombstone, and put remembers the first one it meets and reuses it if the key turns out to be new.
main removes 37 from slot 5, then searches for 59: home 4, past the tombstone at 5, past 6, found in slot 7 after 4 probes. find 48 had already crossed the cluster from slot 4 to the empty slot 1: 9 probes for a key that is not there.
The expected cost
If keys hash independently and evenly, a successful search makes about
probes. At α = 8/11 that is 2.3. The lesson’s keys did worse, 3.4, because they were chosen to collide: four share one home.
Worked example
Without the tombstone. If removing 37 had emptied slot 5, the search for 59 would start at slot 4, find 26, move to slot 5, find it empty and stop: 59 would be reported missing, though it sits in slot 7.
The formula at other loads. At α = 0.5, 1.5 probes; at α = 0.9, 5.5. The cost rises steeply as the table fills.
A missing key. At α = 8/11, the search for 48 crossed 9 slots before it found an empty one.
Two checks. Where would 15 go after the eight keys? (Home 4, the same path as the search for 48: slot 1, after 9 probes.) Why can’t probing fail while a slot is free? (It tries all m slots.)
Where it’s used
Fast in-memory hash tables: keys side by side in one array make probing cheap for the memory cache.
Language runtimes, many of whose dictionaries use open addressing.
Reference card
| Code or idea | Meaning |
|---|---|
(key % M + i) % M | probe i: the home slot, then the next ones |
| a search stops | at the key, or at an empty slot |
| primary clustering | runs of full slots attract keys and merge |
| keep α | below about 0.7 |
| tombstone | a removed key’s slot: searches pass it, puts reuse it |
| successful search | about ½(1 + 1/(1 − α)) probes, with an even hash |
Platform: C11, gcc on x86-64 Linux: int 4 bytes, little-endian. Addresses in the pictures are short stand-ins, by one rule for the whole course; sizes are the real ones.