A deep dive into circular queues — why they exist, the wasted space problem with linear array queues, how modular arithmetic creates the circular wraparound, full implementation with all edge cases, the distinction between full and empty states, and real-world applications as ring buffers.
The Problem with Linear Array Queues
A naive queue using a simple array has a critical flaw: wasted space.
| Initial | [_, _, _, _, _, _, _, _] (capacity = 8) |
| enqueue(A,B,C,D) | [A, B, C, D, _, _, _, _] |
| dequeue(), dequeue() | [_, _, C, D, _, _, _, _] |
| enqueue(E,F,G,H) | [_, _, C, D, E, F, G, H] |
A linear array queue wastes space at the front as elements are dequeued.
Circular Wraparound Using Modular Arithmetic
The core formula:
next_index = (current_index + 1) % capacity
This keeps the index in range [0, capacity-1] and wraps:
| index 0 | next: (0+1)%5 = 1 |
| index 1 | next: (1+1)%5 = 2 |
| index 2 | next: (2+1)%5 = 3 |
| index 3 | next: (3+1)%5 = 4 |
| index 4 | next: (4+1)%5 = 0 ← wraps back to 0 |
Full vs Empty State — The Critical Challenge
Problem: With a circular queue, front == rear could mean EITHER:
- The queue is empty (front caught up to rear)
- The queue is full (rear wrapped all the way around to front)
Solution: Reserve one slot. The queue is "full" when (rear + 1) % capacity == front.
With capacity n, we allocate n+1 slots but only use n slots.
class CircularQueue:
"""
Circular queue using array with one reserved slot.
Capacity n → allocate n+1 slots.
Empty: front == rear
Full: (rear + 1) % (n+1) == front
"""
def __init__(self, capacity: int):
self._size = capacity + 1 # Allocate n+1 slots
self._data = [None] * self._size
self._front = 0
self._rear = 0
def enqueue(self, item) -> bool:
"""Add to rear. Returns True on success, False if full."""
if self.is_full():
return False
self._data[self._rear] = item
self._rear = (self._rear + 1) % self._size
return True
def dequeue(self):
"""Remove from front. Returns None if empty."""
if self.is_empty():
return None
item = self._data[self._front]
self._data[self._front] = None # Clear slot
self._front = (self._front + 1) % self._size
return item
def front_val(self):
"""Peek at front element."""
if self.is_empty():
return None
return self._data[self._front]
def rear_val(self):
"""Peek at rear element."""
if self.is_empty():
return None
return self._data[(self._rear - 1) % self._size]
def is_empty(self) -> bool:
return self._front == self._rear
def is_full(self) -> bool:
return (self._rear + 1) % self._size == self._front
def count(self) -> int:
return (self._rear - self._front) % self._size
def capacity(self) -> int:
return self._size - 1
def display(self):
if self.is_empty():
print("Circular Queue: [empty]")
return
elements = []
i = self._front
while i != self._rear:
elements.append(str(self._data[i]))
i = (i + 1) % self._size
print(f"Circular Queue: [{' → '.join(elements)}] front={self._front}, rear={self._rear}")
Complete operation trace:
cq = CircularQueue(5) # Capacity 5 (internal size 6)
cq.enqueue(1); cq.enqueue(2); cq.enqueue(3)
cq.display() # Circular Queue: [1 → 2 → 3] front=0, rear=3
cq.dequeue(); cq.dequeue()
cq.display() # Circular Queue: [3] front=2, rear=3
cq.enqueue(4); cq.enqueue(5); cq.enqueue(6); cq.enqueue(7)
cq.display() # Circular Queue: [3 → 4 → 5 → 6 → 7] front=2, rear=1 (wrapped!)
print(cq.is_full()) # True
print(cq.enqueue(8)) # False (full)
print(cq.dequeue()) # 3
cq.display() # [4 → 5 → 6 → 7] front=3, rear=1
Ring Buffer for Streaming Data
The circular queue is the basis of the ring buffer — used whenever data is produced at one rate and consumed at another:
class RingBuffer:
"""
Fixed-size ring buffer for streaming data.
Overwrites oldest data when full (unlike the error-raising version above).
Used in: audio buffers, network receive buffers, logging systems.
"""
def __init__(self, capacity: int):
self._capacity = capacity
self._buffer = [None] * capacity
self._write_pos = 0
self._read_pos = 0
self._count = 0
def write(self, data) -> None:
"""Write data. If full, overwrites oldest."""
self._buffer[self._write_pos] = data
self._write_pos = (self._write_pos + 1) % self._capacity
if self._count < self._capacity:
self._count += 1
else:
# Overwrite: advance read position too
self._read_pos = (self._read_pos + 1) % self._capacity
def read(self):
"""Read oldest data."""
if self._count == 0:
raise BufferError("Buffer empty")
data = self._buffer[self._read_pos]
self._read_pos = (self._read_pos + 1) % self._capacity
self._count -= 1
return data
def peek(self):
if self._count == 0:
raise BufferError("Buffer empty")
return self._buffer[self._read_pos]
@property
def is_full(self):
return self._count == self._capacity
@property
def is_empty(self):
return self._count == 0
@property
def available(self):
return self._count
# Audio buffer simulation:
audio_buffer = RingBuffer(8)
# Producer writes samples faster than consumer reads
for i in range(10):
audio_buffer.write(f"sample_{i}")
# Buffer only holds last 8 samples
while not audio_buffer.is_empty:
print(audio_buffer.read())
# Prints: sample_2 through sample_9 (samples 0 and 1 were overwritten)
Circular Queue in BFS
Circular queues are used in BFS implementations where the maximum queue size is bounded by the graph size:
def bfs_circular(graph, start, max_nodes):
"""BFS using a circular queue when graph size is known."""
visited = [False] * max_nodes
queue = CircularQueue(max_nodes)
queue.enqueue(start)
visited[start] = True
while not queue.is_empty():
node = queue.dequeue()
print(node)
for neighbor in graph[node]:
if not visited[neighbor]:
visited[neighbor] = True
queue.enqueue(neighbor)
Summary
- Linear array queue wastes space at the front as elements are dequeued
- Circular queue reuses freed space via modular arithmetic:
next = (curr + 1) % capacity - Full vs empty: Distinguish with reserved slot: full when
(rear+1)%cap == front - Ring buffer: Circular queue that overwrites oldest data when full
- Applications: Audio/video streaming, network buffers, OS I/O buffers, logging
*Next Lesson: Deque — Double-Ended Queue — Elements can be added and removed from both ends.*
Exam Focus
Revise definitions, diagrams, examples, and short-answer points for Circular Queue — The Ring Buffer Explained with Implementation.
Interview Use
Prepare one clear explanation, one practical example, and one common mistake for this Data Structures & Algorithms topic.
Search Terms
data-structures-algorithms, data structures & algorithms, data, structures, algorithms, queues, circular, queue