Lesson 3: The Queue

Topics

This Lesson introduces our second ADT, the Queue. The topics include:

  1. The Queue ADT
  2. Using the Queue
  3. Implementation of the Queue ADT
  4. Problem solving and code development with the Queue

Required Tasks

  1. Complete the tasks of Lab 3.
  2. Complete and submit Assignment 4.

Learning Outcomes

By the end of this lesson, you should be able to

  • Define a Queue ADT
  • Explain how a Queue can be used to solve a problem
  • Use a Queue to solve problems similar to the ones mentioned in the lesson
  • Implement the Queue ADT

Introduction

In this lesson, we are going to introduce the Queue ADT. First, we discuss the concept of a Queue, how it works, what its interface is, and how it could be used for solving a problem. Then we get into the code and see how we can develop and use a Queue in Python.

The Queue ADT

The Queue ADT represents a First-In-First-Out (FIFO) structure. Any waiting line such as the check-out line at a store, the cars at a stop light, or the line-up outside a department store is a Queue.

Figure 2 below shows the conceptual view of a Queue ADT. Queue has two properties named front and rear. When a value is added to the Queue, it will be placed at the rear (end) of the Queue (think of a person joining a line, takes a place at the end of the line). When a value is removed from a Queue, it is the value at the front of the Queue (Think of the person at the front of the line who has waited the longest). The Queue ADT implements First-In-First-Out (FIFO) behaviour because the value removed is the one that has been in the Queue for the longest time.

A Simple Queue

Queue Methods and Properties

The basic queue properties are:

  • rear: keeps track of the last value added to the Queue
  • front: keeps track of the value in the Queue the longest

Queue Methods

initialize()
creates an empty Queue
insert(value)
adds a copy of value to the rear of the Queue
remove()
removes and returns the value at the front of the Queue, if one exists
peek()
returns a copy of the value at the front of the Queue without removing it, if one exists
is_empty()
returns True if the Queue is empty, False otherwise
is_full()
returns True if the Queue is full, False otherwise
len()
returns the number of values in the queue

Any implementation of a Queue must define these methods.

The Array-Based Queue

The rear of the Queue is the right-most element in the Queue's underlying array. The front of the queue is always position 0. As data is added to the rear of the Queue, the index of the right-most inhabited element increases but the index of the front of the queue never changes. The size of the Queue at any given time is simply the size of the Python list in the Queue. A Queue is empty if its underlying list is empty.

This diagram displays the workings of the array-based Queue. Use and to see how it works:

Dynamic Array-Based Queue

Implementing the Array-Based Queue

The following code shows how to implement part of the array-based Queue in Python. The rest is left to you as an exercise for labs and assignments.

Initializing an Array-Based Queue

Queue __init__
    
from copy import deepcopy

class Queue:

    def __init__(self):
        """
        -------------------------------------------------------
        Initializes an empty queue. Data is stored in a Python list.
        Use: queue = Queue()
        -------------------------------------------------------
        Returns:
            a new Queue object (Queue)
        -------------------------------------------------------
        """
        self._values = []

  

The Queue initialization is very simple: the Queue data is held in a Python list attribute named _value, and that attribute is initialized as an empty list. Note the naming convention - the attribute name starts with an underscore '_'. This indicates that no program that uses the Queue should ever refer to this attribute directly. _value is to be treated as a private attribute that is available only to the Queue itself. Any and all attempts to access or modify the Queue must be done through the public methods, those methods whose names do not start with an underscore.

Inserting Items Into an Array-Based Queue

Queue insert
    
    def insert(self, value):
        """
        -------------------------------------------------------
        Adds a copy of value to the rear of the queue.
        Use: queue.insert(value)
        -------------------------------------------------------
        Parameters:
            value - a data element (?)
        Returns:
            None
        -------------------------------------------------------
        """
        self._values.append(deepcopy(value))
        return

  

insert simply uses the Python list method append to add value to the end of the _values list. Except for the name, this method is identical to the Stack push method including the use of deepcopy.

Getting the Length of an Array-Based Queue

The following code defines the Python magic method __len__, which allows the use of the len function, as shown in the Use: comment.

Queue __len__
    
    def __len__(self):
        """
        -------------------------------------------------------
        Returns the length of the queue.
        Use: n = len(queue)
        -------------------------------------------------------
        Returns:
            the number of values in queue.
        -------------------------------------------------------
        """
        return len(self._values)

  

The rest of the implementation is left to you as an exercise. Note that in particular the array-based Queue differs from the array-based Stack in how data is removed.

The Queue Maze

In this section, we are going to solve the maze problem using Queue. In the previous lesson, we used Stack to solve this problem.

Solving a Maze Using a Queue

The following sample maze is the same one seen when we looked at the Stack ADT.

As mentioned in previous lesson, in this sample maze, the letters represent either a decision point or an end point. We can't tell what the letter represents until we visit the point. X represents the exit point. The rules to follow when solving the maze are:

A Sample Maze

Solution

Repeat steps 1 and 2 until the exit is found.

  1. Move to a letter point and add the immediately connected letter points, if any, from that point to the queue (Ignore points already visited.)
  2. When you then need to make a decision as to where to go next, remove a point from the queue and move to that point.

Note that the above algorithm does not return the path from start to exit. It just determines whether there is a path or not. To get the path, we need a more elaborated algorithm.

Operation of the Algorithm

The result queue for this maze follows:

The result queue for this maze follows:

A Sample Queue-Based Solution

Note that although both the stack and queue based algorithms solve the maze, they traverse the maze points in a different order. As with the stack-based solution, changing the order in which points are added to the queue changes the amount of the maze visited.

The Circular Queue

The Circular Array-Based Queue Implementation

The array-based implementation of the Queue that we have already seen is very simple to implement, but that simplicity - and the use of the Python list - hides a very inefficient implementation. The array-based implementation adds data to the end of an array upon insertion and takes data from the front of an array upon removal. If, however, we are always removing data from position 0 in the array, how is it that 0 always holds the front of the queue? Every time a value is removed from position 0 all of the following values must be moved up one position in the array. This is a great deal of work - we don't see it when using the Python list, but you can imagine the overhead necessary to implement a Queue this way. (Note that this problem does not happen with an array-based Stack, because values are pushed and popped from only one end of the array and nothing has to be shifted within the array.)

A solution is to implement a circular array. The circle is conceptual only - computer memory is strictly linear - but this concept is fairly easy to grasp. In a circular array items are never shifted from the position they are given when they are inserted into a queue; instead the indexes of the front and rear of the queue change with each insertion or removal.

These diagrams display the circular array as a circle of memory. Use and to see how it works:

The Circular Queue - Circular View

The diagram above can be illustrated as a linear array of memory. and work for this diagram as well:

The Circular Queue - Linear View

When an item is removed from the front of the queue, the value of the index front must be changed. If front is a value from 0 to capacity-1, it gets changed to front+1. If front is capacity-1, it gets changed to 0. That is easy to do with the modulus operator:

  
front = (front + 1) % capacity

        

The value of rear is handled in a similar manner.

Note that rear refers to the index of the next available slot in the array, not the index of the last item in the queue. Thus when the circular queue is empty, front is None and rear refers to any empty slot in the array. However, when the queue is full, front refers to the location of the data at the front of the queue, and rear should be None, to indicate that there is no location available to insert new data.

Initializing circular-array Queue

Given that Python's list is dynamic, how can we create a fixed size array from it? We can do so quite simply by filling the list with placeholders when we create it and never changing the size of the list (i.e. by never calling pop or append on it) in any of the Circular Queue methods. The initialization is:

The Circular Queue - __init__
class Queue:
    """
    -------------------------------------------------------
    Constants
    -------------------------------------------------------
    """
    # a default capacity when one is not provided
    DEFAULT_CAPACITY = 10

    def __init__(self, capacity=DEFAULT_CAPACITY):
        """
        -------------------------------------------------------
        Initializes an empty queue. Data is stored in a fixed-size list.
        Use: target = Queue(capacity)
        Use: target = Queue()  # uses default max size
        -------------------------------------------------------
        Parameters:
            capacity - maximum size of the queue (int > 0)
        Returns:
            a new Queue object (Queue)
        -------------------------------------------------------
        """
        assert capacity > 0, "Queue size must be > 0"

        self._capacity = capacity
        self._values = [None] * self._capacity
        self._front = None   # queue has no data
        self._rear = 0       # first available index for insertion
        self._count = 0      # number of data items

This queue could then be constructed by a call such as:

          
from Queue_circular import Queue

queue = Queue()

        

Inside the circular queue, this creates the Python list _values:

[None, None, None, None, None, None, None, None, None, None]

The Circular Queue data structure may change the contents of this list, but may not make the list larger or smaller. i.e. do not use any Python list method that changes the list size such as:

  • insert
  • append
  • extend
  • pop
  • remove
  • del

Implementation of the rest of the Circular Queue is left as an exercise for you.

Queue_Applications

Queue ADT has many uses. In this section, we mention some of the applications of Queue ADT:

  • Queues are used in several algorithms on graphs to provide efficient running time. We will not discuss graphs in this course, but you will get to know graphs in more advanced courses on data structures.
  • Most real-life lines are Queues. For example, lines at ticket counters are Queues, because the customers are served on a first-come first-served basis. Another example is a bank where customers arrive and wait on a line until one of the tellers is available.
  • In some computer networks, the computer's disk is attached to a file server which is shared among the users of the computers. The users are given access to files on a first-come first-served basis, so a Queue data structure is used.
  • Queues are used as waiting lists for a single shared resource such as printer, disk, CPU. For example, jobs sent to printer are printed in order of arrival and so they are placed on a Queue.
  • Queues are often used to simulate real-life situations that involve some kind of line-ups. For example, if we simulate the behaviour of a bank system, then we can answer questions such as:
    • How many teller windows should be open at the start of the day?
    • Should there be a separate line for each tellers or one line for all tellers?
    • When should the bank open another teller window?
    • How many tellers do we need to make sure that all customers are served by closing time?

Note that the answers we receive from these simulations are only useful if the inputs are reasonable approximations of reality. This requires observation, record-keeping, and data analysis.

As another example, imagine that a grocery store has to decide between opening up more self-serve check-outs or staffing more check-outs. The store has kept records on how many people use the store throughout the day. It knows the minimum, maximum, and average amount of time a customer takes to use a self-serve checkout. It knows the same information for the standard check-outs. It can then run a series of computer simulations using these numbers with more and fewer of each type of check-out and try to determine the best value for their money.

Summary

In this lesson, we introduced the Queue ADT and discussed different approaches to implement the Queue ADT using arrays. We discussed how a Queue can be implemented using Python lists and how Queues can be used to solve problems. Later we in the course we will talk about another implementation of the Queue ADT using a linked structure.

References

  1. Data Structures and Algorithm Analysis in C++, Mark Allen Weiss. 2006.
  2. CSE373: Data Structures and Algorithms, School of Computer Science and Engineering at University of Washington, Notes on Queue ADT.