Skip to content

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.

  1. 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
  1. cat and ox collide: both land in bucket 4.

  2. Inserting in the order cat, dog, ax, bee, ox with push_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.

  1. With nbuckets = 1, every word takes % 1 = 0, so all five land in buckets[0]:
buckets[0] -> [ox | -] -> [bee | -] -> [ax | -] -> [dog | -] -> [cat | NULL]

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

  1. In (a), e->key == key compares the two pointers (addresses), not the characters they point at. Two strings with the same letters can live at different addresses, so this is false even when the key is present. Compare the contents with strcmp:
if (strcmp(e->key, key) == 0) {
    *out_value = e->value;
    return 1;
}
  1. In (b), ht_free frees the bucket array and the struct but never frees the entries or their keys - every entry and every strcpy'd key leaks. Walk each chain and free both, saving next first:
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;
}
  • h must be unsigned long: the repeated h * 33 overflows, and unsigned overflow wraps cleanly instead of going negative, so % nbuckets stays 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. cat and cap differ 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_get walks only the key's own bucket and compares with strcmp. 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 second cat entry. With it, the value is replaced in place.
  • The insert owns its key (malloc/strcpy) and uses push_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 their strcmp match.

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;
}
valgrind ./myprog
# All heap blocks were freed -- no leaks are possible
  • 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 one free.

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 by strcmp. The prev == NULL case handles removing the chain's head. Because the entry owns its key, we free cur->key as well as cur.

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_put into bigger hashes the key against new_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_put makes its own owned copy of every key, so freeing the old table afterward is safe. The caller reassigns: ht = ht_resize(ht, 32);.