Circular Queue: Fixed-Size FIFO with a Ring Buffer
A circular queue (or ring buffer) is a fixed-capacity queue that reuses its array slots: when the rear reaches the end of the array, it wraps back to index 0. This avoids the wasted space and shifting of a plain array queue while keeping O(1) enqueue and dequeue. Ring buffers are the backbone of streaming, audio, and networking buffers where memory is fixed.
The problem it solves
A simple array queue with a moving head pointer keeps abandoning the front slots — after many dequeues, the used region marches off the end even though the front is empty. A circular queue fixes this by treating the array as a ring: index after the last wraps around to the first using modulo arithmetic.
capacity = 5, after some enqueue/dequeue:
index: 0 1 2 3 4
[ _ ][ C ][ D ][ E ][ _ ]
^head ^tail(next write) wraps to 0
How wrap-around works
Keep a head (front index), a count (number of items), and a fixed array of size capacity. The rear write position is computed, not stored: (head + count) % capacity. After dequeuing, advance head = (head + 1) % capacity. The modulo is what makes the indices loop.
- isFull:
count == capacity - isEmpty:
count == 0 - enqueue: write at
(head + count) % capacity, thencount++ - dequeue: read at
head, thenhead = (head + 1) % capacity,count--
#include <vector>
#include <stdexcept>
class CircularQueue {
std::vector<int> buf;
int head = 0, count = 0, cap;
public:
explicit CircularQueue(int capacity) : buf(capacity), cap(capacity) {}
bool isFull() const { return count == cap; }
bool isEmpty() const { return count == 0; }
void enqueue(int x) {
if (isFull()) throw std::overflow_error("queue full");
buf[(head + count) % cap] = x;
count++;
}
int dequeue() {
if (isEmpty()) throw std::underflow_error("queue empty");
int x = buf[head];
head = (head + 1) % cap;
count--;
return x;
}
int front() const { return buf[head]; }
};
class CircularQueue {
private int[] buf;
private int head = 0, count = 0, cap;
public CircularQueue(int capacity) { buf = new int[capacity]; cap = capacity; }
public boolean isFull() { return count == cap; }
public boolean isEmpty() { return count == 0; }
public void enqueue(int x) {
if (isFull()) throw new IllegalStateException("queue full");
buf[(head + count) % cap] = x;
count++;
}
public int dequeue() {
if (isEmpty()) throw new IllegalStateException("queue empty");
int x = buf[head];
head = (head + 1) % cap;
count--;
return x;
}
public int front() { return buf[head]; }
}
class CircularQueue {
constructor(capacity) {
this.buf = new Array(capacity);
this.head = 0; this.count = 0; this.cap = capacity;
}
isFull() { return this.count === this.cap; }
isEmpty() { return this.count === 0; }
enqueue(x) {
if (this.isFull()) throw new Error("queue full");
this.buf[(this.head + this.count) % this.cap] = x;
this.count++;
}
dequeue() {
if (this.isEmpty()) throw new Error("queue empty");
const x = this.buf[this.head];
this.head = (this.head + 1) % this.cap;
this.count--;
return x;
}
front() { return this.buf[this.head]; }
}
class CircularQueue:
def __init__(self, capacity):
self.buf = [None] * capacity
self.head = 0
self.count = 0
self.cap = capacity
def is_full(self):
return self.count == self.cap
def is_empty(self):
return self.count == 0
def enqueue(self, x):
if self.is_full():
raise OverflowError("queue full")
self.buf[(self.head + self.count) % self.cap] = x
self.count += 1
def dequeue(self):
if self.is_empty():
raise IndexError("queue empty")
x = self.buf[self.head]
self.head = (self.head + 1) % self.cap
self.count -= 1
return x
def front(self):
return self.buf[self.head]
Complexity: every operation is O(1) time; space is O(capacity), fixed up front.
Why store
countinstead of atailpointer? With onlyheadandtail, a full ring and an empty ring look identical (head == tail). Trackingcount(or leaving one slot empty) removes that ambiguity cleanly.
Common pitfalls
- Forgetting the modulo.
head + countcan exceedcapacity; you must wrap with% capon both the write index and when advancinghead. - Off-by-one on full/empty. Decide your disambiguation strategy (count vs. sacrificial slot) and apply it everywhere.
- Overwriting live data. Always check
isFull()before enqueue, or you’ll clobber the front element.
When to use a circular queue
Use it whenever capacity is bounded and known: audio/video buffers, keyboard input buffers, network packet queues, sliding-window streaming, and producer/consumer pipelines. If you need unbounded growth, use the dynamic queue implementation; if you need add/remove at both ends, use a deque.
Next: the deque generalizes this to both ends, or test yourself on the exercises.