Consider queues in the real world. They can be found everywhere. We wait in a queue at the checkout in a supermarket. We wait in a queue to buy o tickets for a movie. When we arrive somewhere and a queue exists, we have to join the queue at the end (it is considered impolite to "cut" in front of people who are already in the queue). As people are processed, we move closer to the front of the queue, and we wait in the queue until we have moved all the way to the front, and then we are processed. We enter the queue at one end, and exit the queue on the other end.
Real-world queues can be easily modeled as a data structure. A queue is a first-in-first-out structure where we process the items in the same order in which we added them. The operations on a queue are:
- enqueue - adds an item to the end of the queue
- dequeue - removes the item from the front of the queue
- peek - tells us what is on the top of the queue without actually removing it (this operation is not always included)
Why Use Queues?
Queues don't have many operations defined upon them. It may seem like they are significantly less powerful than a vector or a linked-list. We can add to either end of a vector or a linked-list, and we can perform many other tasks with them as well. So why would we want to use a queue?
We use queues for the same reasons that we use them in the real world. Sometimes, fewer operations impose more of an order, and therefore make things run more efficiently. Imagine the checkout at a grocery store if there was more than the basic queue operations. Suppose cashiers could choose at random which customer they will process next, or suppose that customers could jump into the line at any random spot they choose. The result would be chaos and the efficiency would be lost.
This need for order is sometimes necessary in the computer world as well. When you print something in a cluster, the print job enters a queue, so the amount of time you have to wait for your printout is only dependent on the jobs that are in the queue in front of you. Suppose that you are trying to meet a deadline, and every now and then some other print job enters the queue ahead of you -- there would be no way to know how long it will take before your print request is processed.
Implementing Queues With Linked Lists
Now that we know what queues are, how do we go about building them? Well, we have already mentioned that vectors and linked-lists have all of the capabilities of queues, so it seems natural to build on top of them.
Building a queue using a linked list is quite trivial. enqueue() is implemented by adding to the tail of the list and dequeue() is implemented by removing from the head of the list. It is especially important to add to the tail and delete from the head, as opposed to adding to the head and deleting from the tail, on singly linked lists. To understand this, think carefully about the expense of deleting from the tail. With, or without, a tail reference -- it involves walking through the list to find the node prior to the tail. Deleting from the head is, however, quite inexpensive, since the head reference is always the predecessor of the head node. And, of course, adding to the tail of a sinly linked list, especially with a tail reference, is quite inexpensive -- only the tail node and the tail reference, itself, need to be updated in the common case.