CSC 161 Grinnell College Spring, 2009 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 * stackArray [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:

• a size function which will return the number of items currently on the stack,

• a print function which will print all of the current elements on the stack, and

• an nth function which takes one parameter (an index) and returns the item at that position from the top in the current stack.

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:

• create
Create a new, empty queue object.

• empty
Determine whether the queue is empty; return true if it is and false if it is not.

• enqueue
Add a new element at the rear of a queue.

• dequeue
Remove an element from the front of the queue and return it. (This operation cannot be performed if the queue is empty.)

## 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:

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:

• void initializeQueue (stringQueue * queue) sets both head and tail fields to NULL.

• int empty (stringQueue queue) tests head (or tail)against NULL.

• int full (stringQueue queue) returns false (or 0 in C), since a linked list array will never encounter a full array. (It is possible that your computer may have no more space to allocation for a node, but we ignore that possibility in this lab.)

• int enqueue (stringQueue * queue, char* item) adds an element to the tail of the list: creating a new node, linking to it as the next item after the current last item, and updating the tail variable. In addition, if the queue had been empty, then head also must be updated to the new node. Recall that enqueue returns the length of the string added to the queue. (In the array implementation, enqueue returned -1 if the array was full, but we can ignore that possibility here.)

• char * dequeue (stringQueue * queue) retrieves the data in the head node, moves head to the second list item, deallocates the space of the old first node, and returns the data in that former head of the queue. In addition, if this item had been the last one on the queue, then tail must be updated to NULL.

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.walker.cs.grinnell.edu/courses/161.fa09/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 last revised 29 April 2009 For more information, please contact Henry M. Walker at walker@cs.grinnell.edu.