Solutions: Lecture 9 In-Class Exercises¶
Solutions to the Lecture 9 in-class exercises. Try each exercise yourself before reading these. Every keyboard solution starts from the same node type:
#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¶
- After
head = push_front(head, 5);:
The new node holding 5 is the head; its next points at the old first node
(10). No existing next pointer changed - only the brand-new node's next
was set, and head was reassigned. That is why front insertion is O(1).
- After
head = delete_value(head, 20);:
Exactly one existing next changed: node 10's next now points at node
30, skipping over 20. Node 20 is unlinked and then freed.
- Inserting at the head touches no existing node because the new node simply points
at the current head and becomes the head - nothing already in the list has to
change. Deleting
20needs a handle on the node before it (10) because unlinking20means rewriting10'snext, and from20alone you cannot reach its predecessor (thenextpointers only go forward).
Exercise A2 - Spot the bug¶
- In (a) the loop does
cur = cur->nextafterfree(cur)- it reads thenextfield of a node it just freed. That is a use-after-free. Fix it by savingnextin a local before freeing:
void free_list(node_t *head) {
node_t *cur = head;
while (cur != NULL) {
node_t *next = cur->next; /* save before freeing */
free(cur);
cur = next;
}
}
- In (b) there are two problems. First,
headis a parameter passed by value, sohead = n;only changes the function's local copy - the caller'sheadis untouched. Second, because the caller never learns the new node's address, that node is leaked on every call. Both are fixed by returning the new head and having the caller reassign:
node_t *push_front(node_t *head, int value) {
node_t *n = malloc(sizeof(node_t));
if (n == NULL) {
return head;
}
n->data = value;
n->next = head;
return n; /* new head */
}
/* caller: */ head = push_front(head, x);
Part B - At the keyboard¶
Exercise B1 - Traverse: print and count¶
#include <stdio.h>
#include <stdlib.h>
struct node { int data; struct node *next; };
typedef struct node node_t;
void print_list(node_t *head) {
for (node_t *cur = head; cur != NULL; cur = cur->next) {
printf("%d -> ", cur->data);
}
printf("NULL\n");
}
int length(node_t *head) {
int n = 0;
for (node_t *cur = head; cur != NULL; cur = cur->next) {
n++;
}
return n;
}
int main(void) {
node_t *a = malloc(sizeof(node_t));
node_t *b = malloc(sizeof(node_t));
node_t *c = malloc(sizeof(node_t));
if (a == NULL || b == NULL || c == NULL) {
return 1;
}
a->data = 10; a->next = b;
b->data = 20; b->next = c;
c->data = 30; c->next = NULL;
node_t *head = a;
print_list(head);
printf("length = %d\n", length(head));
free(a);
free(b);
free(c);
return 0;
}
- Both functions are the same walk: start at
head, advance withcur = cur->next, stop atNULL.print_listprints eachdata;lengthcounts. - On an empty list (
head == NULL) the loop body never runs:print_listprints justNULL, andlengthreturns0.
Exercise B2 - push_front: insert at the head¶
#include <stdio.h>
#include <stdlib.h>
struct node { int data; struct node *next; };
typedef struct node node_t;
void print_list(node_t *head) {
for (node_t *cur = head; cur != NULL; cur = cur->next) {
printf("%d -> ", cur->data);
}
printf("NULL\n");
}
node_t *push_front(node_t *head, int value) {
node_t *n = malloc(sizeof(node_t));
if (n == NULL) {
return head; /* out of memory: unchanged */
}
n->data = value;
n->next = head; /* point at the old first node */
return n; /* the new node is the new head */
}
void free_list(node_t *head) {
node_t *cur = head;
while (cur != NULL) {
node_t *next = cur->next;
free(cur);
cur = next;
}
}
int main(void) {
node_t *head = NULL;
int x;
printf("enter numbers (0 to stop): ");
while (scanf("%d", &x) == 1 && x != 0) {
head = push_front(head, x);
}
printf("list: ");
print_list(head);
free_list(head);
head = NULL;
return 0;
}
push_frontallocates a node, checks it, setsdata, pointsnextat the old head, and returns the new node. The caller must writehead = push_front(...)or the node leaks and the list does not change (the A2(b) bug).- Because each new value is placed on the front, the finished list is in reverse order of entry.
Exercise B3 - delete_value: remove a node¶
#include <stdio.h>
#include <stdlib.h>
struct node { int data; struct node *next; };
typedef struct node node_t;
void print_list(node_t *head) {
for (node_t *cur = head; cur != NULL; cur = cur->next) {
printf("%d -> ", cur->data);
}
printf("NULL\n");
}
node_t *push_front(node_t *head, int value) {
node_t *n = malloc(sizeof(node_t));
if (n == NULL) {
return head;
}
n->data = value;
n->next = head;
return n;
}
node_t *delete_value(node_t *head, int value) {
node_t *prev = NULL;
node_t *cur = head;
while (cur != NULL && cur->data != value) {
prev = cur;
cur = cur->next;
}
if (cur == NULL) {
return head; /* not found: unchanged */
}
if (prev == NULL) {
head = cur->next; /* deleting the head */
} else {
prev->next = cur->next; /* splice cur out */
}
free(cur);
return head;
}
void free_list(node_t *head) {
node_t *cur = head;
while (cur != NULL) {
node_t *next = cur->next;
free(cur);
cur = next;
}
}
int main(void) {
node_t *head = NULL;
head = push_front(head, 30);
head = push_front(head, 20);
head = push_front(head, 10); /* 10 -> 20 -> 30 */
printf("before: "); print_list(head);
head = delete_value(head, 20); printf("delete 20: "); print_list(head);
head = delete_value(head, 10); printf("delete 10: "); print_list(head);
head = delete_value(head, 99); printf("delete 99: "); print_list(head);
free_list(head);
head = NULL;
return 0;
}
before: 10 -> 20 -> 30 -> NULL
delete 20: 10 -> 30 -> NULL
delete 10: 30 -> NULL
delete 99: 30 -> NULL
- The
whileloop stops at the first node matchingvalue, keepingprevone step behind. Three outcomes: not found (cur == NULL, return unchanged), deleting the head (prev == NULL, new head iscur->next), or a middle/last node (prev->next = cur->next). - In every found case the node is unlinked before
free(cur), and the function returns the head because deleting the first node changes it.
Exercise B4 - free_list: free every node¶
free_list appears in the programs above; here it is on its own, with the driver
that exercises it under valgrind.
#include <stdio.h>
#include <stdlib.h>
struct node { int data; struct node *next; };
typedef struct node node_t;
node_t *push_front(node_t *head, int value) {
node_t *n = malloc(sizeof(node_t));
if (n == NULL) {
return head;
}
n->data = value;
n->next = head;
return n;
}
void free_list(node_t *head) {
node_t *cur = head;
while (cur != NULL) {
node_t *next = cur->next; /* save before freeing */
free(cur);
cur = next;
}
}
int main(void) {
node_t *head = NULL;
for (int i = 1; i <= 5; i++) {
head = push_front(head, i * 10);
}
free_list(head);
head = NULL; /* the old address now dangles */
return 0;
}
$ valgrind ./myprog
All heap blocks were freed -- no leaks are possible
ERROR SUMMARY: 0 errors from 0 contexts
- The loop saves
cur->nextintonextbeforefree(cur), then advances to the saved value. Freeing first and then readingcur->nextwould be a use-after-free. - After freeing, the caller sets
head = NULLso no one accidentally follows the now-dangling address. Onefreepermallocmeansvalgrindreports no leaks.
Stretch¶
Append at the tail¶
#include <stdio.h>
#include <stdlib.h>
struct node { int data; struct node *next; };
typedef struct node node_t;
void print_list(node_t *head) {
for (node_t *cur = head; cur != NULL; cur = cur->next) {
printf("%d -> ", cur->data);
}
printf("NULL\n");
}
node_t *push_back(node_t *head, int value) {
node_t *n = malloc(sizeof(node_t));
if (n == NULL) {
return head;
}
n->data = value;
n->next = NULL; /* it will be the last node */
if (head == NULL) {
return n; /* empty list: the new node is the head */
}
node_t *cur = head;
while (cur->next != NULL) { /* walk to the last node */
cur = cur->next;
}
cur->next = n; /* link it on the end */
return head; /* head is unchanged */
}
void free_list(node_t *head) {
node_t *cur = head;
while (cur != NULL) {
node_t *next = cur->next;
free(cur);
cur = next;
}
}
int main(void) {
node_t *head = NULL;
head = push_back(head, 10);
head = push_back(head, 20);
head = push_back(head, 30);
print_list(head); /* 10 -> 20 -> 30 -> NULL */
free_list(head);
head = NULL;
return 0;
}
- The new node's
nextisNULLbecause it goes on the end. If the list is empty, that node becomes the head; otherwise we walk to the last node (the one whosenext == NULL) and hang the new node off it. - Because appending values in order keeps that order, this list is not reversed,
unlike the
push_frontversion. The cost is the walk to the end:push_backis O(n) on a list that tracks only itshead, whilepush_frontis O(1). Keeping a separatetailpointer would make appending O(1) too.