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
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).
1class NaiveArrayQueue {2private final int[] data;3private int head, tail; // live values are data[head .. tail - 1]4NaiveArrayQueue(int capacity) { data = new int[capacity]; }5boolean offer(int x) {6if (tail == data.length) return false; // "full", even if head > 0 left room behind7data[tail++] = x;8return true;9}10Integer poll() {11if (head == tail) return null;12return data[head++];13}14}
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.
1class RingQueue {2private final int[] data; private int head, tail, size;3RingQueue(int capacity) { data = new int[capacity]; }4boolean offer(int x) {5if (size == data.length) return false; // full6data[tail] = x;7tail = (tail + 1) % data.length; // wrap around8size++; return true;9}10Integer poll() {11if (size == 0) return null; // empty12int x = data[head];13head = (head + 1) % data.length; // wrap around14size--; return x;15}16}
Circular queue: head, tail and a size counter
Interactive · edit the inputdata[4] · head 0 · tail 0 · size 0
An array of 4. head is where the next value leaves, tail is where the next value arrives, size counts the values.
1class RingQueue {2private final int[] data; private int head, tail, size;3RingQueue(int capacity) { data = new int[capacity]; }4boolean offer(int x) {5if (size == data.length) return false; // full6data[tail] = x;7tail = (tail + 1) % data.length; // wrap around8size++; return true;9}10Integer poll() {11if (size == 0) return null; // empty12int x = data[head];13head = (head + 1) % data.length; // wrap around14size--; return x;15}16}
Live state
head
0
tail
0
size
0
Execution trace (dry run table)Show 29 stepsHide
| Step | Line | State | What happens |
|---|---|---|---|
| 1 | L2 | head=0 tail=0 size=0 | An array of 4. head is where the next value leaves, tail is where the next value arrives, size counts the values. |
| 2 | L5 | head=0 tail=0 size=0 op=offer(1) | offer(1): there is room. |
| 3 | L6 | head=0 tail=0 size=1 op=offer(1) | data[0] = 1. |
| 4 | L7 | head=0 tail=1 size=1 op=offer(1) | tail = (0 + 1) % 4 = 1. |
| 5 | L5 | head=0 tail=1 size=1 op=offer(2) | offer(2): there is room. |
| 6 | L6 | head=0 tail=1 size=2 op=offer(2) | data[1] = 2. |
| 7 | L7 | head=0 tail=2 size=2 op=offer(2) | tail = (1 + 1) % 4 = 2. |
| 8 | L5 | head=0 tail=2 size=2 op=offer(3) | offer(3): there is room. |
| 9 | L6 | head=0 tail=2 size=3 op=offer(3) | data[2] = 3. |
| 10 | L7 | head=0 tail=3 size=3 op=offer(3) | tail = (2 + 1) % 4 = 3. |
| 11 | L11 | head=0 tail=3 size=3 op=poll() | poll(): there is a value at head. |
| 12 | L12 | head=0 tail=3 size=3 op=poll() returned=1 | x = data[0] = 1. The slot keeps its old number (shown struck out when free): only head and size decide what is live. |
| 13 | L13 | head=1 tail=3 size=2 op=poll() returned=1 | head = (0 + 1) % 4 = 1. Returned 1. |
| 14 | L11 | head=1 tail=3 size=2 op=poll() | poll(): there is a value at head. |
| 15 | L12 | head=1 tail=3 size=2 op=poll() returned=2 | x = data[1] = 2. The slot keeps its old number (shown struck out when free): only head and size decide what is live. |
| 16 | L13 | head=2 tail=3 size=1 op=poll() returned=2 | head = (1 + 1) % 4 = 2. Returned 2. |
| 17 | L5 | head=2 tail=3 size=1 op=offer(4) | offer(4): there is room. |
| 18 | L6 | head=2 tail=3 size=2 op=offer(4) | data[3] = 4. |
| 19 | L7 | head=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. |
| 20 | L5 | head=2 tail=0 size=2 op=offer(5) | offer(5): there is room. |
| 21 | L6 | head=2 tail=0 size=3 op=offer(5) | data[0] = 5. |
| 22 | L7 | head=2 tail=1 size=3 op=offer(5) | tail = (0 + 1) % 4 = 1. |
| 23 | L5 | head=2 tail=1 size=3 op=offer(6) | offer(6): there is room. |
| 24 | L6 | head=2 tail=1 size=4 op=offer(6) | data[1] = 6. |
| 25 | L7 | head=2 tail=2 size=4 op=offer(6) | tail = (1 + 1) % 4 = 2. |
| 26 | L11 | head=2 tail=2 size=4 op=poll() | poll(): there is a value at head. |
| 27 | L12 | head=2 tail=2 size=4 op=poll() returned=3 | x = data[2] = 3. The slot keeps its old number (shown struck out when free): only head and size decide what is live. |
| 28 | L13 | head=3 tail=2 size=3 op=poll() returned=3 | head = (2 + 1) % 4 = 3. Returned 3. |
| 29 | L16 | head=3 tail=2 size=3 return=[4, 5, 6] | Queue contents from head: [4, 5, 6]; 0 operations rejected. |
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.
1class LinkedQueue {2private static class Node { int val; Node next; Node(int v) { val = v; } }3private Node head, tail; private int size;4void enqueue(int x) {5Node n = new Node(x);6if (tail == null) head = n; else tail.next = n; // link behind the old tail7tail = n; size++;8}9int dequeue() {10if (head == null) throw new NoSuchElementException();11int x = head.val; head = head.next;12if (head == null) tail = null; // the queue became empty13size--; 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 inputlinked list · 0 nodes
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.
1class LinkedQueue {2private static class Node { int val; Node next; Node(int v) { val = v; } }3private Node head, tail; private int size;4void enqueue(int x) {5Node n = new Node(x);6if (tail == null) head = n; else tail.next = n; // link behind the old tail7tail = n; size++;8}9int dequeue() {10if (head == null) throw new NoSuchElementException();11int x = head.val; head = head.next;12if (head == null) tail = null; // the queue became empty13size--; return x;14}15}
Live state
size
0
Execution trace (dry run table)Show 20 stepsHide
| Step | Line | State | What happens |
|---|---|---|---|
| 1 | L3 | size=0 | 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. |
| 2 | L5 | op=enqueue(10) size=0 | new Node(10): the node exists but nothing points to it yet. |
| 3 | L6 | op=enqueue(10) | tail == null, so the queue was empty: the new node is also the head. |
| 4 | L7 | op=enqueue(10) size=1 | tail = new node; size is 1. |
| 5 | L5 | op=enqueue(20) size=1 | new Node(20): the node exists but nothing points to it yet. |
| 6 | L6 | op=enqueue(20) | tail.next = new node: the old tail now points at 20. |
| 7 | L7 | op=enqueue(20) size=2 | tail = new node; size is 2. |
| 8 | L5 | op=enqueue(30) size=2 | new Node(30): the node exists but nothing points to it yet. |
| 9 | L6 | op=enqueue(30) | tail.next = new node: the old tail now points at 30. |
| 10 | L7 | op=enqueue(30) size=3 | tail = new node; size is 3. |
| 11 | L11 | op=dequeue() returned=10 | x = head.val = 10; head moves to the next node. The old head is unreachable, so the garbage collector may reclaim it. |
| 12 | L13 | op=dequeue() returned=10 size=2 | Returned 10; size is 2. |
| 13 | L5 | op=enqueue(40) size=2 | new Node(40): the node exists but nothing points to it yet. |
| 14 | L6 | op=enqueue(40) | tail.next = new node: the old tail now points at 40. |
| 15 | L7 | op=enqueue(40) size=3 | tail = new node; size is 3. |
| 16 | L11 | op=dequeue() returned=20 | x = head.val = 20; head moves to the next node. The old head is unreachable, so the garbage collector may reclaim it. |
| 17 | L13 | op=dequeue() returned=20 size=2 | Returned 20; size is 2. |
| 18 | L11 | op=dequeue() returned=30 | x = head.val = 30; head moves to the next node. The old head is unreachable, so the garbage collector may reclaim it. |
| 19 | L13 | op=dequeue() returned=30 size=1 | Returned 30; size is 1. |
| 20 | L15 | size=1 failed=0 return=[40] | Final queue (head to tail): [40]. |
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.
1static List<Integer> drain(int[] values) {2Queue<Integer> q = new ArrayDeque<>();3for (int v : values) q.offer(v);4List<Integer> out = new ArrayList<>();5while (!q.isEmpty()) out.add(q.poll()); // front first: same order as the input6return out;7}
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.
| ArrayDeque | LinkedList | |
|---|---|---|
| Storage | One circular array | One node per element |
| offer / poll | O(1) amortised | O(1) |
| null elements | Not allowed (NullPointerException) | Allowed |
| Indexed access | None | get(i) is O(n) |
| Typical choice for queues | Yes | Only 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.