Chapter 13: Queues, Deques and Circular Buffers
13.03Beginner / Intermediate5 topics15 min3 interactive animations

Circular buffers

Ring buffers in depth: wraparound with modulo, head and tail indices, telling full from empty, fixed capacity policies and the trade-offs against a growing queue.

Learning goals

  • Compute wrapped indices correctly, including the negative case
  • Describe three ways to distinguish a full ring from an empty one
  • Choose between rejecting and overwriting when a ring is full

Prerequisites

Queue implementations

A ring buffer is a circular conveyor belt with a fixed number of trays: the oldest tray is reused as soon as it has been emptied.

13.03.1

Wraparound modulo

Wrapping means index = (index + 1) % n: the index runs 0, 1, …, n-1, 0, 1, …. The remainder operator does the wrap in one expression, so no separate if (index == n) index = 0 is needed.

Two Java details matter. First, % keeps the sign of the dividend, so -1 % 5 is -1, not 4; when an index can go backwards (for example addFirst on a deque) use Math.floorMod(i - 1, n). Second, when n is a power of two, i & (n - 1) is the same as the floored modulo, even for negative i, and it is cheaper than a division.

Expressionn = 5n = 8
(4 + 1) % n05
-1 % n-1-1
Math.floorMod(-1, n)47
(-1) & (n - 1)n/a (n is not a power of 2)7

Circular queue: head, tail and a size counter

Interactive · edit the input

Your input

Try

data[4] · head 0 · tail 0 · size 0

0 HT
1
2
3
Setup

An array of 4. head is where the next value leaves, tail is where the next value arrives, size counts the values.

Step 1 / 29
Java
1class RingQueue {
2 private final int[] data; private int head, tail, size;
3 RingQueue(int capacity) { data = new int[capacity]; }
4 boolean offer(int x) {
5 if (size == data.length) return false; // full
6 data[tail] = x;
7 tail = (tail + 1) % data.length; // wrap around
8 size++; return true;
9 }
10 Integer poll() {
11 if (size == 0) return null; // empty
12 int x = data[head];
13 head = (head + 1) % data.length; // wrap around
14 size--; return x;
15 }
16}

Live state

head

0

tail

0

size

0

No condition evaluated on this step
Execution trace (dry run table)Show 29 steps
StepLineStateWhat happens
1L2head=0 tail=0 size=0An array of 4. head is where the next value leaves, tail is where the next value arrives, size counts the values.
2L5head=0 tail=0 size=0 op=offer(1)offer(1): there is room.
3L6head=0 tail=0 size=1 op=offer(1)data[0] = 1.
4L7head=0 tail=1 size=1 op=offer(1)tail = (0 + 1) % 4 = 1.
5L5head=0 tail=1 size=1 op=offer(2)offer(2): there is room.
6L6head=0 tail=1 size=2 op=offer(2)data[1] = 2.
7L7head=0 tail=2 size=2 op=offer(2)tail = (1 + 1) % 4 = 2.
8L5head=0 tail=2 size=2 op=offer(3)offer(3): there is room.
9L6head=0 tail=2 size=3 op=offer(3)data[2] = 3.
10L7head=0 tail=3 size=3 op=offer(3)tail = (2 + 1) % 4 = 3.
11L11head=0 tail=3 size=3 op=poll()poll(): there is a value at head.
12L12head=0 tail=3 size=3 op=poll() returned=1x = data[0] = 1. The slot keeps its old number (shown struck out when free): only head and size decide what is live.
13L13head=1 tail=3 size=2 op=poll() returned=1head = (0 + 1) % 4 = 1. Returned 1.
14L11head=1 tail=3 size=2 op=poll()poll(): there is a value at head.
15L12head=1 tail=3 size=2 op=poll() returned=2x = data[1] = 2. The slot keeps its old number (shown struck out when free): only head and size decide what is live.
16L13head=2 tail=3 size=1 op=poll() returned=2head = (1 + 1) % 4 = 2. Returned 2.
17L5head=2 tail=3 size=1 op=offer(4)offer(4): there is room.
18L6head=2 tail=3 size=2 op=offer(4)data[3] = 4.
19L7head=2 tail=0 size=2 op=offer(4)tail = (3 + 1) % 4 = 0: past the last index, so it wraps to the start of the array.
20L5head=2 tail=0 size=2 op=offer(5)offer(5): there is room.
21L6head=2 tail=0 size=3 op=offer(5)data[0] = 5.
22L7head=2 tail=1 size=3 op=offer(5)tail = (0 + 1) % 4 = 1.
23L5head=2 tail=1 size=3 op=offer(6)offer(6): there is room.
24L6head=2 tail=1 size=4 op=offer(6)data[1] = 6.
25L7head=2 tail=2 size=4 op=offer(6)tail = (1 + 1) % 4 = 2.
26L11head=2 tail=2 size=4 op=poll()poll(): there is a value at head.
27L12head=2 tail=2 size=4 op=poll() returned=3x = data[2] = 3. The slot keeps its old number (shown struck out when free): only head and size decide what is live.
28L13head=3 tail=2 size=3 op=poll() returned=3head = (2 + 1) % 4 = 3. Returned 3.
29L16head=3 tail=2 size=3 return=[4, 5, 6]Queue contents from head: [4, 5, 6]; 0 operations rejected.
13.03.2

Head/tail indices

head is the slot of the oldest value and tail is the slot where the next value will be written (one past the newest). Enqueue writes at tail then advances it; dequeue reads at head then advances it. Neither moves any data.

The live values are data[head], data[head + 1], … taken modulo n, for size values. The number of live values is not tail - head once the indices have wrapped, which is why a ring needs either a separate size, a spare slot, or a different index scheme.

Java
1static int[] snapshot(int[] data, int head, int size) {
2 int[] live = new int[size];
3 for (int k = 0; k < size; k++) live[k] = data[(head + k) % data.length]; // oldest first
4 return live;
5}

Circular queue: head, tail and a size counter

Interactive · edit the input

Your input

Try

data[4] · head 0 · tail 0 · size 0

0 HT
1
2
3
Setup

An array of 4. head is where the next value leaves, tail is where the next value arrives, size counts the values.

Step 1 / 29
Java
1class RingQueue {
2 private final int[] data; private int head, tail, size;
3 RingQueue(int capacity) { data = new int[capacity]; }
4 boolean offer(int x) {
5 if (size == data.length) return false; // full
6 data[tail] = x;
7 tail = (tail + 1) % data.length; // wrap around
8 size++; return true;
9 }
10 Integer poll() {
11 if (size == 0) return null; // empty
12 int x = data[head];
13 head = (head + 1) % data.length; // wrap around
14 size--; return x;
15 }
16}

Live state

head

0

tail

0

size

0

No condition evaluated on this step
Execution trace (dry run table)Show 29 steps
StepLineStateWhat happens
1L2head=0 tail=0 size=0An array of 4. head is where the next value leaves, tail is where the next value arrives, size counts the values.
2L5head=0 tail=0 size=0 op=offer(1)offer(1): there is room.
3L6head=0 tail=0 size=1 op=offer(1)data[0] = 1.
4L7head=0 tail=1 size=1 op=offer(1)tail = (0 + 1) % 4 = 1.
5L5head=0 tail=1 size=1 op=offer(2)offer(2): there is room.
6L6head=0 tail=1 size=2 op=offer(2)data[1] = 2.
7L7head=0 tail=2 size=2 op=offer(2)tail = (1 + 1) % 4 = 2.
8L5head=0 tail=2 size=2 op=offer(3)offer(3): there is room.
9L6head=0 tail=2 size=3 op=offer(3)data[2] = 3.
10L7head=0 tail=3 size=3 op=offer(3)tail = (2 + 1) % 4 = 3.
11L11head=0 tail=3 size=3 op=poll()poll(): there is a value at head.
12L12head=0 tail=3 size=3 op=poll() returned=1x = data[0] = 1. The slot keeps its old number (shown struck out when free): only head and size decide what is live.
13L13head=1 tail=3 size=2 op=poll() returned=1head = (0 + 1) % 4 = 1. Returned 1.
14L11head=1 tail=3 size=2 op=poll()poll(): there is a value at head.
15L12head=1 tail=3 size=2 op=poll() returned=2x = data[1] = 2. The slot keeps its old number (shown struck out when free): only head and size decide what is live.
16L13head=2 tail=3 size=1 op=poll() returned=2head = (1 + 1) % 4 = 2. Returned 2.
17L5head=2 tail=3 size=1 op=offer(4)offer(4): there is room.
18L6head=2 tail=3 size=2 op=offer(4)data[3] = 4.
19L7head=2 tail=0 size=2 op=offer(4)tail = (3 + 1) % 4 = 0: past the last index, so it wraps to the start of the array.
20L5head=2 tail=0 size=2 op=offer(5)offer(5): there is room.
21L6head=2 tail=0 size=3 op=offer(5)data[0] = 5.
22L7head=2 tail=1 size=3 op=offer(5)tail = (0 + 1) % 4 = 1.
23L5head=2 tail=1 size=3 op=offer(6)offer(6): there is room.
24L6head=2 tail=1 size=4 op=offer(6)data[1] = 6.
25L7head=2 tail=2 size=4 op=offer(6)tail = (1 + 1) % 4 = 2.
26L11head=2 tail=2 size=4 op=poll()poll(): there is a value at head.
27L12head=2 tail=2 size=4 op=poll() returned=3x = data[2] = 3. The slot keeps its old number (shown struck out when free): only head and size decide what is live.
28L13head=3 tail=2 size=3 op=poll() returned=3head = (2 + 1) % 4 = 3. Returned 3.
29L16head=3 tail=2 size=3 return=[4, 5, 6]Queue contents from head: [4, 5, 6]; 0 operations rejected.
13.03.3

Full vs empty distinction

When the ring is empty, head == tail. When it is completely full, the tail has wrapped all the way around and head == tail again. The indices alone cannot tell the two states apart, so every ring design picks one of three fixes.

The animation uses the second fix: allocate one slot more than the usable capacity and never fill it, so head == tail can only mean empty and (tail + 1) % n == head means full. Compare it with the size-counter version in the previous topic.

StrategyEmpty testFull testCost
Size countersize == 0size == nOne extra int to maintain
One slot always emptyhead == tail(tail + 1) % n == headCapacity is n - 1
Free-running counters (never wrapped)head == tailtail - head == nCounters can overflow after 2^31 operations

Circular queue: one slot kept empty to tell full from empty

Interactive · edit the input

Your input

Try

data[5] · head 0 · tail 0 · size 0

0 HT
1
2
3
4
Setup

An array of 5: 4 usable slots plus one that is never filled. head == tail then can only mean empty.

Step 1 / 29
Java
1class RingQueue {
2 private final int[] data; private int head, tail; // one slot always stays empty
3 RingQueue(int capacity) { data = new int[capacity + 1]; }
4 boolean offer(int x) {
5 if ((tail + 1) % data.length == head) return false; // full
6 data[tail] = x;
7 tail = (tail + 1) % data.length; // wrap around
8 return true;
9 }
10 Integer poll() {
11 if (head == tail) return null; // empty
12 int x = data[head];
13 head = (head + 1) % data.length; // wrap around
14 return x;
15 }
16}

Live state

head

0

tail

0

size

0

No condition evaluated on this step
Execution trace (dry run table)Show 29 steps
StepLineStateWhat happens
1L2head=0 tail=0 size=0An array of 5: 4 usable slots plus one that is never filled. head == tail then can only mean empty.
2L5head=0 tail=0 size=0 op=offer(1)offer(1): there is room.
3L6head=0 tail=0 size=0 op=offer(1)data[0] = 1.
4L7head=0 tail=1 size=1 op=offer(1)tail = (0 + 1) % 5 = 1.
5L5head=0 tail=1 size=1 op=offer(2)offer(2): there is room.
6L6head=0 tail=1 size=1 op=offer(2)data[1] = 2.
7L7head=0 tail=2 size=2 op=offer(2)tail = (1 + 1) % 5 = 2.
8L5head=0 tail=2 size=2 op=offer(3)offer(3): there is room.
9L6head=0 tail=2 size=2 op=offer(3)data[2] = 3.
10L7head=0 tail=3 size=3 op=offer(3)tail = (2 + 1) % 5 = 3.
11L11head=0 tail=3 size=3 op=poll()poll(): there is a value at head.
12L12head=0 tail=3 size=3 op=poll() returned=1x = data[0] = 1. The slot keeps its old number (shown struck out when free): only head and size decide what is live.
13L13head=1 tail=3 size=2 op=poll() returned=1head = (0 + 1) % 5 = 1. Returned 1.
14L11head=1 tail=3 size=2 op=poll()poll(): there is a value at head.
15L12head=1 tail=3 size=2 op=poll() returned=2x = data[1] = 2. The slot keeps its old number (shown struck out when free): only head and size decide what is live.
16L13head=2 tail=3 size=1 op=poll() returned=2head = (1 + 1) % 5 = 2. Returned 2.
17L5head=2 tail=3 size=1 op=offer(4)offer(4): there is room.
18L6head=2 tail=3 size=1 op=offer(4)data[3] = 4.
19L7head=2 tail=4 size=2 op=offer(4)tail = (3 + 1) % 5 = 4.
20L5head=2 tail=4 size=2 op=offer(5)offer(5): there is room.
21L6head=2 tail=4 size=2 op=offer(5)data[4] = 5.
22L7head=2 tail=0 size=3 op=offer(5)tail = (4 + 1) % 5 = 0: past the last index, so it wraps to the start of the array.
23L5head=2 tail=0 size=3 op=offer(6)offer(6): there is room.
24L6head=2 tail=0 size=3 op=offer(6)data[0] = 6.
25L7head=2 tail=1 size=4 op=offer(6)tail = (0 + 1) % 5 = 1.
26L11head=2 tail=1 size=4 op=poll()poll(): there is a value at head.
27L12head=2 tail=1 size=4 op=poll() returned=3x = data[2] = 3. The slot keeps its old number (shown struck out when free): only head and size decide what is live.
28L13head=3 tail=1 size=3 op=poll() returned=3head = (2 + 1) % 5 = 3. Returned 3.
29L16head=3 tail=1 size=3 return=[4, 5, 6]Queue contents from head: [4, 5, 6]; 0 operations rejected.
13.03.4

Fixed capacity

A ring buffer has a fixed capacity chosen up front. When it is full there are two policies. **Reject**: offer returns false and the producer must wait or drop the new value, which is right for work queues. **Overwrite**: the new value replaces the oldest one, which is right when only the latest data matters, such as the last 100 log lines or sensor samples.

The overwriting variant needs no full test at all: write at tail, and if the buffer was full, advance head as well so that the oldest value is dropped.

Java
1class LatestN {
2 private final int[] data; private int head, size;
3 LatestN(int capacity) { data = new int[capacity]; }
4 void add(int x) {
5 int tail = (head + size) % data.length;
6 data[tail] = x;
7 if (size == data.length) head = (head + 1) % data.length; // full: the oldest value is overwritten
8 else size++;
9 }
10 int[] latest() {
11 int[] out = new int[size];
12 for (int k = 0; k < size; k++) out[k] = data[(head + k) % data.length];
13 return out;
14 }
15}
13.03.5

Ring buffer trade-offs

A ring buffer trades flexibility for predictability. Memory is allocated once, no operation ever allocates or copies, and every operation is O(1) in the worst case, which is why rings are used in audio pipelines, network drivers and logging.

The cost is the fixed capacity: choosing it too small drops or rejects data, too large wastes memory. A growing queue such as ArrayDeque never rejects, but a resize copies every element, so one operation can be O(n).

Ring buffer (fixed)Growing circular array (ArrayDeque)
CapacityFixed at creationGrows when full
Worst-case enqueueO(1)O(n) at a resize; O(1) amortised
Allocation while runningNoneOccasional
When fullReject or overwriteNever full
Good forStreams, buffers between threads, latest-N dataGeneral-purpose queues

Practice ladder

Now try it yourself

Easy

  • Compute (7 + 1) % 8 and Math.floorMod(-3, 8)
  • Which indices describe an empty ring?

Medium

  • Design a ring buffer that overwrites the oldest value; how do head and size change on a full add?
  • Why can a ring with one empty slot hold only n - 1 values?

Advanced

  • Implement a ring buffer using two free-running int counters and explain when they overflow

Key takeaways

  • Wrap with (i + 1) % n; use Math.floorMod when an index can go negative.
  • head == tail is ambiguous: use a size counter, a spare slot or free-running counters.
  • A ring buffer never allocates or copies, at the price of a fixed capacity.