Chapter 13: Queues, Deques and Circular Buffers
13.02Beginner / Intermediate5 topics16 min2 interactive animations

Implementations

Four ways to build a queue: a plain array, a circular array, a linked list, and Java's ArrayDeque and LinkedList, with the trade-offs that decide which one to use.

Learning goals

  • Explain why a plain array queue wastes space
  • Trace enqueue and dequeue on a circular array and on a linked list
  • Pick between ArrayDeque and LinkedList for a queue

Prerequisites

The queue modelLinked lists
13.02.1

Array queue

The simplest array queue keeps a head index (next to leave) and a tail index (next free slot). Enqueue writes at tail and moves it right; dequeue reads at head and moves it right. Both are O(1).

The flaw: both indices only move right. After the array has been filled and emptied once, head == tail == data.length: the queue is empty, yet offer reports full because there is no room to the right. The slots on the left are wasted. Shifting everything left on each dequeue would fix it but makes dequeue O(n).

Java
1class NaiveArrayQueue {
2 private final int[] data;
3 private int head, tail; // live values are data[head .. tail - 1]
4 NaiveArrayQueue(int capacity) { data = new int[capacity]; }
5 boolean offer(int x) {
6 if (tail == data.length) return false; // "full", even if head > 0 left room behind
7 data[tail++] = x;
8 return true;
9 }
10 Integer poll() {
11 if (head == tail) return null;
12 return data[head++];
13 }
14}
13.02.2

Circular queue

A circular queue treats the array as a ring: when an index reaches the end it wraps to 0 with index = (index + 1) % data.length. Freed slots at the front are reused, so the queue is full only when all data.length slots hold live values.

Run the animation and watch H (head) and T (tail) in the slot captions. The slots keep their old numbers after a dequeue (shown struck out), because only head and size decide what is live. The “Wrap around” preset makes the tail jump back to index 0.

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}

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.02.3

Linked queue

A linked queue stores each value in a node. head points at the front node and tail at the rear node. Enqueue links a new node behind the tail; dequeue moves head forward. Both are O(1), there is no capacity limit and nothing is ever shifted.

Two details cause most bugs: when the first node is added, both head and tail must be set, and when the last node is removed, tail must be reset to null, otherwise it keeps pointing at a node that is no longer in the queue. Both cases appear in the animation.

Java
1class LinkedQueue {
2 private static class Node { int val; Node next; Node(int v) { val = v; } }
3 private Node head, tail; private int size;
4 void enqueue(int x) {
5 Node n = new Node(x);
6 if (tail == null) head = n; else tail.next = n; // link behind the old tail
7 tail = n; size++;
8 }
9 int dequeue() {
10 if (head == null) throw new NoSuchElementException();
11 int x = head.val; head = head.next;
12 if (head == null) tail = null; // the queue became empty
13 size--; return x;
14 }
15}

Best

O(1)

Average

O(1)

Worst

O(1)

Space

O(n)

Linked queue: enqueue at the tail, dequeue at the head

Interactive · edit the input

Your input

Try

linked list · 0 nodes

null
head
tail
New nodeUnlinked node (garbage)
Setup

An empty linked queue: head and tail are both null. Enqueue touches the tail, dequeue touches the head, so both are O(1) with no shifting.

Step 1 / 20
Java
1class LinkedQueue {
2 private static class Node { int val; Node next; Node(int v) { val = v; } }
3 private Node head, tail; private int size;
4 void enqueue(int x) {
5 Node n = new Node(x);
6 if (tail == null) head = n; else tail.next = n; // link behind the old tail
7 tail = n; size++;
8 }
9 int dequeue() {
10 if (head == null) throw new NoSuchElementException();
11 int x = head.val; head = head.next;
12 if (head == null) tail = null; // the queue became empty
13 size--; return x;
14 }
15}

Live state

size

0

No condition evaluated on this step
Execution trace (dry run table)Show 20 steps
StepLineStateWhat happens
1L3size=0An empty linked queue: head and tail are both null. Enqueue touches the tail, dequeue touches the head, so both are O(1) with no shifting.
2L5op=enqueue(10) size=0new Node(10): the node exists but nothing points to it yet.
3L6op=enqueue(10)tail == null, so the queue was empty: the new node is also the head.
4L7op=enqueue(10) size=1tail = new node; size is 1.
5L5op=enqueue(20) size=1new Node(20): the node exists but nothing points to it yet.
6L6op=enqueue(20)tail.next = new node: the old tail now points at 20.
7L7op=enqueue(20) size=2tail = new node; size is 2.
8L5op=enqueue(30) size=2new Node(30): the node exists but nothing points to it yet.
9L6op=enqueue(30)tail.next = new node: the old tail now points at 30.
10L7op=enqueue(30) size=3tail = new node; size is 3.
11L11op=dequeue() returned=10x = head.val = 10; head moves to the next node. The old head is unreachable, so the garbage collector may reclaim it.
12L13op=dequeue() returned=10 size=2Returned 10; size is 2.
13L5op=enqueue(40) size=2new Node(40): the node exists but nothing points to it yet.
14L6op=enqueue(40)tail.next = new node: the old tail now points at 40.
15L7op=enqueue(40) size=3tail = new node; size is 3.
16L11op=dequeue() returned=20x = head.val = 20; head moves to the next node. The old head is unreachable, so the garbage collector may reclaim it.
17L13op=dequeue() returned=20 size=2Returned 20; size is 2.
18L11op=dequeue() returned=30x = head.val = 30; head moves to the next node. The old head is unreachable, so the garbage collector may reclaim it.
19L13op=dequeue() returned=30 size=1Returned 30; size is 1.
20L15size=1 failed=0 return=[40]Final queue (head to tail): [40].
13.02.4

Java ArrayDeque

ArrayDeque is Java's general-purpose queue (and stack). It is a resizable circular array: it wraps its indices exactly like the ring queue above and grows when full, so offer and poll are O(1) amortised. It is not bounded, not thread-safe, and rejects null.

Declare the variable with the interface, Queue<Integer> q = new ArrayDeque<>();, so that only queue operations are visible. Iteration goes from the front to the rear.

Java
1static List<Integer> drain(int[] values) {
2 Queue<Integer> q = new ArrayDeque<>();
3 for (int v : values) q.offer(v);
4 List<Integer> out = new ArrayList<>();
5 while (!q.isEmpty()) out.add(q.poll()); // front first: same order as the input
6 return out;
7}
13.02.5

Java LinkedList as queue

LinkedList also implements Queue and Deque. It is a doubly linked list with one node object per element, so each element costs extra memory and the nodes are scattered in the heap, which is unkind to the CPU cache. It permits null elements, which can make poll() ambiguous: after offer(null), poll() returns null although the queue was not empty.

Unless you need null elements or list operations such as get(i), use ArrayDeque for queues. It is usually faster and smaller.

ArrayDequeLinkedList
StorageOne circular arrayOne node per element
offer / pollO(1) amortisedO(1)
null elementsNot allowed (NullPointerException)Allowed
Indexed accessNoneget(i) is O(n)
Typical choice for queuesYesOnly if null or list methods are needed

Practice ladder

Now try it yourself

Easy

  • Trace a circular queue of capacity 3 through offer 1, 2, 3, poll, offer 4
  • Why does a linked queue need a tail reference?

Medium

  • Implement a queue with a linked list and only a head reference. What does enqueue cost?
  • Why does the naive array queue report full when it is empty?

Advanced

  • Implement a queue with a resizable circular array: what must happen to head and tail when it grows?

Key takeaways

  • A plain array queue wastes the space behind head; a circular array reuses it.
  • A linked queue needs head and tail, and must reset tail when it becomes empty.
  • Use ArrayDeque for queues unless you need null elements or indexed access.