LeetCode 622 - Design Circular Queue
The problem asks us to implement a circular queue, a fixed-size queue where the end wraps back to the start to efficiently reuse empty space.
Difficulty: 🟡 Medium
Topics: Array, Linked List, Design, Queue
Solution
Problem Understanding
The problem asks us to implement a circular queue, a fixed-size queue where the end wraps back to the start to efficiently reuse empty space. In a normal queue implemented with an array, once the array fills, even if elements are dequeued from the front, new elements cannot be enqueued without shifting the array. A circular queue solves this by treating the array as circular, so the next insertion after the last index can occur at index 0 if space is available.
The input consists of method calls to the queue object: enQueue adds elements, deQueue removes elements, Front and Rear read elements from the front and back, and isEmpty / isFull check the queue state. The output is the return value of these calls. Constraints indicate the queue size k is at most 1000 and at most 3000 operations are performed. This makes an array-based solution feasible without worrying about performance limits.
Edge cases include trying to enqueue into a full queue, dequeue from an empty queue, and reading Front or Rear when the queue is empty. Handling the wrap-around correctly is crucial; naive implementations often get off-by-one errors with indices.
Approaches
A brute-force approach could use a dynamic array or list and shift elements on every dequeue to maintain the front at index 0. This is correct but inefficient, as shifting elements on every dequeue results in O(n) time for each dequeue operation, which is unnecessary.
The optimal approach uses a fixed-size array with two pointers, front and rear, and a count of elements. enQueue inserts at rear and moves it forward modulo k. deQueue moves front forward modulo k. isEmpty checks if count is zero, and isFull checks if count equals k. This approach maintains O(1) time for all operations, and no elements need to be shifted.
| Approach | Time Complexity | Space Complexity | Notes |
|---|---|---|---|
| Brute Force | O(n) per deQueue | O(k) | Shift elements on dequeue |
| Optimal | O(1) per operation | O(k) | Use array with front, rear, count; wrap indices modulo k |
Algorithm Walkthrough
- Initialize the queue with a fixed-size array of size
k. Setfrontandrearto0andcountto0. - To
enQueue(value), first check if the queue is full (count == k). If full, returnFalse. Otherwise, insertvalueatrear, incrementrearmodulok, and incrementcount. ReturnTrue. - To
deQueue(), first check if the queue is empty (count == 0). If empty, returnFalse. Otherwise, incrementfrontmodulokand decrementcount. ReturnTrue. Front()returns-1if the queue is empty. Otherwise, return the value atfront.Rear()returns-1if the queue is empty. Otherwise, return the value at(rear - 1 + k) % kbecauserearpoints to the next insertion index.isEmpty()returnsTrueifcount == 0, elseFalse.isFull()returnsTrueifcount == k, elseFalse.
The algorithm works because the combination of front, rear, and count always maintains the correct number of elements and ensures that all operations respect FIFO order. Using modulo arithmetic guarantees the circular behavior.
Python Solution
class MyCircularQueue:
def __init__(self, k: int):
self.queue = [0] * k
self.k = k
self.front = 0
self.rear = 0
self.count = 0
def enQueue(self, value: int) -> bool:
if self.isFull():
return False
self.queue[self.rear] = value
self.rear = (self.rear + 1) % self.k
self.count += 1
return True
def deQueue(self) -> bool:
if self.isEmpty():
return False
self.front = (self.front + 1) % self.k
self.count -= 1
return True
def Front(self) -> int:
if self.isEmpty():
return -1
return self.queue[self.front]
def Rear(self) -> int:
if self.isEmpty():
return -1
return self.queue[(self.rear - 1 + self.k) % self.k]
def isEmpty(self) -> bool:
return self.count == 0
def isFull(self) -> bool:
return self.count == self.k
The Python implementation uses a fixed array queue to store elements and maintains front, rear, and count. All operations use modulo arithmetic to handle the circular behavior. This guarantees O(1) for each operation and correct wrap-around.
Go Solution
type MyCircularQueue struct {
queue []int
front, rear int
count, k int
}
func Constructor(k int) MyCircularQueue {
return MyCircularQueue{
queue: make([]int, k),
k: k,
}
}
func (this *MyCircularQueue) EnQueue(value int) bool {
if this.IsFull() {
return false
}
this.queue[this.rear] = value
this.rear = (this.rear + 1) % this.k
this.count++
return true
}
func (this *MyCircularQueue) DeQueue() bool {
if this.IsEmpty() {
return false
}
this.front = (this.front + 1) % this.k
this.count--
return true
}
func (this *MyCircularQueue) Front() int {
if this.IsEmpty() {
return -1
}
return this.queue[this.front]
}
func (this *MyCircularQueue) Rear() int {
if this.IsEmpty() {
return -1
}
return this.queue[(this.rear-1+this.k)%this.k]
}
func (this *MyCircularQueue) IsEmpty() bool {
return this.count == 0
}
func (this *MyCircularQueue) IsFull() bool {
return this.count == this.k
}
The Go solution is similar to Python but uses slices with fixed length and integer fields to track state. Wrap-around is handled identically using modulo arithmetic.
Worked Examples
Example operations on a queue of size 3:
| Operation | queue | front | rear | count | Output |
|---|---|---|---|---|---|
| enQueue(1) | [1,0,0] | 0 | 1 | 1 | True |
| enQueue(2) | [1,2,0] | 0 | 2 | 2 | True |
| enQueue(3) | [1,2,3] | 0 | 0 | 3 | True |
| enQueue(4) | [1,2,3] | 0 | 0 | 3 | False |
| Rear() | [1,2,3] | 0 | 0 | 3 | 3 |
| isFull() | [1,2,3] | 0 | 0 | 3 | True |
| deQueue() | [1,2,3] | 1 | 0 | 2 | True |
| enQueue(4) | [4,2,3] | 1 | 1 | 3 | True |
| Rear() | [4,2,3] | 1 | 1 | 3 | 4 |
Complexity Analysis
| Measure | Complexity | Explanation |
|---|---|---|
| Time | O(1) per operation | All operations involve simple pointer updates or checks |
| Space | O(k) | Fixed-size array stores up to k elements |
Since we never shift elements and only update indices modulo k, the algorithm achieves constant time per operation.
Test Cases
# Basic functionality
q = MyCircularQueue(3)
assert q.enQueue(1) == True # insert 1
assert q.enQueue(2) == True # insert 2
assert q.enQueue(3) == True # insert 3
assert q.enQueue(4) == False # queue is full
assert q.Rear() == 3 # last element
assert q.isFull() == True # check full
assert q.deQueue() == True # remove front
assert q.enQueue(4) == True # insert 4
assert q.Rear() == 4 # new last element
# Edge cases
empty_q = MyCircularQueue(1)
assert empty_q.deQueue() == False # cannot dequeue empty queue
assert empty_q.Front() == -1 # front is -1 when empty
assert empty_q.Rear() == -1 # rear is -1 when empty
assert empty_q.enQueue(5) == True
assert empty_q.Front() == 5
assert empty_q.Rear() == 5
assert empty_q.isFull() == True
assert empty_q.deQueue() == True
assert empty_q.isEmpty() == True
| Test | Why |
|---|---|
| enQueue into full queue | Validates isFull logic |