Here is our queue nterface, taken roughly from the textbook (you may notice slightly different names for some functions):
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.
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:
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.
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++; }
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; }
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>
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>
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 backHere'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 frontThe 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_capacitywhere 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
The interface for a circular queue is similar to an unbounded queue with two exceptions:
Our container object for the circular queue contains four elements:
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.
To enqueue a new element, we must take the following steps:
back = (front + queue_size) % queue_capacityWe 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.
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++; } }
To dequeue an element, we need to:
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.
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)); }