CS140 Lecture notes -- Queues

  • Jim Plank
  • Directory: ~cs140/www-home/notes/Queues
  • Lecture notes: http://www.cs.utk.edu/~cs140/notes/Queues
  • Tue Oct 13 16:58:39 EDT 1998

    Queues

    Queues are a second linked data structure. They are a ``FIFO'' data structure, meaning ``First-in, first-out.'' Most real-life waiting situations are queus -- at the bank teller, at the Kroger, at the traffic light, etc.

    A queue has three main operations:

    Of course, Queue is a (void *).

    These operations are defined in queue.h, which is in the directory /home/cs140/spring-2004/include. To use them, you must link your code with /home/cs140/spring-2004/objs/queue.o. Since queue.h makes use of Jval's, you'll have to link with jval.o as well.

    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:

    #include < stdio.h >
    #include "jval.h"
    #include "queue.h"
    
    main()
    {
      Queue q;
      int i;
    
      q = new_queue();
    
      queue_enqueue(q, new_jval_i(1));
      queue_enqueue(q, new_jval_i(2));
      queue_enqueue(q, new_jval_i(3));
    
      i = jval_i(queue_dequeue(q));
      printf("First dequeue: %d\n", i);
      i = jval_i(queue_dequeue(q));
      printf("Second dequeue: %d\n", i);
    
      queue_enqueue(q, new_jval_i(4));
    
      i = jval_i(queue_dequeue(q));
      printf("Third dequeue: %d\n", i);
      i = jval_i(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> 
    
    Queues are conceptually simpler than stacks -- a queue is simply like any line you wait in -- first come, first serve. To give it the same visual picture as the stack, again, we maintain a list of items, but this time we put items in one side, and take them off the other.

    Thus, after the first three calls to queue_enqueue(), we have a list (1,2,3). The first call to queue_dequeue() removes the 1, and leaves (2,3). The second call to queue_dequeue() removes the 2, and leaves (3). The last call to queue_enqueue() turns the list into (3,4), and the subsequent queue_dequeue() calls return 3 and 4 respectively.


    Queuehead

    Interestingly, it's easier to find simple applications of stacks than of queues. However, I will give another example: queuehead. This is yet another example of printing 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):

    #include < stdio.h >
    #include < string.h >
    #include "queue.h"
    #include "fields.h"
     
    main(int argc, char **argv)
    {
      Queue q;
      int n, i;
      IS is;
    
      if (argc != 2) {
        fprintf(stderr, "usage: queuehead n\n");
        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, new_jval_s(strdup(is->text1)));
        i++;
      }
    
      while (!queue_empty(q)) {
        printf("%s", jval_s(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> 
    

    Implementation Details

    You are going to implement queue.c in Lab 6. The implementation of queues is slightly more complicated than the implementation of stacks. If you at all uncomfortable with the implementation of stacks, go over the stack implementation lecture notes once more.

    Like the stack implementation, the queue implementation will have two structs -- a header struct (TrueQueue) and a struct (QueueNode) for each node on the list. Here are the typedef's. You must use these typedef's in your implementation in Lab 6:

    typedef struct queuenode {
      struct queuenode *link;
      Jval val;
    } QueueNode;
    
    typedef struct {
      QueueNode *head;
      QueueNode *tail;
      int size;
    } TrueQueue;
    
    Note the QueueNode is just like the StackNode -- it contains a value and a pointer to the next item in the queue. The header struct is slightly different. It contains two QueueNode pointers -- one to the item that is the head of the queue, and one to the item that is the tail of the queue. If the queue is empty, both of these are NULL. Otherwise, head points to the item that will be returned by queue_head() and queue_dequeue(), and tail points to the last item that was put onto the queue with queue_enqueue().

    Here's the subtle part. The link field of each item on the queue points to the next item that was enqueued. If the item is the last one enqueued (i.e., the item is the one pointed to by the queue's tail pointer), then its link field is NULL.

    It is the job of all the queue routines to work with this structure.


    A detailed example with queuesimp

    Now, I am going to go over what the queue q looks like after each call in queuesimp.c. First is the call q = new_queue(). This of course returns a (void *), but it has the following structure. If cast to a TrueQueue named tq, it will look as follows:
    tq ---->  |-------------|
              | head = NULL |
              | tail = NULL |
              | size = 0    |
              |-------------|
    
    Next comes the call queue_enqueue(q, new_jval_i(1)). This will create a new node for the queue, which is the only item on the queue. Therefore, both the head and tail pointers point to this new node, and the node's link field is NULL:
    tq ---->  |-------------|
              | head =  -------/---->|-------------|
              | tail =  ------/      | link = NULL |
              | size = 1    |        | val.i = 1   |
              |-------------|        |-------------|
    
    Next comes the call queue_enqueue(q, new_jval_i(2)). This will create the second node on the queue, which becomes the tail. The head remains the same, but its link field now points to the second node:
    tq ---->  |-------------|
              | head =  ------------>|-------------| 
              | tail =  -------\     | link = -------+->|-------------|
              | size = 2    |  |     | val.i = 1   | |  | link = NULL |
              |-------------|  |     |-------------| |  | val.i = 2   |
                               |                     |  |-------------|
                               \---------------------/      
    
    You'll note that both tq->tail and tq->head->link point to the new node.

    Next comes the call queue_enqueue(q, new_jval_i(3)). This will create the third node on the queue, which becomes the tail. The head still remains the same. The old tail (the second node) now has a link field pointing to the new tail: Here is the picture:

    tq-->|-------------|
         | head =  ------------>|------------| 
         | tail =  -------\     | link = ------->|-------------|
         | size = 3    |  |     | val.i = 1  |   | link = -------+->|-------------|
         |-------------|  |     |------------|   | val.i = 2   | |  | link = NULL |
                          |                      |-------------| |  | val.i = 3   |
                          |                                      |  |-------------|
                          \--------------------------------------/                 
    
    You should study this picture and the above, and think about how to get from the above to this picture. That will be the heart of your implementation of queue_enqueue().

    Now, we call i = jval_i(queue_dequeue(q)). The queue_dequeue() call will return a Jval whose value is the integer 1, and the queue will become:

    tq ---->  |-------------|
              | head =  ------------>|-------------| 
              | tail =  -------\     | link = -------+->|-------------|
              | size = 2    |  |     | val.i = 2   | |  | link = NULL |
              |-------------|  |     |-------------| |  | val.i = 3   |
                               |                     |  |-------------|
                               \---------------------/      
    
    Thus, the first node of the queue is deleted from the queue, freed, and the head pointer is moved to point to the next node. We call i = jval_i(queue_dequeue(q)) again, and this time the queue_dequeue() call returns a Jval whose value is the integer 2. The queue now becomes:
    tq ---->  |-------------|
              | head =  -------/---->|-------------|
              | tail =  ------/      | link = NULL |
              | size = 1    |        | val.i = 3   |
              |-------------|        |-------------|
    
    Next comes the call queue_enqueue(q, new_jval_i(4)). By now, you should be able to figure these things out -- here's the queue:
    tq ---->  |-------------|
              | head =  ------------>|-------------|
              | tail =  -------\     | link = -------+->|-------------|
              | size = 2    |  |     | val.i = 3   | |  | link = NULL |
              |-------------|  |     |-------------| |  | val.i = 4   |
                               |                     |  |-------------|
                               \---------------------/
    
    Next is another queue_dequeue(q) which returns a Jval whose value is the integer 3, and the queue becomes:
    tq ---->  |-------------|
              | head =  -------/---->|-------------|
              | tail =  ------/      | link = NULL |
              | size = 1    |        | val.i = 4   |
              |-------------|        |-------------|
    
    Finally, the last call to queue_dequeue(q) is made, which returns a Jval whose value is the integer 4, and the queue becomes empty:
    tq ---->  |-------------|
              | head = NULL |
              | tail = NULL |
              | size = 0    |
              |-------------|
    

    A little more detail

    Once again, here's the state after the second call to queue_enqueue():
    tq ---->  |-------------|
              | head =  ------------>|-------------| 
              | tail =  -------\     | link = -------+->|-------------|
              | size = 2    |  |     | val.i = 1   | |  | link = NULL |
              |-------------|  |     |-------------| |  | val.i = 2   |
                               |                     |  |-------------|
                               \---------------------/      
    
    I'm going to show pictorally what happens when you call queue_enqueue(q, new_jval_i(3)). I will assume that val is a parameter to queue_enqueue, and there is a local variable QueueNode *qn. When queue_enqueue(q, new_jval_i(3)) is called, the state first looks like:
    val.i = 3
    qn = ?
    tq ---->  |-------------|
              | head =  ------------>|-------------|
              | tail =  -------\     | link = -------+->|-------------|
              | size = 2    |  |     | val.i = 1   | |  | link = NULL |
              |-------------|  |     |-------------| |  | val.i = 2   |
                               |                     |  |-------------|
                               \---------------------/
    
    Now, we malloc a new QueueNode, and set its fields:
    val.i = 3
    qn ------------------------------------------------------------>|-------------|
    tq-->|-------------|                                            | link = NULL |
         | head =  ------------>|------------|                      | val.i = 3   |
         | tail =  -------\     | link = ------+->|-------------|   |-------------|
         | size = 3    |  |     | val.i = 1  | |  | link = NULL |
         |-------------|  |     |------------| |  | val.i = 2   |
                          |                    |  |-------------|
                          \--------------------/
    
    We then link it into the list:
    val.i = 3
    qn -----------------------------------------------------------+->|-------------|
    tq-->|-------------|                                          |  | link = NULL |
         | head =  ------------>|------------|                    |  | val.i = 3   |
         | tail =  -------\     | link = ------+->|-------------| |  |-------------|
         | size = 3    |  |     | val.i = 1  | |  | link = -------/
         |-------------|  |     |------------| |  | val.i = 2   |
                          |                    |  |-------------|
                          \--------------------/
    
    And then reset tail to be this new node:
    val.i = 3
    qn -----------------------------------------------------------+->|-------------|
    tq-->|-------------|                                          |  | link = NULL |
         | head =  ------------>|------------|                    |  | val.i = 3   |
         | tail =  -------\     | link = -------->|-------------| |  |-------------|
         | size = 3    |  |     | val.i = 1  |    | link = -------+
         |-------------|  |     |------------|    | val.i = 2   | |
                          |                       |-------------| |
                          |                                       |
                          \---------------------------------------/
    
    When we return, qn and val go away, and we're left with the picture we desire:
    tq-->|-------------|
         | head =  ------------>|------------| 
         | tail =  -------\     | link = ------->|-------------|
         | size = 3    |  |     | val.i = 1  |   | link = -------+->|-------------|
         |-------------|  |     |------------|   | val.i = 2   | |  | link = NULL |
                          |                      |-------------| |  | val.i = 3   |
                          |                                      |  |-------------|
                          \--------------------------------------/                 
    

    Now, if we call queue_dequeue(), the following happens. We'll have local variables val and qn. When we start, here's the state:
    val = ?
    qn = ?
    tq-->|-------------|
         | head =  ------------>|------------|
         | tail =  -------\     | link = ------->|-------------|
         | size = 3    |  |     | val.i = 1  |   | link = -------+->|-------------|
         |-------------|  |     |------------|   | val.i = 2   | |  | link = NULL |
                          |                      |-------------| |  | val.i = 3   |
                          |                                      |  |-------------|
                          \--------------------------------------/
    
    First, we set val to queue_head(q):
    val.i = 1
    qn = ?
    tq-->|-------------|
         | head =  ------------>|------------|
         | tail =  -------\     | link = ------->|-------------|
         | size = 3    |  |     | val.i = 1  |   | link = -------+->|-------------|
         |-------------|  |     |------------|   | val.i = 2   | |  | link = NULL |
                          |                      |-------------| |  | val.i = 3   |
                          |                                      |  |-------------|
                          \--------------------------------------/
    
    And then we set qn to be the head of the queue:
    val.i = 1
    qn ----------------------\
    tq-->|-------------|     |
         | head =  ----------+->|------------|
         | tail =  -------\     | link = ------->|-------------|
         | size = 3    |  |     | val.i = 1  |   | link = -------+->|-------------|
         |-------------|  |     |------------|   | val.i = 2   | |  | link = NULL |
                          |                      |-------------| |  | val.i = 3   |
                          |                                      |  |-------------|
                          \--------------------------------------/
    
    Next, we remove the head node from the queue, and have the queue's head pointer point to the next node in the queue.
    val.i = 1
    qn ----------------------\
    tq-->|-------------|     |
         | head =  --------\ \->|------------|
         | tail =  -------\ \   | link = ------+->|-------------|
         | size = 3    |  | |   | val.i = 1  | |  | link = -------+->|-------------|
         |-------------|  | |   |------------| |  | val.i = 2   | |  | link = NULL |
                          | |                  |  |-------------| |  | val.i = 3   |
                          | \------------------/                  |  |-------------|
                          \---------------------------------------/
    
    Then, we free qn and decrement the size:
    val.i = 1
    qn = ?
    tq-->|-------------|    
         | head =  ---------\ 
         | tail =  -------\ \-------------------->|-------------|
         | size = 2    |  |                       | link = -------+->|-------------|
         |-------------|  |                       | val.i = 2   | |  | link = NULL |
                          |                       |-------------| |  | val.i = 3   |
                          |                                       |  |-------------|
                          \---------------------------------------/
    
    And we return val. When we're done, the queue looks like it should:
    tq ---->  |-------------|
              | head =  ------------>|-------------|
              | tail =  -------\     | link = -------+->|-------------|
              | size = 2    |  |     | val.i = 2   | |  | link = NULL |
              |-------------|  |     |-------------| |  | val.i = 3   |
                               |                     |  |-------------|
                               \---------------------/
    
    with the above two examples, you should be able to figure out the hard parts of implementing queues: writing queue_enqueue() and queue_dequeue().