Table of Content
Problem statement
Design your implementation of the circular queue. The circular queue is a linear data structure in which the operations are performed based on FIFO (First In First Out) principle and the last position is connected back to the first position to make a circle. It is also called “Ring Buffer”.
One of the benefits of the circular queue is that we can make use of the spaces in front of the queue. In a normal queue, once the queue becomes full, we cannot insert the next element even if there is a space in front of the queue. But using the circular queue, we can use the space to store new values.
Your implementation should support following operations:
- MyCircularQueue(k): Constructor, set the size of the queue to be k.
- Front: Get the front item from the queue. If the queue is empty, return -1.
- Rear: Get the last item from the queue. If the queue is empty, return -1.
- enQueue(value): Insert an element into the circular queue. Return true if the operation is successful.
- deQueue(): Delete an element from the circular queue. Return true if the operation is successful.
- isEmpty(): Checks whether the circular queue is empty or not.
- isFull(): Checks whether the circular queue is full or not.
Example
MyCircularQueue circularQueue = new MyCircularQueue(3); // set the size to be 3circularQueue.enQueue(1); // return truecircularQueue.enQueue(2); // return truecircularQueue.enQueue(3); // return truecircularQueue.enQueue(4); // return false, the queue is fullcircularQueue.Rear(); // return 3circularQueue.isFull(); // return truecircularQueue.deQueue(); // return truecircularQueue.enQueue(4); // return truecircularQueue.Rear(); // return 4
Input
class MyCircularQueue: def __init__(self, k: int): """ Initialize your data structure here. Set the size of the queue to be k. """ self.queue = [ None ] * k self.size = 0 self.capacity = k self.head, self.tail = 0, -1
def enQueue(self, value: int) -> bool: """ Insert an element into the circular queue. Return true if the operation is successful. """ if self.isFull(): return False
self.tail = (self.tail + 1) % self.capacity self.queue[self.tail] = value self.size += 1 return True
def deQueue(self) -> bool: """ Delete an element from the circular queue. Return true if the operation is successful. """ if self.isEmpty(): return False
self.head = (self.head + 1) % self.capacity self.size -= 1 return True
def Front(self) -> int: """ Get the front item from the queue. """
return -1 if self.isEmpty() else self.queue[self.head]
def Rear(self) -> int: """ Get the last item from the queue. """
return -1 if self.isEmpty() else self.queue[self.tail]
def isEmpty(self) -> bool: """ Checks whether the circular queue is empty or not. """ return self.size <= 0
def isFull(self) -> bool: """ Checks whether the circular queue is full or not. """ return self.size == self.capacity# Your MyCircularQueue object will be instantiated and called as such:# obj = MyCircularQueue(k)# param_1 = obj.enQueue(value)# param_2 = obj.deQueue()# param_3 = obj.Front()# param_4 = obj.Rear()# param_5 = obj.isEmpty()# param_6 = obj.isFull()
Standard Solution
Time Complexity: O(N) for deQueue, O(1) for the rest
Space Complexity: O(N)
The easiest and most straightforward solution for the circular queue is to use an array. Your mileage may vary depending on which programming language you’re using, but in python
, []
also acts similar like a queue due to public methods, such as pop
, and push
.
To implement the circular queue, we just need a capacity
to hold k
, which is the maximum size of the circular queue. For enQueue
and deQueue
, we can leverage the builtin pop
to remove an element and push
to add an element.
class MyCircularQueue: def __init__(self, k: int): """ Initialize your data structure here. Set the size of the queue to be k. """ self.queue = [] self.capacity = k
def enQueue(self, value: int) -> bool: """ Insert an element into the circular queue. Return true if the operation is successful. """ if self.isFull(): return False self.queue.append(value) # print(self.queue) return True
def deQueue(self) -> bool: """ Delete an element from the circular queue. Return true if the operation is successful. """ if self.isEmpty(): return False self.queue.pop(0) return True
def Front(self) -> int: """ Get the front item from the queue. """
return -1 if self.isEmpty() else self.queue[0]
def Rear(self) -> int: """ Get the last item from the queue. """
return -1 if self.isEmpty() else self.queue[-1]
def isEmpty(self) -> bool: """ Checks whether the circular queue is empty or not. """ return len(self.queue) <= 0
def isFull(self) -> bool: """ Checks whether the circular queue is full or not. """ return len(self.queue) == self.capacity
Complexity Explanation
It should be clear why the time complexity of all of the methods are O(1)
, except for deQueue
, which is O(N)
. deQueue
uses pop(0)
, which removes the first element from the array, forcing rest of the elements to be shifted.
The space complexity is O(N)
or O(k)
because our circular queue can hold k
elements,
Two pointers Solution
Time Complexity: O(1) for everything
Space Complexity: O(N)
The previous solution is kind of cheating because we’re using builtin methods for []
. To implement a circular queue without using those builtin methods, we can use two pointers to keep track of the beginning and the end of the array. This solution can be implemented in almost any programming language.
To implement this, we need:
- A
queue
array initialized with the size ofk
. - A
size
to count the current number of elements in the circular queue. - A
capacity
to holdk
. - Two pointers,
head
andtail
to keep track of the front and end of the circular queue.
Everything is similar to the previous solution, except for enQueue
and deQueue
.
enQueue
will increment the tail
and save the value
. It is important that we also mod
/%
the tail
with the capacity
to handle the case when tail > capacity
. Because of this, we initialize tail
to -1
, since initially, there’s nothing inside the array and the first enQueue
will place value
at tail
when tail = 0
. We also need to increment the size
.
Similarly, deQueue
will do the same thing as enQueue
except with the head
. Although, there are two key differences.
head
is initialized to0
because in the case when we callFront
, we need the first element, which is in the first index.- We need to decrement the
size
instead of incrementing.
class MyCircularQueue: def __init__(self, k: int): """ Initialize your data structure here. Set the size of the queue to be k. """ self.queue = [ None ] * k self.size = 0 self.capacity = k self.head, self.tail = 0, -1
def enQueue(self, value: int) -> bool: """ Insert an element into the circular queue. Return true if the operation is successful. """ if self.isFull(): return False
self.tail = (self.tail + 1) % self.capacity self.queue[self.tail] = value self.size += 1 return True
def deQueue(self) -> bool: """ Delete an element from the circular queue. Return true if the operation is successful. """ if self.isEmpty(): return False
self.head = (self.head + 1) % self.capacity self.size -= 1 return True
def Front(self) -> int: """ Get the front item from the queue. """
return -1 if self.isEmpty() else self.queue[self.head]
def Rear(self) -> int: """ Get the last item from the queue. """
return -1 if self.isEmpty() else self.queue[self.tail]
def isEmpty(self) -> bool: """ Checks whether the circular queue is empty or not. """ return self.size <= 0
def isFull(self) -> bool: """ Checks whether the circular queue is full or not. """ return self.size == self.capacity
Complexity Explanation
Unlike the previous solution, all the methods in this solution are O(1)
, especially deQueue
because we replaced pop(0)
with an indexing solution, which is constant.
The space complexity is still O(N)
or O(k)
with a little bit more constant overhead from the introduction of additional variables.