Photo by Towfiqu barbhuiya on Unsplash
Leetcode #622 Solved: Design A Circular Queue
Step-by-step to designing your own circular queue
Designing your own implementation of a circular queue is one of the most important Leetcode questions. And it might seem very daunting at first, especially because of the default code provided, but it's actually pretty easy to implement. To really understand what's going on, we're first going to see what a circular queue is and then implement our own. Let's start!
What is a circular queue?
You already know that a queue is a linear data structure where the operations are performed on a first-in-first-out or FIFO basis. This means that when you dequeue an element, you're going to remove the first element that you added to the queue (from the front of the queue).
The difference between a regular queue and a circular queue is that with the latter, you can use space present in front of the queue. In a regular queue, once you reach the end, you can't insert more elements even if there's space available in the front. But a circular queue allows you to store new values in the space at the front. Here's a visualization to understand better:
Now that you understand how a circular queue works, let's work on implementing it.
First Things First: Defining Our Queue
Let's start by filling in the MyCircularQueue
function. Here, the parameter k
refers to the size of the queue. The very first thing we need to do is initialize our array and define the max size, which, in this case, is k
. We also need to keep track of our front and rear, so we'll initialize two variables for that. And finally, we need some way to track the current size of the array, so we'll use another variable for that. If we put everything together, the MyCircularQueue
function will look like this:
var MyCircularQueue = function (k){
this.data = new Array(k)
this.maxSize = k
this.size = 0
this.front = 0
this.rear = -1
};
The purpose of the rear
variable is to keep track of where the next element will be inserted. Initially, it is set to -1 because there are no elements in the queue. Once an element is added, the rear
will become 0 and subsequent elements will be added at increasing indices. If you initially set the rear
to 0, you'd have to add in additional conditional checks to handle empty cases.
Meanwhile, the front
is set to 0 since it indicates the starting index of our queue and is the position from where elements will be dequeued. When an element is added or removed, the front
variable is updated accordingly to reflect the new position of the front element.
Implementing other functions
Now that we have our queue, let's turn to the 6 other functions we need to implement. We're going to ignore the order of functions given to us by Leetcode and start with the easiest: isEmpty()
.
isEmpty()
The purpose of this function is to check if the queue is empty or not. This is where our this.size
variable comes in handy. If it's 0, that means that the queue is empty, and we can just return that. So our function becomes:
MyCircularQueue.prototype.isEmpty = function() {
return this.size == 0
};
isFull()
isFull()
checks if the circular queue is full. And checking that is easy, too - you just have to check if this.size
or our current size of the queue is equal to this.maxSize
. So, all this function has to do is return whether the two are equal or not:
MyCircularQueue.prototype.isFull = function() {
return (this.size === this.maxSize)
};
Front()
This function gets the queue's front item. If the queue is empty, you have to return -1. Implementing this is pretty easy, and we can use the isEmpty()
function we just coded. If isEmpty()
returns true (i.e. the queue is empty), we'll return -1. Otherwise, we'll return the front element, i.e., this.data[this.front]
.
MyCircularQueue.prototype.Front = function() {
return this.isEmpty() ? -1 : this.data[this.front]
};
Rear()
We'll use a similar approach for Rear()
. Once again, we'll check if the queue is empty. If not, we'll return this.data[this.rear]
.
MyCircularQueue.prototype.Rear = function() {
return this.isEmpty()? -1 : this.data[this.rear]
};
enQueue(value)
This is the meat of the whole problem. It might seem difficult to come up with the logic, but it's fairly simple. First, we need to check if there's space in the queue to add more elements. If the queue is full, we're just going to return false. Otherwise, we're going to add the value to the end of the queue.
If there's space for elements in the queue, then we're going to update the rear, the data, and the size. Let's start with the rear. Intuitively, you might just want to do this.rear = this.rear+1
but there's a problem - it doesn't take care of the overflow. For instance, if this.rear
is 4 (maxSize is 5), then incrementing it by 1 will cause the rear to exceed the maximum size. Consider this visualization:
But, if we do (this.rear+1)%(this.maxSize)
, then we can make the rear wrap around to the beginning of the queue once it reaches the end. Of course, this will only work if there's space at the beginning of the queue (which is why we first check to make sure the queue is not full). This is what makes the queue circular.
With the modulo operation in place, if rear is 4 and maxSize
is 5, then this.rear
will be updated to the 0th index (4+1%5 = 0). Similarly, if this.rear
is something like 6, then we won't face any problem with overflowing since our insertion position will be updated to 2 (6+1%5). Here's a visualization to make things clearer:
Once you have the new rear, you can add the data, which is pretty simple (this.data[this.rear] = value
). And then update the size of the queue to reflect the change. If all of this is successful, we'll return true.
MyCircularQueue.prototype.enQueue = function(value) {
if(this.isFull()) return false
this.rear = (this.rear+1)%(this.maxSize)
this.data[this.rear] = value
this.size+=1
return true
};
deQueue()
Finally, we have deQueue, and it follows a pretty similar logic. First, we'll check to make sure there is indeed something to dequeue. If not, we'll just return false.
If there's something to dequeue, then we'll just update the front by incrementing it and using the modulo operation to prevent overflow. Then we'll decrement the size of the queue to reflect the change. If all of this is successful, we'll return true.
MyCircularQueue.prototype.deQueue = function() {
if(this.isEmpty()) return false
this.front = (this.front+1)%(this.maxSize)
this.size -= 1
return true
};
And that's it - you're done with this question! You can now run it on Leetcode and you'll pass all the test cases.
You can find the whole solution, along with an example use case, on my GitHub, or you can run it directly using OneCompiler. And remember, if you're unfamiliar with JavaScript, just copy the code and ask ChatGPT to convert it into the language you prefer. Good luck!
Solving Leetcode problems is hard enough as it is, and add to that the difficulty of finding an in-depth solution that explains everything, and you find yourself stuck. Some videos are hard to follow, some don't explain the basics, while others leave you with more questions than answers. In this series, I break down Leetcode problems one at a time and walk you through a step-by-step solution. Have a question you can't understand? Send it to me, and I'll explain it in detail!