622. Design Circular Queue - Explanation

Problem Link

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 be k.
  • 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. Return true if the operation is successful.
  • boolean deQueue() Deletes an element from the circular queue. Return true if 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 3000 calls will be made to enQueue, deQueue, Front, Rear, isEmpty, and isFull.


Topics

Company Tags

Please upgrade to NeetCode Pro to view company tags.



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

  1. Initialize an empty array q and store the capacity k.
  2. For enQueue(value): If the array size equals k, return false. Otherwise, append the value and return true.
  3. For deQueue(): If the array is empty, return false. Otherwise, remove the first element (index 0) and return true.
  4. For Front(): Return the first element if the array is non-empty, otherwise return -1.
  5. For Rear(): Return the last element if the array is non-empty, otherwise return -1.
  6. For isEmpty(): Return true if the array size is 0.
  7. For isFull(): Return true if the array size equals k.
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.k

Time & Space Complexity

  • Time complexity:
    • O(1)O(1) time for initialization.
    • O(1)O(1) time for each enQueue()enQueue(), Front()Front(), Rear()Rear(), isEmpty()isEmpty() and isFull()isFull() function calls.
    • O(n)O(n) time for each deQueue()deQueue() function call.
  • Space complexity: O(n)O(n)

Where nn 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

  1. Initialize an array of size k, set front = 0, rear = -1, and size = 0.
  2. For enQueue(value): If full, return false. Otherwise, compute rear = (rear + 1) % k, store the value at that index, increment size, and return true.
  3. For deQueue(): If empty, return false. Otherwise, compute front = (front + 1) % k, decrement size, and return true.
  4. For Front(): If empty, return -1. Otherwise, return q[front].
  5. For Rear(): If empty, return -1. Otherwise, return q[rear].
  6. For isEmpty(): Return true if size equals 0.
  7. For isFull(): Return true if size equals k.
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.k

Time & Space Complexity

  • Time complexity:
    • O(n)O(n) time for initialization.
    • O(1)O(1) time for each enQueue()enQueue(), deQueue()deQueue(), Front()Front(), Rear()Rear(), isEmpty()isEmpty() and isFull()isFull() function calls.
  • Space complexity: O(n)O(n)

Where nn 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

  1. Initialize dummy nodes left and right connected to each other, and set space = k.
  2. For enQueue(value): If full, return false. Create a new node, insert it between right.prev and right, decrement space, and return true.
  3. For deQueue(): If empty, return false. Remove the node after left by updating pointers, increment space, and return true.
  4. For Front(): If empty, return -1. Otherwise, return left.next.val.
  5. For Rear(): If empty, return -1. Otherwise, return right.prev.val.
  6. For isEmpty(): Return true if left.next equals right.
  7. For isFull(): Return true if space equals 0.
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 == 0

Time & Space Complexity

  • Time complexity:
    • O(n)O(n) time for initialization.
    • O(1)O(1) time for each enQueue()enQueue(), deQueue()deQueue(), Front()Front(), Rear()Rear(), isEmpty()isEmpty() and isFull()isFull() function calls.
  • Space complexity: O(n)O(n)

Where nn 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

  1. Initialize a dummy node left, set right = left, and set space = k.
  2. For enQueue(value): If full, return false. Create a new node. If empty, set it as left.next and right. Otherwise, link it after right and update right. Decrement space and return true.
  3. For deQueue(): If empty, return false. Remove left.next by updating left.next to left.next.next. If left.next becomes null, reset right = left. Increment space and return true.
  4. For Front(): If empty, return -1. Otherwise, return left.next.val.
  5. For Rear(): If empty, return -1. Otherwise, return right.val.
  6. For isEmpty(): Return true if left.next is null.
  7. For isFull(): Return true if space equals 0.
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 == 0

Time & Space Complexity

  • Time complexity:
    • O(n)O(n) time for initialization.
    • O(1)O(1) time for each enQueue()enQueue(), deQueue()deQueue(), Front()Front(), Rear()Rear(), isEmpty()isEmpty() and isFull()isFull() function calls.
  • Space complexity: O(n)O(n)

Where nn 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 True

Confusing 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.k

Off-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