Solutions: Lecture 10 In-Class Exercises¶
Solutions to the Lecture 10 in-class exercises. Try each exercise yourself before
reading these. Every keyboard solution starts from the same includes, type
definitions, and given ht_create, and reuses the hash_string from B1:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
struct entry {
char *key;
int value;
struct entry *next;
};
typedef struct entry entry_t;
struct hash_table {
entry_t **buckets;
int nbuckets;
};
typedef struct hash_table hash_table_t;
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¶
Toy hash: sum of alphabet positions (a = 1 ... z = 26), then % 5.
- The five sums and buckets:
cat = 3 + 1 + 20 = 24 24 % 5 = 4
dog = 4 + 15 + 7 = 26 26 % 5 = 1
ax = 1 + 24 = 25 25 % 5 = 0
bee = 2 + 5 + 5 = 12 12 % 5 = 2
ox = 15 + 24 = 39 39 % 5 = 4
-
catandoxcollide: both land in bucket 4. -
Inserting in the order
cat, dog, ax, bee, oxwithpush_front, the table is:
buckets[0] -> [ax | ... | NULL]
buckets[1] -> [dog | ... | NULL]
buckets[2] -> [bee | ... | NULL]
buckets[3] -> NULL
buckets[4] -> [ox | ... | -] -> [cat | ... | NULL]
Bucket 4 holds a two-node chain. ox was inserted after cat, and push_front
puts the newest entry at the head, so the chain is ox -> cat.
- With
nbuckets = 1, every word takes% 1 = 0, so all five land inbuckets[0]:
That is a single linked list - exactly last lecture's structure. Lookup has to walk it, so it is back to O(n). The array of buckets only helps when the hash spreads keys across more than one of them.
Exercise A2 - Spot the bug¶
- In (a),
e->key == keycompares the two pointers (addresses), not the characters they point at. Two strings with the same letters can live at different addresses, so this isfalseeven when the key is present. Compare the contents withstrcmp:
- In (b),
ht_freefrees the bucket array and the struct but never frees the entries or their keys - every entry and everystrcpy'd key leaks. Walk each chain and free both, savingnextfirst:
void ht_free(hash_table_t *ht) {
for (int i = 0; i < ht->nbuckets; i++) {
entry_t *cur = ht->buckets[i];
while (cur != NULL) {
entry_t *next = cur->next; /* save before freeing */
free(cur->key);
free(cur);
cur = next;
}
}
free(ht->buckets);
free(ht);
}
Part B - At the keyboard¶
Exercise B1 - hash_string and the bucket index¶
unsigned long hash_string(const char *key) {
unsigned long h = 5381;
for (int i = 0; key[i] != '\0'; i++) {
h = h * 33 + (unsigned char) key[i];
}
return h;
}
int main(void) {
const char *words[] = { "cat", "dog", "ax", "cap" };
int nbuckets = 8;
for (int i = 0; i < 4; i++) {
unsigned long b = hash_string(words[i]) % nbuckets;
printf("%-3s -> bucket %lu\n", words[i], b);
}
return 0;
}
hmust beunsigned long: the repeatedh * 33overflows, and unsigned overflow wraps cleanly instead of going negative, so% nbucketsstays in range.- The exact bucket numbers depend on
nbuckets; the guarantees are only that a word always hashes to the same bucket and that different words spread out.catandcapdiffer by one letter but djb2 sends them to different buckets.
Exercise B2 - ht_get: look up a value¶
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 (strcmp(e->key, key) == 0) {
*out_value = e->value;
return 1;
}
}
return 0;
}
/* helper for testing before ht_put exists: hand-build one entry into its bucket */
static void hand_insert(hash_table_t *ht, const char *key, int value) {
entry_t *e = malloc(sizeof(entry_t));
e->key = malloc(strlen(key) + 1);
strcpy(e->key, key);
e->value = value;
unsigned long i = hash_string(key) % ht->nbuckets;
e->next = ht->buckets[i];
ht->buckets[i] = e;
}
int main(void) {
hash_table_t *ht = ht_create(8);
hand_insert(ht, "cat", 3);
hand_insert(ht, "dog", 5);
int v = 0;
printf("get cat -> %s, value %d\n", ht_get(ht, "cat", &v) ? "found" : "missing", v);
v = 0;
printf("get dog -> %s, value %d\n", ht_get(ht, "dog", &v) ? "found" : "missing", v);
v = 0;
printf("get fox -> %s\n", ht_get(ht, "fox", &v) ? "found" : "not found");
/* (freeing is B4) */
return 0;
}
ht_getwalks only the key's own bucket and compares withstrcmp. The out-parameter carries the value back; the return flag distinguishes "not present" from "present with value 0."
Exercise B3 - ht_put: insert or update¶
void ht_put(hash_table_t *ht, const char *key, int value) {
unsigned long i = hash_string(key) % ht->nbuckets;
for (entry_t *e = ht->buckets[i]; e != NULL; e = e->next) {
if (strcmp(e->key, key) == 0) { /* already present: update in place */
e->value = value;
return;
}
}
entry_t *e = malloc(sizeof(entry_t));
if (e == NULL) {
return;
}
e->key = malloc(strlen(key) + 1); /* own a private copy of the key */
if (e->key == NULL) {
free(e);
return;
}
strcpy(e->key, key);
e->value = value;
e->next = ht->buckets[i]; /* push_front onto the bucket */
ht->buckets[i] = e;
}
int main(void) {
hash_table_t *ht = ht_create(8);
ht_put(ht, "cat", 3);
ht_put(ht, "dog", 5);
ht_put(ht, "cat", 9); /* update, not a duplicate */
int v = 0;
ht_get(ht, "cat", &v);
printf("cat -> %d\n", v); /* 9 */
ht_get(ht, "dog", &v);
printf("dog -> %d\n", v); /* 5 */
return 0;
}
- The update loop is what keeps keys unique: without it, the second
put "cat"would add a secondcatentry. With it, the value is replaced in place. - The insert owns its key (
malloc/strcpy) and usespush_front, so it is O(1) regardless of chain length. Two keys that collide simply share a bucket's chain and are both found by theirstrcmpmatch.
Exercise B4 - ht_free: free every block¶
void ht_free(hash_table_t *ht) {
for (int i = 0; i < ht->nbuckets; i++) {
entry_t *cur = ht->buckets[i];
while (cur != NULL) {
entry_t *next = cur->next; /* save next before freeing */
free(cur->key); /* free the owned key ... */
free(cur); /* ... then the entry */
cur = next;
}
}
free(ht->buckets); /* then the array of heads */
free(ht); /* then the table itself */
}
int main(void) {
hash_table_t *ht = ht_create(8);
ht_put(ht, "cat", 3);
ht_put(ht, "dog", 5);
ht_put(ht, "ax", 1);
for (int i = 0; i < ht->nbuckets; i++) {
for (entry_t *e = ht->buckets[i]; e != NULL; e = e->next) {
printf("%s: %d\n", e->key, e->value);
}
}
ht_free(ht);
ht = NULL; /* the old pointer is now dangling */
return 0;
}
- Freeing runs inside out: each entry's key, then the entry, walking each chain, then
the bucket array, then the struct. Every
malloc(struct, bucket array, each entry, each key) has exactly onefree.
Stretch - take it further¶
Remove a key¶
int ht_remove(hash_table_t *ht, const char *key) {
unsigned long i = hash_string(key) % ht->nbuckets;
entry_t *prev = NULL;
entry_t *cur = ht->buckets[i];
while (cur != NULL && strcmp(cur->key, key) != 0) {
prev = cur;
cur = cur->next;
}
if (cur == NULL) {
return 0; /* not found */
}
if (prev == NULL) {
ht->buckets[i] = cur->next; /* removing the head of the chain */
} else {
prev->next = cur->next; /* splice cur out */
}
free(cur->key);
free(cur);
return 1;
}
- Identical pointer surgery to last lecture's
delete_value, but inside one bucket and matched bystrcmp. Theprev == NULLcase handles removing the chain's head. Because the entry owns its key, we freecur->keyas well ascur.
Grow the table (rehash)¶
hash_table_t *ht_resize(hash_table_t *ht, int new_nbuckets) {
hash_table_t *bigger = ht_create(new_nbuckets);
for (int i = 0; i < ht->nbuckets; i++) {
for (entry_t *e = ht->buckets[i]; e != NULL; e = e->next) {
ht_put(bigger, e->key, e->value); /* re-hash into the bigger table */
}
}
ht_free(ht); /* free the old table (keys were copied) */
return bigger;
}
- Each
ht_putintobiggerhashes the key againstnew_nbuckets, so entries land in new buckets - you cannot just copy the old bucket array over, because a key's bucket depends on the bucket count.ht_putmakes its own owned copy of every key, so freeing the old table afterward is safe. The caller reassigns:ht = ht_resize(ht, 32);.