Design Circular Queue — Ring Buffer with O(1) Enqueue and Dequeue
Advertisement
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 a "Ring Buffer."
Implement the MyCircularQueue class:
MyCircularQueue(k)Initializes the object with sizek.int Front()Gets the front item from the queue. Returns-1if the queue is empty.int Rear()Gets the last item from the queue. Returns-1if the queue is empty.boolean enQueue(int value)Inserts an element. Returnstrueif successful.boolean deQueue()Deletes an element. Returnstrueif successful.boolean isEmpty()Checks whether the circular queue is empty.boolean isFull()Checks whether the circular queue is full.
You must solve the problem without using the built-in queue data structure. Each operation must run in O(1) time.
Constraints:
1 <= k <= 10000 <= value <= 1000- At most
3000calls in total.
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]Input: ["MyCircularQueue","isEmpty","enQueue","Front","deQueue","isEmpty"]
[[2],[],[10],[],[],[]]
Output: [null,true,true,10,true,true]Why This Problem Matters
LeetCode 622 Design Circular Queue is the canonical "implement a primitive data structure" question and shows up at Amazon, Google, Meta, and Apple. Ring buffers are everywhere in production:
- Producer-consumer queues between threads (lock-free single-producer single-consumer ring buffers).
- Audio and video frame buffers in real-time pipelines.
- Network packet rings (DPDK, kernel
sk_buffrings). - Streaming windows with bounded memory.
- Logging frameworks (circular log buffers like dmesg).
Acing this problem demonstrates that you understand index arithmetic, modular wrap-around, and the difference between FIFO and LIFO data structures.
The Core Insight
A circular queue uses a fixed-size array plus two indices:
head(orfront): index of the next element to dequeue.tail(orrear): index after the last element (one past the rear) — or sometimes the index of the rear itself, depending on convention.
Wrap-around uses modulo arithmetic: idx = (idx + 1) % k.
The classic "is it empty or full?" ambiguity arises because head == tail could mean either. Two common fixes:
- Track
countseparately.isEmpty()returnscount == 0;isFull()returnscount == k. Cleanest and most common in interviews. - Sacrifice one slot (so capacity is
k - 1) and use(tail + 1) % k == headas "full." Saves one integer but loses one slot.
I will use approach 1 because it is clearer, uses all k slots, and matches the LeetCode constraints exactly.
Visual Dry Run
Capacity k = 3. Operations: enQueue(1), enQueue(2), enQueue(3), deQueue(), enQueue(4).
| Operation | Array | head | tail | count | Notes |
|---|---|---|---|---|---|
| init | [_,_,_] | 0 | 0 | 0 | empty |
| enQueue(1) | [1,_,_] | 0 | 1 | 1 | tail wraps after k |
| enQueue(2) | [1,2,_] | 0 | 2 | 2 | |
| enQueue(3) | [1,2,3] | 0 | 0 | 3 | full, tail wrapped to 0 |
| deQueue | [_,2,3] | 1 | 0 | 2 | front pointer moves |
| enQueue(4) | [4,2,3] | 1 | 1 | 3 | new item inserted at slot 0 |
| Front | - | 1 | 1 | 3 | returns array[1] = 2 |
| Rear | - | 1 | 1 | 3 | returns array[(tail-1+k) % k] = array[0] = 4 |
Solution (Optimal)
# Python — fixed-size array with head/tail indices and count, all O(1)
class MyCircularQueue:
def __init__(self, k: int):
self.data = [0] * k
self.head = 0
self.tail = 0
self.count = 0
self.capacity = k
def enQueue(self, value: int) -> bool:
if self.isFull():
return False
self.data[self.tail] = value
self.tail = (self.tail + 1) % self.capacity
self.count += 1
return True
def deQueue(self) -> bool:
if self.isEmpty():
return False
self.head = (self.head + 1) % self.capacity
self.count -= 1
return True
def Front(self) -> int:
return -1 if self.isEmpty() else self.data[self.head]
def Rear(self) -> int:
if self.isEmpty():
return -1
return self.data[(self.tail - 1 + self.capacity) % self.capacity]
def isEmpty(self) -> bool:
return self.count == 0
def isFull(self) -> bool:
return self.count == self.capacity// JavaScript — circular queue with array + head/tail + count, all O(1)
class MyCircularQueue {
constructor(k) {
this.data = new Array(k);
this.head = 0;
this.tail = 0;
this.count = 0;
this.capacity = k;
}
enQueue(value) {
if (this.isFull()) return false;
this.data[this.tail] = value;
this.tail = (this.tail + 1) % this.capacity;
this.count++;
return true;
}
deQueue() {
if (this.isEmpty()) return false;
this.head = (this.head + 1) % this.capacity;
this.count--;
return true;
}
Front() {
return this.isEmpty() ? -1 : this.data[this.head];
}
Rear() {
if (this.isEmpty()) return -1;
return this.data[(this.tail - 1 + this.capacity) % this.capacity];
}
isEmpty() { return this.count === 0; }
isFull() { return this.count === this.capacity; }
}Complexity:
| Operation | Time | Space |
|---|---|---|
| All operations | O(1) | O(k) total |
Common Mistakes
-
Using
head == tailfor both empty and full. Without a counter or wasted slot, the two conditions become indistinguishable. -
Forgetting wrap-around for
Rear(). Whentail == 0, the rear element is at indexcapacity - 1. Compute as(tail - 1 + capacity) % capacity. -
Not validating before operations.
enQueueon a full queue must returnfalse, not silently overwrite. Same fordeQueueon an empty queue. -
Confusing tail-as-rear vs tail-as-next-slot conventions. Pick one and stick with it. Mixing them causes off-by-one bugs everywhere.
-
Using a linked list when the constraint says O(1) per operation. A linked list also gives O(1) but has higher constant factors and cache misses. The array variant is preferred unless dynamic resizing is required.
Interview Tips
- Open by asking: "Should the queue resize, or is the capacity strictly fixed?" The problem says fixed, but verifying shows attention to detail.
- Discuss both "count" and "wasted-slot" approaches. Pick "count" and justify.
- Walk through the wrap-around example on paper. Many candidates write code that works for the linear case but breaks at the wrap.
- For the linked-list alternative: "It also gives O(1), but has worse cache locality. For a fixed-size queue, the array variant is faster in practice."
Follow-up Questions
- Design Circular Deque (LC 641). Add
insertFrontanddeleteLast; same template with both ends modular. - Lock-free single-producer single-consumer ring buffer. Use atomic head/tail and avoid count by using power-of-two capacity.
- Resize on overflow. Allocate 2x array, copy in linear order, reset head=0 and tail=count.
- Bounded blocking queue. Add producer-consumer condition variables for thread safety.
- Priority queue with circular slots. Different problem — priority queues are not FIFO and need a heap.
Key Takeaways
- A circular queue is a fixed-size array with head and tail pointers that wrap via modulo arithmetic.
- Track a separate
countto disambiguate empty vs full while using allkslots. - All operations —
enQueue,deQueue,Front,Rear,isEmpty,isFull— run inO(1)time. - The wrap-around for
Rear()requires(tail - 1 + capacity) % capacityto handle the case wheretailis at index0. - Ring buffers are foundational for producer-consumer queues, audio buffers, network packet rings, and bounded streaming windows.
- Mention the "wasted slot" alternative to demonstrate awareness of low-level implementations like lock-free SPSC queues.
Advertisement