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:
- List *new_list() - Create and return a new empty list. This will
consist solely of the header struct whose front and rear pointers
will be NULL.
- int list_empty(List *l) - Is a list empty?
- Node *list_front(List *l) - return a pointer to the first node on the
list, or NULL if the list is empty.
- void list_prepend(List *l, Jval v) - allocate a new node with value v
and put it on the front of the list.
- void list_append(List *l, Jval v) - allocate a new node with value v
and put it on the end of the list.
- void list_insert_after(List *l, Node *n, Jval v) - allocate a new node with value v
and put it after node n.
- void list_destroy(List *l) - free up the header structure and the nodes.
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().