CMSC 14300: Practice Set 6 - Linked Lists and Hash Tables¶
Practice for the material in Week 5: self-referential nodes and the linked-list
traversal/pointer surgery from Lecture 9, and the hash tables from Lecture 10 - a
hash function, bucket indices, and separate chaining. None of these repeats a
lecture or homework exercise; together they drill the new moves - rewiring next
pointers, keeping a list ordered, turning a key into a bucket, and storing keyed data
in an array of chains. This is good preparation for the next quiz.
Work in a fresh directory and compile everything with warnings on. Some problems need
<stdlib.h> (for malloc/free) and <string.h> (for strlen/strcpy/strcmp):
mkdir -p ~/cmsc14300/pset06 && cd ~/cmsc14300/pset06
clang -Wall -Wextra -std=c17 problem1.c -o problem1
Keep last week's rules in view: a self-referential struct keeps its tag, you
save next before you free a node, and every malloc still has exactly one
free. Problems 1 and 2 use the linked-list node; problems 3 to 5 use hashing.
Problem 1 - Reverse a linked list¶
Write a function that reverses a linked list in place and returns the new head.
Nothing is copied and no new nodes are allocated - you only rewire the existing
next pointers.
- Walk the list with three pointers:
prev(startsNULL),cur(startshead), and a savednext. Each step: savenext = cur->next, flipcur->next = prev, then advanceprev = curandcur = next. WhencurisNULL,previs the new head - return it. - Build a small list with
push_front(from Lecture 9), print it, reverse it, print again, then free it. - Check your understanding: why must you save
cur->nextinto a local before writingcur->next = prev? What would break if you did not?
Problem 2 - Keep a list sorted (ordered insert)¶
Write insert_sorted, which inserts a value into a list that is already in
ascending order so that the list stays ascending. It returns the (possibly new)
head. Building a list entirely with insert_sorted produces a sorted list.
- Make the new node first. If the list is empty or the new value belongs before the
head (
value <= head->data), it becomes the new head - point itsnextat the old head and return it. - Otherwise walk with a
prev/curpair untilcurisNULLorcur->data >= value, then splice the new node betweenprevandcur(new->next = cur; prev->next = new;). - Read integers in a loop and
insert_sortedeach one; print the list at the end and confirm it is sorted. Free it when done. - Check your understanding: which two cases does the
prev == NULL(insert at the head) branch cover that the splice branch does not?
Problem 3 - How evenly does the hash spread? (bucket histogram)¶
A hash table is only fast when keys spread across the buckets. Write a function that
hashes a set of usernames into nbuckets and prints how many land in each
bucket - the shape of the load. You do not need a full table for this, just the hash
and a count per bucket.
unsigned long hash_string(const char *key); /* djb2, as in Lecture 10 */
void bucket_histogram(const char *names[], int n, int nbuckets);
- Make an
int counts[nbuckets](a dynamic array, or a fixed one for a known size), zero it, then for each name docounts[hash_string(name) % nbuckets]++. Print each bucket's count. - Try the same names with
nbuckets = 1and with a largernbuckets. With one bucket every name collides (a single count ofn); with more buckets the counts spread out. - Check your understanding: if one bucket's count is much larger than the others, what does that do to lookup time for the keys in that bucket, and why?
Problem 4 - A small price lookup (string keys, int values)¶
Build a hash table that maps item names to prices in cents, then answer a series of
lookup queries, printing not found for a missing item. Reuse your Lecture 10
ht_create, ht_put, ht_get, and ht_free (types repeated below for convenience).
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;
- In
main,ht_puta handful of items (for example"apple"40,"banana"25,"date"90), then run severalht_getqueries and print each result, using the found/not-found flag to printnot foundfor a miss. - Put one item twice with a new price and confirm the lookup returns the updated price - the table maps each key to exactly one value.
- Free the whole table at the end and confirm it is
valgrind-clean. - Check your understanding: why does
ht_getreturn a found/not-found flag through the return value and hand the price back through an out-parameter, instead of just returning the price?
Problem 5 - Have I seen this before? (a hash set)¶
Detect duplicate IDs in a stream. A hash set stores keys with no values: all you ask is "is this key already here?" Use a hash table whose entries hold only a key, and report each ID the first time it repeats.
int set_contains(hash_table_t *set, const char *key); /* 1 if present, else 0 */
void set_add(hash_table_t *set, const char *key); /* add if not already present */
set_containshashes to the bucket and walks the chain withstrcmp, returning1on a match.set_adddoes nothing if the key is already present; otherwise itpush_fronts a new entry that owns a copy of the key (there is no value to store).- For each incoming ID: if
set_containssays it is already there, print it as a duplicate; otherwiseset_addit. Free the set at the end. - Check your understanding: a hash set and the price table from Problem 4 use the same chaining machinery. What is the one field an entry no longer needs when the structure is a set rather than a map?
Self-check¶
You're ready for the upcoming quiz material if you can, without looking it up:
- Draw a linked list as boxes and arrows, and write the walk-to-
NULLtraversal loop from memory. Say why a self-referential node must keep its struct tag. - Rewire
nextpointers to insert at the head, insert in sorted order, and reverse a list - and say why each still returns the (possibly new) head. - Free a chain of nodes without a use-after-free, explaining why you save
nextbefore you free. - Turn a string key into a bucket index with a hash function and
% nbuckets, and say why the hash must be unsigned. - Explain what a collision is, why it is unavoidable, and how separate chaining (an array of linked lists) resolves it.
- Look up, insert-or-update, and free a string-keyed hash table: compare keys with
strcmp(never==), give each entry an owned copy of its key, and free every key and entry plus the bucket array. Say what the load factor is and when a hash table degrades to O(n).