Overview
A circular queue (ring buffer) stores a queue in a fixed-size array and wraps indexes around with modulo arithmetic. When the back reaches the end of the array, it continues at index 0, reusing slots freed by dequeues.
It gives O(1) enqueue and dequeue with no memory allocation after creation, which is why it is used in audio pipelines, network drivers, logging systems, and anywhere memory must be predictable.
The belt has a fixed number of plates. The chef places new dishes in the next empty slot and customers take dishes as they pass. When the belt wraps around, the same slots are reused.
When to use it
- The maximum size is known in advance.
- You need O(1) queue operations with no allocations (real-time or embedded systems).
- You want to keep only the last k items: recent logs, moving averages.
- Round-robin problems where positions wrap around (circular arrays).
Problem patterns it solves
Recognize it when: implement a fixed-capacity queue or deque.
- 622. Design Circular Queue
- 641. Design Circular Deque
Recognize it when: moving average, recent history, fixed-length logs.
- 346. Moving Average from Data Stream
- 1352. Product of the Last K Numbers
Recognize it when: the array wraps around; use index % n or iterate 2n times.
- 503. Next Greater Element II
- 918. Maximum Sum Circular Subarray
- 213. House Robber II
- 2582. Pass the Pillow
Recognize it when: players in a circle, eliminate every kth.
- 1823. Find the Winner of the Circular Game
- 2073. Time Needed to Buy Tickets
Where it is used in real software
Sound cards and media players use ring buffers so the producer (decoder) and consumer (speaker) can run at slightly different speeds without allocating memory.
NIC drivers share ring buffers of packet descriptors with the hardware for receiving and sending packets.
Flight recorders and in-memory log buffers keep only the most recent N entries by overwriting the oldest.
The LMAX Disruptor, used in trading systems, is a lock-free ring buffer handling millions of events per second.
Key terms
- Capacity
- Fixed number of slots k.
- front / head
- Index of the oldest element.
- count
- Number of stored elements; distinguishes full from empty.
- Wrap-around
- next = (index + 1) % capacity.
- rear
- (front + count - 1) % capacity, the index of the newest element.
Operations using front and count
- 1Allocate
data = new Array(k), front = 0, count = 0.
- 2enQueue(x)
If count === k, it is full. Otherwise write data[(front + count) % k] = x and count++.
- 3deQueue()
If count === 0, it is empty. Otherwise front = (front + 1) % k and count--.
- 4Front()
data[front] when not empty.
- 5Rear()
data[(front + count - 1) % k] when not empty.
STEP 1Empty: front = 0, count = 0.
Operation trace (capacity 3)
front = 0, count = 0
| Operation | Slot written / freed | front | count | Result |
|---|---|---|---|---|
| enQueue(1) | write slot 0 | 0 | 1 | true |
| enQueue(2) | write slot 1 | 0 | 2 | true |
| enQueue(3) | write slot 2 | 0 | 3 | true |
| enQueue(4) | - | 0 | 3 | false (full) |
| deQueue() | free slot 0 | 1 | 2 | true |
| enQueue(4) | write slot (1 + 2) % 3 = 0 | 1 | 3 | true |
| Rear() | slot (1 + 3 - 1) % 3 = 0 | 1 | 3 | 4 |
NOWOperation: enQueue(1) | Slot written / freed: write slot 0 | front: 0 | count: 1 | Result: true
Tracking count avoids the classic ambiguity where front === rear could mean either full or empty.
Implementation
class MyCircularQueue { constructor(k) { this.data = new Array(k); this.capacity = k; this.front = 0; this.count = 0; } enQueue(value) { if (this.isFull()) return false; this.data[(this.front + this.count) % this.capacity] = value; this.count++; return true; } deQueue() { if (this.isEmpty()) return false; this.front = (this.front + 1) % this.capacity; this.count--; return true; } Front() { return this.isEmpty() ? -1 : this.data[this.front]; } Rear() { return this.isEmpty() ? -1 : this.data[(this.front + this.count - 1) % this.capacity]; } isEmpty() { return this.count === 0; } isFull() { return this.count === this.capacity; }} // Circular array trick: iterate 2n times with i % nfunction nextGreaterElements(nums) { const n = nums.length; const result = new Array(n).fill(-1); const stack = []; for (let i = 0; i < 2 * n; i++) { const idx = i % n; while (stack.length && nums[stack.at(-1)] < nums[idx]) { result[stack.pop()] = nums[idx]; } if (i < n) stack.push(idx); } return result;}Complexity and performance
Index arithmetic only.
Allocated once.
Predictable memory and latency.
Trade-offs
You must reject, block, or overwrite when full. Choose the policy deliberately: logs overwrite, task queues reject or block.
Modulo is slightly slower than a comparison; high-performance buffers use power-of-two capacities and index & (capacity - 1).
Variants and related techniques
When full, advance front and overwrite the oldest element.
Also insert at the front with front = (front - 1 + k) % k.
Separate read and write indexes updated atomically allow concurrent use without locks.
Common mistakes
- Ambiguous full vs empty when using only front and rear.
Fix: Track count, or leave one slot always empty.
- Negative modulo in JavaScript and Java.
Fix: (-1) % 4 is -1. Use (i - 1 + k) % k when moving backward.
- Reading slots of dequeued items.
Fix: Only read indexes within [front, front + count).
Interview questions
How do you tell full from empty in a ring buffer?
Keep a count, or keep one slot unused so full means (rear + 1) % k === front and empty means front === rear.
How do you handle circular arrays in problems like Next Greater Element II?
Iterate from 0 to 2n - 1 and use i % n, which simulates walking around the circle twice.
Practice problems
| Problem | Difficulty | What it trains |
|---|---|---|
| 622. Design Circular Queue | Medium | front + count design. |
| 641. Design Circular Deque | Medium | Backward wrap-around. |
| 346. Moving Average from Data Stream | Easy | Fixed window buffer. |
| 2582. Pass the Pillow | Easy | Circular movement math. |
| 503. Next Greater Element II | Medium | 2n iteration trick. |
| 918. Maximum Sum Circular Subarray | Medium | Kadane on wrap-around. |