CSC 161 Grinnell College Spring, 2010
 
Imperative Problem Solving and Data Structures
 

Stacks and Queues with Linked Lists

Goals

This laboratory exercise provides practice implementing stacks and queues using a linked list structure.

Acknowledgement

Step 8 in this lab related to testing originally came from a lab on Testing C Programs by Marge Coahran.

Steps for this Lab

Work for this lab involves both implementing stacks and implementing queues with linked list structures.

Implementing Stacks

The earlier lab on stacks described the following function prototypes:

  int empty (stringStack stack)
  int full (stringStack stack)      
  void initializeStack (stringStack * stack) 
  char * pop (stringStack *stack) 
  int push (stringStack *stack, char * item) 
  char * top (stringStack stack)

The reading for this lab discusses these functions in the context of linked lists with these declarations:

   typedef stackNode * stringStack;

   struct node {
      char str [MaxStack];
      struct stackNode * next;
   } stackNode;     

   stringStack stack;
  1. Copy the program you wrote for the lab on stacks with arrays and modify it, so that the stacks are implemented by linked lists. In this, you will need to change the bodies of the prototype functions, and you will need to change the declaration of the stack variables for the three stacks (for bills, magazines, and notes) used in testing. However, you should not have to change any of the code used for testing, and the output of this new program should be identical in all respects to the output of the program from the previous lab.

  2. As with the previous lab on stacks with arrays, expand the code for the stack ADT implementation to include these functions:

  3. Write a printReverse function that prints all items on a stack, from the bottom of the stack to the top. (Thus, the top item will be printed last.)

    Hint: Consider printReverse as a husk procedure that calls a recursive kernel procedure to move along the stack's list and do the printing.

  4. Write a printFirstString function that scans all items on a stack and prints the one that comes first in alphabetical order.

Implementing Queues

From the lab on Queues with Arrays, you know that a queue is a data structure that provides access to its elements on "first-in, first-out" basis, rather than the "last-in, first-out" constraint that a stack imposes. (For example, it might be prudent to treat that pile of unpaid bills a little differently, adding new elements at the bottom of the pile rather than the top. Paying off the most recent bill first, as in a stack, can make one's other, older creditors a little testy.)

Like a line of people waiting for some service, a queue acquires new elements at one end (the rear of the queue) and releases old elements at the other (the front). While queues sometimes are given a large number of properties, the basic queue operations are as follows:

A Conceptual Implementation of Queues

While queues may be implemented in many ways, one of the simplest conceptually uses a singly-linked list with pointers head and tail giving access to the two ends:

A Queue as a Singly-Linked List

With this perspective, an element on a queue is stored in a data field within a list node:

   /* Maximum length of names */
   #define strMax 20

   typedef struct node
   { char data [strMax];
     struct node * next;
   } queueNode;

The queue itself involves two pointer variables:

   typedef struct {
      queueNode * head;
      queueNode * tail;
   } stringQueue;
   
   stringQueue queue;


With such a structure, each queue operation is reasonably straightforward:

While enqueue and dequeue require a little care to handle empty lists, the code follows the outline fairly closely.

Work with Queues

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

  2. 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.

  3. 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.)

  4. 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.


This document is available on the World Wide Web as

http://www.cs.grinnell.edu/~walker/courses/161.fa10/labs/lab-stacks-lists.shtml

testing (step 8): created February 2007 by Marge Coahran
testing (step 8): revised February 2008 by Marge Coahran
other steps: created 17 April 2008
full lab: revised 25 January 2009
revised 29 April 2009
last revised 5 April 2010
Valid HTML 4.01! Valid CSS!
For more information, please contact Henry M. Walker at walker@cs.grinnell.edu.