UMBC CS 201, Fall 06
UMBC CMSC 201
Fall '06

CSEE | 201 | 201 F'06 | lectures | news | help

The Queue

Implementations using arrays

Initial Array Implementation

The most straightforward approach is to create an array, of some size to hold all of the data that think we will ever need to put into it. Let us also maintain head and tail in terms of the corresponding indicies.

   int myQueue[5];
   int head;
   int tail;

Enqueue

To Enqueue, all we need to do is insert at the end of the used portion of the array. In other words insert immediately past the tail and then increment tail to reflect the new end of the queue.

Initially we have head = -1 and tail = -1, as they do not really point to anything in the array, and thus should not have a valid array index.

         
0 1 2 3 4

So lets insert...

   Enqueue ( myQueue, &head, &tail, 5 );
5        
0 1 2 3 4

Now we have head = 0 and tail = 0, as the head and the tail are both pointing to the same thing, which is at myQueue[0].

Then after a couple more inserts...

   Enqueue ( myQueue, &head, &tail, 1 );
   Enqueue ( myQueue, &head, &tail, 6 );
5 1 6    
0 1 2 3 4

Now we have head = 0 and tail = 2

But what happens is we want to do even more inserts...

   Enqueue ( myQueue, &head, &tail, 1 );
   Enqueue ( myQueue, &head, &tail, 9 );
   Enqueue ( myQueue, &head, &tail, 7 );
   Enqueue ( myQueue, &head, &tail, 8 );
5 1 6 1 9
0 1 2 3 4

Best case we check to tell when we have filled the array and tell the user that they have filled it. Worst case, we don't check and corrupt our memory or someone else's memory which results in a segmentation fault.

Dequeue

Well, here is where we begin to run into problems with the array implementation. What happens when we need to Dequeue an element out of the array. Well we get a gap, if we just delete it, so we will shift everything after it.

So given the following queue, where head = 0 and tail = 2, lets see what happens...

5 1 6    
0 1 2 3 4

Okay, lets dequeue...

   tmp = Dequeue ( myQueue, &head, &tail );
  1 6    
0 1 2 3 4

Oops, now we have a gap where the first element was, so lets shift everything down. We will do this in some sort of loop internal to the Dequeue function...

1 6      
0 1 2 3 4

We also have to decrement the tail so that tail = 1.

As you can see dequeueing is a non trivial task. We have to fill the slot that was created by dequeueing the first element. For a queue that is 5 elements in length this is not too bad. But what if we have several hundred thousand elements in the queue? It is not good to have to shift several hundred thousand elements every time we want to dequeue a single element.

Problems with this Initial Array Implementation.

New and Improved Array Implementation

Well, since we said that dequeueing was horribly inefficient, let's try to improve it. Rather than shifting elements down to the front of the array, we will allow the head to advance through the array. This time, let's look at Dequeue first to get the idea.

Dequeue

Let's assume that the user has enqueued some values into the array, and our current queue looks as follows, with head = 0 and tail = 4 ...

5 1 6 1 9
0 1 2 3 4

Now lets dequeue some values to see what happens...

   tmp = Dequeue ( myQueue, &head, &tail );
  1 6 1 9
0 1 2 3 4

Before we shifted everything down, but to be more efficient let's simply move the head forward. So head = 1 and tail = 4. Let's Do some more dequeueing...

   tmp = Dequeue ( myQueue, &head, &tail );
   tmp = Dequeue ( myQueue, &head, &tail );
      1 9
0 1 2 3 4

Now head = 3 and tail = 4. All we are doing is moving the head forward. This is much more efficient than moving everything in the array, especially for large data sets.

Enqueue

Let us now look at enqueue. By improving dequeue, we have in no way hurt the performance of enqueue. Let us first look at the case where the tail is not at the end of the array yet.

For example, when head = 2 and tail = 3 ...

    5 1  
0 1 2 3 4

Doing an enqueue we still simply put the data after the tail and increment it...

   Enqueue ( myQueue, &head, &tail, 6 );
    5 1 6
0 1 2 3 4

So head = 2 and tail = 4.

But what about the more interesting case where there are vacancies in the array, but the tail is at the end...

Such is the case in the following example, where head = 2 and tail = 4...

    5 1 6
0 1 2 3 4

Well there is room, but it is at the beginning. So it requires some more thought, but we can perform this check and go ahead and move the tail to the beginning...

   Enqueue ( myQueue, &head, &tail, 1)
1   5 1 6
0 1 2 3 4

So after this enqueue we have a case where the tail is actually less than the head. In our case head = 2 and tail = 0. This is perfectly acceptable.

Problems with this "new and improved" Array Implementation

Array Implementation - as good as it gets

Building upon the previous implementation, we can take it a step further and manually allocate a contiguous chunk of memory ourselves. This will allow us to potentially allocate more memory as needed, thus in essence allowing us to be able to resize the queue. This will require us to keep track of the current size of the array allocated for queue use.

If the queue is full when we insert, we can allocate a new array copy over all of the elements, and then free the old array.

Problems with this "as good as it gets" Implementation

Implementation using a linked list

Since we now know about linked lists, we should consider using a linked list implementation of the queue. The queue can be implemented without wasting any space, and by keeping pointers to both the beginning of the list (the head) and the end of the list (the tail), we can make our queue work much faster than an array implementation.

Benefits of using a linked list

The user interface for the linked list implementation

/************************************************\ * Filename: queue.h * * Author: Sue Bogar * * Date Written: 4/22/98 * * Date modified: 11/28/98 * * Section: 101 * * EMail: bogar@cs.umbc.edu * * * * Description: This file contains the function * * prototypes to work with queue.c. * * This set of functions provide the operations * * needed including adding an item to the queue, * * enqueue; deleting an item from the queue, * * dequeue; determining if the queue is empty and * * the printing the items in the queue. * * Since the queue is being implemented as a * * linked list, some functions needed for a list * * have been added to this file, although they * * would normally be found in the linked list * * header file. Those functions are CreateNode * * and SetData. * \************************************************/ #ifndef _queue_h #define _queue_h /****************** * This typedef allows us to call the type * of a pointer to a node a NODEPTR ******************/ typedef struct node *NODEPTR; /****************** * The queue is being implemented as a linked list * and a linked list requires nodes. Each node is * a structure that has two members, the first to * hold data and the second, of type nodePtr is * a pointer to the next node in the queue. ******************/ typedef struct node { int data; NODEPTR next; }NODE; /****************** * Enqueue takes two pointers to nodePtrs, and a * nodePtr as arguments. The first argument will * contain the address of head, the second argument * will contain the address of tail, and the third * argument is a pointer to the node to be inserted. * Enqueue will insert the item at the end of the * queue. The addresses of head and tail are passed * into this function, because this function may * need to change the address held in head, and/or * the address held in tail. ******************/ void Enqueue (NODEPTR* headPtr, NODEPTR* tailPtr, NODEPTR temp); /****************** * Dequeue takes two pointers to NODEPTR as * its arguments. The first argument will * contain the address of head, the second * argument will contain the address of tail. * This function removes an item from the * queue and returns the data value stored * there. This function may also alter the * address held in either head and/or tail. ******************/ int Dequeue (NODEPTR *headPtr, NODEPTR *tailPtr); /****************** * IsEmpty takes a NODEPTR as its first * argument, which is a pointer to the list, * known as head. It determines whether the * queue is empty and returns 1 (true) if * the queue is empty and 0 (false) if it * is not empty. ******************/ int IsEmpty (NODEPTR head); /****************** * PrintQueue takes a NODEPTR as an argument * which is initially the head. The queue is * traversed and the value of the data member * of each item is printed. ******************/ void PrintQueue (NODEPTR curr); /****************** * CreateNode mallocs the space needed to * hold a struct of type node, initializes * the members, and returns a pointer to * the new node. ******************/ NODEPTR CreateNode (void); /****************** * SetData assigns a value passed in into the * data member of the node pointed to by the * NODEPTR it receives as an argument. ******************/ void SetData (NODEPTR temp, int value); #endif


CSEE | 201 | 201 F'06 | lectures | news | help

Tuesday, 22-Aug-2006 07:14:15 EDT