CS140 Lecture notes -- Singly-Linked Lists

  • James S. Plank
  • Directory: ~cs140/www-home/notes/Slists
  • Lecture notes: http://www.cs.utk.edu/~plank/plank/classes/cs140/Notes/Slists/
  • Tue Sep 25 16:58:20 EDT 2007
    Singly-linked lists are useful but limited data structures. For that reason, I do not go over any code, but simply talk about them in the abstract. Think about implementation though -- for example, were you given concrete typedef and procedure prototypes for a singly-linked list, could you implement them?

    A singly linked list is like a queue, except you can look at the nodes in the list without destroying the lists. A typical linked list implementation will be something like the following. There will be a header structure and a structure for the nodes of a list. Here is an example:

    A linked list implementation will support the following functionalities: Typically, the Node structure is exposed as part of the header file, so that the user may traverse the list with a for() loop like the following:
      Node *n;
      List *l;
    
      .....
    
      for (n = l->front; n != NULL; n = n->link) {
        /* Do something with n */
      }
    
    Think about how you would implement the various procedures above. For example, here is an implementation of list_append():

    void list_append(List *l, Jval v)
    {
      Node *n;
    
      n = (Node *) malloc(sizeof(Node));
      if (n == NULL) { perror("malloc"); exit(1); }
      n->val = v;
      n->link = NULL;
      l->size++;
      if (l->rear == NULL) {
        l->front = n;
      } else {
        l->rear->link = n;
      }
      l->rear = n;
    }
    


    Using a Sentinel Node

    A standard implementation trick with linked lists is to employ a sentinel node instead of the header. A sentinel node is a list node that does not hold any values. Instead, it is what the main list pointer points to. Its link points to the first node on the list.

    An additional implementational trick is to have the last node point to the sentinel rather than than to NULL. Thus, an empty list would have one node - the sentinel - whose link field points to itself. The pictures below show an empty list, and a list with three elements:

    Now to traverse a list, you have to change your for() loop:
      Node *n, *l;
    
      .....
    
      for (n = l->link; n != l; n = n->link) {
        /* Do something with n */
      }
    
    Using this structure makes implementation very easy, although it has its limitations. For example, you can't perform appending on the list because you don't have a pointer to the rear. However, take a look at hos easy it is to implement list_prepend():

    void list_prepend(Node *l, Jval v)
    {
      Node *n;
    
      n = (Node *) malloc(sizeof(Node));
      if (n == NULL) { perror("malloc"); exit(1); }
      n->val = v;
      n->link = l->link;
      l->link = n;
    }
    

    Since we have the sentinel, we don't need to have special-case code for the empty list.


    A note about list_insert_after() above

    You'll note that one of the parameters to list_insert_after() is the list itself. Why? Well, for two reasons. First, if the size is part of the list header, then you need to include the header when you add a node to the list. Second, if you are going to support appending to the end of the list, you need a pointer to the last node on the list. If you don't have the list in list_insert_after(), then you can't modify the pointer to the last node if you happen to call list_insert_after() on the last node. If you don't support appending and if you don't maintain the size of the list in the header, then you don't need to include a pointer to the list in list_insert_after().