Introduction
A Queue is a data structure that follows the FIFO (First In First Out) principle. Unlike Stacks, the element inserted first must be the first element to be output.
Conversely, this also means that the last element put into the stack will always end up being the last one that goes out.
We can easily visualize it by imagining a waiting line. Queues are used all throughout programming, here are some examples:
Objectives
Operate a queue. Build our own queue structures. Put into practice arrays and linked lists. Understand the advantages / disadvantages of each implementation.
What is next?
If we have not seen the stacks yet, this is the opportunity: they are implemented according to the LIFO principle (Last-In-First-Out) inversely to the queues. We may also continue with more advanced data structures such as: binary trees or hash tables.
Usage
We use C++ for code illustration, but no worries, other languages use the same logic and most often a similar syntax.
A queue is used for the following two main operations:
void enqueue(const T& data); // Add new element at the end of the queue void dequeue(); // Remove element from the beginning of the queue
The following features are added to properly use a queue:
bool empty() const; // Check if queue is empty const T& seek() const; // Get the data at the front of the queue
Example
queue< string > clients; // Init queue clients.enqueue("john"); // ["john"] clients.enqueue("do"); // ["john", "do"] clients.seek(); // --> "john" clients.dequeue(); // ["do"] clients.push("jack"); // ["do", "jack"] clients.push("dany"); // ["do", "jack", "dany"] // Get clients from the first to be handled while (!clients.empty()) { auto currentClient = clients.seek(); clients.dequeue(); }
Build our owns
How to build one? Good news! The answer is: pretty easy. We will look at implementations based upon arrays and linked lists in order to decide which best suits our case.
Note: the Queue is an abstract data structure (as the Stack). The definition of their structure is solely based on their behavior and not the underlying implementation. This is in contrast to more fundamental data structures, such as arrays and linked lists, which have strict requirements for how the storage of their data is implemented.
Linked List implementation
We use a linked list with an extra pointer on the back of the queue. Let's take back the skeleton of our structure seen in the 'linked list'.
template < typename T > class Queue { public: Queue() : head(nullptr), tail(nullptr) {} // Construct as an empty queue ~Queue() { ... } // Destructor (same as linked list) private: // Node structure declaration struct Node { // Constructor Node(const T& data) : data(data), next(nullptr) {} T data; // Value Node* next; // Pointer to the next node }; Node* head; // Pointer on the first element (also the beginning of the queue) Node* tail; // Pointer on the last element (also the end of the queue) }
When we want to add something to the queue (Enqueue), we just need to add it to the end of the list. We memorize the last element of the list in order to make effective insertions.
void Enqueue(const T& data) { Node* newNode = new Node(data); // Allocate memory if (!head) // First element is the head as well as the tail { head = newNode; // Set head to the new node tail = head; // Tail is also the head in this case } else // Otherwise element is added at the end { tail->next = newNode; tail = tail->next; // Update the new tail } }
For the Dequeue operation, nothing is easier: just delete the first item in the list (as in stacks).
void Dequeue() { if (!head) // Empty queue error return; auto tmp = head; // Get the current top head = head->next; // Set the head to point to the next node delete tmp; // Delete the node }
The Empty and Seek functions are also trivial.
bool Empty() const { return (head) ? false : true; } const T& Top() const { if (!head); // Throw Exception return *head; }
The above implementation gives you the ideas involved, and any optimization may be accomplished by modifying the linked list code.
Array implementation
Using a table implementation for a queue is not the simplest approach, but remains very interesting. This involves the use of a circular table often named circular buffer. This is also often used to create caches memory.
template < typename T > class Queue { public: // Allocate/Deallocate stack memory Queue(const int size) : back(0), front(0), count(0), max_size(size) { stack = new T[max_size]; } ~Queue() { delete[] stack; } private: int back; // back index of the queue int front; // front index of the queue int count; // Keep the count to check if full const int max_size; // max size T* Queue; // pointer on the array Queue(); // Forbid default constructor }
When we want to add something to the queue (Enqueue), we just need to add it to the end of the queue and increment counts.
void Enqueue(const T new_data) { if (count == max_size) // Stack overflow exception queue[back++] = new_data; // Set data and then update back index if (back == max_size) // Circular iteration if end reached back = 0; ++count; // Update count }
Dequeue :
void Dequeue() { if (count == 0) // Empty queue error return; if (++front == max_size) // Circular iteration if end reached front = 0; --count; // Change the count }
Empty and Seek
bool Empty() const { return count == 0; } const T& Seek() const { return queue[front]; }
In short
Linked List Insert and delete operations in O(1). Can grow dynamically. Memory allocation/deallocation required for Insert and Delete operations. Slightly more memory usage due to additional storage of pointers.
Circular Array Insert and delete operations in O(1). Can not grow dynamically: it requires a maximum size at initialization. An oversized table causes amount of wasted memory. Uses the notion of a circular array (eg. buffers, caches, ...).