Skip to content

Growing a hash table

When the load factor passes a limit, the table doubles its buckets and re-inserts every key, at an average cost per insertion that stays constant.

Helps to know first
This topic

First, the picture

A chained table of eight buckets fills up. Watch what happens to every key when it doubles to sixteen.

Every key moves when the table grows

Seven keys; eight buckets double to sixteen

static bool grow(struct table *t){    size_t new_m = 2 * t->m;    struct item **nb = malloc(new_m * sizeof *nb);    if (nb == NULL)        return false;    for (size_t b = 0; b < new_m; b++)        nb[b] = NULL;    for (size_t b = 0; b < t->m; b++) {        struct item *it = t->bucket[b];        while (it != NULL) {            struct item *next = it->next;            size_t nbk = (size_t)it->key & (new_m - 1);            it->next = nb[nbk];            nb[nbk] = it;            moved++;            it = next;        }    }    free(t->bucket);    t->bucket = nb;    t->m = new_m;    return true;} bool put(struct table *t, int key){    struct item *it = malloc(sizeof *it);    if (it == NULL)        return false;    size_t b = (size_t)key & (t->m - 1);    it->key = key;    it->next = t->bucket[b];    t->bucket[b] = it;    t->n++;    if (4 * t->n > 3 * t->m)        return grow(t);    return true;} void print(const struct table *t){    for (size_t b = 0; b < t->m; b++) {        if (t->bucket[b] == NULL)            continue;        printf("%zu:", b);        for (const struct item *it = t->bucket[b]; it != NULL; it = it->next)            printf(" %d", it->key);        printf("\n");    }} int main(void){    struct table t;    if (!init(&t, 8))        return 1;    const int keys[7] = { 3, 11, 19, 27, 6, 14, 30 };    for (int i = 0; i < 6; i++)        put(&t, keys[i]);    print(&t);    put(&t, keys[6]);    printf("grown to %zu buckets, %d keys moved\n", t.m, moved);    print(&t);    for (size_t b = 0; b < t.m; b++) {        struct item *it = t.bucket[b];        while (it != NULL) {            struct item *next = it->next;            free(it);            it = next;        }    }    free(t.bucket);    return 0;}

main has started. First an empty table of 8 buckets.

keys
0
buckets
0
α
0
longest chain
-Infinity

Output

0.00 / 15.30 s
Describe this picture

Eight bucket heads in a column, numbered 0 to 7, under the name struct item **bucket: 8 buckets, with the code beside them, the line about to run marked. The clip takes about 15 seconds. The keys 3, 11, 19, 27, 6, 14 and 30 fly in one by one from a line over the table that shows each key’s bucket, such as 3 & 7 = 3, and go on the front of their chains. Six keys fill just two chains: bucket 3 holds 27, 19, 11 and 3, bucket 6 holds 14 and 6. The seventh key, 30, joins bucket 6; every key is marked and the line reads “4 × 7 > 3 × 8: grow”. Then the column opens into 16 buckets and every key slides at once to bucket key & 15. 3, 19 and 6 stay where they were; 11, 27, 14 and 30 move 8 buckets down and are marked. Bucket 3 now holds 3 and 19, bucket 6 holds 6, bucket 11 holds 11 and 27, bucket 14 holds 14 and 30. Last, main frees every item, and each greys. Under the picture are one sentence on the current step and the readouts “keys”, “buckets”, “α” and “longest chain”, which falls from 4 to 2. The transport plays and pauses, steps one beat back or forward, and scrubs; a paused step is kept in the page address as rehashing.step.

Why every key moves

A key’s bucket is its hash mod m, so when m changes, the bucket changes. Growing a table cannot copy the buckets as they are: it allocates a larger array and places every key again. This is rehashing.

The table grows when the load factor α = n/m passes a limit, here 0.75. Doubling m brings α back to about half the limit, and the chains back to their short average length.

The code and its cost

static bool grow(struct table *t)
{
    size_t new_m = 2 * t->m;
    struct item **nb = malloc(new_m * sizeof *nb);
    if (nb == NULL)
        return false;
    for (size_t b = 0; b < new_m; b++)
        nb[b] = NULL;
    for (size_t b = 0; b < t->m; b++) {
        struct item *it = t->bucket[b];
        while (it != NULL) {
            struct item *next = it->next;
            size_t nbk = (size_t)it->key & (new_m - 1);
            it->next = nb[nbk];
            nb[nbk] = it;
            moved++;
            it = next;
        }
    }
    free(t->bucket);
    t->bucket = nb;
    t->m = new_m;
    return true;
}

bool put(struct table *t, int key)
{
    struct item *it = malloc(sizeof *it);
    if (it == NULL)
        return false;
    size_t b = (size_t)key & (t->m - 1);
    it->key = key;
    it->next = t->bucket[b];
    t->bucket[b] = it;
    t->n++;
    if (4 * t->n > 3 * t->m)
        return grow(t);
    return true;
}

The table’s size is a power of two, and the keys are not negative, so key & (m - 1), which keeps the key’s low bits, is key % m. put grows the table when 4n > 3m, that is α > 0.75, using whole numbers. grow does not copy any item: it re-links each one at the front of its new bucket’s chain, so each chain’s order is reversed.

main puts six keys, prints the table, puts the seventh, and prints again:

3: 27 19 11 3
6: 14 6
grown to 16 buckets, 7 keys moved
3: 3 19
6: 6
11: 11 27
14: 14 30

With a doubled power-of-two size, & (new_m - 1) uses one more bit of the key. A key whose next bit is 0 stays in bucket b; a key whose next bit is 1 moves to b + 8. The program places all 7 keys again, which is the 7 keys moved it prints, but only four change bucket: 3, 19 and 6 land in the bucket they had, and 11, 27, 14 and 30 go to b + 8.

The average cost

A growth moves all n keys, which is expensive, but it is rare: each doubling comes after the table has gained about as many keys again as it had at the last one. As with Dynamic arrays, the moves over n insertions add up to about 2n at most, and always to fewer than 3n, so an insertion costs O(1) on average, even counting the growths.

Growing by one bucket at a time would move every key at every insertion: O(n²) in all.

Power-of-two sizes and prime sizes

A power-of-two size lets the bucket be computed with a mask, key & (m - 1), which is fast, and makes doubling simple. But the mask uses only the key’s low bits, so the hash must mix its low bits well. A prime size uses a remainder, key % m, which is slower but forgives a weaker hash, as the Hash functions lesson showed for keys that are all multiples of 4.

Worked example

A thousand keys. Starting from 8 buckets and doubling when α > 0.75, the table grows at the 7th, 13th, 25th, 49th, 97th, 193rd, 385th and 769th key, just past 0.75 × 8, 16, 32, …, 1,024. It moves 7 + 13 + 25 + 49 + 97 + 193 + 385 + 769 = 1,538 keys in all, under 2 × 1,000: 1.54 moves per key, and ends with 2,048 buckets.

Keys that agree in their low bits. With m = 8 and keys that all leave remainder 3 when divided by 8, every key lands in bucket 3. Each doubling uses one more bit of the key, and splits the bucket in two.

Two checks. Which bucket holds 30 when there are 32 buckets? (30 & 31 = 30.) Why not grow by one bucket at a time? (Every key would move at every growth: O(n²) in all.)

Where it’s used

Every growable hash table in language libraries.

Databases’ hash indexes, some of which grow one bucket at a time, splitting it, instead of all at once.

Reference card

Code or ideaMeaning
key & (m - 1)key % m when m is a power of two
grow whenα = n/m passes a limit (here 4n > 3m)
rehashingevery key placed again in the new array
doublingeach key stays in b or moves to b + m
average costabout 2n moves at most over n insertions, under 3n: O(1) each
power of twofast mask; the hash must mix its low bits
prime sizeremainder; forgives a weaker hash

Platform: C11, gcc on x86-64 Linux: int 4 bytes, size_t and pointers 8, struct item 16, struct table 24, little-endian. Addresses in the pictures are short stand-ins, by one rule for the whole course; sizes are the real ones.

End of the lesson

Where to go next.

New lessons by email

Are you 13 or over?

Accounts and email updates are for people aged 13 or over. We keep only your answer, never your date of birth.

Phasorium

Account

Sign inWith Google, GitHub or your email

Subjects

Site

Look