Structs and Linked Lists

Table of Contents

toggle-theme.png

Problem

Linked Lists are a simple data structure that consist of a node that has a piece of data, and a pointer to the next node as it’s elements. This is simple to represent in C using structs, where if we have a Node struct, it would have two elements: a piece of data, and a pointer to a Node struct.

Your goal is to create 5 functions that you’ll need to work with Linked Lists in C.

The Node struct

Before begining, we need to know what our linked list will be represented as. You need to create a new type struct called Node that contains 2 attributes: the first one will be the data for that element. We’re going to assume that it’s a string for now, although you can make it anything. The second attribute is going to be a pointer to the next Node.

// We use typedef so that we can avoid writing struct Node everywhere
// instead of just Node.
// Note that with typedef we also need to include the name after since
// the struct and the type don't necessarily need to have the same name.
typedef struct Node {
  char *data;
  struct Node *next;
} Node;

llCons

The llCons function will return a Node struct and should take in 2 arguments: a variable that contains data as the same type as your Node struct. We’ll assume a string for now. And the second should be a struct that will become the tail of the returned Node.

Example usage:

int main() {
  Node el3 = llCons("element 3", null);
  Node el2 = llCons("element 2", &el3);
  Node el1 = llCons("element 1", &el2);

  printf("%s", el1.data);           // element 1
  printf("%s", el1.next.data);      // element 2
  printf("%s", el1.next.next.data); // element 3

  return 0;
}
Node *llCons(char *data, Node *next) {
  Node *newNode = malloc(sizeof(Node));
  newNode->data = data;
  newNode->next = next;
  return newNode;
}

llNew

The llNew function should return a new list given the elements in an array. Given an array of strings (or whatever the datatype you’re using is) and the length of the array, llNew should create a new linked list and return it’s head.

int main() {
  Node list = llNew({"element 1", "element 2", "element 3"}, 3);

  printf("%s", list.data);           // element 1
  printf("%s", list.next.data);      // element 2
  printf("%s", list.next.next.data); // element 3

  return 0;
}
Node *llNew(char **strs, int count) {
  if (count == 0) {
    return NULL;
  }

  //  Here we're doing pointer arithmetic and manually incrementing the
  //  value of our pointer by 1 so that the "first element" in the array
  //  passed in recursively becomes the next element sequentially since
  //  arrays are contiguous in memory.
  Node *next = llNew(strs + 1, count - 1);
  return llCons(strs[0], next);
}

llHead

llHead should accept a Node as an argument and return the data for the first element in the linked list.

int main() {
  Node list = llNew({"element 1", "element 2", "element 3"}, 3);

  printf("%s", llHead(list)); // element 1

  return 0;
}
char *llHead(Node *list) {
  // We use -> because list is a pointer, this dereferences for us.
  return list->data;
}

llTail

llTail should take in a Node and return the tail of the list.

int main() {
  Node list = llNew({"element 1", "element 2", "element 3"}, 3);

  printf("%s", llHead(llTail(list))); // element 2

  return 0;
}
char *llTail(Node *list) {
  // Pretty much just the same as llHead
  return list->next;
}

llNth

llNth should take a list as an argument and an integer, and return the data for the nth element in the list. If the number is greater than the length of the list, return null.

int main() {
  Node list = llNew({"element 1", "element 2", "element 3"}, 3);

  printf("%s", llNth(list, 0)); // element 1
  printf("%s", llNth(list, 1)); // element 2
  printf("%s", llNth(list, 2)); // element 3

  return 0;
}
char *llNth(Node *list, int n) {
  Node *curr = list;
  for (int i = 0; i < n; i++) {
    if (curr->next != NULL) {
      curr = curr->next;
    } else {
      return NULL;
    }
  }

  return curr->data;
}

llAppend

llAppend should take 2 Nodes as arguments and return a Node. The function should find the last Node in the first list passed in, and make it’s next be the head of the second list passed in.

int main() {
  Node list1 = llNew({"element 1", "element 2", "element 3"}, 3);
  Node list2 = llNew({"element 4", "element 5", "element 6"}, 3);
  Node appended = llAppend(list1, list2);

  printf("%s", llNth(appended, 1)); // element 2
  printf("%s", llNth(appended, 3)); // element 4
  printf("%s", llNth(appended, 5)); // element 6

  return 0;
}
Node *llAppend(Node *list1, Node *list2) {
  Node *last = list1;

  while (last->next != NULL) {
    last = last->next;
  }

  last->next = list2;
  return list1;
}

While this comes after the section of the course on Lisp, I hope that this helps you understand the Lisp functions a little better. Here we’ve implemented cons, list, car, cdr, nth and append, all important functions to know for Lisp programming.

Note that the llAppend function differs from append in Lisp because in Lisp it will return a new list, whereas the one we’ve implemented here will mutate the first list passed in. As such, this is more similar to the nconc function rather than append, but we didn’t cover nconc in class.

Author: Philip Dumaresq

Email: phdumaresq@protonmail.com

Created: 2021-03-13 Sat 19:09