Skip to content

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

  1. After head = push_front(head, 5);:
head -> [5 | -] -> [10 | -] -> [20 | -] -> [30 | NULL]

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).

  1. After head = delete_value(head, 20);:
head -> [10 | -] -> [30 | NULL]         (node 20 is freed)

Exactly one existing next changed: node 10's next now points at node 30, skipping over 20. Node 20 is unlinked and then freed.

  1. 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 20 needs a handle on the node before it (10) because unlinking 20 means rewriting 10's next, and from 20 alone you cannot reach its predecessor (the next pointers only go forward).

Exercise A2 - Spot the bug

  1. In (a) the loop does cur = cur->next after free(cur) - it reads the next field of a node it just freed. That is a use-after-free. Fix it by saving next in 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;
    }
}
  1. In (b) there are two problems. First, head is a parameter passed by value, so head = n; only changes the function's local copy - the caller's head is 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;
}
10 -> 20 -> 30 -> NULL
length = 3
  • Both functions are the same walk: start at head, advance with cur = cur->next, stop at NULL. print_list prints each data; length counts.
  • On an empty list (head == NULL) the loop body never runs: print_list prints just NULL, and length returns 0.

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;
}
enter numbers (0 to stop): 10 20 30 0
list: 30 -> 20 -> 10 -> NULL
  • push_front allocates a node, checks it, sets data, points next at the old head, and returns the new node. The caller must write head = 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 while loop stops at the first node matching value, keeping prev one step behind. Three outcomes: not found (cur == NULL, return unchanged), deleting the head (prev == NULL, new head is cur->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->next into next before free(cur), then advances to the saved value. Freeing first and then reading cur->next would be a use-after-free.
  • After freeing, the caller sets head = NULL so no one accidentally follows the now-dangling address. One free per malloc means valgrind reports 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;
}
10 -> 20 -> 30 -> NULL
  • The new node's next is NULL because it goes on the end. If the list is empty, that node becomes the head; otherwise we walk to the last node (the one whose next == NULL) and hang the new node off it.
  • Because appending values in order keeps that order, this list is not reversed, unlike the push_front version. The cost is the walk to the end: push_back is O(n) on a list that tracks only its head, while push_front is O(1). Keeping a separate tail pointer would make appending O(1) too.