0% found this document useful (0 votes)
43 views17 pages

10 Linkedlist

This document discusses linked lists and their use to implement stacks and queues. It begins by defining a linked list node struct containing an element data and a pointer to the next node. Linked lists allow easy insertion and deletion but slower access to middle elements than arrays. The document discusses list segments, checking for circularity in O(n) time rather than O(n^2) time, and ensures specification functions like is_segment always terminate.

Uploaded by

rishitibest123
Copyright
© © All Rights Reserved
Available Formats
Download as PDF, TXT or read online on Scribd
Download as pdf or txt
0% found this document useful (0 votes)
43 views17 pages

10 Linkedlist

This document discusses linked lists and their use to implement stacks and queues. It begins by defining a linked list node struct containing an element data and a pointer to the next node. Linked lists allow easy insertion and deletion but slower access to middle elements than arrays. The document discusses list segments, checking for circularity in O(n) time rather than O(n^2) time, and ensures specification functions like is_segment always terminate.

Uploaded by

rishitibest123
Copyright
© © All Rights Reserved
Available Formats
Download as PDF, TXT or read online on Scribd
Download as pdf or txt
Download as pdf or txt
You are on page 1/ 17

Lecture 10 Notes

Linked Lists
15-122: Principles of Imperative Computation (Spring 2016)
Frank Pfenning, Rob Simmons, André Platzer

1 Introduction
In this lecture we discuss the use of linked lists to implement the stack and
queue interfaces that were introduced in the last lecture. The linked list im-
plementation of stacks and queues allows us to handle lists of any length.
This fits as follows with respect to our learning goals:

Computational Thinking: We discover that arrays contain implicit infor-


mation, namely the indices of elements, which an be made explicit as
the addresses of the nodes of a linked list. We also encounter the no-
tion of trade-off, as arrays and linked lists have different advantages
and drawbacks and yet achieve similar purposes.

Algorithms and Data Structures: We explore linked lists, a data structure


used pervasively in Computer Science, and examine some basic algo-
rithms about the.

Programming: We see that programming algorithms for linked can be


tricky, which exposes once more the power of stating and checking
invariant. We use linked lists to implement stacks and queues.

2 Linked Lists
Linked lists are a common alternative to arrays in the implementation of
data structures. Each item in a linked list contains a data element of some
type and a pointer to the next item in the list. It is easy to insert and delete
elements in a linked list, which are not natural operations on arrays, since
arrays have a fixed size. On the other hand access to an element in the
middle of the list is usually O(n), where n is the length of the list.

L ECTURE N OTES
Linked Lists L10.2

An item in a linked list consists of a struct containing the data element


and a pointer to another linked list. In C0 we have to commit to the type
of element that is stored in the linked list. We will refer to this data as
having type elem, with the expectation that there will be a type definition
elsewhere telling C0 what elem is supposed to be. Keeping this in mind
ensures that none of the code actually depends on what type is chosen.
These considerations give rise to the following definition:

L ECTURE N OTES
Linked Lists L10.3

1 struct list_node {
2 elem data;
3 struct list_node* next;
4 };
5 typedef struct list_node list;
This definition is an example of a recursive type. A struct of this type
contains a pointer to another struct of the same type, and so on. We usually
use the special element of type t*, namely NULL, to indicate that we have
reached the end of the list. Sometimes (as will be the case for our use of
linked lists in stacks and queues), we can avoid the explicit use of NULL and
obtain more elegant code. The type definition is there to create the type
name list, which stands for struct list_node, so that a pointer to a list
node will be list*. We could also have written these two statements in the
other order, to make better use of the type definition:
1 typedef struct list_node list;
2 struct list_node {
3 elem data;
4 list* next;
5 };
There are some restriction on recursive types. For example, a declara-
tion such as
1 struct infinite {
2 int x;
3 struct infinite next;
4 }
would be rejected by the C0 compiler because it would require an infinite
amount of space. The general rule is that a struct can be recursive, but
the recursion must occur beneath a pointer or array type, whose values are
addresses. This allows a finite representation for values of the struct type.
We don’t introduce any general operations on lists; let’s wait and see
what we need where they are used. Linked lists as we use them here are
a concrete type which means we do not construct an interface and a layer of
abstraction around them. When we use them we know about and exploit
their precise internal structure. This is in contrast to abstract types such as
queues or stacks whose implementation is hidden behind an interface, ex-
porting only certain operations. This limits what clients can do, but it al-
lows the author of a library to improve its implementation without having

L ECTURE N OTES
Linked Lists L10.4

to worry about breaking client code. Concrete types are cast into concrete
once and for all.

3 List segments
A lot of the operations we’ll perform in the next few lectures are on segments
of lists: a series of nodes starting at start and ending at end.

data   next   data   next   data   next   data   next  


  …  
x1     x2   xn  

start   end  

This is the familiar structure of an “inclusive-lower, exclusive-upper” bound:


we want to talk about the data in a series of nodes, ignoring the data in
the last node. That means that, for any non-NULL list node pointer l, a
segment from l to l is empty (contains no data). Consider the following
structure:

According to our definition of segments, the data in the segment from a1 to


a4 is the sequence 3, 7, 3, the data in the segment from a2 to a3 contains the
sequence 7, and the data in the segment from a1 to a1 is the empty sequence.
Note that, if we compare the pointers a1 and a3, C0 will tell us they are not
equal — even though they contain the same data they are different locations
in memory.
Given an inclusive beginning point start and an exclusive ending point
end, how can we check whether we have a segment from start to end? The

L ECTURE N OTES
Linked Lists L10.5

simple idea is to follow next pointers forward from start until we reach end.
If we reach NULL instead of end then we know that we missed our desired
endpoint, so that we do not have a segment. (We also have to make sure
that we say that we do not have a segment if either start or end is NULL, as
that is not allowed by our definition of segments above.) We can implement
this simple idea in all sorts of ways:

Recursively:
1 bool is_segment(list* start, list* end) {
2 if (start == NULL) return false;
3 if (start == end) return true;
4 return is_segment(start->next, end);
5 }

Using a for loop:


1 bool is_segment(list* start, list* end) {
2 for (list* p = start; p != NULL; p = p->next) {
3 if (p == end) return true;
4 }
5 return false;
6 }

Using a while loop:


1 bool is_segment(list* start, list* end) {
2 list* l = start;
3 while (l != NULL) {
4 if (l == end) return true;
5 l = l->next;
6 }
7 return false;
8 }
However, every one of these implementations of is_segment has the same
problem: if given a circular linked-list structure, the specification function
is_segment may not terminate.

L ECTURE N OTES
Linked Lists L10.6

It’s quite possible to create structures like this, intentionally or uninten-


tionally. Here’s how we could create the a circular linked list in Coin:
--> list* start = alloc(list);
--> start->data = 3;
--> start->next = alloc(list);
--> start->next->data = 7;
--> start->next->next = alloc(list);
--> start->next->next->data = 3;
--> start->next->next->next = alloc(list);
--> start->next->next->next->data = 12;
--> start->next->next->next->next = start->next;
--> list* end = alloc(list);
--> end->data = 18;
--> end->next = NULL;
--> is_segment(start, end);
and this is what it would look like:

While it is not strictly necessary, whenever possible, our specification func-


tions should return true or false rather than not terminating or raising an
assertion violation. We do treat it as strictly necessary that our specification
functions should always be safe — they should never divide by zero, access
an array out of bounds, or dereference a null pointer. We will see how to
address this problem in our next lecture.

L ECTURE N OTES
Linked Lists L10.7

4 Checking for Circularity


In order to make sure the is_segment function correctly handles the case of
cyclic loops, let’s write a function to detect whether a list segment is cyclic.
We can call this function before we call is_segment, and then be confident
that is_segment will always terminate.
One of the simplest solutions proposed in class keeps a copy of the
start pointer. Then when we advance p we run through an auxiliary loop
to check if the next element is already in the list. The code would be some-
thing like this:
1 bool is_acyclic(list* start, list* end) {
2 for (list* p = start; p != end; p = p->next)
3 //@loop_invariant is_segment(start, p);
4 {
5 if (p == NULL) return true;
6

7 for (list* q = start; q != p; q = q->next)


8 //@loop_invariant is_segment(start, q);
9 //@loop_invariant is_segment(q, p);
10 {
11 if (q == p->next) return false; /* circular */
12 }
13 }
14 return true;
15 }
This solution requires O(n2 ) time for a list with n elements, whether it
is circular or not. This doesn’t really matter, because we’re only using
is_acyclic as a specification function, but there is an O(n) solution. See if
you can find it before reading on.

L ECTURE N OTES
Linked Lists L10.8

For a more efficient solution, create two pointers, a fast and a slow one.
Let’s name them h for hare and t for tortoise. The slow pointer t traverses the
list in single steps. Fast h, on the other hand, skips two elements ahead for
every step taken by t. If the faster h starts out ahead of t and ever reaches
the slow t, then it must have gone in a cycle. Let’s try it on our list. We
show the state of t and h on every iteration.

L ECTURE N OTES
Linked Lists L10.9

In code:
1 bool is_acyclic(list* start) {
2 if (start == NULL) return true;
3 list* h = start->next; // hare
4 list* t = start; // tortoise
5 while (h != t) {
6 if (h == NULL || h->next == NULL) return true;
7 h = h->next->next;
8 //@assert t != NULL; // hare is faster and hits NULL quicker
9 t = t->next;
10 }
11 //@assert h == t;
12 return false;
13 }
A few points about this code: in the condition inside the loop we exploit
the short-circuiting evaluation of the logical or ‘||’ so we only follow the
next pointer for h when we know it is not NULL. Guarding against trying to
dereference a NULL pointer is an extremely important consideration when
writing pointer manipulation code such as this. The access to h->next and
h->next->next is guarded by the NULL checks in the if statement.
This algorithm is a variation of what has been called the tortoise and the
hare and is due to Floyd 1967.

L ECTURE N OTES
Linked Lists L10.10

5 Queues with Linked Lists


Here is a picture of the queue data structure the way we envision imple-
menting it, where we have elements 1, 2, and 3 in the queue.

A queue is implemented as a struct with a front and back field. The


front field points to the front of the queue, the back field points to the back
of the queue. We need these two pointers so we can efficiently access both
ends of the queue, which is necessary since dequeue (front) and enqueue
(back) access different ends of the list.
In the array implementation of queues, we kept the back as one greater
than the index of the last element in the array. In the linked-list implemen-
tation of queues, we use a similar strategy, making sure the back pointer
points to one element past the end of the queue. Unlike arrays, there must
be something in memory for the pointer to refer to, so there is always one
extra element at the end of the queue which does not have valid data or
next pointer. We have indicated this in the diagram by writing X.
The above picture yields the following definition.
1 typedef struct queue_header queue;
2 struct queue_header {
3 list* front;
4 list* back;
5 };
We call this a header because it doesn’t hold any elements of the queue, just
pointers to the linked list that really holds them. The type definition allows
us to use queue as a type that represents a pointer to a queue header. We
define it this way so we can hide the true implementation of queues from
the client and just call it an element of type queue.

L ECTURE N OTES
Linked Lists L10.11

When does a struct of this type represent a valid queue? In fact, when-
ever we define a new data type representation we should first think about
the data structure invariants. Making these explicit is important as we
think about and write the pre- and postconditions for functions that im-
plement the interface.
What we need here is if we follow front and then move down the
linked list we eventually arrive at back. We call this a list segment. We
also want both front and back not to be NULL so it conforms to the pic-
ture, with one element already allocated even if the queue is empty; the
is_segment function we already wrote enforces this.
1 bool is_queue(queue* Q) {
2 return Q != NULL && is_segment(Q->front, Q->back);
3 }
To check if the queue is empty we just compare its front and back. If
they are equal, the queue is empty; otherwise it is not. We require that we
are being passed a valid queue. Generally, when working with a data struc-
ture, we should always require and ensure that its invariants are satisfied
in the pre- and post-conditions of the functions that manipulate it. Inside
the function, we will generally temporarily violate the invariants.
1 bool queue_empty(queue Q)
2 //@requires is_queue(Q);
3 {
4 return Q->front == Q->back;
5 }
To obtain a new empty queue, we just allocate a list struct and point both
front and back of the new queue to this struct. We do not initialize the list
element because its contents are irrelevant, according to our representation.
Said this, it is good practice to always initialize memory if we care about
its contents, even if it happens to be the same as the default value placed
there.
1 queue* queue_new()
2 //@ensures is_queue(\result);
3 //@ensures queue_empty(\result);
4 {
5 queue* Q = alloc(queue);
6 list* p = alloc(list);
7 Q->front = p;
8 Q->back = p;

L ECTURE N OTES
Linked Lists L10.12

9 return Q;
10 }
To enqueue something, that is, add a new item to the back of the queue,
we just write the data into the extra element at the back, create a new back
element, and make sure the pointers are updated correctly. You should
draw yourself a diagram before you write this kind of code. Here is a
before-and-after diagram for inserting 3 into a list. The new or updated
items are dashed in the second diagram.

In code:
1 void enq(queue* Q, elem x
2 //@requires is_queue(Q);
3 //@ensures is_queue(Q);
4 {
5 list* p = alloc(list);
6 Q->back->data = x;
7 Q->back->next = p;

L ECTURE N OTES
Linked Lists L10.13

8 Q->back = p;
9 }
Finally, we have the dequeue operation. For that, we only need to
change the front pointer, but first we have to save the dequeued element
in a temporary variable so we can return it later. In diagrams:

And in code:
1 elem deq(queue* Q)
2 //@requires is_queue(Q);
3 //@requires !queue_empty(Q);
4 //@ensures is_queue(Q);
5 {
6 elem x = Q->front->data;
7 Q->front = Q->front->next;
8 return x;
9 }

L ECTURE N OTES
Linked Lists L10.14

Let’s verify that the our pointer dereferencing operations are safe. We have
Q->front->data
which entails two pointer dereference. We know is_queue(Q) from the
precondition of the function. Recall:
1 bool is_queue(queue Q) {
2 return Q != NULL && is_segment(Q->front, Q->back);
3 }
We see that Q->front is okay, because by the first test we know that Q != NULL
is the precondition holds. By the second test we see that both Q->front and
Q->back are not null, and we can therefore dereference them.
We also make the assignment Q->front = Q->front->next. Why does
this preserve the invariant? Because we know that the queue is not empty
(second precondition of deq) and therefore Q->front != Q->back. Be-
cause Q->front to Q->back is a valid non-empty segment, Q->front->next
cannot be null.
An interesting point about the dequeue operation is that we do not ex-
plicitly deallocate the first element. If the interface is respected there cannot
be another pointer to the item at the front of the queue, so it becomes un-
reachable: no operation of the remainder of the running programming could
ever refer to it. This means that the garbage collector of the C0 runtime sys-
tem will recycle this list item when it runs short of space.

6 Stacks with Linked Lists


For the implementation of stacks, we can reuse linked lists and the basic
structure of our queue implementation, except that we read off elements
from the same end that we write them to. We call the pointer to this end
top. Since we do not perform operations on the other side of the stack, we
do not necessarily need a pointer to the other end. For structural reasons,
and in order to identify the similarities with the queue implementation,
we still decide to remember a pointer bottom to the bottom of the stack.
With this design decision, we do not have to handle the bottom of the stack
much different than any other element on the stack. The difference is that
the data at the bottom of the stack is meaningless and will not be used in

L ECTURE N OTES
Linked Lists L10.15

our implementation. A typical stack then has the following form:

Here, 3 is the element at the top of the stack.


We define:
1 typedef struct stack_header stack;
2 struct stack_header {
3 list* top;
4 list* bottom;
5 };
6

7 bool is_stack(stack* S) {
8 return S != NULL && is_segment(S->top, S->bottom);
9 }
Popping from a stack requires taking an item from the front of the
linked list, which is much like dequeuing.
1 elem pop(stack* S)
2 //@requires is_stack(S);
3 //@requires !stack_empty(S);
4 //@ensures is_stack(S);
5 {
6 elem x = S->top->data;
7 S->top = S->top->next;
8 return x;
9 }
To push an element onto the stack, we create a new list item, set its data
field and then its next field to the current top of the stack — the opposite
end of the linked list from the queue. Finally, we need to update the top

L ECTURE N OTES
Linked Lists L10.16

field of the stack to point to the new list item. While this is simple, it is still
a good idea to draw a diagram. We go from

to

In code:
1 void push(stack* S, elem x)
2 //@requires is_stack(S);
3 //@ensures is_stack(S);
4 {
5 list* p = alloc(list);
6 p->data = x;
7 p->next = S->top;
8 S->top = p;
9 }
This completes the implementation of stacks.

Exercises
Exercise 1. The implementation of circularity checking we gave has an assertion,
t != NULL, which we can’t prove with the given loop invariants. What loop

L ECTURE N OTES
Linked Lists L10.17

invariants would allow us to prove that assertion correct? Can we write loop
invariants that allow us to prove, when the loop exits, that we have found a cycle?

Exercise 2. Consider what would happen if we pop an element from the empty
stack when contracts are not checked in the linked list implementation? When
does an error arise?

Exercise 3. Stacks are usually implemented with just one pointer in the header, to
the top of the stack. Rewrite the implementation in this style, dispensing with the
bottom pointer, terminating the list with NULL instead.

L ECTURE N OTES

You might also like