CS140 Lecture notes -- Doubly Linked Lists

  • Jim Plank (with modifications by Brad Vander Zanden)
  • Directory: ~cs140/www-home/notes/Dllists
  • Lecture notes: http://www.cs.utk.edu/~cs140/notes/Dllists
  • Tue Mar 2 20:00:38 EST 1999

    Doubly Linked Lists

    This is the last linked list data structure that I'll go over, and it is by far the most useful. In fact, it's really the only one I use because it is the most flexible.

    Doubly linked lists are like singly linked lists, except each node has two pointers -- one to the next node, and one to the previous node. This makes life nice in many ways:

    The API for doubly linked lists is in dllist.h. It defines a doubly-linked list node:
    typedef struct dllist {
      struct dllist *flink;
      struct dllist *blink;
      Jval val;
    } *Dllist;
    

    Here are the operations supported by dllist.o:

    Finally, there are two macros for traversing dllists forwards and backwards. ptr should be a Dllist and list should be a Dllist:
    #define dll_traverse(ptr, list) \
      for (ptr = (list)->flink; ptr != (list); ptr = ptr->flink)
    #define dll_rtraverse(ptr, list) \
      for (ptr = (list)->blink; ptr != (list); ptr = ptr->blink)
    

    Implementation

    The implementation of each dllists is as a circular doubly-linked list with a sentinel node. The code is in dllist.c.

    The typedef for a dllist node is:

    typedef struct dllist {
      struct dllist *flink;
      struct dllist *blink;
      Jval val;
    } *Dllist;
    
    Note that each node has two pointers -- a forward link (flink) to the next node on the list, and a backward link (blink) to the previous node on the list. A Dllist is a pointer to the sentinel node.

    The list is circular in both directions -- the sentinel's flink points to the first node on the list, and its blink points to the last node on the list. The first node's blink points to the sentinel, as does the last node's flink.

    Some ascii art: Here's an empty list l:

    l -------------+--> |-----------|
                   |    | flink ---------\
                   |    | blink -------\ |
                   |    | val = ?   |  | |
                   |    |-----------|  | |
                   |                   | |
                   \-------------------+-/
    
    And here's that list after calling dll_append(l, new_jval_i(3));: (or dll_prepend(l, new_jval_i(3)) for that matter).
    l -------------+--> |-----------|  /-+->|-----------|
                   |    | flink -------/ |  | flink ---------\
                   |    | blink ---------/  | blink -------\ |
                   |    | val = ?   |       | val.i = 3 |  | |
                   |    |-----------|       |-----------|  | |
                   |                                       | |
                   \---------------------------------------+-/
    
    Actually, it makes the drawing cleaner to have the back links go backwards:
    l ----------------->|-----------|       |-----------|
                   /--->| flink ----------->| flink ---------\
                   |  /------ blink |<----------- blink |<-\ |
                   |  | | val = ?   |       | val.i = 3 |  | |
                   |  | |-----------|       |-----------|  | |
                   |  |                                    | |
                   |  \------------------------------------/ |
                   |                                         |
                   \-----------------------------------------/
    
    Here's that list after calling dll_append(l, new_jval_i(5));:
    l ---------->|-----------|       |-----------|       |-----------|
            /--->| flink ----------->| flink ----------->| flink ---------\
            |  /------ blink |<----------- blink |<----------- blink |<-\ |
            |  | | val = ?   |       | val.i = 3 |       | val.i = 5 |  | |
            |  | |-----------|       |-----------|       |-----------|  | |
            |  |                                                        | |
            |  \--------------------------------------------------------/ |
            |                                                             |
            \-------------------------------------------------------------/
    
    I won't go over more examples with ascii art. You should be getting the hang of this by now.

    Many of the procedure implementations are trivial:


    Dllist new_dllist() { Dllist d; d = (Dllist) malloc (sizeof(struct dllist)); d->flink = d; d->blink = d; return d; }
    dll_empty(Dllist l) { return (l->flink == l); }
    free_dllist(Dllist l) { while (!dll_empty(l)) { dll_delete_node(dll_first(l)); } free(l); }
    Dllist dll_first(Dllist d) { return d->flink; }
    Dllist dll_last(Dllist d) { return d->blink; }
    Dllist dll_nil(Dllist l) { return l; }
    Dllist dll_next(Dllist d) { return d->flink; }
    Dllist dll_prev(Dllist l) { return (Dllist) (l->blink); }
    Jval dll_val(Dllist l) { return l->val; }
    The only subtle pieces of code are dll_insert_b() and dll_delete_node. With dll_insert_b(n, v) we malloc() a new node, set its value to v, and then link it into the list right before n. This means that we set the new node's flink field to n, and its flink field to n->blink. Then we set n->blink to the new node, and the old n->blink's flink field to the new node. Here's the code:
    dll_insert_b(Dllist node, Jval v)       /* Inserts before a given node */
    {
      Dllist new;
    
      new = (Dllist) malloc (sizeof(struct dllist));
      new->val = v;
    
      new->flink = node;
      new->blink = node->blink;
      new->flink->blink = new;
      new->blink->flink = new;
    }
    
    Once we have dll_insert_b() the other three list insertion routines are simply calls to dll_insert_b():
    dll_insert_a(Dllist n, Jval val)        /* Inserts after a given node */
    {
      dll_insert_b(n->flink, val);
    } 
    
    dll_append(Dllist l, Jval val)     /* Inserts at the end of the list */
    { 
      dll_insert_b(l, val);
    }
    
    dll_prepend(Dllist l, Jval val)    /* Inserts at the beginning of the list */
    { 
      dll_insert_b(l->flink, val);
    } 
    
    Deletion is pretty easy too. First you must remove the node n's from the list by setting n->flink->blink to n->blink and by setting n->blink->flink to n->flink. Then you free n:
    dll_delete_node(Dllist node)            /* Deletes an arbitrary iterm */
    {
      node->flink->blink = node->blink;
      node->blink->flink = node->flink;
      free(node);
    }
    

    Usage examples

    The first example is one of our standards: reversing standard input. This is simple enough to need no explanation. It's in dllreverse.c.

    The second example updates our sorting program from the singly linked list lecture. The code is identical except for the code for inserting items into the linked list. Recall that the strategy for inserting a name into a linked list is to traverse the nodes in the list until we find a node whose name is alphabetically greater than the name we want to insert. We then insert the new name before this node. When we use a singly linked list we need to maintain a pointer to the previous node as well as the current node because a singly linked list cannot support an "insert before" operation. However, a node in a doubly linked list has a pointer to its predecessor and therefore doubly linked lists can support an "insert before" operation. As a result we do not need to maintain the prev node pointer anymore and our insertion code is simplified:

    #include <stdio.h>
    #include <string.h>
    #include "fields.h"
    #include "dllist.h"
    
    typedef struct {
      char *fname;
      char *lname;
      double score;
    } Person;
    
    /* Insert the new person into the list while maintaining a sorted order.
     * we need to traverse through the list until we find a node whose name
     * is alphabetically greater than the inserted name. We then need to
     * insert the new node *before* the node at which we stopped. Doubly
     * linked lists easily support this operation while singly linked lists
     * do not. 
     * 
     */
    void insert_person(Person *p, Dllist student_list) {
      Dllist current_node;
      Person *current_person;
    
      /* I could have used dll_traverse here but I want to make explicit
       * how list traversal occurs.
       */
      for (current_node = dll_first(student_list); 
            current_node != dll_nil(student_list);
    	current_node = dll_next(current_node)) {
        current_person = (Person *)current_node->val.v;
        if (strcmp(p->lname, current_person->lname) < 0)
          break;
      }
      dll_insert_b(current_node, new_jval_v((void *)p));
    }
    
    ...
    


    Comparison Between Singly Linked and Doubly Linked Lists

    At the beginning of these notes I cited a number of advantages that doubly linked lists have over singly linked lists. Now that you have seen how doubly linked lists are implemented you will probably agree that they are a little more complicated to implement and take a little more space than singly linked lists. Space is normally not an issue in today's computers. If you have an application that requires a linked list implementation and you have a pre-defined library such as the dllists library available, you should use it. If you have to write your own code, then use a singly linked list if:

    1. You only need to insert at the front or back of the list
    2. You only need to delete from the front of the list
    3. You only need to traverse the list in the forward direction

    If one of these conditions is not met, then use a doubly linked list.