Welcome!
This page is the main page for Queues
This page is the main page for Queues
A queue (pronounced like the letter “q”) data structure organizes data in a First In, First Out (FIFO) order: the first piece of data put into the queue is the first piece of data available to remove from the queue. A queue functions just like the line you would get into to go into a ballgame, movie, or concert: the person that arrives first is the first to get into the venue.
^[https://commons.wikimedia.org/w/index.php?title=Special:CiteThisPage&page=File%3AReichstag_queue_2.JPG&id=379395710&wpFormIdentifier=titleform]
You might be thinking that this sounds a lot like the stack structure we studied a few modules back, with the exception that the stack was a Last in, First Out (LIFO) structure. If so, you are correct. The real difference between a stack and a queue is how we take data out of the data structure. In a stack, we put data onto the top of the stack and removed it from the top as well. With a queue, we put data onto the end (or rear) of the queue and remove it from the start (or front) of the queue.
The name for queues comes the word in British English used to describe a line of people. Instead of forming lines to wait for some service, British form queues. Thus, when we think of queues, often the first picture to come to mind is a group of people standing in a line. Of course, this is exactly how a computer queue operates as well. The first person in line gets served first. If I get into line before you do, then I will be served before you do.
^[File:BNSF GE Dash-9 C44-9W Kennewick - Wishram WA.jpg. (2019, July 1). Wikimedia Commons, the free media repository. Retrieved 19:30, March 30, 2020 from https://commons.wikimedia.org/w/index.php?title=File:BNSF_GE_Dash-9_C44-9W_Kennewick_-_Wishram_WA.jpg&oldid=356754103.]
Of course, there are other examples of queues besides lines of people. You can think of a train as a long line of railway cars. They are all connected and move together as the train engine pulls them. A line of cars waiting to go through a toll booth or to cross a border is another good example of a queue. The first car in line will be the first car to get through the toll booth. In the picture below, there are actually several lines.
^[File:El Paso Ysleta Port of Entry.jpg. (2018, April 9). Wikimedia Commons, the free media repository. Retrieved 19:30, March 30, 2020 from https://commons.wikimedia.org/w/index.php?title=File:El_Paso_Ysleta_Port_of_Entry.jpg&oldid=296388002.]
How do we implement queues in code? Like we did with stacks, we will use an array, which is an easily understandable way to implement queues. We will store data directly in the array and use special start
and end
variables to keep track of the start of the queue and the end of the queue.
The following figure shows how we might implement a queue with an array. First, we define our array myQueue
to be an array that can hold 10 numbers, with an index of 0 to 9. Then we create a start
variable to keep track of the index at the start of the queue and an end
variable to keep track of the end of the array.
Notice that since we have not put any items into the queue, we initialize start
to be -1
. Although this is not a legal index into the array, we can use it like we did with stacks to recognize when we have not yet put anything into the queue. As we will see, this also makes manipulating items in the array much simpler. However, to make our use of the array more efficient, -1
will not always indicate that the queue is empty. We will allow the queue to wrap around the array from the start
index to the end
index. We’ll see an example of this behavior later.
When we want to enqueue an item into the queue, we follow the simple procedure as shown below. Of course, since our array has a fixed size, we need to make sure that we don’t try to put an item in a full array. Thus, the precondition is that the array cannot be full. Enforcing this precondition is the function of the if
statement at line 1. If the array is already full, then we’ll throw an exception in line 2 and let the caller handle the situation. Next, we store item
at the end
location and then compute the new value of end
in line 4. Line 4 uses the modulo operator %
to return the remainder of the division of $(\text{end} + 1) / \text{length of myQueue}$. In our example, this is helpful when we get to the end of our ten-element array. If end == 9
before enqueue
was called, the function would store item
in myQueue[9]
and then line 4 would cause end
to be $(9 +1) % 10$ or $10 % 10$ which is simply $0$, essentially wrapping the queue around the end of the array and continuing it at the beginning of the array.
function ENQUEUE (item)
if ISFULL() then (1)
raise exception (2)
end if
MYQUEUE[END] = ITEM (3)
END = (END + 1) % length of MYQUEUE (4)
if START == -1 (5)
START = 0 (6)
end if
end function
Given our initial configuration above, if we performed an enqueue(7)
function call, the result would look like the following.
Notice that the value 7 was stored at myQueue[0]
in line 3, end
was updated to 1
in line 4, and start
was set to 0
in line 7. Now, let’s assume we continue to perform enqueue
operations until myQueue
is almost filled as shown below.
If at this point, we enqueue another number, say -35
, the modulo operator in line 4 would help us wrap the end of the list around the array and back to the beginning as expected. The result of this function call is shown below.
Now we have a problem! The array is full of numbers and if we try to enqueue another number, the enqueue
function will raise an exception in line 2. However, this example also gives us insight into what the isFull
condition should be. Notice that both start
, and end
are pointing at the same array index. You may want to think about this a little, but you should be able to convince yourself that whenever start == end
we will be in a situation like the one above where the array is full, and we cannot safely enqueue another number.
To rectify our situation, we need to have a function to take things out of the queue. We call this function dequeue
, which returns the item at the beginning of the queue (pointed at by start
) and updates the value of start
to point to the next location in the queue. The pseudocode for the dequeue
is shown below.
function DEQUEUE ()
if ISEMPTY() then (1)
raise exception (2)
end if
ITEM = MYQUEUE[START] (3)
START = (START + 1) % length of MYQUEUE (4)
if START == END (5)
START = -1 (6)
END = 0 (7)
end if
return ITEM (8)
end function
Line 1 checks if the queue is empty and raises an exception in line 2 if it is. Otherwise, we copy the item at the start
location into item
at line 3 and then increment the value of start
by 1
, using the modulo operator to wrap to the beginning of the array if needed in line 4. However, if we dequeue
the last item in the queue, we will actually run into the same situation that we ran into in the enqueue
when we filled the array, start == end
. Since we need to differentiate between being full or empty (it’s kind of important!), we reset the start
and end
values back to their initial state when we dequeue the last item in the queue. That is, we set start = -1
and end = 0
. This way, we will always be able to tell the difference between the queue being empty or full. Finally, we return the item to the calling function in line 8.
We have already seen the pseudocode for the two key operations for queues: enqueue
and dequeue
. However, there are several others that make the queue data structure much easier to use:
We will discuss each of these operations. But first, let’s talk about the constructor for the queue
class and what it must do to properly set up a queue
object.
The main responsibility of the constructor is to initialize all the attributes in the queue
class. As we discussed above, the attributes include the myQueue
array and the start
and end
variables that hold indexes into myQueue
.
Since we are using an array for our queue, we will need to know how big to make the array in our constructor. There are two options. We could just use a default size for the array. Or, we could allow the user to pass in a positive integer to set the size. In this module we assume the caller must provide a capacity
value, which must be greater than 0
.
function QUEUE (CAPACITY)
if CAPACITY is not an integer then (1)
throw exception (2)
else if CAPACITY <= 0 then (3)
throw exception (4)
end if
MYQUEUE = new array of size capacity (5)
START = -1 (6)
END = 0 (7)
end function
The first thing we do in the code is to check to make sure that capacity
is actually an integer that is greater than 0
. Essentially, this is our precondition for the method. If our precondition is not met, we throw an exception. (If we are using a typed language such as Java, we can enforce our precondition by requiring that capacity
be of type integer
instead of explicitly checking it in the code.) Once we’ve validated our precondition, we create a new array of size capacity
for the myQueue
array and set the attribute start
to -1
and end
to 0
.
We have already discussed the enqueue
operation and seen it in operation above. In the pseudocode below, we see that we must first check to ensure that the queue is not already full. Again, this is our precondition.
function ENQUEUE (item)
if ISFULL() then (1)
raise exception (2)
end if
MYQUEUE[END] = ITEM (3)
END = (END + 1) % length of MYQUEUE (4)
if START == -1 (5)
START = 0 (6)
end if
end function
Once our precondition is validated, we simply increment and store the item
into the array at index end
. Then we increment end
, using the modulo operator to wrap end
to point to the beginning of the array if warranted. Next, we check for the condition of an empty queue. If start = 1
, then we know the queue is empty, so we set start = 0
. The enqueue
function does not return a value.
Because there are no loops in the enqueue function, the function operates in constant time regardless of the size of the array or the number of items in it.
Like enqueue
, we have already seen the dequeue
operation. It simply takes the first item from the start
of the queue and returns it. However, before we can do that, we need to validate our precondition. For the dequeue
operation, our precondition is that the queue must not already be empty, which is detected by the isEmpty
function in line 1.
function DEQUEUE ()
if ISEMPTY() then (1)
raise exception (2)
end if
ITEM = MYQUEUE[START] (3)
START = (START + 1) % length of MYQUEUE (4)
if START == END (5)
START = -1 (6)
END = 0 (7)
end if
return ITEM (8)
end function
Once we have validated our precondition, we simply copy the item from the myQueue[start]
in line 3 and increment start
. Again, we use the modulo operator in line 4 to wrap start
back to 0
if it is needed. Next, we check to see if the myQueue
is empty, and, if it is, reset the values of start
and end
back to their initial values. Finally, we return the item to the calling function in line 8. Like the enqueue
operation, the dequeue
function operates in constant time.
The peek
operation returns the item at the start of the queue, without removing it from the array. Like the dequeue
operation it has the precondition that the queue must not be empty. The pseudocode for the peek
operation is shown below. It is also a constant time operation.
function PEEK() (1)
if ISEMPTY() then (2)
raise exception (3)
else
return MYQUEUE[START] (4)
end if
end function
To allow the calling program to detect when the queue is full, we define an isFull
operation. Notice that code external to the queue
class cannot access the value of start
or end
so it cannot simply check if start == end
on its own. In this case, the operation is very simple as we only need to return the Boolean value of start == end
as shown below. There is no precondition for isFull
and isFull
operates in constant time.
function ISFULL()
return START == END (1)
end function
The isEmpty
operation is very similar to the isFull
operation except that we return the Boolean value of the condition start == -1
instead of start == end
.
function ISEMPTY()
return START == -1 (1)
end function
This size
method returns the number of items in the queue. However, it is not as straightforward as it might sound. Actually, there are several cases that we must consider, based on the fact that both start
and end
can “wrap around” the end of the array:
start == -1
—the queue is empty, and size = 0
,start == end
—the queue is full, and size
equals the capacity of the array,start < end
—size = end – start
, andstart > end
—size = capacity of array - start + end + 1
.Thus, in our function, we simply need to check four conditions.
function SIZE()
if START = -1 (1)
return 0 (2)
else if START == END (3)
return capacity of MYQUEUE (4)
else if START < END (5)
return END – START (6)
else
return capacity of MYQUEUE – START + END (7)
end if
end function
Notice that the conditions that are checked in lines 3 and 5 ensure that start
must be greater than end
. Therefore, we can simply use an else
statement to capture the last case in line 7. Once again, this is a constant time function.
The doubleCapacity
operation doubles the size of the array holding our queue. If we started with an array of size 4
, the doubleCapacity
operation will result in an array of size 8
with the contents of our original array stored in it. Unfortunately, most programming languages (like Java) do not simply let you double the size of the array. A noted exception to this is Python, which does allow you to directly extend an array.
In a traditional programming language, the easiest way to accomplish the doubleCapacity
operation is to complete the following steps:
start
and end
variables to point at the correct elements, thenmyQueue
array at the new array.The pseudocode for the doubleCapacity
operation is shown below.
function DOUBLECAPACITY()
NEWQUEUE = new array of MYQUEUE capacity * 2 (1)
LENGTH = SIZE() (2)
for I = 0 to LENGTH - 1 (3)
NEWQUEUE[I] = DEQUEUE() (4)
end for
START = 0 (5)
END = LENGTH (6)
MYQUEUE = NEWQUEUE (7)
end function
In the function, we create the new array in line 1 and then save the total number of items in the array for use later in line 2. Next, we use a for
loop in lines 3 and 4 to copy the contents from myQueue
into newQueue
. Since the contents of myQueue
are not necessarily stored neatly in the array (i.e., from $0$ to $n$), it is easier for us to use the existing size
and dequeue
functions to get access to each item in the queue in order. Once we have copied the items from myQueue
to newQueue
, we simply need to set the start
and end
variables in line 5 and 6, and then set myQueue = newQueue
in line 7 to complete the process.
The doubleCapacity
operation is not a constant time operation since copying the contents of the original array into the new array requires us to copy each item via a loop. This requires $N$ steps. Thus, we would say that doubleCapacity
runs in “order $N$” time.
The halveCapacity
operation is similar to the doubleCapacity
operation except that we now have a precondition. We must make sure that when we reduce the space for storing the queue that we still have enough space to store all the items currently in the queue. For example, if we have 10 items in a queue with a capacity of 16, we can’t successfully perform halveCapacity
. Doing so would only leave us a queue with a capacity of 8 and we would not be able to fit all 10 items in the new queue.
The pseudocode for the halveCapacity
function is shown below, with the precondition being checked in line 2. Once we create newQueue
to be half the capacity of myQueue
in line 4, the remainder of the function is exactly the same as the doubleCapacity
function, since lines 5-10 are just concerned with copying the items from myQueue
to newQueue
and setting the associated variables.
function HALVECAPACITY()
if SIZE() > MYQUEUE capacity / 2 then (2)
throw exception (3)
end if
NEWQUEUE = new array of MYQUEUE capacity / 2 (4)
LENGTH = SIZE() (5)
for I = 0 to LENGTH - 1 (6)
NEWQUEUE[I] = DEQUEUE() (7)
end for
START = 0 (8)
END = LENGTH % length of NEWQUEUE (9)
MYQUEUE = NEWQUEUE (10)
end function
Like the doubleCapacity
operation, halveCapacity
is not a constant time operation since copying the contents of the original array requires us to loop $N$ times. So, halveCapacity
runs in “order $N$” time.
The toString
function returns a string that concatenates the strings representing all the items stored in an array. In most programming languages, each object class must implement the toString
operation. For instance, in the queue below where each item is a character, if we called myQueue.toString()
, we would expect to be returned the string "Wildcats"
.
Notice that we must read the queue array from start
to end
to get the proper output string.
In the pseudocode below we first create an empty string called output
in line 1. Then, we create a loop in line 2 that counts using i
the number of items in the queue from 0
to the size
of the queue. However, we can’t use this counter i
to directly index into the array, since start
and end
may be almost anywhere in the array. Thus, we use i
to compute next
in line 3, which we will use as our index into the array. Our index i
should begin with start
and finish with the end
value, which can also be computed as start + size() – 1
modulo the capacity of myQueue
. We then use the index next
in line 4 to select the appropriate element of myQueue
to append to our output string. Once the loop ends, we simply return our output
string.
function TOSTRING()
OUTPUT = "" (1)
for I = 0 to SIZE() - 1 (2)
NEXT = (START + I) % MYQUEUE capacity (3)
OUTPUT = OUTPUT + MYQUEUE[next].TOSTRING() (4)
end for (5)
return OUTPUT (6)
end function
The toString
function includes a loop that, at most, looks at each element in myQueue
; therefore, toString
executes in order $N$ time.
The following table shows an example of how to use the above operations to create and manipulate a queue. It assumes the steps are performed sequentially and the result of the operation is shown.
Queues are useful in many applications. Classic real-world software which uses queues includes the scheduling of tasks, sharing of resources, and processing of messages in the proper order. A common need is to schedule tasks to be executed based on their priority. This type of scheduling can be done in a computer or on an assembly line floor, but the basic concept is the same.
Let’s assume that we are putting windshields onto new cars in a production line. In addition, there are some cars that we want to rush through production faster than others. There are actually three different priorities:
Ideally, as cars come to the windshield station, we would be able to put their windshields in and send them to the next station before we received the next car. However, this is rarely the case. Since putting on windshields often requires special attention, cars tend to line up to get their windows installed. This is when their priority comes into account. Instead of using a simple queue to line cars up first-come, first-served in FIFO order, we would like to jump high priority cars to the head of the line.
While we could build a sophisticated queueing mechanism that would automatically insert cars in the appropriate order based on priority and then arrival time, we could also use a queue to handle each set of car priorities. A figure illustrating this situation is shown below. As cars come in, they are put in one of three queues: high priority, medium priority, or low priority. When the windshield station completes a car it then takes the next car from the highest priority queue.
The interesting part of the problem is the controller at the windshield station that determines which car will be selected to be worked on next. The controller will need to have the following interface:
function receiveCar(car, priority)
// receives a car from the previous station and places into a specific queue
function bool isEmpty()
// returns true if there are no more cars in the queue
function getCar() returns car
// retrieves the next car based on priority
Using this simple interface, we will define a class to act as the windshield station controller. It will receive cars from the previous station and get cars for the windshield station.
We start by defining the internal attributes and constructor for the Controller
class as follows, using the Queue functions defined earlier in this module. We first declare three separate queues, one each for high
, medium
, and low
priority cars. Next, we create the constructor for the Controller
class. The constructor simply initializes our three queues with varying capacities based on the expected usage of each of the queues. Notice, that the high
priority queue has the smallest capacity while the low
priority queue has the largest capacity.
class Controller
declare HIGH as a Queue
declare MEDIUM as a Queue
declare LOW as a Queue
function Controller()
HIGH = new Queue(4)
MEDIUM = new Queue(6)
LOW = new Queue(8)
end function
Next, we need to define the interface function as defined above. We start with the receiveCar
function. There are three cases based on the priority
of the car
. If we look at the first case for priority == high
, we check to see if the high
queue is full before calling the enqueue
function to place the car
into the high
queue. If the queue is full, we raise an exception. We follow the exact same logic for the medium
and low
priority cars as well. Finally, there is a final else
that captures the case where the user did not specific either high
, medium
, or low
priority. In this case, an exception is raised.
function receiveCar(CAR, PRIORITY)
if PRIORITY == high
if HIGH.isFull()
raise exception
else
HIGH.enqueue(CAR)
end if
else PRIORITY == medium
if MEDIUM.isFull()
raise exception
else
MEDIUM.enqueue(CAR)
end if
else PRIORITY == low
if LOW.isFull()
raise exception
else
LOW.enqueue(CAR)
end if
else
raise exception
end if
end function
Now we will define the isEmpty
function. While we do not include an isFull
function due to the ambiguity of what that would mean and how it might be useful, the isEmpty
function will be useful for the windshield station to check before they request another call via the getCar
function.
As you can see below, the isEmpty
function simply returns the logical AND of each of the individual queue’s isEmpty
status. Thus, the function will return true if, and only if, each of the high
, medium
, and low
queues are empty.
function isEmpty()
return HIGH.isEmpty() and MEDIUM.isEmpty() and LOW.isEmpty()
end function
Finally, we are able to define the getCar
function. It is similar in structure to the receiveCar
function in that it checks each queue individually. In the case of getCar
, the key to the priority mechanism we are developing is in the order we check the queues. In this case, we check them in the expected order from high to low. If the high
queue is not empty, we get the car from that queue and return it to the calling function. If the high
queue is empty, then we check the medium
queue. Likewise, if the medium
queue is empty, we check the low queue
. Finally, if all of the queues are empty, we raise an exception.
function getCar()
if not HIGH.isEmpty()
return HIGH.dequeue()
else not MEDIUM.isEmpty()
return MEDIUM.dequeue()
else not LOW.isEmpty()
return LOW.dequeue()
else
raise exception
end if
end function
The following example shows how the Controller
class would work, given specific calls to receiveCar
and getCar
.
In this module we looked at the queue data structure. Queues are a “first in first out” data structure that use two main operations, enqueue and dequeue, to put data into the queue and to remove data from the queue. Queues are useful in many applications including the scheduling of tasks, sharing of resources, and processing of messages in the proper order.