Laboratory Exercise on Queues

Goals

This laboratory exercise introduces the concept of the queue abstract data type and provides experience implementing this ADT.

After introductory material on queues as abstract data types, the lab is organized into two parts:

  1. Implementation of queues with arrays
  2. Implementation of queues with linked lists

Queues as ADTs

Today's reading on queues describes a queue as an ADT that can store data and that has the following operations:

A Potential Queue Header File in C

A typical header for a queue, in which each queue item is a reference to a string stored elsewhere.

  /* initialize queue referenced by the parameter
   */
   void initializeQueue (stringQueue * queue)
   
  /* return whether the queue is empty
     pre:  queue is an initialized queue
     post: returns 1 (true) if queue is empty;
           0 otherwise
   */
  int isEmpty (stringQueue queue)
  
  /* return whether the queue is full
     pre:  queue is an initialized queue
     post: returns 1 (true) if an additional item
              cannot be added to the queue;
           0 otherwise
   */
   int isFull (stringQueue queue)

  /* insert given item into back of the queue
     pre:  the specified queue is initialized
     returns:  -1 if queue is full
               otherwise, length of item, if item added
   */
   int enqueue (stringQueue * queue, char* item)

  /* removes front item from queue referenced by parameter
     pre:  the specified queue is initialized
     returns:  0 if queue is empty
               otherwise, pointer to the item removed
   */
  char * dequeue (stringQueue * queue)

Part I: Implementing Queues with Arrays

Be sure you have read the reading on queues, particularly the reading on the implementation of queues by arrays, before continuing.

In this part of the lab, we will focus on arrays of strings, and this leads to the following declarations for queues:

#define MaxQueue 50  /* size of all queue arrays */

typedef struct {
   int first;
   int last;
   int count;
   char * queueArray [MaxQueue];
} stringQueue;

Work Started in Class: Day 1

Additional implementation notes may be found in today's reading on Queues.

  1. Write two implementations of a queue of strings by implementing these operations.

    1. In one implementation, use the following interpreations for the first and last fields (as discussed in the right column).
      • last designates the location of the most recent item inserted into the queue. (lastA in the diagram.)
      • first designations the location where the next item will be removed from the queue.
    2. In the second implementation, use the following interpreations for the first and last fields (as discussed in the right column).
      • last designates the location where the next item will be inserted into the queue. (lastB in the diagram.)
      • first designations the location where the previous item was removed from the queue.
  2. Use the queue operations within a main procedure to provide thorough testing of each operation.

Homework after Day 1

  1. Add to the queue ADT an additional procedure print that displays each of the elements of the queue on a separate line (without actually removing any of them from the queue).

  2. An alternative approach for the dequeue procedure would add a parameter item to the list of parameters and change the return type to an int type. The idea is that the string would be returned as a parameter char * item and the procedure would return the length of the string item or -1 if the queue was empty. The relevant procedure signature might be:

      int dequeue (stringQueue * queue, char ** item)
    

    and this procedure would be called within the context:

      char * frontItem;
      int returnValue;
      stringQueue myQueue;
      ...
      returnValue = dequeue (&myQueue, &frontItem)
    
    1. The parameter item has type char **, and the call to queue includes &frontItem. Explain why ** and & are needed here.
    2. Write this alternative version of the dequeue operation.

Two alternative interpretations of last, and two for first

Typically, the last records where new items will be added to a queue. However, at least two interpretations are possible:

  • last might designate the location of the most recent item inserted into the queue. (lastA in the diagram.)
  • last might designate the location where the next item will be inserted. (lastB in the diagram.) Alternative interpretations for the last field

    In the diagram, note lastB = lastA + 1

    In an array implementation of a queue, either interpretation can work without trouble, but the details are different:

    Also, assuming the very first item will be placed in array element 0, the initializations of the index fields must be different.

    As noted, either approach can work without trouble, but mixing the two interpretations will likely yield errors. Choosing an interpretation may be called specifying an invariant of the queue code. Throuhgout all queue operations, the interpretation of last (i.e., using lastA or lastB) never varies. Likely the code will use the simple field name last, but the programmer will need to have decided which approach is being used.

    A similar two choices are possible for first


  • Part II: Implementing Queues with Linked Lists

    Be sure you have read the reading on the implementation of queues by linked lists before continuing.

    Work Started in Class: Day 2

    1. Rewrite your program from the first part of this lab on Implementing Queues with Arrays, changing the implementation to use a linked list instead of an array.

    Homework after Day 2

    1. The enqueue operation allocates space for a new node and copies the string item into that node. The dequeue could return a pointer to the character array within the node or it could copy the string back to a newly created array before passing back a reference. Is there an advantage of one of these approaches over the other? Explain.

      Note: dequeue should deallocate space for the node that is removed.

    2. Write a print function that prints all elements on a queue, from the head of the queue to its tail. (This function can be helpful in testing.)

    For those with extra time:

    1. Test your program carefully.

      1. Think of a set of test cases that will thoroughly test your program. What test cases should you include?

        It is troublesome, but true, that there is as much art as science in testing programs well, given that one goal of testing is to think of unusual occurrences that may not come readily to mind.

        At the very least, be sure that your cases include an example of each response required by the problem specification. (For example, you should consider when error conditions should arise, and your testing should include those cases — does the program handle these cases appropriately?) You should also pay particular attention to "boundary cases" that may arise: in the context of queues, reasonable candidates for boundary cases might include adding or deleting items from queues with 0, 1, or perhaps more items.

      2. Note that this program for queues is written to accept input repeatedly from the user. To "automate" such a user, enter your test data in a file with one input value per line, so that the newline character in the file simulates the user pressing the enter key. You do not need to type ctrl-d into your test data file to indicate the end of the file: just end the file, and your program will correctly detect when it reaches the end of the file.

        Now rebuild your program and run it, re-directing it to get its input from the test data file. For example, your run command might look like this:

        ./a.out < queue-test.dat

        Obviously, you will want to examine your output for correctness.

        Your output from running the program this way may look strange because the input prompts appear, but the user input does not. Depending on the situation, you may want to comment the prompts out of your code, or you may want to just put up with odd looking output.

        The value of testing C programs in this way is that it allows you to use the same test cases multiple times without retyping them. Why is this useful? Consider the possibility that the first time you test a given case, your program gives an incorrect response. Once you fix the problem, you will want to test it again, and you will want to be sure that you have tested it on the same data. Further, you will want to re-test all of your previously working cases to make sure that your most recent change did not cause other cases to fail.

        It is good practice (though a somewhat difficult habit to get started) to maintain a set of test cases for each program you write. This makes it easy to re-test your entire program when a new change is made. Re-running all your test cases for each new change is known as system testing.

    Queue Function Prototypes

      void initializeQueue (stringQueue * queue)
      int isEmpty (stringQueue queue)
      int isFull (stringQueue queue)
      int enqueue (stringQueue * queue, char* item)
      char * dequeue (stringQueue * queue)
    


    created 16 August 2011 by Dilan Ustek (questions by Henry Walker)
    revised 16 August 2011 by Dilan Ustek
    reformatted and edited 4 February 20134 by Henry M. Walker
    readings added 19 September 2014 by Henry M. Walker
    revised to updated format 14-16 April 2016 by Henry Walker
    Valid HTML 4.01! Valid CSS!