implement queue image

The queue is one of the most fundamental data structures. 

A queue observes the First-In-First-Out (FIFO) principle, where the first element inserted is the first one to be removed. The structure can be visualized as in a line of people waiting for a service, where the person who enters first is served first.

The Queue Abstract Data Type

A Queue have two main operations, enqueue and dequeue. The enqueue operations adds an elements at the rear end of the queue, while  the dequeue operation removes the element at the front of the Queue.

Altogether, a queue should support the following operations.

enqueue(e):  Add element e at the end of the queue.

dequeue(): Remove and return the first element. 

first(): Return a reference to the first element in the queue without removing it.

is_empty(): Check whether the queue is empty. True if so, False otherwise.

size(): Return the number of elements in the queue.

features of the queue data structure

List-based Queue implementation

We will first look on how to implement a Queue class from scratch to better understand its mechanisms before exploring better built-in implementations. 

We will implement the Queue class with a list as the underlying structure for storing the queue elements.

ExampleEdit & Run
#exception to be raised by an empty queue
class EmptyQueue(Exception):
   pass

class Queue:
   def __init__(self):
         self._data = [] #a non-public list for storing the elements
 
   def __len__(self):
        return len(self._data)

   def is_empty(self):
      return len(self) == 0

   def first(self):
      if self.is_empty():
         raise EmptyQueue("Queue is empty, can't dequeue")

      return self._data[-1]

   def enqueue(self, e):
      self._data.append(e)

   def dequeue(self):
      if self.is_empty():
         raise EmptyQueue("Queue is empty, can't dequeue")
      return self._data.pop(-1) #This operation is very inefficient.

#test the queue
Q = Queue()
Q.enqueue("Python")
Q.enqueue("Java")
Q.enqueue("C++")
Q.enqueue("Ruby")

print(len(Q))
print(Q.dequeue())
print(Q.dequeue())
print(Q.dequeue())
print(Q.dequeue())
print(Q.dequeue())
Output:
4RubyC++JavaPythonTraceback (most recent call last):  File "<string>", line 41, in <module>  File "<string>", line 26, in dequeueEmptyQueue: Queue is empty, can't dequeue[Finished in 0.011304096085950732s]

Note: The implementation of the dequeue() method  in the above class is very inefficient. Calling pop(-1) of  a lists always has a worst-case time complexity of O(n), where n is the size of the list. This is because after the first element is removed, each of the remaining elements will have to shift to the left, which takes O(n) time.  

While the Queue class above serves as a simple and intuitive example, it is not meant for use beyond simple learning projects due to the aforementioned inefficiency. In the following parts we will discuss more robust and efficient implementations of the queue data structure.

Using collections.deque

The collections module in the standard library defines the deque class, pronounced as "deck" which offers very efficient insertion and deletion operations on either ends. We can use this class to create LIFO queues for  use even in a production setup.

The deque class have various methods, we will only discuss the ones that enables us to use it as a FIFO queue.

append(e) Add element e at the right end.
popleft() Remove the item at the left end.
len() get the size of the deque.
ExampleEdit & Run
#import the deque class
from collections import deque

#create a queue
q = deque()

#enqueue elements 
q.append(10)
q.append(20)
q.append(30)

#the size
print("Size: ", len(q))

#dequeue elements
print(q.popleft())
print(q.popleft())
print(q.popleft())
print(q.popleft())
Output:
Size:  3102030Traceback (most recent call last):  File "<string>", line 19, in <module>IndexError: pop from an empty deque[Finished in 0.012002755887806416s]

Since deque objects supports efficient insertions and deletions from either end, we can also add elements at the front(left) end and remove them from the back(right) end, without any change in functionality or performance. This way we can use the following methods:

appendleft(e) Add element at the left end.
pop() Remove an element from right. 
ExampleEdit & Run
#import the deque class
from collections import deque

#create a queue
q = deque()

#enqueue elements 
q.appendleft(10)
q.appendleft(20)
q.appendleft(30)

#the size
print("Size: ", len(q))

#dequeue elements
print(q.pop())
print(q.pop())
print(q.pop())
print(q.pop())
Output:
Size:  3102030Traceback (most recent call last):  File "<string>", line 19, in <module>IndexError: pop from an empty deque[Finished in 0.012024050112813711s]

The queue module

The  queue module in the standard library also provides an efficient implementation of the queue data structure. This implementation is optimized for performance and is thread-safe. The class is more suitable for use with multi-threaded applications, as it allows multiple threads to safely insert and remove items from the queue without causing any conflicts or errors.   

The Queue class in the module defines the following basic methods.

put(e) Adds element e at the end of the Queue. 
get() Removes and returns the first element.
empty() Checks whether the queue is empty. True if so, False otherwise.
qsize() Returns the size of the queue.
ExampleEdit & Run

Use the Queue class 

#import the Queue class
from queue import Queue

#create a queue object
q = Queue()

#enqueue items
q.put(10)
q.put(20)
q.put(30)
q.put(40)

#get the size
print("Size: ", q.qsize())

#dequeue items
print(q.get())
print(q.get())
print(q.get())
print(q.get())
Output:

Size:  4
10
20
30
40