Hello! Welcome back.
In our previous lesson, we designed the complete architecture for a generic, non-thread-safe hash table using separate chaining. We defined the C structs (ch_node, ch_hash), established the pattern of using function pointers (key_ops, val_ops) for generic data handling, and discussed the importance of a good hash function.
Today, we will bring that blueprint to life. This lesson is entirely focused on implementing the core operations for our hash table: insert, lookup, and delete. By building a correct, functional, but non-thread-safe version first, we establish a solid baseline that we can then make concurrent in the upcoming lessons.
The Core Logic of a Chained Hash Table
As a quick refresher, our hash table structure consists of an array of "buckets," where each bucket is a pointer to the head of a linked list. All operations begin by finding the correct bucket for a given key.

The logic for insert, lookup, and delete all share a common first step: hashing the key to find a bucket, and then traversing the linked list at that bucket.
For a practical demonstration of how these operations work on a hash table with separate chaining, let's turn to a video walkthrough.
Understanding and implementing a Hash Table (in C)
This segment from Jacob Sorber's video on hash tables demonstrates how to adapt the insert, lookup, and delete logic to handle collisions using separate chaining (linked lists).
Please watch from 20:25 to 24:18. Focus on how each of the three main functions (insert, lookup, delete) is modified to traverse the linked list at the target bucket. This provides an excellent dynamic view of the algorithms we're about to implement.
Now, let's translate this into a robust C implementation based on the generic design from our last lesson. We'll examine the logic for each operation one by one.
A Complete Implementation
The following article provides a clean, well-explained implementation of a hash table that closely matches our design goals. It handles key/value updates, memory management, and all the edge cases of linked list manipulation.
a simple hash table in C - Leo Robinovitch @ The Leo Zone
This article, 'a simple hash table in C' by Leo Robinovitch, provides a concise and complete implementation of the functions we need to build. We will be studying its approach to insert, lookup, and delete.
Please read the sections titled 'Associating a key and value' (for the HashTable_set function), 'Looking up values by key' (for HashTable_get), and 'Deleting entries' (for HashTable_delete). As you read, compare the code to the algorithms described below.
Let's dissect the logic for each of these functions.
1. Lookup (HashTable_get)
Looking up a key is the most straightforward operation.
Algorithm:
- Calculate the key's hash and use the modulo operator to find the bucket index.
- Get the pointer to the head of the linked list at
buckets[index]. - If the pointer is
NULL, the key doesn't exist. ReturnNULL. - If the list is not empty, traverse it node by node.
- In each node, use your key comparison function (e.g.,
strcmpfor strings, orkey_ops.eqin our generic design) to check if the node's key matches the search key. - If a match is found, return the pointer to the value stored in that node (
v->val). - If you reach the end of the list without finding a match, the key is not in the table. Return
NULL.
The HashTable_get function in the recommended reading implements this logic perfectly.
2. Insertion (HashTable_set)
Insertion must handle two scenarios: the key already exists (an update), or the key is new (an insert).
Algorithm:
- Calculate the bucket index for the key.
- Traverse the linked list at that index to see if the key already exists.
- If a match is found: This is an update.
- You can simply overwrite the
valpointer in the existing node (v->val = new_val). - In a more robust system, you would first free the old value (
val_ops.free(v->val)) before assigning the new one.
- You can simply overwrite the
- If no match is found: This is an insert.
- Allocate memory for a new
Entry(orch_node). - Create a persistent copy of the key and value on the heap (using
strdupor ourkey_ops.cpandval_ops.cpfunctions). - Set the
nextpointer of the new node to point to the current head of the list (h->buckets[bucket]). - Update the bucket to point to the new node, making it the new head of the list (
h->buckets[bucket] = new_node). This is called "prepending." - Increment the hash table's size counter.
- Allocate memory for a new
This process of inserting new elements is visualized below.

The HashTable_set function from the article implements this combined insert/update logic.
3. Deletion (HashTable_delete)
Deletion is the most complex operation because it requires careful manipulation of the linked list pointers to remove a node without breaking the chain.
Algorithm:
- Calculate the bucket index for the key.
- Traverse the linked list, but this time you must keep track of both the
currentnode and thepreviousnode. - When you find the node to delete:
- Case 1: The node is the head of the list (
previous == NULL).- Update the bucket pointer to point to the next node in the list:
h->buckets[bucket] = current->next;.
- Update the bucket pointer to point to the next node in the list:
- Case 2: The node is in the middle or at the end of the list.
- Update the
nextpointer of thepreviousnode to skip over the current node:previous->next = current->next;.
- Update the
- Case 1: The node is the head of the list (
- After unlinking the node, you must free all memory associated with it:
- Free the memory for the key (e.g.,
free(current->key)orkey_ops.free(current->key)). - Free the memory for the value if your design requires it (
val_ops.free(current->val)). - Free the memory for the node struct itself (
free(current)).
- Free the memory for the key (e.g.,
- Decrement the hash table's size counter.
The HashTable_delete function in the article correctly handles these cases.
Test your understanding!
Imagine you have a hash table with TABLE_SIZE = 5. You are using a simple string hash function: hash(key) = strlen(key). Your operations are:
insert("cat", "meow")insert("bee", "buzz")insert("wolf", "howl")insert("cat", "purr")delete("bee")
Describe the final state of bucket 3 and bucket 4. strlen("cat") is 3, strlen("bee") is 3, and strlen("wolf") is 4.
Show answer
insert("cat", "meow"):strlen("cat")is 3.hash % 5is3 % 5 = 3. Bucket 3 now points to a node("cat", "meow").insert("bee", "buzz"):strlen("bee")is 3.hash % 5is3. A collision occurs. The new node("bee", "buzz")is prepended to the list. Bucket 3 now points to("bee", "buzz"), which points to("cat", "meow").- State of Bucket 3:
("bee", "buzz") -> ("cat", "meow") -> NULL
- State of Bucket 3:
insert("wolf", "howl"):strlen("wolf")is 4.hash % 5is4. Bucket 4 now points to a node("wolf", "howl").insert("cat", "purr"):strlen("cat")is 3. Thesetfunction finds the existing "cat" node and updates its value. The list structure doesn't change.- State of Bucket 3:
("bee", "buzz") -> ("cat", "purr") -> NULL
- State of Bucket 3:
delete("bee"): Thedeletefunction finds the "bee" node. Since it's the head of the list in bucket 3, it updates the bucket pointer to point to the next element ("cat").
Final State:
- Bucket 3: Points to the node
("cat", "purr"), which then points toNULL. - Bucket 4: Points to the node
("wolf", "howl"), which then points toNULL.
Conclusion
In this lesson, you have learned how to implement the three fundamental operations for a non-thread-safe hash table using separate chaining.
Key Takeaways:
- Lookup (
get) involves hashing to find the right bucket and then traversing a linked list to find the key. - Insertion (
set) is a two-part process: first search for the key to see if it's an update, and if not, create a new node and prepend it to the linked list. - Deletion (
delete) requires careful pointer manipulation to unlink a node from its list, with special handling for deleting the head node. - Proper memory management is crucial:
mallocing nodes and copying data on insert, andfreeing them on delete.
Preview of the Next Lesson:
We now have a complete, working hash table. However, if multiple threads try to insert or delete at the same time, we will encounter race conditions and corrupt our data structure. In the next lesson, we will finally introduce threading by designing a fine-grained locking strategy for the hash table using one mutex per bucket, making our data structure thread-safe.