Circular Queue
A fixed-capacity queue over an array whose head and tail indices wrap around using modular arithmetic.
Definition
A circular queue stores elements in a fixed-size array and tracks head (index of the front) and a count (or tail). When an index reaches the end of the array it wraps to 0 via (i + 1) % capacity, so the slots freed by dequeues at the front are reused by enqueues at the back.
This removes the two flaws of a plain array queue: no O(n) shifting on dequeue and no unbounded growth of a "dead" prefix. It is the standard implementation of bounded buffers in operating systems, audio pipelines, and network drivers.
Intuition
A mental model before the formal terms.
Imagine seats around a round table numbered 0..k−1. Guests sit at the next free seat clockwise and leave from the oldest occupied seat. After seat k−1 comes seat 0 again — the table never "runs out of edge" the way a straight bench does.
A clock face: after 11 comes 0, not 12. Indices behave the same way under % capacity.
How it works
- Allocate
bufof sizek; sethead = 0,size = 0. The back index is derived:tail = (head + size) % k. enqueue(x): ifsize == kthe queue is full — reject. Elsebuf[(head + size) % k] = x,size++.dequeue(): ifsize == 0— reject. Else readbuf[head],head = (head + 1) % k,size--.front()isbuf[head];rear()isbuf[(head + size - 1) % k].- Tracking
sizeexplicitly avoids the classic ambiguity wherehead == tailcould mean either empty or full.
Why it works
Elements are always stored in size consecutive slots (mod k) starting at head, so FIFO order is preserved: the oldest element is at head, the newest at head + size - 1.
Modular arithmetic makes every index computation O(1) and guarantees indices stay in [0, k).
Operations
| Operation | Description | Cost |
|---|---|---|
| enqueue(x) | Insert at the back if not full. | O(1) |
| dequeue() | Remove from the front if not empty. | O(1) |
| front() | Peek the oldest element. | O(1) |
| rear() | Peek the newest element. | O(1) |
| isFull() / isEmpty() | Capacity checks. | O(1) |
Recognition
How to tell a problem wants this.
- A bounded buffer with a known maximum size is required (embedded systems, producer/consumer).
- The problem literally says "design a circular queue" or "ring buffer".
- You want a queue with array cache locality and no allocation per operation.
- A sliding window of the last k events where old ones are overwritten.
Interactive demo
Play, step, change the input. ← → and space work too.
Showing the closely related Queue visualization.
1enqueue(x): items.append(x) # at the back2dequeue(): return items.pop_front() # from the front3peek(): return items[front]Pseudocode
1class CircularQueue(k):2 buf = array of size k; head = 0; size = 03 enqueue(x): if size == k: return false; buf[(head + size) % k] = x; size++; return true4 dequeue(): if size == 0: return false; head = (head + 1) % k; size--; return true5 front(): return buf[head]6 rear(): return buf[(head + size - 1) % k]Implementation
1from typing import Generic, Optional, TypeVar2 3T = TypeVar("T")4 5 6class CircularQueue(Generic[T]):71 · Fixed buffer, head index, and count8 def __init__(self, k: int) -> None:9 self._buf: list[Optional[T]] = [None] * k10 self._cap = k11 self._head = 0 # index of the front12 self._n = 0 # number of stored elements13 # back slot is derived: (head + n) % cap14 152 · Enqueue into the derived back slot16 def enqueue(self, x: T) -> bool:17 if self.is_full():18 return False19 self._buf[(self._head + self._n) % self._cap] = x20 self._n += 121 return True22 233 · Dequeue by advancing head modulo capacity24 def dequeue(self) -> bool:25 if self.is_empty():26 return False27 self._buf[self._head] = None # release the reference28 self._head = (self._head + 1) % self._cap29 self._n -= 130 return True31 324 · Peek front and rear33 def front(self) -> Optional[T]:34 return None if self.is_empty() else self._buf[self._head]35 36 def rear(self) -> Optional[T]:37 if self.is_empty():38 return None39 return self._buf[(self._head + self._n - 1) % self._cap]40 415 · Emptiness and fullness from the count42 def is_empty(self) -> bool:43 return self._n == 044 45 def is_full(self) -> bool:46 return self._n == self._cap- The buffer is
[None] * k;Optional[T]in the type says empty slots areNone. enqueuewrites to(head + n) % capand increments the count; a full buffer returnsFalse.dequeueclears the slot (dropping the reference), advancesheadwith%, and decrements the count.front/rearreturnNonewhen empty — mirrored from the buffer's own empty-slot convention.is_empty/is_fullread only the count.
Python % always returns a non-negative result for a positive modulus, so even head - 1 style arithmetic would be safe here — unlike C++/JS.
- Python's
%is floored:-1 % 5 == 4. The wraparound bug class that plagues C++/JS/Java rings simply does not exist here. collections.deque(maxlen=k)is a bounded ring with *overwrite* semantics: appending to a full deque silently evicts the oldest — different from the reject semantics implemented here.- For numeric rings,
array.arrayor NumPy arrays avoid per-element object overhead.
- Reaching for
deque(maxlen=k)when the task requires rejecting on full — it evicts instead. - Storing
Noneas a payload value and then misreadingfront() is Noneas "empty". - Recomputing
tailin several places instead of deriving it once fromhead + n.
- Modulo of negatives: Python
%is always non-negative; C++/JS keep the dividend's sign — decrementing indices needs+ capbefore%there. - Empty-peek reporting: C++ uses
std::optional, TS types it asT | undefined, Python returnsNone, JS returnsundefined— all four make emptiness a value, not an exception. - Built-ins: Python
deque(maxlen=k)gives a bounded ring with overwrite-oldest semantics; C++/JS/TS have no bounded-queue primitive in the standard library. - The C++ vector default-constructs all k elements at once; JS/TS/Python allocate k references and fill lazily.
Complexity
| Operation | Average | Worst | Note |
|---|---|---|---|
| Access | O(1) | O(1) | Front/rear; arbitrary index via (head+i)%k. |
| Search | O(n) | O(n) | |
| Insert | O(1) | O(1) | Fails when full. |
| Delete | O(1) | O(1) | Front only. |
| Update | — | — | |
| Enqueue | O(1) | O(1) | |
| Dequeue | O(1) | O(1) | |
| Front / Rear | O(1) | O(1) | |
| Space | O(k) | k is the fixed capacity, allocated up front. | |
Advantages & disadvantages
- All operations
O(1)with zero allocations after construction. - Contiguous memory, cache-friendly, predictable footprint — ideal for real-time and embedded code.
- Naturally provides back-pressure (full buffer) or overwrite semantics (oldest evicted).
- Fixed capacity: must be sized up front or resized with an
O(n)copy that un-wraps the elements. - Index bookkeeping is error-prone (empty vs. full ambiguity, off-by-one in wraparound).
- No random access or search better than
O(n).
Use cases
- Keyboard, serial, and audio I/O buffers in operating systems and drivers.
- Bounded producer/consumer channels between threads.
- Keeping the most recent
ksamples for moving averages or log rings. - CPU round-robin scheduling queues.
- The maximum number of buffered items is known and bounded.
- You need a queue without per-operation allocation (real-time, embedded, lock-free rings).
- You want to keep only the most recent
kitems and overwrite the oldest.
Alternatives
Common mistakes
- Using
head == tailto mean both "empty" and "full" — tracksizeor leave one slot unused. - Writing
tail = tail + 1without% capacity, running off the end of the array. - Computing
rearastail - 1without addingcapacitybefore the modulo, producing a negative index in languages where%can be negative (Java, C++, Go, JS). - Resizing by copying
bufverbatim instead of un-wrapping fromhead, which scrambles element order.
Interview patterns
- Design Circular Queue (LeetCode 622) — implement with
head+size. - Moving average from a data stream: ring of the last
kvalues plus a running sum. - Design Hit Counter: ring of timestamps indexed by
t % 300.
- Course ScheduleIntermediate
- Network Delay TimeAdvanced
- Number of IslandsIntermediate
Interview problems
No linked problems yet.