Handout: Lecture 9 In-Class Exercises¶
Part A is pen-and-paper: trace the pointer surgery of a push_front and a
delete on the board, and spot two classic linked-list bugs. Part B is at the
keyboard: build the core linked-list operations one at a time - traverse, insert at
the head, delete a node, and free the whole list. None of these repeats an earlier
exercise; they drill today's moves - the self-referential node, the walk-to-NULL
loop, and the pointer surgery behind insert, delete, and free.
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 node is a self-referential struct:
struct node { int data; struct node *next; };. It must keep its tag -node_tdoes not exist yet inside its own definition. - The list is named by a
headpointer;NULLmarks the end; an empty list ishead == NULL. Traverse withfor (node_t *cur = head; cur != NULL; cur = cur->next). - Insert and delete return the new head, because both can change which
node is first. In the caller, write
head = push_front(head, x);. - To delete, unlink first (
prev->next = cur->next;) thenfree(cur)- never the other way round. - To free the list, save
cur->nextbefore youfree(cur), or you read freed memory. Onefreepermalloc, node by node.
Set up¶
Start every keyboard exercise from this node type and includes:
#include <stdio.h>
#include <stdlib.h>
struct node { int data; struct node *next; };
typedef struct node node_t;
Part A - Tracing pointers (pen and paper)¶
Exercise A1 - Draw the surgery¶
Start from this three-node list:
- Draw the list after
head = push_front(head, 5);. Which node is the new head, and what does itsnextpoint at? How many existingnextpointers changed? - Starting again from the original three-node list, draw the list after
head = delete_value(head, 20);. Which singlenextpointer changed, and which node getsfreed? - In one sentence each: why does inserting at the head touch no existing node,
while deleting
20requires a handle on the node before it?
Exercise A2 - Spot the bug¶
Each function below has one bug that today's rules warn about. Name the bug and fix it.
/* (a) free every node in the list */
void free_list(node_t *head) {
for (node_t *cur = head; cur != NULL; cur = cur->next) {
free(cur);
}
}
/* (b) insert value at the front; caller writes: push_front(head, x); */
void push_front(node_t *head, int value) {
node_t *n = malloc(sizeof(node_t));
n->data = value;
n->next = head;
head = n;
}
- In (a): the loop advances with
cur = cur->nextright afterfree(cur). What is that called, and how do you fix it? (Hint: you need a temporary.) - In (b): the caller's list never changes and one node leaks on every call. Give both reasons and rewrite the signature and the call so it works.
Part B - At the keyboard¶
Exercise B1 - Traverse: print and count¶
Build the three-node list 10 -> 20 -> 30 by hand (three mallocs, link them,
terminate with NULL), then write the two traversal functions.
void print_list(node_t *head); /* prints: 10 -> 20 -> 30 -> NULL */
int length(node_t *head); /* returns the number of nodes */
print_listwalkscur = headtoNULL, printingcur->data ->each step, thenNULL\nat the end. On an empty list (head == NULL) it prints justNULL.lengthis the same walk, counting nodes instead of printing. Test it on your three-node list and on the empty list (NULL), which should give0.- Free your nodes at the end (you will write
free_listin B4; until then, free the three by hand).
Exercise B2 - push_front: insert at the head¶
Write push_front, then use it to build a list from numbers you read in a loop.
- In
push_front:malloca node, check it againstNULL, setdata, point itsnextat the oldhead, and return the new node as the new head. - In
main: start withnode_t *head = NULL;, read integers withscanfuntil you read0, and for each dohead = push_front(head, x);. Print withprint_listfrom B1. Notice the list comes out reversed - each new value goes on the front. - Forgetting
head =in the caller leaks the new node and leaves the list unchanged - exactly the bug from A2(b).
Exercise B3 - delete_value: remove a node¶
Write delete_value, which removes the first node whose data equals a given
value and returns the (possibly new) head.
before: 10 -> 20 -> 30 -> NULL
delete 20: 10 -> 30 -> NULL
delete 10: 30 -> NULL
delete 99: 30 -> NULL (unchanged)
- Walk with two pointers,
prev(startingNULL) andcur(startinghead), stopping whencur->data == valueorcur == NULL. - If
cur == NULL, the value is not present - returnheadunchanged. Ifprev == NULL, you are deleting the head - the new head iscur->next. Otherwise splice:prev->next = cur->next;. In all found cases,free(cur)after unlinking. - Test all four cases: delete the head, a middle node, the last node, and a value that is not in the list.
Exercise B4 - free_list: free every node¶
Write free_list and use it to clean up. Confirm it is leak-free under valgrind.
- Walk the list, but save
cur->nextinto a local beforefree(cur), then advance to the saved value. Freeing first and then readingcur->nextis a use-after-free (the bug from A2(a)). - In
main, after you are done with the list, callfree_list(head)and sethead = NULL. Build a list with B2, print it, delete a node or two with B3, then free it - and run the whole thing undervalgrind.
Stretch - take it further¶
Append at the tail¶
Add push_back, which inserts a new node at the end of the list (the opposite
end from push_front), and returns the head.
- Make the new node with
next = NULL(it will be the last one). If the list is empty (head == NULL), the new node is the head - return it. Otherwise walk to the last node (the one whosenext == NULL) and point itsnextat the new node, then return the unchangedhead. - Notice the asymmetry:
push_frontis O(1), butpush_backmust walk the whole list to find the end - O(n) - because a plain singly linked list only holds ahead. (Keeping atailpointer too would make it O(1); that is a design we will revisit.)