Learn hash table implementation in C with hash functions, collision resolution using chaining and open addressing, performance analysis, and practical examples.
A hash table is a data structure that provides near-constant time O(1) average-case performance for insertion, deletion, and lookup operations. It uses a hash function to compute an index into an array of buckets, where the desired value can be found. Hash tables are the backbone of dictionaries, caches, database indexes, and symbol tables in compilers.
How Hash Tables Work
- Key → passed to a hash function → produces an index
- Value stored at that index in an array
- On retrieval, same hash function maps key back to the same index
The challenge arises when two different keys produce the same index – this is called a collision.
Hash Function Design
A good hash function should:
- Distribute keys uniformly across the table
- Be fast to compute
- Minimize collisions
#include <stdio.h>
#include <string.h>
#define TABLE_SIZE 10
// Simple hash for integers
unsigned int hashInt(int key) {
return (unsigned int)key % TABLE_SIZE;
}
// Hash function for strings (djb2 algorithm)
unsigned int hashString(const char *key) {
unsigned long hash = 5381;
int c;
while ((c = *key++))
hash = ((hash << 5) + hash) + c; // hash * 33 + c
return hash % TABLE_SIZE;
}
int main() {
printf("Hash of 42: %u\n", hashInt(42));
printf("Hash of 137: %u\n", hashInt(137));
printf("Hash of \"hello\": %u\n", hashString("hello"));
printf("Hash of \"world\": %u\n", hashString("world"));
printf("Hash of \"name\": %u\n", hashString("name"));
return 0;
}
Hash of 42: 2
Hash of 137: 7
Hash of "hello": 5
Hash of "world": 2
Hash of "name": 8
Implementation with Chaining (Separate Chaining)
Each bucket contains a linked list of entries that hash to the same index. This is the most common collision resolution strategy.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define TABLE_SIZE 10
typedef struct Entry {
char *key;
int value;
struct Entry *next;
} Entry;
typedef struct {
Entry *buckets[TABLE_SIZE];
int count;
} HashTable;
unsigned int hash(const char *key) {
unsigned long h = 5381;
int c;
while ((c = *key++))
h = ((h << 5) + h) + c;
return h % TABLE_SIZE;
}
HashTable* createTable() {
HashTable *table = (HashTable*)malloc(sizeof(HashTable));
table->count = 0;
for (int i = 0; i < TABLE_SIZE; i++)
table->buckets[i] = NULL;
return table;
}
// Insert or update a key-value pair
void insert(HashTable *table, const char *key, int value) {
unsigned int index = hash(key);
// Check if key already exists
Entry *current = table->buckets[index];
while (current) {
if (strcmp(current->key, key) == 0) {
current->value = value; // Update existing
return;
}
current = current->next;
}
// Create new entry
Entry *entry = (Entry*)malloc(sizeof(Entry));
entry->key = strdup(key);
entry->value = value;
entry->next = table->buckets[index];
table->buckets[index] = entry;
table->count++;
}
// Search for a key
int search(HashTable *table, const char *key, int *found) {
unsigned int index = hash(key);
Entry *current = table->buckets[index];
while (current) {
if (strcmp(current->key, key) == 0) {
*found = 1;
return current->value;
}
current = current->next;
}
*found = 0;
return -1;
}
// Delete a key
void delete(HashTable *table, const char *key) {
unsigned int index = hash(key);
Entry *current = table->buckets[index];
Entry *prev = NULL;
while (current) {
if (strcmp(current->key, key) == 0) {
if (prev) prev->next = current->next;
else table->buckets[index] = current->next;
free(current->key);
free(current);
table->count--;
return;
}
prev = current;
current = current->next;
}
}
// Display the hash table
void display(HashTable *table) {
printf("\n--- Hash Table (size: %d) ---\n", table->count);
for (int i = 0; i < TABLE_SIZE; i++) {
printf("[%d]: ", i);
Entry *current = table->buckets[i];
while (current) {
printf("(%s=%d) -> ", current->key, current->value);
current = current->next;
}
printf("NULL\n");
}
}
// Free the hash table
void freeTable(HashTable *table) {
for (int i = 0; i < TABLE_SIZE; i++) {
Entry *current = table->buckets[i];
while (current) {
Entry *temp = current;
current = current->next;
free(temp->key);
free(temp);
}
}
free(table);
}
int main() {
HashTable *table = createTable();
insert(table, "alice", 25);
insert(table, "bob", 30);
insert(table, "charlie", 35);
insert(table, "david", 28);
insert(table, "eve", 22);
insert(table, "frank", 40);
display(table);
int found;
int val = search(table, "charlie", &found);
printf("\nSearch 'charlie': %s (value=%d)\n",
found ? "Found" : "Not found", val);
val = search(table, "george", &found);
printf("Search 'george': %s\n", found ? "Found" : "Not found");
delete(table, "bob");
printf("\nAfter deleting 'bob':");
display(table);
freeTable(table);
return 0;
}
--- Hash Table (size: 6) ---
[0]: NULL
[1]: (frank=40) -> NULL
[2]: (eve=22) -> NULL
[3]: (david=28) -> (alice=25) -> NULL
[4]: NULL
[5]: NULL
[6]: (bob=30) -> NULL
[7]: (charlie=35) -> NULL
[8]: NULL
[9]: NULL
Search 'charlie': Found (value=35)
Search 'george': Not found
After deleting 'bob':
--- Hash Table (size: 5) ---
[0]: NULL
[1]: (frank=40) -> NULL
[2]: (eve=22) -> NULL
[3]: (david=28) -> (alice=25) -> NULL
[4]: NULL
[5]: NULL
[6]: NULL
[7]: (charlie=35) -> NULL
[8]: NULL
[9]: NULL
Open Addressing (Linear Probing)
Instead of linked lists, find the next available slot in the array when a collision occurs.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdbool.h>
#define TABLE_SIZE 13
#define DELETED ((char*)-1)
typedef struct {
char *keys[TABLE_SIZE];
int values[TABLE_SIZE];
int count;
} OpenHashTable;
unsigned int hash(const char *key) {
unsigned long h = 5381;
while (*key) h = h * 33 + *key++;
return h % TABLE_SIZE;
}
void initTable(OpenHashTable *t) {
t->count = 0;
for (int i = 0; i < TABLE_SIZE; i++) t->keys[i] = NULL;
}
bool insert(OpenHashTable *t, const char *key, int value) {
if (t->count >= TABLE_SIZE) return false;
unsigned int index = hash(key);
for (int i = 0; i < TABLE_SIZE; i++) {
unsigned int probe = (index + i) % TABLE_SIZE;
if (t->keys[probe] == NULL || t->keys[probe] == DELETED) {
t->keys[probe] = strdup(key);
t->values[probe] = value;
t->count++;
return true;
}
if (strcmp(t->keys[probe], key) == 0) {
t->values[probe] = value; // Update
return true;
}
}
return false;
}
int search(OpenHashTable *t, const char *key, bool *found) {
unsigned int index = hash(key);
for (int i = 0; i < TABLE_SIZE; i++) {
unsigned int probe = (index + i) % TABLE_SIZE;
if (t->keys[probe] == NULL) break;
if (t->keys[probe] != DELETED && strcmp(t->keys[probe], key) == 0) {
*found = true;
return t->values[probe];
}
}
*found = false;
return -1;
}
int main() {
OpenHashTable t;
initTable(&t);
insert(&t, "apple", 5);
insert(&t, "banana", 3);
insert(&t, "cherry", 8);
insert(&t, "date", 12);
bool found;
printf("apple: %d\n", search(&t, "apple", &found));
printf("cherry: %d\n", search(&t, "cherry", &found));
search(&t, "grape", &found);
printf("grape: %s\n", found ? "Found" : "Not found");
return 0;
}
apple: 5
cherry: 8
grape: Not found
Collision Resolution Comparison
| Method | Pros | Cons |
|---|
| Separate Chaining | Simple, no clustering, unlimited entries | Extra memory for pointers |
| Linear Probing | Cache-friendly, no extra allocation | Clustering, performance degrades |
| Quadratic Probing | Reduces clustering | May not find empty slot |
| Double Hashing | Best distribution | More complex hash computation |
| Operation | Average Case | Worst Case |
|---|
| Insert | O(1) | O(n) |
| Search | O(1) | O(n) |
| Delete | O(1) | O(n) |
The load factor (α = n/table_size) determines performance. Keep α < 0.75 for good performance. Resize (rehash) when it exceeds this threshold.
Interview Questions on Hash Tables
Q1: What makes a good hash function? Uniform distribution, deterministic output, fast computation, and avalanche effect (small input change causes large output change).
Q2: What is the load factor and why does it matter? Load factor = number of entries / table size. As it increases, collision probability rises. Most implementations resize at 0.75 to maintain O(1) performance.
Q3: How do you handle hash table resizing? Create a new table with double the size, rehash all existing entries into the new table (since indices change with table size), then free the old table.
Q4: What's the difference between HashMap and HashSet? HashMap stores key-value pairs; HashSet stores only keys (values are implicit). Both use hash tables internally.
Q5: How is a hash table different from a binary search tree? Hash table: O(1) average lookup, unordered. BST: O(log n) lookup, ordered. Use hash tables for fast lookups; use BSTs when you need sorted order or range queries.
Summary
Hash tables provide the fastest average-case lookup of any data structure – O(1) for insert, search, and delete. The key challenge is handling collisions: separate chaining uses linked lists per bucket, while open addressing probes for empty slots. Choose your hash function carefully for uniform distribution, monitor the load factor, and resize when needed. Hash tables are indispensable in real-world software for implementing dictionaries, caches, sets, and database indexes.