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:
- A hash function turns the key (say, a string) into a large integer.
- That integer, reduced modulo the number of slots, picks a bucket in an array.
- 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
- Comparing keys with
==. That compares pointers. Use strcmp(a, b) == 0. - Storing the caller's key pointer instead of a copy, so keys change or dangle.
- Using the old bucket count after resizing, or computing a bucket index before growing and using it after.
- Freeing entries during a resize. Only the old array is freed; the entries move.
- Signed hash arithmetic.
int h; h *= 16777619; overflows: undefined behavior. Use uint32_t. - Relying on iteration order. It is arbitrary and changes on resize.
Recap
- A hash table maps a key through a hash function to a bucket index:
hash(key) % nbuckets. - Collisions are handled by chaining: each bucket is a linked list, searched with
strcmp. - A good hash spreads keys uniformly; FNV-1a is simple and decent for strings.
- The table owns copies of its keys and frees them.
- Keep the load factor bounded by doubling and rehashing; operations stay O(1) on average.
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).
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):
Entry *table_find(const Table *t, const char *key) returns the entry for key, or NULL.int table_add(Table *t, const char *key) adds 1 to the count for key, creating an entry with count 1 (and its own copy of the key) if it does not exist yet. Returns 0, or -1 if out of memory. t->distinct counts entries.
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;
}