Name: _____________________ Class: CET 375
SSN/ID:   _____________________ Section & Group: ____________
Linked Lists


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:

  1. a data member, in which a data value is stored; and
  2. 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:

data
11
next

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:

  1. Allocate a new structure to hold the new value:

    11 22 33 44 66 77 88 99

    55
     
  2. Store the address of its successor node (containing 66) in its next member.
  3. Store its address in the next member of its predecessor (containing 44).

    11 22 33 44 66 77 88 99

    55
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:

In the context of our previous example, we can picture such an object as follows:

data
11
next
next node

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.

  1. 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>;
  1. 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.

  1. 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:

Part 1 of the project that accompanies this lab exercise adds three other operations:

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:

  1. Set the ptrFirst data member of class LinkedList to the null pointer (0).
  2. Set the numNodes member to 0.

  1. 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:

  1. Set a NodePointer named nPtr to point to the first node in the list.
  2. While nPtr is not null:
    1. Display the data value of the Node pointed to by nPtr.
    2. 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:

ptr->dataMember

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.

  1. Using these ideas, overload the output operator << for the LinkedList class using one of the following methods:
    • Add a member function called Display() to the LinkedList class and then define operator<<() after the class declaration to call Display().
      • Your Display() function will probably receive an ostream object as a reference parameter
      • The operator<<() function should also return this ostream object as a reference parameter
      • Please note, the Display() function should probably be a public function in this case
    • Alternatively, make operator<<() a friend function of class LinkedList.
      • The operator<<() function should receive (and return) an ostream object as a reference parameter and also receive a LinkedList object as a constant reference parameter
      • The operator<<() function should be a friend function (in order to access the private members of LinkedList, namely, the Node members)
      • Please note, the operator<<() function should probably be a public function in this case
    • In both cases, you'll have to precede the operator<<() definition (if it's outside the class declaration) as a function template using something like:
      
      	template <typename DataType>
      	

    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.

  1. 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:

nPtr
   
data
11
next

  1. 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:

nPtr
   
data
11
next

ptrFirst
    22 44 66 88
numNodes
4

And we need to change it to:

nPtr
   
data
11
next

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):

  1. Set nPtr->next to ptrFirst.
  2. Set ptrFirst to nPtr.
  3. 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:

nPtr
   
data
55
next

ptrFirst
    22 44 66 88
numNodes
4

And we need to change it to:

nPtr
   
data
55
next

ptrFirst
    22 44 66 88
numNodes
5
predPtr
   

To accomplish this, we proceed as follows:

  1. 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.
  2. Set nPtr->next to predPtr->next.
  3. Set predPtr->next to nPtr.
  4. Increment numNodes.

  1. 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:

    1. Construct a new Node pointed to by nPtr that contains the value to be inserted.
    2. Declare another pointer predPtr.
    3. If position is zero:
      1. Set nPtr->next to ptrFirst.
      2. Set ptrFirst to nPtr.
      Else
      1. Set predPtr to ptrFirst.
      2. For each integer i in the range 1 through position - 1:

            Set predPtr to predPtr->next.

      3. Set nPtr->next to predPtr->next.
      4. Set predPtr->next to nPtr.
    4. Increment numNodes
  2. 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