Handout: Lecture 10 In-Class Exercises¶
Part A is pen-and-paper: hash a few words into buckets by hand and draw the
resulting array-of-lists, then spot two classic hash-table bugs. Part B is at the
keyboard: build the hash table one operation at a time - the hash function, lookup,
insert-or-update, and the nested free. None of these repeats an earlier exercise;
they drill today's moves - the hash function, the bucket index, separate chaining
over last lecture's linked lists, strcmp on keys, and heap-owned key copies.
Try each one yourself first; we will discuss in class, and the solutions are in a separate document afterward. Compile with warnings on:
Reminders that will keep you out of trouble today:
- A bucket index is
hash_string(key) % ht->nbuckets. Keep the hashunsignedso the%never gives a negative index. - Each bucket is the head of a linked list; an empty bucket is
NULL. To search a bucket, walk it:for (entry_t *e = ht->buckets[i]; e != NULL; e = e->next). - Compare keys with
strcmp(e->key, key) == 0, nevere->key == key-==compares addresses, not letters. - The table owns its keys: on insert,
malloc(strlen(key) + 1)andstrcpya private copy. On free, that copy needs its ownfree. - To free the table, save
cur->nextbefore freeing, free each entry'skeythen the entry, then freeht->buckets, thenht. Onefreepermalloc.
Set up¶
Start every keyboard exercise from these includes, type definitions, and a given
ht_create. You will write hash_string in B1 and reuse it from then on.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
struct entry {
char *key; /* heap-owned copy of the key string */
int value; /* the value mapped to this key */
struct entry *next; /* next entry in this bucket's chain, or NULL */
};
typedef struct entry entry_t;
struct hash_table {
entry_t **buckets; /* array of nbuckets chain heads */
int nbuckets; /* number of buckets */
};
typedef struct hash_table hash_table_t;
/* given: allocate a table with all buckets empty (NULL) */
hash_table_t *ht_create(int nbuckets) {
hash_table_t *ht = malloc(sizeof(hash_table_t));
ht->buckets = malloc(nbuckets * sizeof(entry_t *));
ht->nbuckets = nbuckets;
for (int i = 0; i < nbuckets; i++) {
ht->buckets[i] = NULL;
}
return ht;
}
Part A - Hashing by hand (pen and paper)¶
Exercise A1 - Put five words in their buckets¶
For this exercise only, use a toy hash that is easy to compute by hand: add up
the alphabet positions of the letters (a = 1, b = 2, ..., z = 26), then take
% nbuckets. Use nbuckets = 5. (The real hash_string you write in B1 is
stronger, but the bucketing idea is identical.)
Hash these five words: cat, dog, ax, bee, ox.
- Compute the bucket index for each word. (For example,
dogis4 + 15 + 7 = 26, and26 % 5 = 1, sodoglands in bucket 1.) - Two of these words collide into the same bucket. Which two, and which bucket?
- Draw the whole table as an array of chains,
buckets[0]throughbuckets[4], showing each chain (usepush_front, so the most recently inserted word is at the head of its chain). - Suppose you had built the table with
nbuckets = 1instead. Draw it. What structure is it now, and what does that do to lookup time?
Exercise A2 - Spot the bug¶
Each function below has one bug that today's rules warn about. Name the bug and fix it.
/* (a) return 1 and write the value if key is present, else return 0 */
int ht_get(hash_table_t *ht, const char *key, int *out_value) {
unsigned long i = hash_string(key) % ht->nbuckets;
for (entry_t *e = ht->buckets[i]; e != NULL; e = e->next) {
if (e->key == key) { /* <-- */
*out_value = e->value;
return 1;
}
}
return 0;
}
- In (a): the comparison finds nothing even for a key that is in the table. What is being compared instead of the letters, and what should it be?
- In (b): the program runs but
valgrindreports leaked blocks. What memory never gets freed? Rewriteht_freeso everymallochas itsfree.
Part B - At the keyboard¶
Exercise B1 - hash_string and the bucket index¶
Write the djb2 hash and use it to print which bucket each word lands in.
- Start
hat5381(anunsigned long), and for each character doh = h * 33 + (unsigned char) key[i], walking to the'\0'. - In
main, pick annbuckets(say 8) and printhash_string(word) % nbucketsfor several words. Your exact bucket numbers will differ from the sample - what matters is that the same word always gives the same bucket, and different words spread out. - Try enough words and you will see two land in the same bucket: a collision. That is
what B3's chaining handles. (
catandcapdiffer only in the last letter but should land in different buckets - notice how djb2 spreads them.)
Exercise B2 - ht_get: look up a value¶
Before you have an insert function, build a small table by hand to test against,
the way you hand-built a linked list last lecture. Make two entries, strcpy their
keys, and link them into their buckets. Then write ht_get.
- Hash the key to a bucket, then walk that one chain comparing with
strcmp(e->key, key) == 0. On a match, write*out_valueand return1; if the walk reachesNULL, return0. - To build the test table:
entry_t *e = malloc(sizeof(entry_t));, thene->key = malloc(strlen("cat") + 1); strcpy(e->key, "cat"); e->value = 3;, and splice it intoht->buckets[hash_string("cat") % ht->nbuckets]. - Return the flag, not the value: a missing key must be distinguishable from a value of 0.
Exercise B3 - ht_put: insert or update¶
Write ht_put, then use it to build a real table with no hand-linking.
- First walk the key's bucket. If
strcmpfinds the key already there, sete->value = valueand return - that is the update case. - Otherwise
malloca new entry, give it an owned copy of the key (malloc(strlen(key) + 1)thenstrcpy), set its value, andpush_frontit onto the bucket (e->next = ht->buckets[i]; ht->buckets[i] = e;). - Test all three cases: a fresh insert, an update (put
"cat"twice with different values and confirmht_getreturns the second), and a collision (two different keys that land in the same bucket - confirm both are findable).
Exercise B4 - ht_free: free every block¶
Write ht_free and confirm the whole program is leak-free under valgrind.
- For each bucket, walk the chain: save
cur->nextfirst, thenfree(cur->key), thenfree(cur), then advance. After the loop over all buckets,free(ht->buckets)and finallyfree(ht). - Build a table with B3, print it, then free it, and run the whole thing under
valgrind. You should see zero leaks and zero invalid frees.
Stretch - take it further¶
Remove a key¶
Add ht_remove, which deletes the entry for a key (if present) and frees both the
entry and its key. Return 1 if something was removed, 0 if the key was not there.
- This is last lecture's
delete_valuesurgery, but inside one bucket and keyed by string. Walk the bucket withprevandcur, stopping whenstrcmp(cur->key, key) == 0. Ifprev == NULLyou are removing the chain's head (ht->buckets[i] = cur->next); otherwise splice (prev->next = cur->next). Thenfree(cur->key)andfree(cur).
Grow the table (rehash)¶
When chains get long, a table performs better with more buckets. Write ht_resize,
which builds a bigger table and re-inserts every entry into it.
- Make a new table with
ht_create(new_nbuckets). Walk every chain of the old table andht_puteach entry's key and value into the new one - each key gets rehashed into a bucket of the bigger array. Thenht_freethe old table and return the new. - Notice why you cannot just copy the old buckets over: a key's bucket index depends
on
nbuckets, so changing the count sends keys to different buckets. This is the cliffhanger from the notes, and the hash-table version of a dynamic array growing by copying.