Linked Lists 1

J. Hardie

Introduction

We now (temporarily) leave the semantics of C++ and begin discussion of some fundamental algorithms and data structures which recur frequently in many branches of computer science.

Before we do, I'd like to give the following definitions:

Algorithm:
A formal procedure for accomplishing some specified task.
Data Structure:
A way of arranging information to reflect the underlying structure of the data being stored.

We will begin our study of data structures by examining an implementation of a stack. In this lecture, we will implement a stack which is supposed to contain only positive integers. We discussed a different implementation of a stack in a previous lecture (lecture 11). That lecture presented a stack of integers which was implemented as a dynamically allocated array.

This time, we will present a stack which is implemented as a linked list.

What is a linked list?

A linked list is a very general (and very useful) data structure. It consists of a group of objects which contain information and which are dynamically allocated. These objects also contain a pointer which is used to attach them to the next piece of information in the list.

For example, suppose we have a number of employees. We might design a payroll program to use a linked list of employees. We could define an object (in this case a struct) like:

struct Employee
{
    char *FirstName;
    char *LastName;
    Employee *NextOne;
}

The struct contains the needed information (first and last names) and an extra pointer field. This pointer field will provide the link to other employee objects. So, we could write the part of our payroll program which reads in all the employees using the following logic:

main()
{
   Employee *EmployList = 0;
   Employee *NewOne = 0;

   char First[30],Last[30];

   // Open the input file

   while (input)
   {
      ReadNames(input,First,Last);
      NewOne = new Employee;
      SetFirstName(NewOne,First);
      SetLastName(NewOne,Last);
      NewOne->NextOne = EmployList;
      EmployList = NewOne;
   }

   // rest of code

The read loop in this code does several things:

After the read loop finishes executing, we have a structure like:

               +------+       +------+       +------+       
               |f.n. 1|	      |f.n. 2|       |f.n. 3|       
               +------+	      +------+       +------+       
               |l.n. 1|	      |l.n. 2|       |l.n. 3|       
               +------+	      +------+       +------+       
EmployList --> |   ---------->|   ---------->|   0  |
               +------+       +------+       +------+

where the EmployList pointer points to the first Employee. The NextOne pointer in the first Employee points to the second Employee, etc. By convention, when we reach the end of the list (i.e. the third Employee in the above diagram) the pointer in that object is supposed to be Null, indicating that we have come to the end of the line.

The above data structure is called a linked list. The pointers in the structs are called the Link Fields and they are used to construct the representation of the data structure.

Notice that we have said nothing about how the objects are put into the list and how they are removed. The Linked List structure itself is independent of the insertion/removal methods. This flexibility allows the same basic data structure to be used to implement a large number of operations, just by changing the algorithms for insertion and deletion. For example, we can implement:

A Stack:
by inserting only at the beginning of the list and removing only from the beginning of the list.
A Queue:
by inserting at the beginning of the list and removing at the end of the list.
A Priority Queue:
By inserting at a position specified by a number in the data itself and removing at the end of the list.
An Associative Array:
by inserting in alphabetical order according to one data item in the object and removing by searching for a specific name in the list.

In short, Linked lists are very flexible things. Don't worry too much at this point about the different methods of insertion/deletion, we will get to it eventually.

Stacking it up...

For now, we see that we can make a stack out of a linked list by writing functions Push() and Pop() which do the following:

Push() :
will be used for insertion. It needs to
1.
Create a new object and store the data in that object.
2.
Check to see if the list is empty. If so make the object point to null.
3.
Otherwise Make the pointer in the new object point to the last object entered.
4.
Make the head of the list point to the new object.
This algorithm will ensure that objects are placed at the beginning of the list and that we keep track of all the previously entered items.

Pop() :
will be used for removal. It needs to
1.
Check to see if the stack is empty (i.e. no allocated objects remain in the list). If so, we should return some kind of error flag. In the following example, we will be making a stack of positive integers so the flag will be a negative number.
2.
If the stack is not empty, we get a pointer to the first object in the list.

3.
We make the head of the list point to the second object in the list, which effectively removes the first object from the list.

4.
We store the integer in a temporary variable.

5.
We delete the struct which was used to store the integer in the list.

6.
We return the integer.

From the specification, it seems that the pop operation is very complicated and the push operation only slightly less so. In actuality, both are very short functions.

So, to actually implement this beast we need to define:

Let's get to the implementation:

The Header File

We will implement our stack using the following header file:

struct PosInt
{
   int value;
   PosInt *next;
};


class IntStack
{
   PosInt *top;

public:
   // the usual suspects.
   IntStack();
   ~IntStack();
   IntStack(const IntStack &s);
   const IntStack &operator=(const IntStack &s);

   // insertion.  Takes a single integer and returns void.
   void push(int val);

   // removal.  Takes no arguments and returns the top number
   int pop();
};

This header file defines the structure we'll use in our list, and the interface to the list class itself. Now we need to implement these things:

The Implementation File

We will begin by looking at the constructor for the list object. It has only one responsibility - to make sure that the top pointer is null.

IntStack::IntStack()
{
   top = 0;
}

With the guarantee that the stack starts off empty, we can write the push function quite easily. We first allocate a new object, store our data and then put the object at the head of the list.

// We will assume that val is positive.  In reality, we should
// check.
void IntStack::push(int val)
{
   PosInt *p = new PosInt;
  
   p->value = val;         // store the integer

   // insert at the head of the list.
   p->next = top;
   top = p;
}

This is almost magical. The last two lines handle both the case of an empty list (in which case top is null) and a full list. In either case, the pointer in the new node should be made the same as the top of the stack, since we're going to be placing the new object in front of the old ones. If the stack is currently empty, the new object gets a null pointer, just as required. Otherwise, it now points to the rest of the list.

Once that is done, we just make the top of the list point to the new object.

After writing the push() function, we can go ahead and write the pop function. We are guaranteed the following:

These properties are ensured by the action of the constructor and of the push() function.

So, with those guarantees, here is the pop function:

int IntStack::pop()
{
   if (!top)            // list is empty
      return -1;

   PosInt *p = top;     // get the first item

   top = p->next;       // Move the top to the next item

   int val = p->value;  // save the current value

   delete p;            // free the memory used by p

   return val;
}

Again, this code is remarkably simple. We just find the first node on the list, make the top pointer skip over it, and clean up a little to avoid memory leaks. Nothing to it.

Nothing is ever free, so we have to pay back the simplicity of the previous functions by a little bit of care in the remaining ones. The destructor for our IntStack object must be sure to delete all the allocated PosInt objects remining in the list when the destructor gets called. We do this by looping over the elements in the list, explicitly deleting each one:

IntStack::~IntStack()
{
   while(top)
   {
      PosInt *p = top;
      top = p->next;
      delete p;
   }
}

This doesn't look too intimidating, and it isn't really. You should make sure that you understand what's going on here - the operations have to occur in the correct order: get the top, move the top, delete the removed node. Doing things in a different order can result in corrupted memory and segmentation faults.

The copy constructor is a bit more complicated. I'll present it first, then discuss it:

IntStack::IntStack(const IntStack &s)
{
   // We need to make a temporary list.
   PosInt *templist = 0;
   
   // this will be used to allocate new nodes
   PosInt *newstruct;

   // This will be used to scan the old list.
   PosInt *copynode = s.top;

   // copy the nodes from the old list to the new one.
   // This will copy the nodes in the reverse order.
   while(copynode)
   {
      newstruct = new PosInt;
      newstruct->value = copynode->value;
      newstruct->next = templist;
      templist = newstruct;
      copynode = copynode->next;
   }

   // Then, reverse the order of the nodes again.
   top = 0;
   while (templist)
   {
       newstruct = templist;
       newstruct->next = top;
       top = newstruct;
       templist = templist->next;
   }
}

This constructor requires some explanation. Go over the code carefully to be sure that you know what it is doing. In particular, note the following:

Finally, the assignment operator is very similar to the copy constructor, except that we have to check for allocated memory and free it first. Again we copy the list in reverse order and then have to reverse it one more time to get the original order back.

const IntStack &IntStack::operator=(const IntStack &s)
{
   if (this == &s)
      return *this;      // copy to self

   // Free the currently allocated memory
   while (top)
   {
      PosInt *p = top;
      top = top->next;
      delete p;
   }

   // Copy the old list
   // Again, we need to make a temporary list.
   PosInt *templist = 0;
   PosInt *newstruct;
   PosInt *copynode = s.top;

   // copy the nodes from the old list to the new one.
   // This will copy the nodes in the reverse order.
   while(copynode)
   {
      newstruct = new PosInt;
      newstruct->value = copynode->value;
      newstruct->next = templist;
      templist = newstruct;
      copynode = copynode->next;
   }

   // Then, reverse the order of the nodes again.
   top = 0;
   while (templist)
   {
       newstruct = templist;
       newstruct->next = top;
       top = newstruct;
       templist = templist->next;
   }

   return *this;
}

This function is almost identical to the destructor followed by the copy constructor.

What Next?

Read sections 8.1, 8.2, 8.3 and 8.4 for tuesday. We will be discussing several slightly different methods of constructing linked lists, and examining in more detail how they behave in programs.

We will also look at a client program which uses the linked lists that we create.

Finally, on tuesday and continuing into thursday, we will talk about modified versions of linked lists - how to write a queue (and what a queue is), priority queues and their relation to generic linked lists.

Then, on thursday, we will begin by creating a general linked list class and discuss how to make stacks, queues, etc, by inheritance from the original list class.

About this document ...

Linked Lists 1

This document was generated using the LaTeX2HTML translator Version 97.1 (release) (July 13th, 1997)

Copyright © 1993, 1994, 1995, 1996, 1997, Nikos Drakos, Computer Based Learning Unit, University of Leeds.

The command line arguments were:
latex2html -split 0 -link 4 -no_subdir -no_navigation -prefix lecture13 lecture13.tex.

The translation was initiated by John G. Hardie on 10/16/1997


John G. Hardie
10/16/1997