CS140 Lecture notes -- Queues


Queues

These notes are primarily about queue implementations. You can read the Wikipedia notes and the textbook to find out about uses of queues. The implementation of queues in the book is fine, but I am going to do it in terms of doubly linked lists, and then follow it up with an array implementation for circular queues. In general queues are open-ended, in which case you almost have to use a linked list. With stacks an array was an acceptable alternative because insertions and deletions always occurred at the end of the array. However, with a queue, deletions occur at the front of the queue, and if you recall our discussion about using arrays, arrays do not work well when you need to delete elements from anywhere other than the end of an array. One could imagine leaving the front part of an array empty as items are removed from the queue and then trying to wrap around when the array size is exceeded. However, if the queue becomes bigger then the size of the array, then the tail of the queue will overflow the head of the queue and you will get into difficult issues of re-sizing the array. Hence using a linked list implementation where the queue can grow unboundedly is usually easier. There is an important special case where the size of the queue can be bounded, in which case we can use an array and a so-called circular queue. We will discuss this special case at the end of these notes.


Queue Interface

Here is our queue nterface, taken roughly from the textbook (you may notice slightly different names for some functions):

As always we return a "void *" handle to the queue so that we can hide its implementation from the user.

These operations are defined in queue.h, which is in the directory /home/bvz/cs140/include. To use them, you must link your code with /home/bvz/cs140/objs/queue.o.


Linked List Implementation of a Queue

We must use a doubly-linked list to implement a queue because our singly-linked list implemention does not support deletion. As an added bonus, if we later added a new operation that allows you to delete an element from the queue, our dllist implementation would handle it gracefully.

Our container object for the queue contains two elements:

  1. a pointer to the list that holds the queue elements, and
  2. an integer representing the size of the queue.

Here is the typedef for a queue, which can be found in queue.c:

typedef struct {
  dllist *data;
  int size;
} queue;

queue.c contains the code itself.

The two interesting functions are queue_enqueue and queue_dequeue, and these two functions are discussed below.


queue_enqueue

To enqueue an item, we simply append it to the end of the dllist and increment the queue's size:

void queue_enqueue(void *queue, void *value) {
  Queue *q = (Queue *)queue;
  dll_append(q->data, value);
  q->size++;
}

queue_dequeue

The dequeue operation requires that we return the value of the first queue element and remove that element from the queue. This operation requires some care, in that we must save the value of the first queue element, then free the node associated with the first element, and finally return the value. If we first free the node associated with the first element and then try to retrieve the value from this node, we may get strange results because the node has already been deleted:

void *queue_dequeue(void *queue) {
  Queue *q = (Queue *)queue;
  // don't do anything if the queue is empty
  if (queue_empty(q)) 
    return NULL;

  // save the first value, then destroy its node, then return the value
  Dllist_Node *first_node = dll_first(q->data);
  void *save_val = dll_val(first_node);
  dll_delete_node(first_node);
  q->size--;
  return save_val;
}

Queuesimp

Here's a simple example borrowed from Dr. Plank. Queuesimp.c shows a very simple example of using a queue. First, we enqueue three integers on the queue -- 1, 2 and 3. Then we call queue_dequeue() twice, and print out the values. Finally, we push 4 onto the queue, and call queue_dequeue() twice more, printing out the values.

Here's the code:

main()
{
  void *q;
  int i;

  q = new_queue();

  // a trick that allows me to store an int in a void * field. I know
  // that an int requires fewer bytes than a void * so I lie to C and
  // tell it that I'm giving it a void *. Later, when I retrieve the
  // value I will cast it back to an int.
  queue_enqueue(q, (void *)1);
  queue_enqueue(q, (void *)2);
  queue_enqueue(q, (void *)3);

  // Here I reverse my lie and cast the stored value back to an int
  i = (int)(queue_dequeue(q));
  printf("First dequeue: %d\n", i);
  i = (int)(queue_dequeue(q));
  printf("Second dequeue: %d\n", i);

  queue_enqueue(q, (void *)4);

  i = (int)(queue_dequeue(q));
  printf("Third dequeue: %d\n", i);
  i = (int)(queue_dequeue(q));
  printf("Fourth dequeue: %d\n", i);
}
And here's its output:
UNIX> queuesimp
First dequeue: 1
Second dequeue: 2
Third dequeue: 3
Fourth dequeue: 4
UNIX> 

Queuehead

Here's another example borrowed from Dr. Plank that prints the first n lines of standard input, where n is the command line argument. What we'll do is read the first n lines (or less if standard input ends before n lines) and enqueue a copy of each line. When we're done with that, we'll call queue_dequeue() for each line on the queue and print it out.

Here's the code (in queuehead.c):

main(int argc, char **argv)
{
  void *q;
  int n, i;
  IS is;

  if (argc != 2) {
    fprintf(stderr, "usage: %s n\n", argv[0]);
    exit(1);
  }

  if ((sscanf(argv[1], "%d", &n) !=1) || (n < 0)) {
    fprintf(stderr, "n (%s) must be an integer that is >= 0\n", argv[1]);
    exit(1);
  }

  q = new_queue();
  is = new_inputstruct(NULL);

  i = 0;
  while (i < n && get_line(is) >= 0) {
    queue_enqueue(q, strdup(is->text1));
    i++;
  }

  while (!queue_empty(q)) {
    printf("%s", (char *)queue_dequeue(q));
  }
} 
It works just fine. Of course, it's easier to write head without queues, but this is a nice illustration of using queues:
UNIX> cat input
Give 
Him
Six!
UNIX> queuehead 2 < input
Give 
Him
UNIX> queuehead 100 < input
Give 
Him
Six!
UNIX> queuehead 0 < input
UNIX> 

Circular Queues

If the maximum size of a queue is known in advance, then it can be more efficient to use an array to store the queue contents. To solve the problem with arrays mentioned at the outset of these notes, namely that the first array entries become empty as items are dequeued, we allow the queue to wrap around in order to re-use these array entries. For this reason the queue is called a circular queue. In lecture I will draw some pictures of circular queues. An example use of circular queues is when we are streaming video to a video card, and the card cannot hold the entire video. We would typically stream video to the card, and when the card has exhausted its memory, then wrap around to the front, where presumbably the video has already been displayed to the user and is no longer needed.

Here is a sample picture of what a circular queue might look like:

   0    1     2    3    4    5    6
-------------------------------------
|    |  22 |  7 | -3 | 58 |    |    |
-------------------------------------
        ^               ^
      front           back
Here's an example of what a circular queue that has wrapped around might look like:
   0    1     2    3    4    5    6
-------------------------------------
| 16 |  22 |    |    |    | 17 |  1 |
-------------------------------------
        ^                    ^
      back                 front 
The front and back indices point to the front and back of the queue respectively.

In my implementation of a circular queue, I found it easier to compute the back index when I need it. I can compute it using the formula:

back = (front-1 + queue_size) % queue_capacity
where queue_size is the number of elements currently in the queue and queue_capacity is the maximum number of elements that the queue can hold. The mod operator (%) allows us to wrap around to the front of the array. It may seem odd to subtract 1 from front, but if the queue is of size 1, then front and back should be the same, which is what is accomplished by subtracting 1. Note that if the queue is empty, then back will be 1 less than front, which at least to my mind, also seems reasonable. For example, in the above wrapped example, front is 5, the size of the queue is 4, and the capacity of the queue is 7, so back can be computed as:
back = (5-1 + 4) % 7 = 1

Circular Queue Interface

The interface for a circular queue is similar to an unbounded queue with two exceptions:

  1. void *new_queue(int max_capacity): we should specify the maximum size of the queue when we create the queue, and
  2. int queue_full(void *queue): we need to be able to determine if the queue is full, since it has a limited capacity.
The full interface can be found in cqueue.h.

Circular Queue Implementation

Our container object for the circular queue contains four elements:

  1. a pointer to the array that holds the queue elements,
  2. an integer representing the index of the first element in the queue,
  3. an integer representing the size of the queue,
  4. an integer representing the capacity of the queue (i.e., the maximum number of elements that the queue may hold)

Here is the typedef for a circular queue, which can be found in cqueue.c:

typedef struct {
  void **data;
  int capacity;
  int front;
  int size;
} Queue;

cqueue.c contains the code itself.

Once again, the two interesting functions are queue_enqueue and queue_dequeue, and these two functions are discussed below.


queue_enqueue

To enqueue a new element, we must take the following steps:

  1. ensure that there is space in the queue for the new element, and return if there is no space available.
  2. compute the index of the entry where the new element should be placed. The new entry should be placed one entry after the back of the current array. Our formula for back will now be:
    back = (front + queue_size) % queue_capacity
    
    We have dropped the subtraction of 1 from front because we want the next free entry, and that is 1 to the right of the current back.
  3. assign the value to the free entry
  4. increment the queue size
Here is the code:
void queue_enqueue(void *queue, void *value) {
  Queue *q = (Queue *)queue;
  int back;
  if (!queue_full(queue)) {
    back = (q->front + q->size) % q->capacity;
    q->data[back] = value;
    q->size++;
  }
}

queue_dequeue

To dequeue an element, we need to:

  1. advance front 1 entry to the right, wrapping it if necessary using the mod operator (%),
  2. decrement the size of the queue by 1, and
  3. return the value that front previously pointed to. It is convenient to save the old value of front before incrementing it, because the new front might have wrapped around, in which case it could be tricky to compute the old value of front.
Here is the code:
void *queue_dequeue(void *queue) {
  Queue *q = (Queue *)queue;
  int old_front = q->front;
  if (queue_empty(queue))
    return NULL;
  q->size--;
  q->front = (q->front + 1) % q->capacity;
  return q->data[old_front];
}
Note that I delete a value from the queue by advancing front and that the value is still in the array. Hence it is safe for queue_dequeue to "reach back" one entry and return the value of that entry.


cqueuetail

As an example of how one might advantageously use a circular queue, I have implemented the unix tail command in cqueuetail.c. The tail command prints the last n lines of a file, where n is a command line argument. The key section of the code is shown below, along with comments that explain what is happening:

  // n is the number of lines to print, so create a queue of this size
  q = new_queue(n);
  is = new_inputstruct(NULL);

  // read lines from the file and place them on the queue. When the
  // queue fills up after the first n lines are read, the queue_full
  // function will return true, and we will dequeue a line before
  // enqueuing the new one, thus ensuring that we always have the last
  // n lines of the file on the queue
  while (get_line(is) >= 0) {
    if (queue_full(q))
      queue_dequeue(q);
    queue_enqueue(q, strdup(is->text1));
  }

  // the queue has the last n lines of the file in it so 
  // print the queue from front to back
  while (!queue_empty(q)) {
    printf("%s", (char *)queue_dequeue(q));
  }