CSCI 161 lab exercise 2: a linked list class

Lab exercise 2 is to be submitted electronically by 10am Feb. 4th, and covers the work from the lab sessions of Jan. 21-23, and 28-30, suitably cleaned up to adhere to standards. The submission mechanism is detailed below, and must be followed to obtain credit for the lab.

The exercise will be marked out of 10, and is worth 5% of your total grade.

Late submissions will be penalized 2.5 marks per day late or part thereof.


Exercise details:

For lab exercise 2 you will be implementing the phone book as class, again using a linked list for the internal storage.

In lab exercise 1 your phonebook was a collection of standalone items:
- a struct held individual entries in the phone book as part of a linked list,
- a variable in the main routine kept track of the front list element, and
- you implemented a number of functions to build, search, and manipulate the list.

In lab exercise 2, you will implement a collection of class methods that provide all the list functionality, this time as a doubly-linked list which tracks both the front and back of the list.

The main routine and support functions are provided for you in file labex2.C, and the definition of the class and some supporting constants are provided in header file LList.h.

Your implementations of the class methods will go in file LList.C, which will be the file you submit for the exercise.

Here are the files labex2.C and LList.h
// LList.h
#ifndef LLIST_H
#define LLIST_H

#include <string>
using namespace std;

// =========================================================
// ====== Data types and constants used in the program =====

// define a maximum length for text 
const int MaxTextLen = 256;

// define the list class that stores keys/values
//    in a linked list
class LList {

   private:

      // define the contents of a single key/value pair
      struct keyValue {
         string key;       // search key
         string value;    // associated data value
         keyValue *next;   // next key/value in the list
         keyValue *prev;   // previous key/value in the list
      };

      // maintain pointers to the front and back items in the list
      keyValue *front, *back;

      // lookupEntry  (internal use)
      // -----------
      // search the list or an entry matching the specified key,
      //    return a pointer to the entry if found,
      //    return null otherwise
      keyValue* lookupEntry(string key);

   public:
      
      // constructor to initialize the empty list
      LList();

      // destructor to deallocate all list entries
      //    when the list is deleted
      ~LList();

      // adds one key/value pair to the list
      //    as long as the key does not already appear in the list
      // if the addFront flag is true then the entry
      //    is inserted at the front of the list,
      //    otherwise it is inserted at the back
      // returns true if successful, false otherwise
      bool insertEntry(string key, string value, bool addFront=true);
   
      // search the list or an entry matching the specified key,
      //    set the matching value and return true if found,
      //    return false otherwise
      bool lookupEntry(string key, string &value);
   
      // search the list or an entry matching the specified key,
      //    set the new matching value and return true if found,
      //    return false otherwise
      bool changeEntry(string key, string &value);
   
      // search the list or an entry matching the specified key,
      //    remove the entry from the list, delete it, 
      //       and return true if found,
      //    return false otherwise
      bool deleteEntry(string key);
   
      // display each key/value pair in the list,
      //   with one key/value pair per line of output
      void displayEntries();
      
      // if the specified file cannot be opened return -1,
      // otherwise
      //     add each key/value pair in the file to the list
      //     returning a count of the total number of successful inserts
      // assumes the file contains an even number of lines, 
      //    alternating between keys and values, e.g.
      //                Fred Jones
      //                555-123-4567
      //                Scoobert Doo
      //                555-987-6543
      int insertFromFile(char filename[]);

      // if the list is empty simply return false,
      // otherwise remove and delete one element from the list
      //    after copying its key/value to the parameters
      // removes the front element if atFront is true,
      //    the back element otherwise
      // returns true if successful
      bool removeFrom(string &key, string &value, bool atFront=true);
};  
   
#endif
// labex2.C
#include "LList.h"
#include <iostream>
using namespace std;

// =========================================================
// ====== Data types and constants used in the program =====

// define the valid single-character commands
enum Command { 
               QuitCmd = 'Q',       InsertCmd = 'I', 
               EditCmd = 'E',       LookupCmd = 'L', 
               HelpCmd = 'H',       RemoveCmd = 'R', 
               PrintCmd = 'P',      FileInsertCmd = 'F', 
               InvalidCmd = '-',     ExtractCmd = 'X',
             };

// define the character used to select front vs back of list
const char ListFront = 'F';

   
// =========================================================
// ====== Prototypes for provided support routines =========

// get the user to pick which end of the list to work on,
//     returns true for front, false for back
bool pickEnd();

// read a name from the user
//    and store it in the parameter provided
void getName(string &name);

// read a number from the user
//    and store it in the parameter provided
void getNumber(string &number);

// display the list of valid commands
void printCommands();

// read and return a valid single-character command,
//    limits to one command per line
// (anything beyond the first command is discarded)
Command getCommand();

// process a command and update the name/number list
void processCommand(Command cmd, LList &phoneBook);

// =========================================================
// ====== Provided main routine ============================

int main(int argc, char* argv[])
{
   // current list of messages, initially empty
   LList phoneBook;

   // current user command
   Command cmd;

   // display the welcome message
   cout << "Welcome to the phonebook system\n" << endl;

   // load names/numbers from the file specified in 
   //    the command line argument (if any)
   if (argc > 1) {
      int numLoaded = phoneBook.insertFromFile(argv[1]);
      if (numLoaded < 0) {
         cout << "Attempt to load from file \"" << argv[1];
         cout << "\" was unsuccessful" << endl;
      } else {
         cout << "Load from file \"" << argv[1];
         cout << "\" inserted " << numLoaded << " entries\n" << endl;
      }
   }

   // display the command list on startup
   printCommands();

   // keep obtaining and processing commands
   //   until the user decides to quit
   do {
      cmd = getCommand();
      processCommand(cmd, phoneBook);
   } while (cmd != QuitCmd);

   // shut down
   cout << "Exiting the phonebook system\n" << endl;
   return 0;
}

// =========================================================
// ====== Implementations of provided support routines =====

// get the user to pick which end of the list to work on,
//     returns true for front, false for back
bool pickEnd()
{
   char choice;
   char junk[MaxTextLen];
   cout << "Please enter " << ListFront;
   cout << " to work from the front of the list,\n";
   cout << "or anything else to work from the back: " << endl;
   cin >> choice;
   cin.getline(junk, MaxTextLen);
   if (toupper(choice) == ListFront) {
      return true;
   }
   return false;
}

// read a name from the user
//    and store it in the parameter provided
void getName(string &name)
{
   char text[MaxTextLen];
   cout << "Please enter the name of the person or organization" << endl;
   cin.getline(text, MaxTextLen);
   name = text;
}

// read a number from the user
//    and store it in the parameter provided
void getNumber(string &number)
{
   char text[MaxTextLen];
   cout << "Please enter the phone number of the person or organization" << endl;
   cin.getline(text, MaxTextLen);
   number = text;
}

// display the list of valid commands
void printCommands()
{
   cout << "Enter " << (char)(HelpCmd) << " to print this menu,\n";
   cout << "   or " << (char)(InsertCmd) << " to insert a new phone book entry,\n";
   cout << "   or " << (char)(PrintCmd) << " to print the phone book contents,\n";
   cout << "   or " << (char)(LookupCmd) << " to lookup a phone number,\n";
   cout << "   or " << (char)(EditCmd) << " to edit someone\'s phone number,\n";
   cout << "   or " << (char)(RemoveCmd) << " to search for and remove a phone book entry,\n";
   cout << "   or " << (char)(ExtractCmd) << " to remove the first or last phone book entry,\n";
   cout << "   or " << (char)(QuitCmd) << " to quit,\n" << endl;
}

// read and return a valid single-character command
//    limits to one command per line
// (anything beyond the first command is discarded)
Command getCommand()
{
   char cmd = QuitCmd;
   char junk[MaxTextLen]; // used to discard rest of line
   do {
      cout << "Please enter a command: ";
      cin >> cmd;
      cin.getline(junk, MaxTextLen);
      cmd = toupper(cmd);
      switch (cmd) {
         case HelpCmd: return HelpCmd;
         case InsertCmd: return InsertCmd;
         case PrintCmd: return PrintCmd;
         case LookupCmd: return LookupCmd;
         case EditCmd: return EditCmd;
         case RemoveCmd: return RemoveCmd;
         case ExtractCmd: return ExtractCmd;
         case QuitCmd: return QuitCmd;
         default: 
            cout << "Invalid command entered: " << cmd << endl;
            cout << "Please try again" << endl;
            cmd = InvalidCmd;
            break;
      }
   } while (cmd == InvalidCmd);
   return InvalidCmd;
}

// process a command and update the name/number list
void processCommand(Command cmd, LList &phoneBook)
{
   string name, number;
   bool atFront;
   switch (cmd) {
      case HelpCmd: 
         printCommands();
         break;
      case InsertCmd: 
         getName(name);
         getNumber(number);
         atFront = pickEnd();
         cout << "Insert attempt (";
         cout << name << ":" << number << ")" << endl;
         if (phoneBook.insertEntry(name, number, atFront)) {
            cout << "...succeeded" << endl;
         } else {
            cout << "...failed" << endl;
         }
         break;
      case PrintCmd: 
         cout << "Phone book contents:" << endl;
         phoneBook.displayEntries();
         break;
      case LookupCmd:
         getName(name); 
         cout << "Lookup attempt for " << name << endl;
         if (phoneBook.lookupEntry(name, number)) {
            cout << "...number is " << number << endl;
         } else {
            cout << "...no entry found" << endl;
         }
         break;
      case EditCmd: 
         getName(name);
         getNumber(number);
         cout << "Edit attempt for " << name << endl;
         if (phoneBook.changeEntry(name, number)) {
            cout << "...new number is " << number << endl;
         } else {
            cout << "...no entry found" << endl;
         }
         break;
      case RemoveCmd: 
         getName(name); 
         cout << "Attempting to remove entry for " << name << endl;
         if (phoneBook.deleteEntry(name)) {
            cout << "...succeeded" << endl;
         } else {
            cout << "...failed - no entry found" << endl;
         }
         break;
      case ExtractCmd:
         atFront = pickEnd();
         cout << "Extracting element from phonebook end\n";
         if (phoneBook.removeFrom(name, number, atFront)) {
             cout << "...removed " << name << ":" << number << endl;
         } else {
            cout << "...failed, list is empty" << endl;
         }
         break;
      case QuitCmd:
         // no action required
         break;
      default: 
         cout << "Invalid command supplied, please try again\n";
         printCommands();
         break;
   }
}


Recommended development sequence

Again, I strongly advise you follow a well-thought out sequence of development steps in completing the exercise.

    PHASE 0: set up the files

  1. copy labex2.C and LList.h to your labex2 directory

  2. create file LList.C, and make sure it #include's "LList.h"

    PHASE I: study the definition of the LList class

    Study LList.h, noting the definition of the structs (with pointers to next and prev), the fields of the class (including pointers to front and back), and the various methods.

    PHASE II: implement skeletons for the LList methods

    This involves just getting empty bodies created in LList.C for all the class methods.

  3. For example, add an empty constructor implementation.
    LList::LList()
    {
    }
    

  4. Add a destructor (LList::~LList()) implementation, again just an empty skeleton for now

  5. For each of the other methods prototyped within the class, provide a skeleton implementation.

    Note that, unlike labex1, we now are maintaining pointers to both the front and back of the list.

    Note that for the private lookupEntry method you will also need to tell the compiler where the keyValue struct is defined, so that method definition becomes:
    LList::keyValue* LList::lookupEntry(string key)

    (The LList:: is not needed before any of the other methods' return types, since they are all types that the compiler already recognizes: bool, int, void, etc.)

  6. Compile, test, and debug.
    As with labex1, we need to compile the .C files together:
    g++ -Wall labex2.C LList.C -o labex2
    We can't test any of the functionality yet, but at least compiling lets us check that we've defined all the methods correctly.
    Once it compiles cleanly, move on to the next phase.

    PHASE III: implement the basic LList methods

  7. For the constructor, it simply needs to initialize front and back to null.

  8. Finish displayEntries and get an initial version of insertEntry working.

    These will work much like they did in labex1, except some tweaks since it is now part of a class and we have back and prev to deal with.

    For insert, at first just worry about inserting at the front of the list:

    Once that is working, go through and handle inserts at the back, which are essential a mirror image of inserts at the front (just swapping front/back, next/prev).

  9. Finish lookupEntry: both the public and private versions.

    The public version can simply call the private one.
    If the private one returns null, the public one returns false.
    Otherwise, the public one copies the data value out of the struct returned by the private one, and returns true.

    The private version starts at the front of the list and scans until it finds what it's looking for (and returns the pointer) or it hits the end of the list (and returns null).

  10. Finish changeEntry and insertEntry
    These will both use the private lookupEntry

    changeEntry would call the private lookup, and if that returns null just return false, otherwise update the value field and return true.

    Now let's finish off the implementation of insertEntry:
    The list isn't allowed to hold duplicates, so before inserting we should really check that the key isn't anywhere in the list.

    To do this we can simply call the private lookupEntry on the key value, and if it returns true then simply return false instead of performing the insert.

  11. Compile, test, and debug.
    At this point, you can start with the same test cases as you did for labex1 (except the removes) then add additional tests for adding specifically at the front/back.

    PHASE IV: implement the deleteEntry method

    This is a little trickier now that it is a doubly-linked list, with tracking of both back and front.

    In addition to the general case where the item is somewhere in the middle of the list, we also have to deal with special cases where the element deleted is (i) the only thing in the list, (ii) the front thing in the list, or (iii) the back thing in the list.

  12. We can use the private version of lookupEntry to find the item if it is in the list (or just return false if lookupEntry doesn't find it).

  13. If both front and back point to the item found, then it must be the only thing in the list, so we can simply copy it's value out then delete it (and return true).

  14. If just front is null then it must be the first of multiple items in the list, so copy it's next pointer to front (so front knows of the new start of the list), set the new front item's prev field to null (so it knows it is now the front of the list), copy the target item's value out, delete it, and return true.

  15. Handling the case where the element is at the back of the list is pretty much a mirror of the front case, swapping front/back and prev/next.

  16. For the general case, we need to update the pointers around the target node correctly.
    Let's call our target node T, the node before it B, and the node after it A, i.e.:
    ( B <-> T <-> A )
    So to chop out T,
    - B's prev needs to point to A
    - A's prev needs to point to B
    Then we can copy out T's value, delete T, and return true

  17. Compile, test, and debug.
    Now, in addition to your other tests, you can check for all your remove cases (items not in the list, items at the ends of the list, items in the middle of the list, items that are the ony thing in the list, etc).

    PHASE V: implement the removeFrom method

    This deals with removing the first or last item specifically, rather than searching for an item by its key.

  18. There are again multiple cases to deal with:

  19. Compile, test, and debug.
    Again, in addition to your other test cases, check for functionality when you remove the only thing in the list (from front, from back), when you remove something from a list with multiple items, when you try to remove something from an empty list, etc.

    PHASE VI: use removeFrom to finish the destructor

  20. If you set up a while loop that just keeps doing removeFrom until front is null,
    then you've effectively deleted everything in the list.

    PHASE VII: complete the insertFromFile routine
    Since this routine just relies on calls to insertEntry, this should actually be identical to the working solution from labex1.

    Get this working, clean things up, and labex2 is done!


Submission mechanism

All the code for the assignment must be in a single file named LList.C (correct spelling and capitalization is essential for the submission to work correctly).

To submit the assignment, cd to the directory that contains your LList.C file and type the following command:

   /home/faculty/wesselsd/bin/submit  161  LList.C
Note that each submission overwrites any previous submission you have made.


Marking guidelines:

Marks will be based on:

Code standards:

The submission must follow the code standards for labex1, plus the following standards for classes:

  • The layout/indentation for a class should be as follows:
    // a short description of the purpose and use of the class
    class ClassName {
       
       private:
          // any private data types, fields, and methods,
          // suitably commented and indented
    
       protected:
          // any protected data types, fields, and methods,
          // suitably commented and indented
    
       public:
          // any public data types, fields, and methods,
          // suitably commented and indented
    
    };
    

  • All the method implementations for the class should appear in the appropriate .C file.

    Each should have appropriate commenting, following a similar style as was specified for functions in labex1.