Topic 5: Queues

Introduction

We have seen that a stack is a "last-in-first-out", or LIFO, data structure - in other words, the last item added (pushed) to the stack is the first item to be removed (popped). However, in certain circumstances it is more appropriate to use a "first-in-first-out", or FIFO, data structure. Luckily such a data structure exists, and that is the queue, so called because it behaves like a real-world queue. Items get added onto the back of the queue, and are removed from the front of the queue.

When would we use a queue? It would be used in any situation where we want to process tasks - or "jobs", in the order that they are submitted. There are many scenarios wheere we would like to do this. For example:

Implementing a queue

How might we implement a queue? Let's start by going back to how we implemented a stack, using an underlying array. Can we do the same with a queue? The diagram below demonstrates this.

Queue with capacity of 5

Note how the queue is implemented in this example using an internal array with a capacity of 5. When we add an item, the item is added to the first avaiable position in the internal array. So if the queue is empty, the item is added to position 0 of the array. (Note how I have reversed the array so that index 0 is on the right and index 4 on the left. This is so that the diagram better reflects a real-world queue, with new items added to the back).

So the first item - 3 - is added to the internal array at index 0. We then add another item - 99 - which is added to the back of the queue, in other words added to the internal array at index 1. Similarly, the next item - 7 - is added to the internal array at index 2.

As a consequence of this, we will need a variable to represent the index in the internal array of the back of the queue, so we know where to add elements. This variable will be 0 when the queue is empty, 1 after we have added 3, and 2 after we have added 99. In other words it is the index one position beyond the back of the queue. Can you see why it's better to point to the position beyond the back of the queue, rather than the back of the queue itself?

What happens, though, when we remove an item? This happens next - we remove the front item on the queue (3). This results in position 0 of the internal array becoming empty so that the remaining queue - 99 and 7 - starts at index 1 of the array and not index 0. This illustrates a key point. Because of the first-in-first-out nature of a queue, the queue is not always stored beginning at index 0 in the internal array. The queue can "shift" down the array, as has happened here; when we remove 3, the remaining items - 99 and 7 - are at indices 1 and 2 of the array. This is done because it is more efficient. If we wanted to ensure the queue always started at index 0 in the internal array, we'd have to do some wasteful additional processing to move 99 to index 0 in the internal array and move 7 to index 1. By letting the queue "shift" down the array in the manner shown in the diagram, we avoid the need for this additional processing.

The consequence of this in terms of implementation, though, is that we will need another variable to represent the index in the internal array where the queue starts, so that we can remove the correct item from the queue. (This is originally 0, but when we remove 3 from the queue it will change to 1.).

We then remove 99 from the queue, and consequently the queue shifts back one more place so that the start of the queue is at index 2. At this point, the queue will only store one item - 7 - so the start and the end of the queue both have index 2 in the internal array.

We then start adding items again. First we add 9, so that the queue occupies indices 2 and 3 of the internal array. Then we add 6, so that the queue occupies indices 2, 3, and 4.

What happens now, though, if we want to add a new item? We'd like to add a new member - 10 - which we should be able to do as our queue has a capacity of 5. However we have a problem because we've reached the end of the internal array. So we need to do something else.

The diagram below shows a common approach, which is to implement a circular array. When we reach the back of the array, we start adding items to the front again. So if our queue occupies indices 2, 3 and 4, and we wish to add a fourth item, this will be added at index 0 of the array. So the queue will now occupy indices 2,3,4 and 0 - in that order. If you try to visualise the array as having a circular rather than linear structure, this is probably easier to understand. This is also shown on the diagram.

Circular Queue

This shows how we add the fourth item (10) to index 0 as explained above, so that the queue occupies the array indices 2, 3, 4 and 0 in that order. Then we remove the front item of the queue, which is now 7, at index 2. Make sure you don't get confused here into thinking that the item at index 0, i.e. 10, is at the front of the queue! It is not - the queue wraps around. The front of the queue is at index 2, then the next member is at index 3, then the next at index 4, and the BACK of the queue is at index 0!

After removing 7, the front item of the queue is now 9, at index 3 in the array, so the queue occupies indices 3, 4, and 0 - with index 0 still at the back. So if we add yet another item - 8 - to the queue, it will be added at the next available space at the back of the queue, which is index 1 in the array, due to the wrap-around effect.

The top of this diagram shows the queue in a regular, linear view whereas the bottom of the diagram shows the same operations using a circular view of the array, in which the array is represented as a circle from indices 0 to 4 and then back to 0. Note that this is just a diagrammatic representation to make the concept of a circular, wrap-around array easier to understand. The array is not actually stored in a circular way in memory!!!

This question gets you to think about the reasons behind one of the implementation details for a queue. You will notice in the diagram above that when we remove the value "3" from the front of the queue, we set index 0 to None and keep 99 and 7 in their original positions in the internal array. An alternative approach could be to move 99 and 7 forward within the internal array so that they occupy positions 0 and 1. This alternative approach would mean that we would not need to implement the circular aspect of the queue.

The reason for this is efficiency. Think about what you would have to do if you shifted the values 99 and 7 forward from positions 1 and 2 to positions 0 and 1. You'd have to implement a loop to move each member of the queue forward one position. This loop would have to run as many times as there are items in the queue, which is rather inefficient. By contrast, implementing the circular aspect does not require a loop. It merely requires an if statement to check whether we have reached the end of the internal array, and to reset the next available space to 0 if so.

Paper Exercise 5.1

Exercise 1 is a "paper" exercise to help you understand the operations of a queue. Code is not needed for this. However please complete it electronically (e.g. on a text editor, Word, or Microsoft Paint) as it will be easier for me to check.

Start with an empty circular queue, represented internally by an array with capacity 8, and perform the following operations on it. After each operation, draw how the queue is represented by showing the internal array and the queue within it.

Note that size() should give the size of the queue itself, not the capacity of the underlying array.

add (a), add (b), remove (), add (c), remove (), remove (), 
add (d), add (e), add (f), remove (), add (g), add (h), 
remove (), remove (), remove (), add (i), remove (), size().

This exercise gets you to think about how you will design the queue class before you code it. This week you will use a NumPy array to represent the internal array of the queue, to prevent you using shortcuts available with lists!

As well as the actual internal array, we'd need these attributes:

Coding Exercise 5.2