Facebook Pixel

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 of k.
  • 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. Returns true if successful, false if the queue is full.
  • boolean deQueue(): Removes an element from the front of the queue. Returns true if successful, false if the queue is empty.
  • boolean isEmpty(): Returns true if the queue is empty, false otherwise.
  • boolean isFull(): Returns true 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.

Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

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:

  1. Where to insert new elements - This depends on where the queue currently starts (front) and how many elements are already in it (size).
  2. 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 element
  • size: 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 decrease size
  • 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 size k to store the elements
  • front: A pointer indicating the index of the front element (initially 0)
  • size: A counter tracking the current number of elements (initially 0)
  • capacity: The maximum capacity k

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, and size tells us how many positions to skip
    • The modulo ensures we wrap around to the beginning if needed
  • 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 position 0
  • 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
  • Return the element at the calculated position

5. isEmpty and isFull Operations

These are straightforward checks:

  • isEmpty(): Return size == 0
  • isFull(): Return size == 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 states
  • front marks our starting position in the circular array
  • All positions are calculated relative to front using size, 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 Evaluator

Example 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 time
  • deQueue: Updates self.front using (self.front + 1) % self.capacity and decrements size - constant time
  • Front: Direct array access at index self.front - constant time
  • Rear: Calculates position using (self.front + self.size - 1) % self.capacity and returns the value - constant time
  • isEmpty: Simple comparison self.size == 0 - constant time
  • isFull: Simple comparison self.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 exactly k 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.

Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

How many ways can you arrange the three letters A, B and C?


Recommended Readings

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More