A. Introduction
Arrays (allocated either at compile time or at run time) and
vector provide a way to store and process lists. Usually,
list elements are stored in consecutive array/vector locations.
Inserting a value into such a list, however, can involve extensive
copying of values to make room for the new item. And deleting a value
from such a list can also involve extensive copying of values to close
the gap left by the removed item. (Please see Section 8.1 of the text
for a description of the inefficiency of these list operations in such
an array/vector-based implementation.)
Chapter 8 of the text describes an alternative way to implement lists
that permit insertions and deletions without the expensive copying
associated with the sequential-storage implementation using
arrays/vectors — linked lists. This linked-list
approach is the subject of this lab exercise.
The basic idea is to build a list of structures, each of which
consists of:
- a data member, in which a data value is stored; and
- a next pointer member, in which the address of the
same kind of structure can be stored.
These structures that contain both a value and a pointer are called
nodes. We might visualize a node as follows:
The next members in the nodes make it possible to link nodes
together to form a list. This is accomplished by storing (in the next
member of a node) the address of a node that contains the next list
value.
This method of linking values together solves the efficiency problem
of the sequential-storage implementation of lists. To illustrate,
suppose that the list of values 11,22,33,44,66,77,88, and 99 is stored
as folows:
| 11 |
|
22 |
|
33 |
|
44 |
|
66 |
|
77 |
|
88 |
|
99 |
|
|
→ |
|
→ |
|
→ |
|
→ |
|
→ |
|
→ |
|
→ |
|
≈ |
Then, we insert 55 after 44 as follows:
- Allocate a new structure to hold the new value:
| 11 |
|
22 |
|
33 |
|
44 |
|
66 |
|
77 |
|
88 |
|
99 |
|
|
→ |
|
→ |
|
→ |
|
→ |
|
→ |
|
→ |
|
→ |
|
≈ |
- Store the address of its successor node (containing 66) in its
next member.
- Store its address in the next member of its predecessor
(containing 44).
| 11 |
|
22 |
|
33 |
|
44 |
|
66 |
|
77 |
|
88 |
|
99 |
|
|
→ |
|
→ |
|
→ |
|
↓ |
|
→ |
|
→ |
|
→ |
|
≈ |
Notice that no data values had to be moved!
Given that nodes can be linked together in this way, we can then
construct a linked structure called a simply-linked list,
which stores the address of the first node in the list and perhaps the
number of nodes in the list, also:
| |
→ |
11 |
|
22 |
|
33 |
|
44 |
|
55 |
|
66 |
|
77 |
|
88 |
|
99 |
|
| 9 |
|
|
→ |
|
→ |
|
→ |
|
→ |
|
→ |
|
→ |
|
→ |
|
→ |
|
≈ |
This is one form of linked list and simplicity is its primary
advantage. Other forms are circular linked lists and
doubly-linked lists described in Section 9.1 of the textbook.
Note: In this lab exercise, you will be using the two files
LinkedList.h and linktester.cpp. Get a copy of
these program files by right-clicking and saving the following links:
source_lab12_LinkedList.h
&
source_lab12_linktester.cpp
and then rename them to LinkedList.h and
linktester.cpp, respectively.
B. Design Issues
Since a linked list is made up of nodes, we have two different objects
to consider: LinkedList objects and Node objects.
However, if we implement Node and LinkedList as two
separate classes, then the LinkedList operations will not be
able to access the (private) members of the class Node.
The easiest way to circumvent this difficulty is to declare
Node as a class (or struct) inside the class
LinkedList. We will use a "public class" rather than a
struct because we will be adding member functions, and structs are
commonly used only for C-style structures, which haven no function
members.
class LinkedList {
private:
class Node {
public:
// members of class Node...
};
...
// members of class LinkedList...
};
This makes it possible for members and friends of LinkedList
to access Node and its members because it is a part of class
LinkedList. However, other classes, functions, and programs
cannot access Node or its members because it is declared as a
private member of class LinkedList. This is as it should be
because a Node is really just an implementation detail for
linked lists.
Because linked lists consist of linked nodes, we will begin with the
class Node.
C. The Data Members of Class Node
From the preceding discussion, it should be evident that class
Node needs two data members:
- data, whose type is the type of value being stored in
the list; and
- next, whose type is a pointer to a Node.
In the context of our previous example, we can picture such an object
as follows:
The first decision to make is what to use for the type of the data
values. To maximize reusability, we will make LinkedList a
class template and pass the type DataType of values to be
stored in data as a type parameter.
Place a declaration in the appropriate place to make
LinkedList a class template with type parameter DataType.
Declarations of linked lists in programs that use LinkedList
will thus have the form:
LinkedList list<type-of-data-values>;
Inside the stub of class Node, declare data members:
- data, whose type is DataType
- next, whose type is a pointer to a Node
Note that, following the declaration of class Node in the file
LinkedList.h, we have added a typedef declaration:
typedef Node * NodePointer;
This defines the name NodePointer as a more readable alias
for the type Node * so that we can declare variables of type
NodePointer within the member functions of class
LinkedList. Note that the alias NodePointer cannot
be used to declare the next data member in class
Node because the declaration of next precedes the
definition of NodePointer.
D. The Data Members of LinkedList
As described previously, our simple implementation of a linked list
has two data members, one to keep track of the first node in the list
and the other to keep a count of the nodes.
Add declarations of these two data members to class
LinkedList (but outside class Node's
declaration).
- ptrFirst, whose type is a pointer to a Node
- numNodes, an integer
E. LinkedList Operations
A linked list consists of a sequence of linked nodes. Because of
this, some of the LinkedList operations will require us to
implement Node operations. It is difficult, however, to
anticipate in advance what Node operations will be needed.
For this reason, we will work on LinkedList operations until
a particular Node operation is needed; at that point, we will
implement the needed operation.
We will do only a few of the many LinkedList operations that
could be implemented, enough to gain some familiarity with linked
structures. (The list class template in STL provides a full
slate of list operations.) The operations we will consider are:
- A class constructor, so that empty LinkedList objects
can be declared and initialized
- An output operation, so that the contents of a
LinkedList can be displayed
- An Insert(v,i) operation, to insert a value v
into a LinkedList at position i
Part 1 of the project that accompanies this lab exercise adds three
other operations:
- A destructor, so that the run-storage of LinkedList
objects can be reclaimed
- A copy constructor, so that LinkedList objects can be
copied when needed
- Assignment, so that one LinkedList object can be copied
and assigned to another
F. The LinkedList Class Constructor
This is the simplest of the operations to implement. The problem is
specified as follows:
-
Precondition: A LinkedList object has been declared.
-
Postcondition: Its data members have been initialized
appropriately for an empty list.
To accomplish this, our constructor must:
-
- Set the ptrFirst data member of class LinkedList
to the null pointer (0).
- Set the numNodes member to 0.
These actions are simple enough to inline this constructor. Do
so by putting the prototype inside the class declaration and
inlining the definition after the end of the class declaration.
(Or, if you prefer, define the constructor in the public section
of LinkedList — the compiler will then treat it as
being inlined.)
Test what you have done so far by compiling and executing linktester.cpp.
G. Displaying a LinkedList
To output a linked list, we must visit each node in the list,
beginning with the fist node, display the value of its data
member, and then move to the next node. We stop when the end of the
list is reached. These observations lead to the following algorithm:
-
- Set a NodePointer named nPtr to point to the
first node in the list.
- While nPtr is not null:
- Display the data value of the Node pointed
to by nPtr.
- Make nPtr point to the next Node in the list.
Note that because we have used a pretest loop, this algorithm will
also work correctly for an empty list.
Step 2a requires accessing the data member of a class object through a
pointer. For this, we could use an expression of the form:
(*ptr).dataMember
to dereference ptr and then access dataMember. (The
parentheses are needed because the dot operator has higher precedence
than * — see Table C.2 in Appendix C of the text.)
However, because this notation is clumsy and such accesses are needed
so frequently, C++ provides an equivalent, but more readable,
expression:
Think of -> as an arrow from a pointer variable to a data
member in the object to which it points. This arrow operator
is the standard way to access data members via pointers.
This means that if nPtr contains the address of a node in a
linked list, we can use:
nPtr->data
to access the list item stored in the data part of the node
pointed to by nPtr as required in step 2a. Similarly, we can
use the assignment statement:
nPtr = nPtr->next;
to then make nPtr point to the next node in the linked list.
This statement changes the address in nPtr to that of the
node that follows the one to which it was pointing.
Using these ideas, overload the output operator
<< for the LinkedList class using one of the
following methods:
Note: you will need a pointer of type NodePointer
to traverse the linked list. Here, it is important to remember
that:
|
To use a type (or constant) name declared within a class in
an outside-the-class declaration, the type (or constant)
name must be qualified with the name of the class and the
scoping operator (::):
ClassName::TypeName
|
Thus, to use the type NodePointer outside of the class
LinkedList, we must use its fully-qualifed name
LinkedList<DataType>::NodePointer.
To test your output operator, comment out with // (or
remove) the comments in the section labelled PART 1 in
linktester.cpp and then compile and execute the program.
H. Inserting a Value into a LinkedList
In designing an operation, we must specify the insertion point. Here,
we will simply number each value in the list and then specify the
position at which the value is to be inserted. We will use the same
convention as for C++ arrays and vector: The first value in
the list will be numbered 0, the second value in the list numbered 1,
and so on.
Assuming this convention, we can specify our problem as follows:
- Receive:
A LinkedList object with n nodes;
dataVal, a DataType value;
position, an integer (default value, 0).
- Pass back:
A LinkedList object with n + 1 nodes, such
that:
nodes 0 through position - 1 are unchanged;
dataVal is stored in node position;
nodes position + 1 through n + 1 are those that
were formerly numbered position through n.
Using this specification, declare Insert() as a member
function of class LinkedList (which thus receives and
passes back the LinkedList implicitly), but don't write
a definition for Insert() yet.
I. A Digression: The Node Constructor
Inserting a data value requires first storing it in a Node,
and so Insert() must allocate a Node (using the
new operation). It would be convenient if we could simply
write:
nPtr = new Node(dataValue);
to obtain a Node pointed to by nptr where a
Node constructor initializes the data member of the
Node to dataValue and the next member to
the null pointer. For example, if dataValue is 11, this
statement should produce the following result:
Write a definition for a constructor within class
Node that behaves as specified. Because of its
simplicity, you may inline this function by putting its definition
inside the declaration of class Node.
To test what you have written, write a definition of
Insert() whose body contains only a declaration of
nPtr and the preceding assignment statement (or use it as
an initializer in the declaration statement) and then compile
linktester.cpp.
J. Back to the Insertion Operation for LinkedList
Now that we can construct a Node in which a data value is
stored, we are ready to actually insert this new node into the
appropriate place in the linked list:
Note: In developing a linked-list operation, it is always
helpful to draw pictures to determine what must be done and to test
the algorithm for the operation.
As described in Section 8.5 of the text, there are two cases to
consider.
Case 1: A new value is to be inserted at the beginning of the
list (i.e., position = 0).
For example, suppose we wish to insert 11 at the beginning of a linked
list containing 22,44,66, and 88. The situation can be pictured as
follows:
ptrFirst
|
| → |
22 |
|
44 |
|
66 |
|
88 |
|
numNodes
| 4 |
|
|
→ |
|
→ |
|
→ |
|
≈ |
And we need to change it to:
ptrFirst
|
| ↑ |
22 |
|
44 |
|
66 |
|
88 |
|
numNodes
| 5 |
|
|
→ |
|
→ |
|
→ |
|
≈ |
We can do this with the following three operations (but be sure to
do the first two in the order shown):
- Set nPtr->next to ptrFirst.
- Set ptrFirst to nPtr.
- Increment numNodes.
Case 2: dataVal is to be inserted somewhere within the
list.
To illustrate, suppose we wish to insert 55 in the linked list
containing 22,44,66, and 88 between 44 and 66 (i.e., position =
2). The situation can be pictured as follows:
ptrFirst
|
| → |
22 |
|
44 |
|
66 |
|
88 |
|
numNodes
| 4 |
|
|
→ |
|
→ |
|
→ |
|
≈ |
And we need to change it to:
ptrFirst
|
| → |
22 |
|
44 |
|
66 |
|
88 |
|
numNodes
| 5 |
|
|
→ |
|
↑ |
|
→ |
|
≈ |
|
predPtr
| |
|
|
| ↑ |
To accomplish this, we proceed as follows:
- Traverse the list, to position a pointer named predPtr
(predecessor pointer) at the node containing the
predecessor (44) of the new data value; that is, predPtr
is positioned at the node numbered position - 1.
- Set nPtr->next to predPtr->next.
- Set predPtr->next to nPtr.
- Increment numNodes.
Check that these steps will also work correctly when the new
node is being inserted at the end of the list by drawing similar
"before and after" pictures for inserting 99 at the end of the
linked list containing 22,44,66, and 88 (i.e., position =
3).
We can handle both cases with the following algorithm:
- Construct a new Node pointed to by nPtr
that contains the value to be inserted.
- Declare another pointer predPtr.
- If position is zero:
- Set nPtr->next to ptrFirst.
- Set ptrFirst to nPtr.
Else
- Set predPtr to ptrFirst.
- For each integer i in the range 1 through
position - 1:
Set predPtr to predPtr->next.
- Set nPtr->next to predPtr->next.
- Set predPtr->next to nPtr.
- Increment numNodes
Using this algorithm, complete the definition of
Insert() in the file LinkedList.h. Note that
you've already done step 1. To test it, remove the comments in
the section labelled PART 2 in linktester.cpp
and then compile and execute the program.
Hand In: This lab handout with the answers filled in
attached to a listing of your final program files
(use the enscript command from your Programming
Style Sheet to print them out:
enscript -E -G -2rj -M Letter -PECT2_PS
<filename>. Or, you may use the a2ps
command: a2ps <filename>).
Ricky J. Sethi <rickys at sethi.org>
Last modified: Mon Mar 28 18:37:15 PST 2005