Data structures in C · about 65 min

Hash tables

Why this matters

Finding something in an unsorted array or a linked list means looking at every element: O(n). A hash table finds, inserts and deletes by key in O(1) on average, whatever the size. It is the most useful data structure in practical programming: Python's dict and set, JavaScript objects, symbol tables in compilers, caches, database indexes, routing tables, and the uniq-like counting you will do in the projects module are all hash tables. In C there is no built-in one, so writing your own (or knowing exactly what a library's does) is a rite of passage. It also brings together everything in this module: arrays, linked lists, strings and careful ownership of heap memory.

The idea

Suppose keys were small integers: you could use the key itself as an array index, and lookup would be one memory access. A hash table generalizes that trick to any key:

  1. A hash function turns the key (say, a string) into a large integer.
  2. That integer, reduced modulo the number of slots, picks a bucket in an array.
  3. The entry is stored in that bucket.
  "apple" --hash--> 280767167 --% 8--> 7
  "kiwi"  --hash--> .........  --% 8--> 1

  buckets[0]  NULL
  buckets[1]  -> [kiwi=40]
  buckets[2]  NULL
  buckets[3]  -> [plum=30]
  buckets[4]  -> [lime=50]
  buckets[5]  -> [fig=99] -> [pear=10]      two keys, same bucket: a collision
  buckets[6]  NULL
  buckets[7]  -> [apple=0]

Different keys can land in the same bucket, a collision; with more keys than buckets it is unavoidable. The simplest way to handle it is separate chaining: each bucket holds a linked list of the entries that hash there. Lookup hashes the key, picks the bucket, and walks that (short) list comparing keys. (The alternative, open addressing, stores entries directly in the array and probes neighbouring slots on a collision; Python's dict does that.)

A good hash function

A hash function must be deterministic (same key, same hash) and should spread keys uniformly over the buckets, so that similar keys like "item1" and "item2" end up far apart. For strings, a simple and decent choice is FNV-1a:

uint32_t hash(const char *s)
{
    uint32_t h = 2166136261u;           /* "offset basis" */
    for (; *s != '\0'; s++) {
        h ^= (unsigned char)*s;          /* mix in one byte */
        h *= 16777619u;                  /* "FNV prime": spreads the bits */
    }
    return h;
}

The multiplication wraps modulo 2^32, which is well defined for uint32_t (and would be undefined for a signed int). XOR-then-multiply makes every input byte influence all the bits of the result. Production systems facing untrusted input use keyed hashes such as SipHash, because an attacker who knows your hash function can choose keys that all collide and turn every lookup into O(n): a real denial-of-service attack against web servers in 2011.

A complete chained table

typedef struct Entry {
    char *key;              /* owned copy of the key */
    int value;
    struct Entry *next;     /* next entry in the same bucket */
} Entry;

typedef struct {
    Entry **buckets;        /* array of nbuckets list heads */
    size_t nbuckets;
    size_t count;
} Table;

int table_init(Table *t, size_t nbuckets)
{
    t->buckets = calloc(nbuckets, sizeof *t->buckets);   /* all NULL */
    t->nbuckets = nbuckets;
    t->count = 0;
    return t->buckets ? 0 : -1;
}

Entry *table_find(const Table *t, const char *key)
{
    Entry *e = t->buckets[hash(key) % t->nbuckets];
    for (; e != NULL; e = e->next)
        if (strcmp(e->key, key) == 0)
            return e;
    return NULL;
}

/* Insert or update. Returns 0, or -1 if out of memory. */
int table_put(Table *t, const char *key, int value)
{
    Entry *e = table_find(t, key);
    if (e != NULL) {
        e->value = value;
        return 0;
    }
    e = malloc(sizeof *e);
    if (e == NULL)
        return -1;
    size_t len = strlen(key);
    e->key = malloc(len + 1);
    if (e->key == NULL) {
        free(e);
        return -1;
    }
    memcpy(e->key, key, len + 1);
    e->value = value;
    size_t b = hash(key) % t->nbuckets;
    e->next = t->buckets[b];        /* push onto the front of the bucket */
    t->buckets[b] = e;
    t->count++;
    return 0;
}

void table_free(Table *t)
{
    for (size_t b = 0; b < t->nbuckets; b++) {
        Entry *e = t->buckets[b];
        while (e != NULL) {
            Entry *next = e->next;
            free(e->key);
            free(e);
            e = next;
        }
    }
    free(t->buckets);
    t->buckets = NULL;
}

buckets is an array of Entry *, one list head per bucket, hence the type Entry **. calloc gives all-zero memory, which for pointers means NULL on every platform you will meet: empty lists.

Storing apple pear fig plum kiwi lime with values 0, 10, ... 50 into 8 buckets, then updating fig to 99, and printing every bucket gives the picture from the start of the lesson:

hash("apple") = 280767167, bucket 7
bucket 0:
bucket 1: kiwi=40
bucket 2:
bucket 3: plum=30
bucket 4: lime=50
bucket 5: fig=99 pear=10
bucket 6:
bucket 7: apple=0
plum -> 30, grape -> missing, count 6

Note the iteration order: it follows the buckets, not insertion order, and it changes whenever the table is resized. Never rely on a hash table's order. (That is why the exercise prints counts for words you name, rather than dumping the table.)

Owning the keys

table_put copies the key into its own allocation. That matters: the caller's string might be a buffer that is reused for the next word (exactly what scanf("%255s", word) in a loop does), or a local array that disappears when a function returns. If the table stored the caller's pointer, every entry would end up pointing at the same buffer. The table owns its key copies, and table_free releases them. This is the ownership discipline from the dynamic memory module applied to a container. (POSIX's strdup does the malloc + memcpy in one call, but it is not part of C17, so it needs a feature macro; writing it out keeps the code portable.)

Load factor and resizing

The load factor is count / nbuckets, the average chain length. Lookups cost about one hash plus a walk of length proportional to the load factor. With a fixed bucket array, inserting 1,000,000 keys into 64 buckets makes chains of about 15,000 entries, and the "O(1)" table is a slow linked list.

The fix is the same as for a dynamic array: when the load factor passes a threshold (0.75 for open addressing, 1 to 2 for chaining), double the number of buckets and rehash: move every entry to the bucket hash(key) % new_size, since its old index was computed with the old size. Rehashing is O(n), but doubling means it happens rarely enough that inserts stay O(1) amortized. Entries can be moved by relinking their nodes; the keys themselves never need copying.

Common mistakes

Recap

Going further: add table_remove using the pointer-to-pointer technique from the linked list lesson. Then read about open addressing with linear probing and why it is often faster than chaining on modern CPUs (hint: the cache, again).

Exercises

Exercise 1 · quiz

A terrible hash function

Someone writes a hash table with 1024 buckets and this hash function for strings:

uint32_t hash(const char *s) { return (unsigned char)s[0]; }

What happens when they store 100,000 English words?

Answer
  • All words land in the few buckets matching their first letter, so lookups walk long chains: close to O(n) instead of O(1)

Correctness survives (chaining handles any number of collisions, and keys are compared with strcmp), but performance collapses. Only about 26 of 1024 buckets are used, so each holds thousands of words and every lookup is a linear search of a long list. A good hash spreads keys uniformly over all buckets, so that with a sensible load factor each chain is only one or two entries long.

Exercise 2 · write code

Implement FNV-1a

Implement the 32-bit FNV-1a hash described in the lesson:

h = 2166136261
for each byte b of the string (as unsigned char):
    h = h XOR b
    h = h * 16777619        (modulo 2^32: use uint32_t and let it wrap)

uint32_t fnv1a(const char *s). main reads words from stdin and prints each hash in decimal:

$ echo "apple a" | ./prog
280767167
3826002220

The empty string hashes to 2166136261.

File: main.c

Starting point

#include <stdio.h>
#include <stdint.h>

uint32_t fnv1a(const char *s)
{
    uint32_t h = 0;
    return h;
}

int main(void)
{
    char word[256];
    while (scanf("%255s", word) == 1)
        printf("%u\n", (unsigned)fnv1a(word));
    return 0;
}

Example

input

apple a

expected output

280767167
3826002220
Hint 1

Start h at 2166136261u. Loop over the characters until the terminating '\0'.

Hint 2

Cast each char to unsigned char before XORing, so bytes above 127 are not sign-extended.

Reference solution
#include <stdio.h>
#include <stdint.h>

uint32_t fnv1a(const char *s)
{
    uint32_t h = 2166136261u;
    for (; *s != '\0'; s++) {
        h ^= (unsigned char)*s;
        h *= 16777619u;
    }
    return h;
}

int main(void)
{
    char word[256];
    while (scanf("%255s", word) == 1)
        printf("%u\n", (unsigned)fnv1a(word));
    return 0;
}

Exercise 3 · write code

Count words with a hash table

Complete table_find and table_add for the chained hash table in the starter (it has 64 buckets and uses FNV-1a):

main reads words from stdin, then prints the count for each word given as a command-line argument, and finally the number of distinct words. table_free is written for you; the check runs under valgrind.

$ echo "the cat saw the dog and the bird" | ./prog the cat fish
the 3
cat 1
fish 0
distinct 6

File: main.c

Starting point

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>

#define NBUCKETS 64

typedef struct Entry {
    char *key;
    int count;
    struct Entry *next;
} Entry;

typedef struct {
    Entry *buckets[NBUCKETS];
    size_t distinct;
} Table;

static uint32_t hash(const char *s)
{
    uint32_t h = 2166136261u;
    for (; *s != '\0'; s++) {
        h ^= (unsigned char)*s;
        h *= 16777619u;
    }
    return h;
}

Entry *table_find(const Table *t, const char *key)
{
    return NULL;
}

int table_add(Table *t, const char *key)
{
    return -1;
}

void table_free(Table *t)
{
    for (size_t b = 0; b < NBUCKETS; b++) {
        Entry *e = t->buckets[b];
        while (e != NULL) {
            Entry *next = e->next;
            free(e->key);
            free(e);
            e = next;
        }
        t->buckets[b] = NULL;
    }
    t->distinct = 0;
}

int main(int argc, char *argv[])
{
    Table t = {0};
    char word[256];
    while (scanf("%255s", word) == 1)
        if (table_add(&t, word) != 0) {
            table_free(&t);
            return 1;
        }
    for (int i = 1; i < argc; i++) {
        Entry *e = table_find(&t, argv[i]);
        printf("%s %d\n", argv[i], e ? e->count : 0);
    }
    printf("distinct %zu\n", t.distinct);
    table_free(&t);
    return 0;
}

Example with arguments the cat fish

input

the cat saw the dog and the bird

expected output

the 3
cat 1
fish 0
distinct 6

Example with arguments x

expected output

x 0
distinct 0
Hint 1

find: walk the list in buckets[hash(key) % NBUCKETS] and compare keys with strcmp (not ==, which compares addresses).

Hint 2

add: if find returns an entry, increment its count. Otherwise malloc an Entry and a copy of the key (strlen + 1 bytes, then memcpy), and push it onto the front of its bucket.

Hint 3

Do not store the word pointer itself: main reuses that buffer for every word, so all entries would share (and see) the last word read.

Reference solution
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>

#define NBUCKETS 64

typedef struct Entry {
    char *key;
    int count;
    struct Entry *next;
} Entry;

typedef struct {
    Entry *buckets[NBUCKETS];
    size_t distinct;
} Table;

static uint32_t hash(const char *s)
{
    uint32_t h = 2166136261u;
    for (; *s != '\0'; s++) {
        h ^= (unsigned char)*s;
        h *= 16777619u;
    }
    return h;
}

Entry *table_find(const Table *t, const char *key)
{
    for (Entry *e = t->buckets[hash(key) % NBUCKETS]; e != NULL; e = e->next)
        if (strcmp(e->key, key) == 0)
            return e;
    return NULL;
}

int table_add(Table *t, const char *key)
{
    Entry *e = table_find(t, key);
    if (e != NULL) {
        e->count++;
        return 0;
    }
    e = malloc(sizeof *e);
    if (e == NULL)
        return -1;
    size_t len = strlen(key);
    e->key = malloc(len + 1);
    if (e->key == NULL) {
        free(e);
        return -1;
    }
    memcpy(e->key, key, len + 1);
    e->count = 1;
    size_t b = hash(key) % NBUCKETS;
    e->next = t->buckets[b];
    t->buckets[b] = e;
    t->distinct++;
    return 0;
}

void table_free(Table *t)
{
    for (size_t b = 0; b < NBUCKETS; b++) {
        Entry *e = t->buckets[b];
        while (e != NULL) {
            Entry *next = e->next;
            free(e->key);
            free(e);
            e = next;
        }
        t->buckets[b] = NULL;
    }
    t->distinct = 0;
}

int main(int argc, char *argv[])
{
    Table t = {0};
    char word[256];
    while (scanf("%255s", word) == 1)
        if (table_add(&t, word) != 0) {
            table_free(&t);
            return 1;
        }
    for (int i = 1; i < argc; i++) {
        Entry *e = table_find(&t, argv[i]);
        printf("%s %d\n", argv[i], e ? e->count : 0);
    }
    printf("distinct %zu\n", t.distinct);
    table_free(&t);
    return 0;
}

Exercise 4 · write code

Grow the table

A fixed number of buckets means chains get longer as the table fills. Make this table grow: in table_put, before inserting a new key, if count + 1 > 2 * nbuckets (load factor above 2), call table_grow, which you also write:

int table_grow(Table *t);   /* double nbuckets and rehash every entry; 0 or -1 */

table_grow allocates a new zeroed bucket array of twice the size, moves every existing Entry into the bucket hash(key) % new_size (relink the nodes, do not copy them or their keys), frees the old array, and updates t->buckets and t->nbuckets. If the allocation fails, leave the table unchanged and return -1.

main inserts the numbers 1 to N as string keys (N from stdin) into a table that starts with 4 buckets, then prints the bucket count, and looks up a few keys:

$ echo 1000 | ./prog
count=1000 buckets=512
key 1 -> 1, key 1000 -> 1000, key 1001 -> missing

File: main.c

Starting point

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>

typedef struct Entry {
    char *key;
    int value;
    struct Entry *next;
} Entry;

typedef struct {
    Entry **buckets;
    size_t nbuckets;
    size_t count;
} Table;

static uint32_t hash(const char *s)
{
    uint32_t h = 2166136261u;
    for (; *s != '\0'; s++) {
        h ^= (unsigned char)*s;
        h *= 16777619u;
    }
    return h;
}

int table_init(Table *t, size_t nbuckets)
{
    t->buckets = calloc(nbuckets, sizeof *t->buckets);
    t->nbuckets = nbuckets;
    t->count = 0;
    return t->buckets ? 0 : -1;
}

Entry *table_find(const Table *t, const char *key)
{
    for (Entry *e = t->buckets[hash(key) % t->nbuckets]; e != NULL; e = e->next)
        if (strcmp(e->key, key) == 0)
            return e;
    return NULL;
}

int table_grow(Table *t)
{
    return 0;
}

int table_put(Table *t, const char *key, int value)
{
    Entry *e = table_find(t, key);
    if (e != NULL) {
        e->value = value;
        return 0;
    }
    /* grow here when the load factor would exceed 2 */
    e = malloc(sizeof *e);
    if (e == NULL)
        return -1;
    size_t len = strlen(key);
    e->key = malloc(len + 1);
    if (e->key == NULL) {
        free(e);
        return -1;
    }
    memcpy(e->key, key, len + 1);
    e->value = value;
    size_t b = hash(key) % t->nbuckets;
    e->next = t->buckets[b];
    t->buckets[b] = e;
    t->count++;
    return 0;
}

void table_free(Table *t)
{
    for (size_t b = 0; b < t->nbuckets; b++) {
        Entry *e = t->buckets[b];
        while (e != NULL) {
            Entry *next = e->next;
            free(e->key);
            free(e);
            e = next;
        }
    }
    free(t->buckets);
    t->buckets = NULL;
    t->nbuckets = t->count = 0;
}

int main(void)
{
    int n;
    if (scanf("%d", &n) != 1)
        return 1;
    Table t;
    if (table_init(&t, 4) != 0)
        return 1;
    char key[32];
    for (int i = 1; i <= n; i++) {
        snprintf(key, sizeof key, "%d", i);
        if (table_put(&t, key, i) != 0) {
            table_free(&t);
            return 1;
        }
    }
    printf("count=%zu buckets=%zu\n", t.count, t.nbuckets);
    Entry *a = table_find(&t, "1"), *b = table_find(&t, "1000"), *c = table_find(&t, "1001");
    printf("key 1 -> %d, key 1000 -> ", a ? a->value : -1);
    if (b) printf("%d", b->value); else printf("missing");
    printf(", key 1001 -> ");
    if (c) printf("%d\n", c->value); else printf("missing\n");
    table_free(&t);
    return 0;
}

Example

input

1000

expected output

count=1000 buckets=512
key 1 -> 1, key 1000 -> 1000, key 1001 -> missing

Example

input

8

expected output

count=8 buckets=4
key 1 -> 1, key 1000 -> missing, key 1001 -> missing
Hint 1

In table_put, after the 'already present' check: if (t->count + 1 > 2 * t->nbuckets) grow first. Compute the bucket index only after growing, since nbuckets changes.

Hint 2

table_grow: calloc the new array, then for each old bucket walk its list. Save e->next before relinking e onto the front of nb[hash(e->key) % new_n].

Hint 3

Free only the old bucket array at the end, not the entries: they now live in the new array.

Reference solution
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>

typedef struct Entry {
    char *key;
    int value;
    struct Entry *next;
} Entry;

typedef struct {
    Entry **buckets;
    size_t nbuckets;
    size_t count;
} Table;

static uint32_t hash(const char *s)
{
    uint32_t h = 2166136261u;
    for (; *s != '\0'; s++) {
        h ^= (unsigned char)*s;
        h *= 16777619u;
    }
    return h;
}

int table_init(Table *t, size_t nbuckets)
{
    t->buckets = calloc(nbuckets, sizeof *t->buckets);
    t->nbuckets = nbuckets;
    t->count = 0;
    return t->buckets ? 0 : -1;
}

Entry *table_find(const Table *t, const char *key)
{
    for (Entry *e = t->buckets[hash(key) % t->nbuckets]; e != NULL; e = e->next)
        if (strcmp(e->key, key) == 0)
            return e;
    return NULL;
}

int table_grow(Table *t)
{
    size_t new_n = t->nbuckets * 2;
    Entry **nb = calloc(new_n, sizeof *nb);
    if (nb == NULL)
        return -1;
    for (size_t b = 0; b < t->nbuckets; b++) {
        Entry *e = t->buckets[b];
        while (e != NULL) {
            Entry *next = e->next;
            size_t nbi = hash(e->key) % new_n;
            e->next = nb[nbi];
            nb[nbi] = e;
            e = next;
        }
    }
    free(t->buckets);
    t->buckets = nb;
    t->nbuckets = new_n;
    return 0;
}

int table_put(Table *t, const char *key, int value)
{
    Entry *e = table_find(t, key);
    if (e != NULL) {
        e->value = value;
        return 0;
    }
    if (t->count + 1 > 2 * t->nbuckets && table_grow(t) != 0)
        return -1;
    e = malloc(sizeof *e);
    if (e == NULL)
        return -1;
    size_t len = strlen(key);
    e->key = malloc(len + 1);
    if (e->key == NULL) {
        free(e);
        return -1;
    }
    memcpy(e->key, key, len + 1);
    e->value = value;
    size_t b = hash(key) % t->nbuckets;
    e->next = t->buckets[b];
    t->buckets[b] = e;
    t->count++;
    return 0;
}

void table_free(Table *t)
{
    for (size_t b = 0; b < t->nbuckets; b++) {
        Entry *e = t->buckets[b];
        while (e != NULL) {
            Entry *next = e->next;
            free(e->key);
            free(e);
            e = next;
        }
    }
    free(t->buckets);
    t->buckets = NULL;
    t->nbuckets = t->count = 0;
}

int main(void)
{
    int n;
    if (scanf("%d", &n) != 1)
        return 1;
    Table t;
    if (table_init(&t, 4) != 0)
        return 1;
    char key[32];
    for (int i = 1; i <= n; i++) {
        snprintf(key, sizeof key, "%d", i);
        if (table_put(&t, key, i) != 0) {
            table_free(&t);
            return 1;
        }
    }
    printf("count=%zu buckets=%zu\n", t.count, t.nbuckets);
    Entry *a = table_find(&t, "1"), *b = table_find(&t, "1000"), *c = table_find(&t, "1001");
    printf("key 1 -> %d, key 1000 -> ", a ? a->value : -1);
    if (b) printf("%d", b->value); else printf("missing");
    printf(", key 1001 -> ");
    if (c) printf("%d\n", c->value); else printf("missing\n");
    table_free(&t);
    return 0;
}