DS & A(Queue)
| CHAPTER 14 |
In this chapter, our topic of discussion will be the "Queue" Data Structure. But, before we get started, I would recommend you to read the previous chapters on Data Structures & Algorithms (especially the chapters on Array List, Linked List, Array Stack, and Linked Stack), that is in case it's your first time visiting my blog.
Queue:
- Just like Stacks, Queues also are a restricted version of List but with a few minor differences compared to Stack. The following are some of the notable characteristics of a Queue:
- Data can only be inserted from the rear/back. Data insertion is referred to as ENQUEUE.
- Data can only be deleted from the front. Data deletion is referred to as DEQUEUE.
- Alternatively, ENQUEUE and DEQUEUE should always be executed on the opposite ends of the Queue.
- A Queue follows the FIFO(First In First Out) principle.
- The first and the last elements are referred to as FRONT and REAR respectively.
Visual Representation:
- Array-Based Queue:
Some Theory on Array Based Queue...
As mentioned above a Queue is a restricted version of List so all we need to do is modify the Array-Based list to get our Queue implementation, isn't it? Not actually, If we simply try to implement the Array-Based List to work as an Array-Based Queue the resulting implementation will be kinda inefficient. Why is that? Let's figure that out.
Consider, we have to store n items in a Queue. So, if we try to implement the Queue to work just like an Array-based list then we will need to store those n items in the first n positions of the array, i.e. (0 to n-1). And the Queue will look something like the following.
A Queue has two access points namely FRONT and REAR, we have something analogous to those in an Array List as well, remember HEAD and TAIL?
So, if we try to implement a Queue just like an Array List we would be left with the following two choices:
- Case 1: (ENQUEUE - Rear Shifts Right)
As mentioned previously implementing a Queue in this manner will be inefficient, but why is it so? Everything feels right, Isn't it? Not actually. To find the issues we have to analyze the ENQUEUE and DEQUEUE operation in both cases.
Case 1 Analysis:
As we already know that ENQUEUE operation is executed from the REAR and DEQUEUE operation is executed from the FRONT. So it can be easily observed that an ENQUEUE operation will require O(1) time only as it is similar to the Append function of the List. But a DEQUEUE operation will require O(n) time, as we will need to shift the remaining n-1 items to the left after removing the element at the FRONT.
Case 2 Analysis:
In Case 2, an ENQUEUE operation will require O(1) time whereas a DEQUEUE operation will require O(n) time, as we will need to shift the pre-existing items to the right before inserting a new element at the REAR.
Solution:
Since both Case 1 and Case 2 are inefficient, we have to approach this problem in a slightly different way, unlike a normal Array-based list. So, we will be making the following changes for a more efficient implementation.
Since both Case 1 and Case 2 are inefficient, we have to approach this problem in a slightly different way, unlike a normal Array-based list. So, we will be making the following changes for a more efficient implementation.
- FRONT and REAR will shift right on DEQUEUE and ENQUEUE respectively and that also in just O(1) time. This implementation will also let us know that the array is empty when REAR<FRONT.
Queue ADT:
- //Queue.h
- template<typename e> class Queue
- {
- public:
- virtual bool enqueue() = 0;
- virtual bool dequeue() = 0;
- virtual void print() const = 0;
- }
Queue Implementation:
- Array Based Queue Implementation
- //AQueue.h
- #include"Queue.h"
- template<typename e> class AQueue:public Queue<e>
- {
- int maxSize;
- int front;
- int rear;
- e* queueArray;
- public:
- AQueue(int sizeValue = 0)
- {
- maxSize = sizeValue;
- rear = 0;
- front = 1;
- queueArray = new e[maxSize];
- }
- ~AQueue ()
- {
- delete [] queueArray;
- }
- int queueLength()
- {
- return (rear-front)+1;
- }
- bool enqueue(const e&);
- bool dequeue(e&);
- void print() const;
- };
- template <typename e>
- bool AQueue<e>::enqueue(const e& item)
- {
- if (rear == maxSize) return false;
- queueArray[++rear] = item;
- return true;
- }
- template <typename e>
- bool AQueue<e>::dequeue(e& item)
- {
- if (queueLength() == 0) return false;
- item = queueArray[front++];
- return true;
- }
- template<typename e>
- void AQueue<e>::print() const
- {
- int tempCounter = front;
- std::cout<<"< ";
- while(tempCounter <= rear)
- {
- std::cout<<queueArray[tempCounter]<<" ";
- tempCounter++;
- }
- std::cout<<">";
- }
- Linked Queue Implementation
- //Node.h
- template<typename e> class Node
- {
- public:
- e element;
- Node<e>* next;
- Node(e elemVal = 0,Node<e>* nextVal = nullptr)
- {
- element = elemVal;
- next = nextVal;
- }
- Node(Node<e>* nextVal = nullptr)
- {
- next = nextVal;
- }
- };
- //LQueue.h
- #include"Queue.h"
- #include"Node.h"
- #include<iostream>
- template<typename e> class LQueue : public Queue<e>
- {
- Node<e>* rear;
- Node<e>* front;
- int size;
- public:
- LQueue()
- {
- rear = front = nullptr;
- size = 0;
- }
- bool enqueue(const e&);
- bool dequeue(e&);
- void print() const;
- void clear();
- int length(){ return size;}
- ~LQueue(){clear();}
- };
- template<typename e>
- bool LQueue<e>:: enqueue(const e& item)
- {
- if(rear == nullptr)
- {
- front = rear = new Node<e>(item,nullptr);
- }
- else
- {
- rear->next = new Node<e>(item,nullptr);
- rear = rear->next;
- }
- size++;
- return true;
- }
- template<typename e>
- bool LQueue<e>:: dequeue(e& item)
- {
- if(size == 0) return false;
- item = front->element;
- Node<e>* temp = front;
- front = front->next;
- delete temp;
- if(front==nullptr) rear = nullptr; //if not done rear will be a dangling pointer
- size--;
- return true;
- }
- template<typename e>
- void LQueue<e>:: print() const
- {
- Node<e>* temp = front;
- std::cout<<"< ";
- while(temp != rear->next)
- {
- std::cout<<temp->element<<" ";
- temp = temp->next;
- }
- std::cout<<">";
- delete temp;
- }
- template<typename e>
- void LQueue<e>:: clear()
- {
- delete rear;
- delete front;
- }
- //main.cpp
- #include<iostream>
- #include"AQueue.h" //Use this only for testing Array based Queue
- #include"AQueue.h" //Use this only for testing Linked Queue
- using namespace std;
- int main()
- {
- AQueue<int> newQueue(10); //Use this only for testing Array based Queue
- LQueue<int> newQueue; //Use this only for testing Linked Queue
- newQueue.enqueue(1);
- newQueue.enqueue(2);
- newQueue.enqueue(3);
- newQueue.enqueue(4);
- newQueue.enqueue(5);
- newQueue.enqueue(6);
- newQueue.enqueue(7);
- newQueue.enqueue(8);
- newQueue.enqueue(9);
- newQueue.enqueue(0);
- newQueue.print();
- int dequeued_element;
- int length = newQueue.length(); // initialize newQueue.length() because it's value
- // changes with every enqueue or dequeue
- cout<<"\n\n";
- for(int i = 0; i<length ; i++)
- {
- newQueue.dequeue(dequeued_element);
- cout<<"Removed: "<<dequeued_element<<"\n";
- }
- return 0;
- }
Array-Based Queue Implementation: More Issues
If you guys thought that our implementation for the Array-based Queue above is perfect I would have to disappoint you guys cos it still has some daunting issues.
After the second DEQUEUE
After the third DEQUEUE
I am sure that by this point it's pretty clear that if we use the Array-Based Queue implementation mentioned above then the Queue will become unusable after several ENQUEUE & DEQUEUE operations. So, what's the solution to that problem. Any guess? Don't worry if you can't figure it out, the following section is all about the solution we are looking for.
Circular Queue (aka Circular/Ring Buffer) : The solution
As the name suggests it is a circular Queue Data Structure. But don't get fooled by the name, just because the name contains the term Circular it doesn't mean that the data structure itself is circular, but it's the implementation that makes it behave as if it's circular in nature. We will still be using an Array which is a Linear Data Structure. The following are some of the properties of a Circular Queue implementation.
- FRONT and REAR keeps on getting incremented till it reaches the final index.
- When FRONT is at the last index the next DEQUEUE will move the FRONT to its initial index again. Similarly, when REAR is at the last index the next ENQUEUE will move the REAR to its initial index again.
- A starting empty array slot is necessary. So, a Queue that stores n elements will always require an array of size n+1.
Why do we need the empty array slot?
The empty slot is necessary because of the otherwise possible ambiguity that the circular Queue might encounter. Let's discuss it in detail, Consider the following two cases of circular Queue without an empty array slot.
The empty slot is necessary because of the otherwise possible ambiguity that the circular Queue might encounter. Let's discuss it in detail, Consider the following two cases of circular Queue without an empty array slot.
- Case 1: When the Queue is empty
- Case 2: When the Queue is full
As you can see from Case 1 and Case 2 that if the empty array slot in the Queue implementation is not present then a full and an empty Queue cannot be distinguished leading to an ambiguous scenario. So, the empty array is mandatory in a Circular implementation.
Circular Queue Implementation:
I leave it to you guys as an exercise. Try implementing it. All the best...
OR
You can join our Facebook group for the code
( If you like the content here, follow TSP on social media to get notified on future updates)
( If you like the content here, follow TSP on social media to get notified on future updates)
Comments
Post a Comment