622. Design Circular Queue
Problem Description
You need to design and implement a circular queue data structure. A circular queue is a linear data structure that operates on the First In First Out (FIFO) principle, where the last position connects back to the first position to form a circle (also known as a "Ring Buffer").
The main advantage of a circular queue is that it can reuse the space at the front of the queue. In a regular queue, once it becomes full, you cannot insert new elements even if there's available space at the front (after elements have been removed). A circular queue solves this problem by wrapping around to use that space.
Your implementation should create a class MyCircularQueue
with the following operations:
MyCircularQueue(k)
: Constructor that initializes the circular queue with a maximum size ofk
.int Front()
: Returns the front element in the queue. If the queue is empty, return-1
.int Rear()
: Returns the last element in the queue. If the queue is empty, return-1
.boolean enQueue(int value)
: Adds an element to the rear of the queue. Returnstrue
if successful,false
if the queue is full.boolean deQueue()
: Removes an element from the front of the queue. Returnstrue
if successful,false
if the queue is empty.boolean isEmpty()
: Returnstrue
if the queue is empty,false
otherwise.boolean isFull()
: Returnstrue
if the queue is full,false
otherwise.
You must implement this without using any built-in queue data structure from your programming language.
The solution uses an array of fixed size k
to store elements, a front
pointer to track the position of the first element, and a size
variable to track the current number of elements. The circular nature is achieved using modulo arithmetic - when inserting at position (front + size) % capacity
and when moving the front pointer as (front + 1) % capacity
, the indices wrap around to the beginning of the array when they reach the end.
Intuition
The key insight for implementing a circular queue is recognizing that we need to efficiently utilize a fixed-size array by treating it as if it wraps around - when we reach the end, we continue from the beginning.
Think of it like seats arranged in a circle. When people leave from the front seats, those spaces become available for new people joining at the rear. But how do we keep track of where the front and rear are when they keep moving around the circle?
The challenge is managing two main aspects:
- Where to insert new elements - This depends on where the queue currently starts (front) and how many elements are already in it (size).
- How to wrap around - When our indices go beyond the array boundary, they need to circle back to the beginning.
The modulo operator (%
) becomes our best friend here. It naturally handles the wrap-around behavior. For any index i
, computing i % capacity
ensures we stay within the valid array bounds [0, capacity-1]
.
Instead of maintaining both front and rear pointers (which can be tricky to manage, especially determining when the queue is full vs empty), we can simplify by tracking:
front
: The index of the first elementsize
: The number of elements currently in the queue
With these two values, we can always calculate where the rear is: (front + size - 1) % capacity
. Similarly, when inserting a new element, it goes to position (front + size) % capacity
.
This approach elegantly handles all edge cases:
- Empty queue:
size = 0
- Full queue:
size = capacity
- Wrap-around: Automatic through modulo arithmetic
- Dequeue: Simply move
front
forward by 1 (with modulo) and decreasesize
- Enqueue: Place at calculated position and increase
size
The beauty of this solution is that it avoids the classic problem of distinguishing between a full and empty queue when using just front and rear pointers (where front == rear
could mean either state).
Learn more about Queue and Linked List patterns.
Solution Approach
We implement the circular queue using an array-based approach with careful index management through modulo arithmetic.
Data Structure Setup
We initialize our circular queue with:
q
: An array of sizek
to store the elementsfront
: A pointer indicating the index of the front element (initially0
)size
: A counter tracking the current number of elements (initially0
)capacity
: The maximum capacityk
Core Operations Implementation
1. EnQueue Operation
When inserting a new element:
- First check if the queue is full by verifying if
size == capacity
- If not full, calculate the insertion position:
(front + size) % capacity
- This formula works because
front
is where the queue starts, andsize
tells us how many positions to skip - The modulo ensures we wrap around to the beginning if needed
- This formula works because
- Place the value at the calculated position
- Increment
size
by 1 - Return
true
for successful insertion
2. DeQueue Operation
When removing an element:
- Check if the queue is empty (
size == 0
) - If not empty, move the front pointer forward:
front = (front + 1) % capacity
- The modulo ensures that if
front
was at the last position, it wraps to position0
- The modulo ensures that if
- Decrement
size
by 1 - Return
true
for successful removal
3. Front Operation
To get the front element:
- If the queue is empty (
size == 0
), return-1
- Otherwise, return
q[front]
directly
4. Rear Operation
To get the rear element:
- If the queue is empty, return
-1
- Otherwise, calculate the rear position:
(front + size - 1) % capacity
- We subtract 1 because
size
tells us the count, but we need the index of the last element - The modulo handles wrap-around cases
- We subtract 1 because
- Return the element at the calculated position
5. isEmpty and isFull Operations
These are straightforward checks:
isEmpty()
: Returnsize == 0
isFull()
: Returnsize == capacity
Why This Works
The elegance of this solution lies in how size
and front
work together:
size
tells us exactly how many elements we have, eliminating ambiguity between full and empty statesfront
marks our starting position in the circular array- All positions are calculated relative to
front
usingsize
, with modulo arithmetic handling the circular nature
For example, if capacity = 5
, front = 3
, and size = 3
, the elements occupy positions [3, 4, 0]
in the array. The rear element is at position (3 + 3 - 1) % 5 = 0
, and a new element would be inserted at (3 + 3) % 5 = 1
.
This approach achieves O(1) time complexity for all operations and uses O(k) space for the array storage.
Ready to land your dream job?
Unlock your dream job with a 5-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through a sequence of operations with a circular queue of capacity 3 to see how the solution works in practice.
Initial State:
- Create
MyCircularQueue(3)
- Array:
[_, _, _]
(size = 3) front = 0
,size = 0
Operation 1: enQueue(1)
- Check if full:
size (0) == capacity (3)
? No - Insert position:
(front + size) % capacity = (0 + 0) % 3 = 0
- Array:
[1, _, _]
- Update:
size = 1
- Returns:
true
Operation 2: enQueue(2)
- Check if full:
size (1) == capacity (3)
? No - Insert position:
(0 + 1) % 3 = 1
- Array:
[1, 2, _]
- Update:
size = 2
- Returns:
true
Operation 3: enQueue(3)
- Check if full:
size (2) == capacity (3)
? No - Insert position:
(0 + 2) % 3 = 2
- Array:
[1, 2, 3]
- Update:
size = 3
- Returns:
true
Operation 4: deQueue()
- Check if empty:
size (3) == 0
? No - Remove from
front = 0
- Update:
front = (0 + 1) % 3 = 1
,size = 2
- Array:
[_, 2, 3]
(logically, position 0 is now "empty") - Returns:
true
Operation 5: enQueue(4)
- Check if full:
size (2) == capacity (3)
? No - Insert position:
(front + size) % capacity = (1 + 2) % 3 = 0
- Array:
[4, 2, 3]
- Update:
size = 3
- Returns:
true
- Note: We wrapped around and reused position 0!
Operation 6: Rear()
- Check if empty:
size (3) == 0
? No - Rear position:
(front + size - 1) % capacity = (1 + 3 - 1) % 3 = 0
- Returns:
4
(the element at position 0)
Operation 7: Front()
- Check if empty:
size (3) == 0
? No - Returns:
q[front] = q[1] = 2
Current logical queue: [2, 3, 4] where:
- Position 1 holds the front element (2)
- Position 2 holds the middle element (3)
- Position 0 holds the rear element (4)
This demonstrates how the circular queue efficiently reuses space. After removing element 1 from position 0, we were able to insert element 4 at that same position, treating the array as circular. The modulo arithmetic automatically handles the wrap-around, and tracking size
separately from the pointers eliminates any ambiguity about whether the queue is full or empty.
Solution Implementation
1class MyCircularQueue:
2 def __init__(self, k: int):
3 """
4 Initialize the circular queue with capacity k.
5
6 Args:
7 k: Maximum capacity of the queue
8 """
9 self.queue = [0] * k # Pre-allocate array with fixed size
10 self.size = 0 # Current number of elements in queue
11 self.capacity = k # Maximum capacity
12 self.front = 0 # Index of the front element
13
14 def enQueue(self, value: int) -> bool:
15 """
16 Insert an element into the circular queue.
17
18 Args:
19 value: Element to be inserted
20
21 Returns:
22 True if operation successful, False if queue is full
23 """
24 # Check if queue is full
25 if self.isFull():
26 return False
27
28 # Calculate rear position and insert value
29 rear_index = (self.front + self.size) % self.capacity
30 self.queue[rear_index] = value
31 self.size += 1
32 return True
33
34 def deQueue(self) -> bool:
35 """
36 Delete an element from the circular queue.
37
38 Returns:
39 True if operation successful, False if queue is empty
40 """
41 # Check if queue is empty
42 if self.isEmpty():
43 return False
44
45 # Move front pointer forward and decrease size
46 self.front = (self.front + 1) % self.capacity
47 self.size -= 1
48 return True
49
50 def Front(self) -> int:
51 """
52 Get the front item from the queue.
53
54 Returns:
55 Front element if exists, -1 if queue is empty
56 """
57 if self.isEmpty():
58 return -1
59 return self.queue[self.front]
60
61 def Rear(self) -> int:
62 """
63 Get the last item from the queue.
64
65 Returns:
66 Rear element if exists, -1 if queue is empty
67 """
68 if self.isEmpty():
69 return -1
70
71 # Calculate rear position: (front + size - 1) mod capacity
72 rear_index = (self.front + self.size - 1) % self.capacity
73 return self.queue[rear_index]
74
75 def isEmpty(self) -> bool:
76 """
77 Check whether the circular queue is empty.
78
79 Returns:
80 True if queue is empty, False otherwise
81 """
82 return self.size == 0
83
84 def isFull(self) -> bool:
85 """
86 Check whether the circular queue is full.
87
88 Returns:
89 True if queue is full, False otherwise
90 """
91 return self.size == self.capacity
92
93
94# Your MyCircularQueue object will be instantiated and called as such:
95# obj = MyCircularQueue(k)
96# param_1 = obj.enQueue(value)
97# param_2 = obj.deQueue()
98# param_3 = obj.Front()
99# param_4 = obj.Rear()
100# param_5 = obj.isEmpty()
101# param_6 = obj.isFull()
102
1class MyCircularQueue {
2 // Array to store queue elements
3 private int[] queue;
4 // Index pointing to the front element of the queue
5 private int frontIndex;
6 // Current number of elements in the queue
7 private int currentSize;
8 // Maximum capacity of the queue
9 private int maxCapacity;
10
11 /**
12 * Initialize the circular queue with size k
13 * @param k Maximum capacity of the queue
14 */
15 public MyCircularQueue(int k) {
16 queue = new int[k];
17 frontIndex = 0;
18 currentSize = 0;
19 maxCapacity = k;
20 }
21
22 /**
23 * Insert an element into the circular queue
24 * @param value The value to be inserted
25 * @return true if the operation is successful, false if queue is full
26 */
27 public boolean enQueue(int value) {
28 // Check if queue is at maximum capacity
29 if (isFull()) {
30 return false;
31 }
32
33 // Calculate the rear position using circular indexing
34 int rearIndex = (frontIndex + currentSize) % maxCapacity;
35 queue[rearIndex] = value;
36 currentSize++;
37
38 return true;
39 }
40
41 /**
42 * Delete an element from the circular queue
43 * @return true if the operation is successful, false if queue is empty
44 */
45 public boolean deQueue() {
46 // Check if queue is empty
47 if (isEmpty()) {
48 return false;
49 }
50
51 // Move front pointer to next position using circular indexing
52 frontIndex = (frontIndex + 1) % maxCapacity;
53 currentSize--;
54
55 return true;
56 }
57
58 /**
59 * Get the front item from the queue
60 * @return The front element if queue is not empty, -1 otherwise
61 */
62 public int Front() {
63 if (isEmpty()) {
64 return -1;
65 }
66
67 return queue[frontIndex];
68 }
69
70 /**
71 * Get the last item from the queue
72 * @return The rear element if queue is not empty, -1 otherwise
73 */
74 public int Rear() {
75 if (isEmpty()) {
76 return -1;
77 }
78
79 // Calculate the rear position using circular indexing
80 int rearIndex = (frontIndex + currentSize - 1) % maxCapacity;
81 return queue[rearIndex];
82 }
83
84 /**
85 * Check whether the circular queue is empty
86 * @return true if empty, false otherwise
87 */
88 public boolean isEmpty() {
89 return currentSize == 0;
90 }
91
92 /**
93 * Check whether the circular queue is full
94 * @return true if full, false otherwise
95 */
96 public boolean isFull() {
97 return currentSize == maxCapacity;
98 }
99}
100
101/**
102 * Your MyCircularQueue object will be instantiated and called as such:
103 * MyCircularQueue obj = new MyCircularQueue(k);
104 * boolean param_1 = obj.enQueue(value);
105 * boolean param_2 = obj.deQueue();
106 * int param_3 = obj.Front();
107 * int param_4 = obj.Rear();
108 * boolean param_5 = obj.isEmpty();
109 * boolean param_6 = obj.isFull();
110 */
111
1class MyCircularQueue {
2private:
3 int front; // Index of the front element
4 int size; // Current number of elements in the queue
5 int capacity; // Maximum capacity of the queue
6 vector<int> q; // Underlying array to store queue elements
7
8public:
9 /**
10 * Initialize the circular queue with given capacity k
11 * @param k: The maximum capacity of the queue
12 */
13 MyCircularQueue(int k) {
14 capacity = k;
15 q = vector<int>(k);
16 front = 0;
17 size = 0;
18 }
19
20 /**
21 * Insert an element into the circular queue
22 * @param value: The value to be inserted
23 * @return: true if the operation is successful, false if queue is full
24 */
25 bool enQueue(int value) {
26 if (isFull()) {
27 return false;
28 }
29
30 // Calculate the rear position using modulo arithmetic
31 int rear_index = (front + size) % capacity;
32 q[rear_index] = value;
33 ++size;
34
35 return true;
36 }
37
38 /**
39 * Delete an element from the circular queue
40 * @return: true if the operation is successful, false if queue is empty
41 */
42 bool deQueue() {
43 if (isEmpty()) {
44 return false;
45 }
46
47 // Move front pointer forward circularly
48 front = (front + 1) % capacity;
49 --size;
50
51 return true;
52 }
53
54 /**
55 * Get the front item from the queue
56 * @return: The front element value, or -1 if queue is empty
57 */
58 int Front() {
59 if (isEmpty()) {
60 return -1;
61 }
62
63 return q[front];
64 }
65
66 /**
67 * Get the last item from the queue
68 * @return: The rear element value, or -1 if queue is empty
69 */
70 int Rear() {
71 if (isEmpty()) {
72 return -1;
73 }
74
75 // Calculate the rear position based on front and size
76 int rear_index = (front + size - 1) % capacity;
77 return q[rear_index];
78 }
79
80 /**
81 * Check whether the circular queue is empty
82 * @return: true if empty, false otherwise
83 */
84 bool isEmpty() {
85 return size == 0;
86 }
87
88 /**
89 * Check whether the circular queue is full
90 * @return: true if full, false otherwise
91 */
92 bool isFull() {
93 return size == capacity;
94 }
95};
96
97/**
98 * Your MyCircularQueue object will be instantiated and called as such:
99 * MyCircularQueue* obj = new MyCircularQueue(k);
100 * bool param_1 = obj->enQueue(value);
101 * bool param_2 = obj->deQueue();
102 * int param_3 = obj->Front();
103 * int param_4 = obj->Rear();
104 * bool param_5 = obj->isEmpty();
105 * bool param_6 = obj->isFull();
106 */
107
1// Array to store circular queue elements
2let queue: number[];
3// Index pointing to the front element position
4let left: number;
5// Index pointing to the next insertion position (one past the rear)
6let right: number;
7// Maximum capacity of the circular queue
8let capacity: number;
9
10/**
11 * Initialize the circular queue with size k
12 * @param k - The maximum size of the queue
13 */
14function MyCircularQueue(k: number): void {
15 queue = new Array(k);
16 left = 0;
17 right = 0;
18 capacity = k;
19}
20
21/**
22 * Insert an element into the circular queue
23 * @param value - The value to be inserted
24 * @returns true if the operation is successful, false if queue is full
25 */
26function enQueue(value: number): boolean {
27 // Check if queue is at full capacity
28 if (isFull()) {
29 return false;
30 }
31 // Insert value at the current right position (wrapped using modulo)
32 queue[right % capacity] = value;
33 // Move right pointer forward
34 right++;
35 return true;
36}
37
38/**
39 * Delete an element from the circular queue
40 * @returns true if the operation is successful, false if queue is empty
41 */
42function deQueue(): boolean {
43 // Check if queue is empty
44 if (isEmpty()) {
45 return false;
46 }
47 // Move left pointer forward to remove front element
48 left++;
49 return true;
50}
51
52/**
53 * Get the front item from the queue
54 * @returns The front element, or -1 if queue is empty
55 */
56function Front(): number {
57 // Check if queue is empty
58 if (isEmpty()) {
59 return -1;
60 }
61 // Return element at left position (wrapped using modulo)
62 return queue[left % capacity];
63}
64
65/**
66 * Get the last item from the queue
67 * @returns The rear element, or -1 if queue is empty
68 */
69function Rear(): number {
70 // Check if queue is empty
71 if (isEmpty()) {
72 return -1;
73 }
74 // Return element at position before right pointer (wrapped using modulo)
75 return queue[(right - 1) % capacity];
76}
77
78/**
79 * Check whether the circular queue is empty
80 * @returns true if queue is empty, false otherwise
81 */
82function isEmpty(): boolean {
83 // Queue is empty when left and right pointers are at same position
84 return right - left === 0;
85}
86
87/**
88 * Check whether the circular queue is full
89 * @returns true if queue is full, false otherwise
90 */
91function isFull(): boolean {
92 // Queue is full when difference equals capacity
93 return right - left === capacity;
94}
95
Time and Space Complexity
Time Complexity: All operations (enQueue
, deQueue
, Front
, Rear
, isEmpty
, isFull
) have a time complexity of O(1)
. Each operation performs a constant number of operations:
enQueue
: Calculates the insertion position using(self.front + self.size) % self.capacity
and assigns the value - constant timedeQueue
: Updatesself.front
using(self.front + 1) % self.capacity
and decrements size - constant timeFront
: Direct array access at indexself.front
- constant timeRear
: Calculates position using(self.front + self.size - 1) % self.capacity
and returns the value - constant timeisEmpty
: Simple comparisonself.size == 0
- constant timeisFull
: Simple comparisonself.size == self.capacity
- constant time
Space Complexity: O(k)
, where k
is the capacity of the circular queue. The space is used by:
- The array
self.q
which stores exactlyk
elements - A constant amount of additional space for the instance variables (
self.size
,self.capacity
,self.front
)
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Confusing Full vs Empty States Without Size Variable
Pitfall: A common mistake is trying to determine if the queue is full or empty using only front
and rear
pointers without a size
variable. When front == rear
, it's ambiguous whether the queue is completely full or completely empty.
Example of problematic approach:
class MyCircularQueue:
def __init__(self, k):
self.queue = [0] * k
self.front = 0
self.rear = 0 # Using rear pointer instead of size
self.capacity = k
def isEmpty(self):
# When front == rear, is it empty or full?
return self.front == self.rear # AMBIGUOUS!
Solution: Always maintain a size
counter or waste one slot in the array to differentiate between full and empty states. The provided solution correctly uses a size
variable to eliminate this ambiguity.
2. Incorrect Rear Position Calculation
Pitfall: Calculating the rear element's position incorrectly, especially forgetting to subtract 1 or not handling the modulo properly.
Common mistakes:
def Rear(self):
if self.isEmpty():
return -1
# WRONG: This gives the position where next element would be inserted
return self.queue[(self.front + self.size) % self.capacity]
# WRONG: Forgetting modulo can cause index out of bounds
return self.queue[self.front + self.size - 1]
Solution: The correct formula is (self.front + self.size - 1) % self.capacity
. Remember that size
represents the count of elements, so we need to subtract 1 to get the last element's index.
3. Not Updating Front Pointer Correctly in DeQueue
Pitfall: Forgetting to apply modulo when moving the front pointer forward, causing index out of bounds errors.
Incorrect implementation:
def deQueue(self):
if self.isEmpty():
return False
# WRONG: Will cause index error when front reaches capacity-1
self.front = self.front + 1 # Missing modulo!
self.size -= 1
return True
Solution: Always use modulo arithmetic: self.front = (self.front + 1) % self.capacity
4. Mixing Zero-Based and One-Based Logic
Pitfall: Confusion between array indices (0-based) and element count (1-based), leading to off-by-one errors.
Example mistake:
def enQueue(self, value):
if self.isFull():
return False
# WRONG: Using size+1 instead of size for calculating position
rear_index = (self.front + self.size + 1) % self.capacity
self.queue[rear_index] = value
self.size += 1
return True
Solution: Remember that when you have size
elements starting at front
, the next available position is at (front + size) % capacity
, not (front + size + 1) % capacity
.
5. Not Handling Edge Cases in Front/Rear Methods
Pitfall: Accessing array elements without checking if the queue is empty first, leading to returning incorrect values.
Problematic code:
def Front(self):
# WRONG: Returns whatever garbage value is at front position
return self.queue[self.front] # What if queue is empty?
Solution: Always check isEmpty()
first and return -1
for empty queue cases, as shown in the correct implementation.
How many ways can you arrange the three letters A, B and C?
Recommended Readings
Queue Intro Think of the last time you stood in line to buy movie tickets The first person in line gets their ticket first and any newcomers join the end of the line This system where the first person to arrive is the first to be served is a queue in real
Linked List Cycle Given a linked list with potentially a loop determine whether the linked list from the first node contains a cycle in it For bonus points do this with constant space Parameters nodes The first node of a linked list with potentially a loop Result Whether there is a loop contained
Coding Interview Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way
Want a Structured Path to Master System Design Too? Don’t Miss This!