Chapter 7: Linked ADTs

Chapter 7: Linked ADTs

Contents

7.1. Introduction

In this chapter, we look at the use of pointers to build "linked" data types. A linked data type allows separate objects to be linked together into more complex information structures. There are many examples of such linked structures including lists, queues, trees and graphs. It is not the goal of this course to explore these in detail - this is left to a later course on Data Structures. Instead, we focus on the relatively simple example if a list in order to demonstrate the overall approach.

This chapter introduces the following key concepts:

Lists are a familiar feature of our lives and examples include shopping lists and address books containing lists of names. For our purposes we shall consider a list to have the following basic properties:

  • The elements of the list are ordered.
  • The elements of the list are of the same type.
  • The list may be "empty" - in other words have no elements.
  • The list may grow to an arbitrary size.
  • It is possible to insert an element anywhere into the list.
  • It is possible to delete an element from anywhere in the list.

    The properties of insertion and deletion are shown below and make lists quite different from arrays where these operations are not possible.

    These basic properties led me to the following design of a list Abstract Data Type (based on our five categories of interface function).

    7.3. Definition of a list class

    We will limit our first example to be a list of single characters. However, it should be easy to see how to extend this to other types, including your own classes. The design above yields the following class definition for a new List class. Notice that in this definition I have left out the data members - these will be introduced in the implementation section below.

    Notice that the retrieveN method places the retrieved value into the argument char &c - a deliberate side effect. As a result, this argument must be passed by reference. List arguments are also passed by reference, but in this case for reasons of increased efficiency.

    7.4. Implementing the List Class

    The implementation of the List class uses the fact the pointers can be used to link objects together. First, consider the following definition of a simple Element class.

    An element object may contain a single character (val) and also a pointer (next) which can be set to point at some other element. The following constructor function is provided to initialise an Element object.

    Now, assuming that our program has permission to manipulate the data-members of the class Element, we can build chains of elements in the following way.

    Or better ...

    We have now managed to build up a chain of linked objects - in other words a simple list. The following code prints out the values in this list, in sequence.

    These ideas form the basis of our list implementation. However, instead of adding each element to the chain explicitly we need a much more flexible approach. This is given by implementing the methods insertN and deleteN in a general way so that we can access any part of an arbitrary long list.

    Overall, our List implementation will require two classes - the basic Element class described above and the more general List class. Thus, once again we see an example of a using relationship between different classes.

    The following is the completed header file which defines both classes. Note that we have also added some extra methods to the List class. These include a destructor and an overloaded assignment operator. These are required because we will we be doing our own memory allocation.

    A new searchN method has been introduced into the private section of the class. This method returns the address of the nth element in the list (and the 0 pointer if there is no such element). This is generally useful for implementing insertN, retrieveN and deleteN. However, a user of the class should not see such addresses and so searchN is defined as a private method, not a public one. In other words, it can only be used by other methods of the List class.

    Notice that we define two error codes to be returned by public functions. One represents the fact that a requested element is not in the list (e.g. trying to delete the fourth element of a list that is three elements long). The other indicates that the operation completed successfully.

    Note also that the class List is declared as a friend to the class Element. This enables ALL methods of List to manipulate the internal data-members of Element objects (e.g. making and breaking links via the next pointers). We also have to declare the overloaded output operator as a friend to both List and Element.

    The List class defines two private data members, len which indicates how many elements are in the list at a given time and first which is a pointer to the first element in a chain. Thus the overall approach is to have a List object point to an Elment object which, in turn, is linked to a chain of further Element objects. The following figures show two example lists, one empty (i.e. no elements) and the other containing several elements.

    Each of the methods defined for List will need to access and manipulate this structure in some way. We will now look at each of these methods in turn.

    The constructor to build an empty list merely sets the pointer first to be null and notes that there are zero elements in the list.

    Next, we need to look at searchN. The goal of this private method is to return a pointer to the nth element in the list (where n is its argument). To do this, a while loop starts at the first element and moves down the list, following the next pointers, until either the end of the list is reached (the 0 pointer) or it has seen n elements. Of course, it needs to keep a record of how many elements have already been seen. This is achieved through the local variable int seen. Finally, it returns a pointer to the element at which it stopped (this will be the 0 address if it ran over the end of the list).

    Next we look at the insert function. There are two distinct cases to be considered - inserting at the beginning of the list and inserting somewhere in the middle or at the end of the list.

    First, we check whether the subscript is in bounds. Then we construct a new Element with the correct value. If we are inserting at the first position, we link this new element to the one that first is currently pointing at and then link first to our new element. This is shown below.

    If we are inserting in the middle of the list we get a pointer to the preceding element and then follow these steps:

  • Link our new element to the ones following the preceding element.
  • Line the preceding element to our new element.

    In both cases we increase the length of the list.

    Deletion also starts with bounds checking. Beyond this, the procedure is the reverse to insertion. Notice that as well as making and breaking pointers, we also free the memory associated with the element being deleted.

    Retrieving a value involves getting a pointer to the element in question using searchN and then extracting its value.

    The destructor needs to free all of the memory that has been allocated for the list. This means going down the list and element at a time freeing each element. Note that it is important to get the address of the next element in the list BEFORE deleting the current one.

    As with the String class in the preceding chapter, we need to overload the assignment operator (otherwise memberwise copying would result in different lists sharing the same memory). First, we go down the list deleting each of the old elements. Then we reset the list to be empty. Finally we go down the list to be copied, an element at a time, inserting each value. Notice how easy this is once we have defined the insertN method.

    The copy constructor is very like the assignment operator except that we don't need to delete any existing elements first.

    Concatenation involves the following:

  • Build a result list, initially empty;
  • Go down the first list inserting the elements one at a time;
  • Go down the second list, inserting the elements one at a time.

    The empty list test is trivial.

    In order to test equality we first compare the lengths of the lists and, if they are equal, then compare each element in turn.

    The following implements the remaining List methods.

    To finish this chapter, the following user program demonstrates the syntax for using the List class.


    Notes converted from troff to HTML by an Eric Foxley shell script, email errors to me!