622. Design Circular Queue - Explanation
Description
Design and implement 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.
Implement the MyCircularQueue class:
MyCircularQueue(k)Initializes the object with the size of the queue to bek.int Front()Gets the front item from the queue. If the queue is empty, return-1.int Rear()Gets the last item from the queue. If the queue is empty, return-1.boolean enQueue(int value)Inserts an element into the circular queue. Returntrueif the operation is successful.boolean deQueue()Deletes an element from the circular queue. Returntrueif the operation is successful.boolean isEmpty()Checks whether the circular queue is empty or not.boolean isFull()Checks whether the circular queue is full or not.- You must solve the problem without using the built-in queue data structure in your programming language.
Example 1:
Input: ["MyCircularQueue", "enQueue", "enQueue", "enQueue", "enQueue", "Rear", "isFull", "deQueue", "enQueue", "Rear"]
[[3], [1], [2], [3], [4], [], [], [], [4], []]
Output: [null, true, true, true, false, 3, true, true, true, 4]Explanation:
MyCircularQueue myCircularQueue = new MyCircularQueue(3);
myCircularQueue.enQueue(1); // return True
myCircularQueue.enQueue(2); // return True
myCircularQueue.enQueue(3); // return True
myCircularQueue.enQueue(4); // return False
myCircularQueue.Rear(); // return 3
myCircularQueue.isFull(); // return True
myCircularQueue.deQueue(); // return True
myCircularQueue.enQueue(4); // return True
myCircularQueue.Rear(); // return 4
Constraints:
1 <= k <= 1000.0 <= value <= 1000- At most
3000calls will be made toenQueue,deQueue,Front,Rear,isEmpty, andisFull.
Topics
Prerequisites
Before attempting this problem, you should be comfortable with:
- Arrays - Fixed-size data structures with index-based access
- Modulo Arithmetic - Using the % operator for circular/wrap-around behavior
- Linked Lists - Singly and doubly linked structures for dynamic memory allocation
- Queue Operations - Understanding enqueue, dequeue, front, and rear operations
1. Brute Force
Intuition
The simplest approach is to use a dynamic array and treat it like a regular queue. We add elements to the back and remove from the front. While this works, removing from the front requires shifting all remaining elements, making it inefficient. We check the array size against the capacity to determine if the queue is full.
Algorithm
- Initialize an empty array
qand store the capacityk. - For
enQueue(value): If the array size equalsk, returnfalse. Otherwise, append the value and returntrue. - For
deQueue(): If the array is empty, returnfalse. Otherwise, remove the first element (index0) and returntrue. - For
Front(): Return the first element if the array is non-empty, otherwise return-1. - For
Rear(): Return the last element if the array is non-empty, otherwise return-1. - For
isEmpty(): Returntrueif the array size is0. - For
isFull(): Returntrueif the array size equalsk.
class MyCircularQueue:
def __init__(self, k: int):
self.q = []
self.k = k
def enQueue(self, value: int) -> bool:
if len(self.q) == self.k:
return False
self.q.append(value)
return True
def deQueue(self) -> bool:
if not self.q:
return False
self.q.pop(0)
return True
def Front(self) -> int:
if self.q:
return self.q[0]
return -1
def Rear(self) -> int:
if self.q:
return self.q[-1]
return -1
def isEmpty(self) -> bool:
return len(self.q) == 0
def isFull(self) -> bool:
return len(self.q) == self.kTime & Space Complexity
- Time complexity:
- time for initialization.
- time for each , , , and function calls.
- time for each function call.
- Space complexity:
Where is the size of the queue.
2. Array
Intuition
To achieve O(1) operations, we use a fixed-size array with two pointers: front pointing to the first element and rear pointing to the last. The "circular" aspect comes from using modulo arithmetic so that when we reach the end of the array, we wrap around to the beginning. We track the current size separately to distinguish between empty and full states.
Algorithm
- Initialize an array of size
k, setfront = 0,rear = -1, andsize = 0. - For
enQueue(value): If full, returnfalse. Otherwise, computerear = (rear + 1) % k, store the value at that index, incrementsize, and returntrue. - For
deQueue(): If empty, returnfalse. Otherwise, computefront = (front + 1) % k, decrementsize, and returntrue. - For
Front(): If empty, return-1. Otherwise, returnq[front]. - For
Rear(): If empty, return-1. Otherwise, returnq[rear]. - For
isEmpty(): Returntrueifsizeequals0. - For
isFull(): Returntrueifsizeequalsk.
class MyCircularQueue:
def __init__(self, k: int):
self.q = [0] * k
self.k = k
self.front = 0
self.rear = -1
self.size = 0
def enQueue(self, value: int) -> bool:
if self.isFull():
return False
self.rear = (self.rear + 1) % self.k
self.q[self.rear] = value
self.size += 1
return True
def deQueue(self) -> bool:
if self.isEmpty():
return False
self.front = (self.front + 1) % self.k
self.size -= 1
return True
def Front(self) -> int:
if self.isEmpty():
return -1
return self.q[self.front]
def Rear(self) -> int:
if self.isEmpty():
return -1
return self.q[self.rear]
def isEmpty(self) -> bool:
return self.size == 0
def isFull(self) -> bool:
return self.size == self.kTime & Space Complexity
- Time complexity:
- time for initialization.
- time for each , , , , and function calls.
- Space complexity:
Where is the size of the queue.
3. Doubly Linked List
Intuition
A doubly linked list allows O(1) insertions and deletions at both ends. We use dummy head and tail nodes to simplify edge cases. New elements are inserted before the tail (at the rear), and elements are removed after the head (from the front). We track remaining space to know when the queue is full.
Algorithm
- Initialize dummy nodes
leftandrightconnected to each other, and setspace = k. - For
enQueue(value): If full, returnfalse. Create a new node, insert it betweenright.prevandright, decrementspace, and returntrue. - For
deQueue(): If empty, returnfalse. Remove the node afterleftby updating pointers, incrementspace, and returntrue. - For
Front(): If empty, return-1. Otherwise, returnleft.next.val. - For
Rear(): If empty, return-1. Otherwise, returnright.prev.val. - For
isEmpty(): Returntrueifleft.nextequalsright. - For
isFull(): Returntrueifspaceequals0.
class ListNode:
def __init__(self, val, nxt, prev):
self.val, self.next, self.prev = val, nxt, prev
class MyCircularQueue:
def __init__(self, k: int):
self.space = k
self.left = ListNode(0, None, None)
self.right = ListNode(0, None, self.left)
self.left.next = self.right
def enQueue(self, value: int) -> bool:
if self.isFull(): return False
cur = ListNode(value, self.right, self.right.prev)
self.right.prev.next = cur
self.right.prev = cur
self.space -= 1
return True
def deQueue(self) -> bool:
if self.isEmpty(): return False
self.left.next = self.left.next.next
self.left.next.prev = self.left
self.space += 1
return True
def Front(self) -> int:
if self.isEmpty(): return -1
return self.left.next.val
def Rear(self) -> int:
if self.isEmpty(): return -1
return self.right.prev.val
def isEmpty(self) -> bool:
return self.left.next == self.right
def isFull(self) -> bool:
return self.space == 0Time & Space Complexity
- Time complexity:
- time for initialization.
- time for each , , , , and function calls.
- Space complexity:
Where is the size of the queue.
4. Singly Linked List
Intuition
A singly linked list can also work, using less memory per node than a doubly linked list. We maintain a dummy head node and a pointer to the actual tail. New elements are added at the tail, and elements are removed from the front (after the dummy head). The only complication is updating the tail pointer when the queue becomes empty.
Algorithm
- Initialize a dummy node
left, setright = left, and setspace = k. - For
enQueue(value): If full, returnfalse. Create a new node. If empty, set it asleft.nextandright. Otherwise, link it afterrightand updateright. Decrementspaceand returntrue. - For
deQueue(): If empty, returnfalse. Removeleft.nextby updatingleft.nexttoleft.next.next. Ifleft.nextbecomesnull, resetright = left. Incrementspaceand returntrue. - For
Front(): If empty, return-1. Otherwise, returnleft.next.val. - For
Rear(): If empty, return-1. Otherwise, returnright.val. - For
isEmpty(): Returntrueifleft.nextisnull. - For
isFull(): Returntrueifspaceequals0.
class ListNode:
def __init__(self, val, nxt=None):
self.val = val
self.next = nxt
class MyCircularQueue:
def __init__(self, k: int):
self.space = k
self.left = ListNode(0)
self.right = self.left
def enQueue(self, value: int) -> bool:
if self.isFull(): return False
cur = ListNode(value)
if self.isEmpty():
self.left.next = cur
self.right = cur
else:
self.right.next = cur
self.right = cur
self.space -= 1
return True
def deQueue(self) -> bool:
if self.isEmpty(): return False
self.left.next = self.left.next.next
if self.left.next is None:
self.right = self.left
self.space += 1
return True
def Front(self) -> int:
if self.isEmpty(): return -1
return self.left.next.val
def Rear(self) -> int:
if self.isEmpty(): return -1
return self.right.val
def isEmpty(self) -> bool:
return self.left.next is None
def isFull(self) -> bool:
return self.space == 0Time & Space Complexity
- Time complexity:
- time for initialization.
- time for each , , , , and function calls.
- Space complexity:
Where is the size of the queue.
Common Pitfalls
Incorrect Modulo Arithmetic for Wrap-Around
The circular nature requires careful modulo operations. A common mistake is forgetting to apply modulo when updating pointers, causing index out of bounds errors.
# Wrong - no wrap-around
def enQueue(self, value: int) -> bool:
if self.isFull():
return False
self.rear += 1 # Will exceed array bounds!
self.q[self.rear] = value
self.size += 1
return True
# Correct - use modulo for circular behavior
def enQueue(self, value: int) -> bool:
if self.isFull():
return False
self.rear = (self.rear + 1) % self.k
self.q[self.rear] = value
self.size += 1
return TrueConfusing Empty and Full States
When using only front and rear pointers without a separate size variable, distinguishing between empty and full queue becomes ambiguous since both states can have front == rear.
# Problematic - can't distinguish empty from full
def isEmpty(self) -> bool:
return self.front == self.rear # Also true when full!
# Solution: Track size separately
def isEmpty(self) -> bool:
return self.size == 0
def isFull(self) -> bool:
return self.size == self.kOff-By-One Errors in Rear Initialization
Initializing rear to 0 instead of -1 causes the first element to be placed at index 1, wasting space and causing incorrect behavior.
# Wrong initialization
def __init__(self, k: int):
self.q = [0] * k
self.front = 0
self.rear = 0 # First enQueue goes to index 1!
self.size = 0
# Correct initialization
def __init__(self, k: int):
self.q = [0] * k
self.front = 0
self.rear = -1 # First enQueue goes to index 0
self.size = 0
Sign in to join the discussion