A circular queue solves a subtle but annoying problem with a naive array-based queue: wasted space. In a normal array queue, once you dequeue a few elements from the front, those slots sit empty forever — the queue reports "full" long before the underlying array actually is, because new elements only ever get added at the growing end.
A circular queue fixes this by treating the array as if its end wraps back around to its beginning, like a clock face. Once the "rear" pointer reaches the last index, the next enqueue wraps around to index 0 — as long as that slot has already been freed by a dequeue.
The Problem With a Naive Array Queue
Picture a fixed-size array of length 5 used as a queue. You enqueue 5 items, filling it completely, then dequeue 3 from the front. Three slots are now free — but a naive implementation that only tracks a single growing "rear" index can't reuse them, because rear is already sitting at index 5 (out of bounds). The queue looks full even though 3 out of 5 slots are empty.
How Wrapping Around Fixes It
A circular queue tracks two indices, front and rear, and advances each one using the modulo operator so it wraps back to 0 after hitting the array's length:
class CircularQueue:
def __init__(self, capacity):
self.capacity = capacity
self.queue = [None] * capacity
self.front = 0
self.rear = 0
self.size = 0
def enqueue(self, value):
if self.size == self.capacity:
raise OverflowError("Queue is full")
self.queue[self.rear] = value
self.rear = (self.rear + 1) % self.capacity # wrap around
self.size += 1
def dequeue(self):
if self.size == 0:
raise IndexError("Queue is empty")
value = self.queue[self.front]
self.front = (self.front + 1) % self.capacity # wrap around
self.size -= 1
return value
cq = CircularQueue(5)
for i in [10, 20, 30, 40, 50]:
cq.enqueue(i)
print(cq.dequeue())
print(cq.dequeue())
print(cq.dequeue())
# These slots are now free (indices 0, 1, 2) and can be reused:
cq.enqueue(60)
cq.enqueue(70)
print("rear index:", cq.rear)
Output:
10
20
30
rear index: 2
Notice rear wrapped from 5 back to 0, then advanced to 1, then to 2 — reusing the slots freed by the three dequeues, instead of reporting the queue as full.
Full vs Empty: The Tricky Edge Case
Because front and rear can both land on the same index when the queue is either completely empty or completely full, implementations typically track a separate size counter (as above) to tell the two states apart — relying purely on front == rear is ambiguous.
Circular queues are a core building block for fixed-size buffering problems, such as streaming data through a bounded buffer. If you haven't yet, it's worth first getting comfortable with the regular FIFO queue this structure is built on top of — the enqueue/dequeue logic is identical, only the index math changes.
