The implementation of each dllist is as a circular doubly-linked list with a sentinel node. Each node has two pointers -- a forward link (next) to the next node on the list, and a backward link (prev) to the previous node on the list. A Dllist container object contains a pointer to the sentinel node and is the object that is returned to the user.
The list is circular in both directions -- the sentinel's next points to the first node on the list, and its prev points to the last node on the list. The first node's prev points to the sentinel, as does the last node's next.
Some ascii art: Here's an empty list l:
l -->sentinel_node-----------+--> |-----------|
| | next ---------\
| | prev -------\ |
| | val = ? | | |
| |-----------| | |
| | |
\-------------------+-/
Note that the sentinel node's next and prev fields simply point
back to itself.
Here's the list after calling dll_append(l, p) (or dll_prepend(l, p) for that matter). We will assume that p contains the pointer address 0x100:
l -->sentinel_node-----------+--> |-----------| |-------------|
| | next -----|------>| next ----------\
| --| prev |<------|-prev | |
| | | val = ? | | val = 0x100 | |
| | |-----------| /-->|-------------| |
| \----------------/ |
\---------------------------------------+-/
Here's that list after calling dll_append(l, p1) with p1 having
the value 0x200:
l -->sentinel_node-------->|--------| |-------------| |-------------|
/--->| next --|----->| next -------|----->| next -------|----\
| /-|-prev |<-----|-prev |<-----|-prev |<-\ |
| | | val = ?| | val = 0x100 | | val = 0x200 | | |
| | |--------| |-------------| |-------------| | |
| | | |
| \-------------------------------------------------------/ |
| |
\------------------------------------------------------------/
I won't go over more examples with ascii art. You should be getting
the hang of this by now.
The API for doubly linked lists is in dllist.h. Like singly lists, it defines two structs: a container struct called Dllist that holds administrative information about the list, and a node struct called Dllist_Node that contains the information for a single node in the list.
typedef struct dllist_node {
struct dllist_node *next;
struct dllist_node *prev;
void *val;
} Dllist_Node;
typedef struct {
Dllist_Node *sentinel_node;
} Dllist;
Here are the operations supported by dllist.o:
The implementations for several of the dllist functions are given here, and you will be expected to write the rest in lab:
The code to create a new dllist performs the following set of tasks:
Dllist* new_dllist()
{
Dllist *d;
Dllist_Node *node;
d = (Dllist *) malloc (sizeof(Dllist));
node = (Dllist_Node *)malloc(sizeof(Dllist_Node));
d->sentinel_node = node;
node->next = node;
node->prev = node;
return d;
}
The code to destroy a dllist performs the following two tasks:
void free_dllist(Dllist *l)
{
while (!dll_empty(l)) {
dll_delete_node(dll_first(l));
}
free(l->sentinel_node);
free(l);
}
There are four ways we can insert into a dllist:
These notes show you the implementation for dll_insert_before. You will implement the remaining three functions in lab.
dll_insert_before(node, value) needs to create a new node and insert it before node. The following list of steps will accomplish this task:
Dllist_Node *dll_insert_before(Dllist_Node *node, void *v)
{
Dllist_Node *newnode;
Dllist_Node *prev_node = node->prev;
newnode = (Dllist_Node *) malloc (sizeof(Dllist_Node));
newnode->val = v;
newnode->next = node;
newnode->prev = prev_node;
node->prev = newnode;
prev_node->next = newnode;
return newnode;
}
You can use similar logic to write the other insertion functions, or you can take
advantage of the following observations and make each of the other insertion functions
call dll_insert_before:
You will write the dll_delete_node function in lab.
The dll_first function returns the first element after the sentinel node:
Dllist_Node *dll_first(Dllist *l) {
return l->sentinel_node->next;
}
Similarly,
the dll_last function returns the first element before the sentinel node.
Dllist_Node *dll_last(Dllist *l) {
return l->sentinel_node->prev;
}
The dll_next function returns the node after the current one in the list and the dll_prev function returns the node before the current one in the list:
Dllist_Node *dll_next(Dllist_Node *n) {
return n->next;
}
Dllist_Node *dll_prev(Dllist_Node *n) {
return n->prev;
}
The implementations for the other accessor functions are similarly trivial
and can be found in dllist.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 score is greater than the score of the person 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 score
* is greater than the score of the inserted person. 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_Node *current_node;
Person *current_person;
for (current_node = dll_first(student_list);
current_node != dll_sentinel(student_list);
current_node = dll_next(current_node)) {
current_person = (Person *)dll_val(current_node);
if (p->score < current_person->score)
break;
}
dll_insert_before(current_node, p);
}
...
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:
If one of these conditions is not met, then use a doubly linked list.