Project 1: Doubly Linked Circular List

Circular list implementation with memory conservation and dynamic head and tail nodes

Revision dated 01/05/19

Educational Objectives: After completing this assignment, the student should be able to accomplish the following:

  • Use linked structures to implement a dynamically sized data structure
  • Test dynamic data structures for functionality
  • Test dynamic data structures for resource leaks
  • Implement the ADT List using linked structures
  • Explain the use of single and double links and the advantages of each in an implementation
  • Define and implement iterators for a data structure
  • =========================================================
    Rubric to be used in assessment
    ---------------------------------------------------------
    name.com                    [0..5]:    x
    build (student makefile)    [0..5]:    x
    build (assess makefile)     [0..5]:    x
    testing:
     ftest.x com1               [0..5]:    x
     ftest.x com2               [0..5]:    x
     ftest.x com3               [0..5]:    x
     ftest.x com4               [0..5]:    x
     mtest.x                    [0..5]:    x
    log & testing report        [0..5]:    x
    requirements and specs      [0..5]:    x
    software engineering      [-10..0]:  ( x)
    dated submissions    [-2 pts each]:  ( x)
                                          --
    total:                     [0..50]:   xx
    =========================================================
    

Operational Objectives: Supply source code implementing the template class alt2c::List<T> and its associated Iterator classes. This is a List implementation using a circular linked list model with floating head and tail nodes and memory conservation. Code should be thoroughly tested for functional correctness, robustness, and memory management. The supplied code should function correctly and be free of memory leaks, and your tests should provide evidence of both. This evidence should be summarized in a test report.

Deliverables: Five files: name.com, list2c.h, list2c.cpp, makefile2c, log.txt

Variations on List implementations

These are some of the commonly encountered variations in the way the List data structure can be implemented:

  • Doubly linked: Each node has two links, pointing to the next and previous links in the structure. Double links facilitate navigation both "forward" (toward the back) and "backward" (toward the front) in the structure, and List::Iterators can therefore be made bidirectional, with both operator++() and operator--() in the Iterator API. fsu::List<T> has this feature.

  • Singly Linked: Each node has only one link, pointing to the next link in the structure. Single links facilitate only forward navigation, and List::Iterators are forward only, with operator++() but not operator--() in the Iterator API. Singly linked structures are significantly more memory efficient, but some List operations are less time efficient.

  • Head and Tail Nodes: Some implementations create extra nodes that are a hidden (private) part of the structure and are positioned at one or both ends: before and/or after all of the links containing client data.

    Head and tail nodes can be useful in two ways: (1) simplifying the implementation code by eliminating special cases and (2) providing places for iterators to end up after a traversal. In the case with no tail node, for example, an end iterator is essentially a nullptr, from which we cannot recover using the Iterator API.

  • Linearly Linked: The typical use of linked nodes to implement List has some nodes with null link pointers. At the end of the list, there is a last node whose next pointer is null. In the double linked model, there is also a first node whose prev pointer is null. These two nodes define the begining and end of the list data, and a list can be traversed by starting at the beginning node and following the next pointers until a null is encountered.

  • alt1::List<T> is implemented as a singly linked linear structure without head and tail nodes.

  • alt2::List<T> is implemented as a doubly linked linear structure without head and tail nodes.

  • fsu::List<T> is implemented as a doubly linked linear structure with head and tail nodes.

  • Circularly Linked: The cases above are all examples of linear linked structures, that is, with a fixed notion of front [first link] and back [last link]. fsu::List<T> is implemented in this linear fashion.

    An alternative design is the circular construction, in which no link pointer field is null. The resulting linked structure is called the carrier ring. Wherever one starts in the carrier ring, the nodes can be traversed using the Link::next pointer all the way around the ring back to where the traversal began. (If the links are double, the same would hold for reverse traversals.) Circularly linked structures can be singly or doubly linked with support for forward or bidirectional iterators as the case may be.

    One major advantage of using a circular list design is that links that are removed from the visible list can be saved in the carrier ring and re-used later when needed, thus saving significant processing time of calls to operators new and delete. This is facilitated by maintaining class variables pointing to the first and one-past-the-last links in the carrier ring, indicating where the current public list is stored in the ring. One node in the ring is always excluded from the list in order to disambiguate the empty list from the full carrier, but the excluded node varies dynamically as the list is used.

    Head and tail nodes can be used in the circular model as well, providing a "safe harbor" for iterators to exist before the first and after the last nodes containing list values. In this implementation both the head and tail nodes are outside the list data, essentially demarking a linear list inside the circular carrier ring. The head and tail nodes are always extra nodes in the ring, not containing data belonging to the list. Exactly which two nodes are designated "head" and "tail" may vary dynamically as the list is used (the "floating" case) or remain static (the "fixed" case). Any extra nodes stored in the ring are in the "dark side" between the tail node and the head node.

    In the floating case, due to the dynamic nature of the designated head and tail nodes in a circular list implementation, clients must always assume iterators are invalidated by any modification of the list. Additional care must be taken to handle iterators correctly. Iterators can inadvertantly point into the "dark side" of the carrier ring. For example, an End iterator can be incremented past the end of the list. As another example, the loop
      for(i = list.Begin(); i.Valid(); ++i) {}
    runs forever!

    In the fixed case, the End() iterator has a stable meaning, but iterators pointing into the body of a list may still be rendered invalid by changes made to the list.

    Developers and users must be very careful with the iterator interface.

  • alt2c::List<T> is implemented as a doubly linked circular structure with floating head and tail nodes.

  • alt2d::List<T> is implemented as a doubly linked circular structure with fixed head and tail nodes.

Procedural Requirements

  1. Begin with understanding the chapters on Lists and Deques and a working knowledge of the techniques involved in creating linked structures dynamically.

  2. The official development/testing/assessment environment is specified in the Course Organizer.

  3. Make sure you understand the implementation plan for List<T> described in the references above.

  4. Work within your subdirectory called cop4530/proj1. Keep in mind that with all assignments it is a violation of course policy and the FSU Honor Code to give or receive help on assignments from anyone other than the course instruction staff or to copy code from any source other than those explicitly distributed in the course library.

  5. Copy the following files from the course LIB into your proj1 directory:

    LIB/tests/flist2c.cpp
    LIB/tests/mlist2c.cpp
    LIB/proj1/list2c.api
    LIB/proj1/deliverables.sh
    
  6. Create the first deliverable: a text file name.com that is a command file for flist?.cpp [ElementType = char] that inserts the characters of your full name in alphbetical order such that your name appears correctly spelled in the list. This exercise will help understand the List API and also how to devise tests for List using the supplied test harness.

  7. Create the other four deliverables:

    1. A header file file list2c.h defining the class templates alt2c::List, alt2c::ConstListIterator, and alt2c::ListIterator conforming to the specs below.
    2. A source code file list2c.cpp implementing the class templates defined in the header file.
    3. A makefile named makefile2c with targets flist.x, mlist.x, flist2c.x, mlist2c.x along with the default target that builds all four.
    4. A text file log.txt consisting of a log of all development activity, including documentation for all testing. All five files should be placed in the proj1 directory: These are the project deliverables.

  8. Also in the log, keep detailed notes on procedures and results as you test your implementation list2c.cpp. Use these testing notes to create a summary testing report to conclude the file log.txt.

  9. Turn in the files name.com, list2c.h, list2c.cpp, makefile2c, and log.txt by executing the submit script.

  10. Warning: Submit scripts do not work on the program and linprog servers. Use shell.cs.fsu.edu to submit this assignment. If you do not receive the second confirmation with the contents of your project, there has been a malfunction.

Technical Requirements and Specifications

  1. The first deliverable name.com is a command file for flist?.cpp [ElementType = char] such that:

    1. The characters of your first name are inserted into x1 in alphabetical order, with the first letter of your name capitalized
    2. The characters of your last name are inserted into x2 in alphabetical order, with the first letter of your last name capitalized
    3. Finishing with the commands to accomplish x3 = x1; x3 += x2; x3.Display(std::cout) results in printing your name to screen, with first and last name separated by the underscore character '_'.
    4. Note character insert order is alphabetical, and list traversal order spells your name.
  2. Your implementation of alt2c::List should follow the packaging methodology used for fsu::List: a header file list2c.h and a "slave" file list2c.cpp. The header file contains the class API (including Iterator classes). The code file contains implementing code. The code file is #included into the header file after all of the API is defined but before the multiple read protection and the namespace are closed. (See the chapter on Vectors for an explanation of "slave" file.)

  3. Your implementation should follow the circular doubly linked list plan with head and tail nodes:

    1. Define a full carrier to mean that tail_->next_ == head_. More generally, the unused nodes are those between tail_ and head_. The number of such nodes is the value returned by Excess(). The number of nodes between head_ and tail_ is the value returned by Size(). Capacity is the sum of size and excess.
    2. In general, this relation should always hold: Capacity() + 2 = Size() + Excess() + 2 = n, where n is the total number of nodes currently allocated.
    3. Nodes between head_ and tail_ are those that contain List data and could at times be called the "list side" of the carrier ring. Nodes between tail_ and head_ are excess (allocated but not currently in use). These are sometimes called the "dark side" of the carrier ring.
    4. Pop and Remove operations always conserve allocated memory.
    5. Implement PopFront() by advancing the head node: head_ = head_->next_ (only when the list is not empty, of course). This conveniently "subtracts" a node from the list and "adds" the node to the excess storage. Similarly, PopBack() retreats the tail node.
    6. Remove(i) is a bit more tricky - the node should be unlinked from the list and then linked behind the tail node for later re-use.
    7. Push and Insert operations re-use links whenever there are any available.
    8. When there are no excess nodes, Push and Insert operations operate exactly as in the linear cases.
    9. When there is excess capacity, PushFront(tval) re-uses a node by retreating the head node pointer: head_ = head_->prev_ and copying tval into the data field of the old head node. Similarly, PushBack(tval) re-uses a node by advancing the tail_ node pointer. Insert(iter,tval) has to unlink the unused node following the tail_ node, link it in at the specified location, and update the data field.
    10. Size() is calculated by traversing from head_ to tail_. Excess() is calculated by traversing from tail_ to head_. Empty() is implemented as { return head_->next_ == tail_; }. Full() is implemented as { return tail_->next_ == head_; }.

  4. Required Functionality. The functionality is outlined in the file list2c.api. Be sure to run area51/flist2c_i.x to see the effects of these List:: methods:

    Clear() and Release()             # 'c' and 'C' in the flist2c menu
    Full() and Empty()                # 'e' 
    Size(), Excess(), and Capacity()  # 's' 
    

    The Dump() method ('d' in the flist2c menu) is useful in viewing the carrier ring structure.

  5. Required Runtime Constraints. The runtime requirements are given in list2c.api.

    Note that this means that we can use alt2c::List to efficiently implement both Stack and Queue: Stack using PushFront/PopFront and Queue using PushBack/PopFront (or PushFront/PopBack).

  6. Your implementations should be tested for both functionality and memory containment using at least the classes T = char and T = fsu::String. Two test programs, clients of alt2c::List<T>, are supplied. Specific instructions for testing for memory leaks are included as comment at the top of the file mlist2c.cpp. DO NOT TEST FOR MEMORY LEAKS WITHOUT FOLLOWING THESE INSTRUCTIONS.

  7. Document all testing in your log.txt, which will be collected by the submit script. This file should contain your daily activity log as well as document your testing - what was done, what the results were, and how those results helped make the software correct.

Hints:

  • The following files are used directly from the course library:

    cpp/xran.h             // the fsu::xran family of random object generators
    cpp/xran.cpp           // ...
    cpp/xranxstr.h         // ...
    cpp/xranxstr.cpp       // ... these are used by mlist.cpp and mlist2c.cpp
    tcpp/list_sort.cpp     // slave file for list2c.h
    tcpp/list2c_macro.cpp  // slave file for list2c.h
    

  • The deliverable name.com for Chris Lacher is shown here:

    #
    # name.com
    # 
    # input order:     Chirs acehLr
    # traversal order: Chris Lacher
    #
    
    11C
    12h
    12i
    1a
    1++
    1++
    1ir
    12s
    12_
    21a
    22c
    22e
    2a
    2++
    2++
    2ih
    21L
    22r
    3=1
    3+=2
    3d
    q
    

    You can copy/paste this set of commands into a file and run it as follows:

    flist.x name.com
    

    and see the result "Chris_Lacher" to screen at the last command. You can copy flist.x from area51/flist_i.x, or you can compile your own using your makefile.

  • The file flist2c.cpp contains a typical "functionality" test program. The idea is to provide access to the entire public interface of class alt2c::List<> and alt2c::ListIterator<> so that you can perform operations on three distinct List objects and associated iterators. It is up to you to use this test program effectively.

  • The file mlist2c.cpp contains a dynamic test for correct memory and pointer management in the implementation of alt2c::List<> and alt2c::ListIterator<>. In contrast to the functionality test, this one runs without user input. It sets up three List<> objects and associated iterators, much as is done in flist, but the operations are called randomly in a loop that runs until Ctrl-C is entered or until the program crashes. This is a very dangerous program that will crash the entire server on which it runs if a defective implementation of List is tested without careful containment of the runspace for the program. Therefore it is imperative that the precautions delineated in the documentation be followed.

  • Your project makefile makefile2c should compile separate executables for the client programs. You should also be able to compile the two supplied test programs using this makefile by entering the command "make -f makefile2c all". You can compile individual test programs or any other target in the makefile by entering "make -f makefile2c xxx" where "xxx" is the target. For example, "make -f makefile2c flist.x" creates the executable for flist.cpp and "make -f makefile2c mlist.o" creates the object code for mlist.cpp.

  • The help flag is operational for flist.x and flist2c.x: enter "flist.x -h" to get info on command line arguments. These are optional file names. The first is an optional command file. The second is an optional output file. When no argument is supplied, the test operates purely interactively. When a command file is specified, the commands in the file are executed with the simulated keyboard entry displayed in red. When an output file is specified, the colors are suppressed and all output is sent to the file. Note that the 'x' option is intended to switch to interactive mode after a series of commands is read from a file.

  • A good strategy to get started on the code for the project begins by copying list2c.api to list2c.h and building the class definition from that information.

  • Sample executables for flist, flist2c, mlist, and mlist2c are available in LIB/area51.