Chapter 8: Queues and Deques
Introduction
Learning Outcomes
Explain the first-in, first-out behavior of a queue.
Implement a queue with an array and with a linked list.
Tell a circular queue from a double-ended queue (deque) and use each where it fits.
Choose the right kind of queue for real-world problems.
The Queue ADT
The Queue
- A linear data structure that is open at both ends.
- Items leave in the order they arrived: first in, first out (FIFO).
- Items join at one end (the rear) and leave from the other (the front).
- Think of the line at a checkout or a ticket window: whoever arrives first is served first.
The Queue ADT
Basic Queue Operations
Horses walking single file: the one in front passes first, and newcomers join at the back.
The Queue ADT
The Queue Abstract Data Type
| Operation | Description |
|---|---|
enqueue(e) | adds e at the rear |
dequeue() | removes and returns the front item |
first() / peek() | returns the front item without removing it |
rear() | returns the rear item without removing it |
size() | number of items |
isEmpty() | is the queue empty? |
isFull() | is it full? (fixed capacity only) |
public interface Queue<E> { int size(); boolean isEmpty(); void enqueue(E e); E first(); // null if empty E dequeue(); // null if empty }
null when the queue is empty; some implementations throw an exception.The Queue ADT
Queue Operations, Step by Step
Initially the queue is empty: front and rear point to nothing (null).
The first item goes in: a lone item is both the front and the rear.
10 joins at the rear: rear advances, front stays put.
15 joins at the rear. Queue: 20, 10, 15 (front to rear).
20 leaves from the front; front advances to the next item (10).
10 leaves. Now front and rear point to the same item (15).
When the last item leaves, both become null. Items left in the order 20, 10, 15, the order they arrived (FIFO).
The Queue ADT
Queues in Java: java.util.Queue
| Operation | Throws an exception | Returns a special value |
|---|---|---|
| Insert | add(e) | offer(e) → false |
| Remove | remove() | poll() → null |
| Examine | element() | peek() → null |
ArrayDeque: backed by a circular array; usually the fastest choice.LinkedList: backed by a linked list.
Queue<Integer> q = new ArrayDeque<>(); q.offer(20); q.offer(10); q.offer(15); // [20, 10, 15] q.peek(); // 20, not removed q.poll(); // 20 → [10, 15] q.poll(); // 10 → [15] q.size(); // 1 q.poll(); // 15 → [] q.poll(); // null (empty)
enqueue/dequeue; the Java library uses offer/poll/peek.The Queue ADT
Where Queues Are Used
Array-Based Queues
A Queue in an Array
- The items live in an array, and two indices mark the ends of the queue:
- front: index of the first item.
- rear: index of the last item.
- Enqueue increments
rear; dequeue incrementsfront. Items never move. - Simple and fast, but the array has a fixed capacity.
The item at index 0 was dequeued earlier. Its slot is free now, but a simple array queue cannot reuse it.
Array-Based Queues
The Trouble with a Simple Array Queue
Empty queue, capacity N = 6. rear = −1: no items yet.
enqueue(10): rear++ → 0 → data[0].
enqueue(20): rear++ → 1 → data[1].
enqueue(30): rear++ → 2 → data[2].
enqueue(40): rear++ → 3 → data[3].
dequeue() → 10: front++ → 1. The freed slot is never reused.
dequeue() → 20: front++ → 2. The freed slot is never reused.
dequeue() → 30: front++ → 3. The freed slot is never reused.
enqueue(50): rear = 4.
enqueue(60): rear = 5 = N − 1, the last index of the array.
“Full” with only 3 items: the 3 orange slots are wasted. Shifting costs O(n); the fix is a circular queue.
Array-Based Queues
The Circular Queue
- FIFO like an ordinary queue, but after the last slot of the array comes the first slot again.
- The two ends of the array are logically joined into a ring.
- Indices advance modulo N:
(i + 1) % N. - Freed slots are reused, so items never need shifting.
- Also known as a ring buffer.
Here rear is the free slot where the next item will go: rear = (front + size) % N = (0 + 5) % 8 = 5.
Array-Based Queues
Moving the Indices with Mod
| What | Formula | Example: N = 8, front = 6, size = 4 |
|---|---|---|
| Next free slot (where to insert) | (front + size) % N | (6 + 4) % 8 = 2 |
| Last item | (front + size − 1) % N | (6 + 3) % 8 = 1 |
| k-th item from the front (k = 0, 1, …) | (front + k) % N | k = 2 → (6 + 2) % 8 = 0 |
| front after a dequeue | (front + 1) % N | (6 + 1) % 8 = 7 |
Array-Based Queues
Full or Empty?
With only the front and rear indices, an empty and a full queue look the same: both indices point to the same slot. Two common fixes:
size == 0 · Full: size == NAll N slots are used. This is the method used in this course.
front == rear · Full: (rear + 1) % N == frontNo counter is needed, but at most N − 1 items fit.
Array-Based Queues
Circular Queue, Step by Step (N = 5)
Empty queue: size = 0. On the ring, the red-edged slot is front; in the array, ▲avail marks the next write position.
enqueue(10) → data[0]; size = 1.
enqueue(20) → data[1]; size = 2.
enqueue(30) → data[2]; size = 3.
dequeue() → 10; front = (0 + 1) % 5 = 1. The items do not move.
dequeue() → 20; front = (1 + 1) % 5 = 2. The items do not move.
enqueue(40) → data[3].
enqueue(50) → data[4].
enqueue(60) → data[0]: the index wraps around, reusing a slot that a simple array queue would call “full.”
enqueue(70) → data[1]. size == N → full; front == avail, but thanks to size the queue is not mistaken for empty.
dequeue() → 30; front = 3.
dequeue() → 40; front = 4.
dequeue() → 50; front = 0. (4 + 1) % 5 = 0, so front wraps around too.
dequeue() → 60; front = 1.
dequeue() → 70; front = 2. size = 0 → the queue is empty again. Items left in the order 30, 40, 50, 60, 70, the order they arrived (FIFO).
Array-Based Queues
Circular Queue: Class and Status Queries
public class CircularQueue<E> { private E[] data; // stores the items private int front = 0; // index of the first item private int size = 0; // number of items @SuppressWarnings("unchecked") public CircularQueue(int capacity) { data = (E[]) new Object[capacity]; } public int size() { return size; } public boolean isEmpty() { return size == 0; } public boolean isFull() { return size == data.length; } public E first() { return isEmpty() ? null : data[front]; } // enqueue(), dequeue() }
(front + size) % data.length, so two fields can never disagree.Array-Based Queues
Circular Queue: enqueue and dequeue
public void enqueue(E item) { // enqueue if (isFull()) throw new IllegalStateException("full"); int avail = (front + size) % data.length; data[avail] = item; size++; }
public E dequeue() { // dequeue if (isEmpty()) throw new IllegalStateException("empty"); E item = data[front]; data[front] = null; // drop the reference front = (front + 1) % data.length; size--; return item; }
Array-Based Queues · Step through the code
Code Trace: Circular enqueue()
Start: data.length = 5, front = 2, size = 2 (30 and 40). Next we enqueue 50, 60, 70, 80.
enqueue(50): isFull() → size 2 == 5? no.
avail = (2 + 2) % 5 = 4 % 5 = 4.
data[4] = 50. The other items do not move.
size++ → 3. front is unchanged (data[2]).
enqueue(60): isFull() → size 3 == 5? no.
avail = (2 + 3) % 5 = 5 % 5 = 0 → past the end of the array, the index wraps around.
data[0] = 60. The other items do not move.
size++ → 4. front is unchanged (data[2]).
enqueue(70): isFull() → size 4 == 5? no.
avail = (2 + 4) % 5 = 6 % 5 = 1 → past the end of the array, the index wraps around.
data[1] = 70. The other items do not move.
size++ → 5. front is unchanged (data[2]). The queue is now full.
enqueue(80): isFull() → size 5 == 5? yes.
The queue is full → IllegalStateException("full") is thrown and the array is unchanged. All 5 slots were used; none was wasted.
public void enqueue(E item) { if (isFull()) throw new IllegalStateException("full"); int avail = (front + size) % data.length; data[avail] = item; size++; }
Array-Based Queues · Step through the code
Code Trace: Circular dequeue()
Start: a full queue, front = 2. Front to rear: 30, 40, 50, 60, 70. We call dequeue() three times.
isEmpty(): size = 5 ≠ 0 → not empty.
item = data[2] = 30: the front (oldest) item.
data[2] = null: the slot is cleared, so the garbage collector can reclaim the object.
front = (2 + 1) % 5 = 3.
size-- → 4; return 30.
isEmpty(): size = 4 ≠ 0 → not empty.
item = data[3] = 40: the front (oldest) item.
data[3] = null: the slot is cleared, so the garbage collector can reclaim the object.
front = (3 + 1) % 5 = 4.
size-- → 3; return 40.
isEmpty(): size = 3 ≠ 0 → not empty.
item = data[4] = 50: the front (oldest) item.
data[4] = null: the slot is cleared, so the garbage collector can reclaim the object.
front = (4 + 1) % 5 = 0 → 5 % 5 = 0: front wraps around.
size-- → 2; return 50. A constant number of steps: O(1).
public E dequeue() { if (isEmpty()) throw new IllegalStateException("empty"); E item = data[front]; data[front] = null; // drop the reference front = (front + 1) % data.length; size--; return item; }
Array-Based Queues · Step through the code
Code Trace: Growing a Full Circular Queue
Full queue (front = 2): the order is A, B, C, D, but the array holds C, D, A, B. Now enqueue(E).
4 == 4 → full. Instead of failing, grow to twice the capacity.
A new array of 8 cells is allocated.
k = 0: temp[0] = data[(2 + 0) % 4] = data[2] = A.
k = 1: temp[1] = data[(2 + 1) % 4] = data[3] = B.
k = 2: temp[2] = data[(2 + 2) % 4] = data[0] = C. The mod wrapped around to the start.
k = 3: temp[3] = data[(2 + 3) % 4] = data[1] = D.
data = temp: the items are now in order (Arrays.copyOf would have kept them scrambled).
front = 0: the front item (A) is now in data[0].
Back in enqueue: (0 + 4) % 8 = 4 → data[4] = E.
size → 5. Growing costs O(n), but because the capacity doubles, enqueue is amortized O(1).
private void resize(int newCap) { E[] temp = (E[]) new Object[newCap]; for (int k = 0; k < size; k++) temp[k] = data[(front + k) % data.length]; data = temp; front = 0; } public void enqueue(E item) { if (size == data.length) resize(2 * data.length); data[(front + size) % data.length] = item; size++; }
Linked Queues
A Queue in a Linked List
- Each queue item is a node (data plus a next reference).
frontmarks the head andrearthe tail: insert at the tail, remove at the head.- The size is dynamic; there is no capacity limit (no full queue).
- Each node costs one extra reference and an object allocation.
class Node { int data; Node next; Node(int data) { this.data = data; } } // fields: Node front, rear; int size;
Why insert at the tail and remove at the head? In a singly linked list, removing the head is O(1), but removing the tail means finding its predecessor: O(n).
Linked Queues
Enqueue and Dequeue Algorithms
- Is the queue full? (fixed-capacity array only)
- If so, report an overflow and stop.
- Otherwise, write x at the rear position.
- Advance the rear to the next position.
- Increment the item count and report success.
- Is the queue empty?
- If so, report an underflow and stop.
- Otherwise, take the front item.
- Advance the front to the next item.
- Decrement the item count and return the item.
| Step | Circular array | Linked list |
|---|---|---|
| Write at the rear | data[(front + size) % N] = x | rear.next = temp; rear = temp |
| Advance the front | front = (front + 1) % N | front = front.next |
| Full check | size == N | not needed (while memory lasts) |
Linked Queues · Step through the code
Code Trace: Linked Queue enqueue
Empty queue: front = rear = null. We call enqueue(10), enqueue(15), enqueue(20) in turn.
new Node(10): a new node with next = null, not yet in the list.
isEmpty() → true (size 0).
First node: front = temp.
rear = temp: rear is now the new node. The lone node is both front and rear.
size++ → 1.
new Node(15): a new node with next = null, not yet in the list.
isEmpty() → false; take the else branch.
rear.next = temp: the old rear (10) links to the new node.
rear = temp: rear is now the new node.
size++ → 2.
new Node(20): a new node with next = null, not yet in the list.
isEmpty() → false; take the else branch.
rear.next = temp: the old rear (15) links to the new node.
rear = temp: rear is now the new node.
size++ → 3. The list was never traversed: each enqueue is O(1).
public void enqueue(int data) { Node temp = new Node(data); if (isEmpty()) { front = temp; } else { rear.next = temp; } rear = temp; size++; }
Linked Queues · Step through the code
Code Trace: Linked Queue dequeue
Queue: 10, 15, 20. We call dequeue() three times.
isEmpty() → false (size 3).
result = front.data = 10.
front = front.next → node(15). Nothing refers to the old node now; the garbage collector reclaims it.
front == null → false; rear stays the same.
size-- → 2; return 10.
isEmpty() → false (size 2).
result = front.data = 15.
front = front.next → node(20). Nothing refers to the old node now; the garbage collector reclaims it.
front == null → false; rear stays the same.
size-- → 1; return 15.
isEmpty() → false (size 1).
result = front.data = 20.
front = front.next → null. But rear still points to the removed node!
front == null → true: the queue is now empty.
rear = null: without this line, rear would go on pointing to the removed node in an empty queue (an inconsistent state).
size-- → 0; return 20. Items left in the order 10, 15, 20 (FIFO), each in O(1).
public int dequeue() { if (isEmpty()) throw new NoSuchElementException(); int result = front.data; front = front.next; if (front == null) rear = null; size--; return result; }
Linked Queues
Circular Array or Linked List?
Circular array
- Contiguous in memory: cache-friendly and fast.
- No extra reference per item.
- Fixed capacity: when full, either overflow or copy into a larger array (O(n), amortized O(1)).
Linked list
- No capacity limit; grows as needed.
- Every insertion and removal is strictly O(1).
- Each node costs an extra reference and an object allocation.
| Operation | Circular array | Linked list |
|---|---|---|
| enqueue | O(1) | O(1) |
| dequeue | O(1) | O(1) |
| first / size / isEmpty | O(1) | O(1) |
| Memory | O(N) (the capacity) | O(n) (the number of items) |
Kinds of Queues
Kinds of Queues
| Kind | Insert | Remove | Note |
|---|---|---|---|
| Simple queue | rear | front | FIFO |
| Input-restricted | rear only | front and rear | restricted deque |
| Output-restricted | front and rear | front only | restricted deque |
| Circular | rear | front | array slots are reused |
| Double-ended (deque) | front and rear | front and rear | not bound to FIFO |
| Priority | anywhere | highest priority | ordered by priority |
dequeue, the remove-from-queue operation.Kinds of Queues
The Double-Ended Queue (Deque)
- A double-ended queue supports insertion and removal at both the front and the rear.
- Not bound to FIFO: used only at the rear, a deque is a stack (LIFO); inserting at the rear and removing at the front makes it a queue.
| Operation | Java ArrayDeque | Time |
|---|---|---|
| addFirst(e) | offerFirst(e) | O(1) |
| addLast(e) | offerLast(e) | O(1) |
| removeFirst() | pollFirst() | O(1) |
| removeLast() | pollLast() | O(1) |
| first() / last() | peekFirst() / peekLast() | O(1) |
Kinds of Queues
Deque Operations, Step by Step
An empty deque. We implement it with a doubly linked list: every node has prev and next.
First node: head = tail = node.
Insert at the tail: node.prev = tail; tail.next = node; tail = node.
Insert at the head: node.next = head; head.prev = node; head = node.
Both ends are held by references, so every insertion is O(1).
Remove from the tail: tail = tail.prev; tail.next = null. The prev reference makes this O(1).
Remove from the head: head = head.next; head.prev = null.
prev reference makes it O(1).Kinds of Queues
Deque: Inserting at Either End
public void addFirst(E data) { Node<E> node = new Node<>(data); if (isEmpty()) { head = tail = node; } else { node.next = head; head.prev = node; head = node; } size++; }
public void addLast(E data) { Node<E> node = new Node<>(data); if (isEmpty()) { head = tail = node; } else { node.prev = tail; tail.next = node; tail = node; } size++; }
class Node<E> { E data; Node<E> prev, next; Node(E data) { this.data = data; } }
The two methods mirror each other: head ↔ tail, prev ↔ next.
Kinds of Queues · Step through the code
Code Trace: Deque addFirst()
Empty deque. First addFirst(5), then addFirst(3): 3 will end up in front.
new Node<>(5): prev = next = null.
isEmpty() → true.
head = tail = node: a single node forms both ends.
size++ → 1.
new Node<>(3): prev = next = null.
isEmpty() → false.
node.next = head: the new node points to the old head (5).
head.prev = node: the back link; the list can now be walked in both directions.
head = node: the head reference moves to the new node.
size++ → 2. No traversal: O(1). addLast is its mirror image.
public void addFirst(E data) { Node<E> node = new Node<>(data); if (isEmpty()) { head = tail = node; } else { node.next = head; head.prev = node; head = node; } size++; }
Kinds of Queues
Deque: Removing from Either End
public E removeFirst() { if (isEmpty()) return null; E data = head.data; head = head.next; if (head == null) tail = null; // now empty else head.prev = null; size--; return data; }
public E removeLast() { if (isEmpty()) return null; E data = tail.data; tail = tail.prev; if (tail == null) head = null; // now empty else tail.next = null; size--; return data; }
tail still points to the removed node once head is null: the node cannot be collected, and last() returns a wrong value. An empty deque returns null, as in Goodrich.Kinds of Queues · Step through the code
Code Trace: Deque removeLast()
Deque: 3 ⇄ 5. Two calls to removeLast(): 5 comes out first, then 3, and the deque is left empty.
isEmpty() → false (size 2).
data = tail.data = 5.
tail = tail.prev → node(3). Thanks to prev, there is no need to search for the second-to-last node.
tail == null → false: the deque is not empty yet.
tail.next = null: the link to node 5 is cut, and the node leaves the list.
size-- → 1; return 5.
isEmpty() → false (size 1).
data = tail.data = 3.
tail = tail.prev → null (there was only one node). But head still points to it!
tail == null → true: the deque is now empty.
head = null: both ends must be null; otherwise head would point to the removed node.
size-- → 0; return 3. O(1) in both cases.
public E removeLast() { if (isEmpty()) return null; E data = tail.data; tail = tail.prev; if (tail == null) head = null; // now empty else tail.next = null; size--; return data; }
Kinds of Queues · Step through the code
A Deque Application: Sliding Window Maximum
k = 3. d holds indices whose values decrease from front to rear, so the maximum is always at the front.
a[0] = 1: 0 goes in at the rear.
a[1] = 3: drop 0:1 from the rear. 1 goes in at the rear.
a[2] = -1: 2 goes in at the rear. Max = 3.
a[3] = -3: 3 goes in at the rear. Max = 3.
a[4] = 5: drop 1 from the front; drop 3:-3 and then 2:-1 from the rear. 4 goes in at the rear. Max = 5.
a[5] = 3: 5 goes in at the rear. Max = 5.
a[6] = 6: drop 5:3 and then 4:5 from the rear. 6 goes in at the rear. Max = 6.
Each index enters once and leaves at most once → O(n) (brute force takes O(n·k)).
int[] windowMax(int[] a, int k) { int[] maxes = new int[a.length - k + 1]; Deque<Integer> d = new ArrayDeque<>(); // indices for (int i = 0; i < a.length; i++) { if (!d.isEmpty() && d.peekFirst() <= i - k) d.pollFirst(); // out of window while (!d.isEmpty() && a[d.peekLast()] <= a[i]) d.pollLast(); // not a candidate d.offerLast(i); if (i >= k - 1) maxes[i - k + 1] = a[d.peekFirst()]; } return maxes; }
Applications
Application: A Round-Robin Scheduler
The operating system gives the CPU to processes in turn: the process at the front runs for at most q time units and, if unfinished, rejoins at the rear.
Three processes in the ready queue, with 5, 3, and 2 units of work left. Time slice (quantum) q = 2.
P1 leaves the front (dequeue), runs for 2 units, and rejoins at the rear with 3 left (enqueue).
P2 runs for 2 units and goes back to the rear with 1 left.
P3's 2 units fit in one slice → it finishes at t = 6 and does not rejoin.
P1 runs for another 2 units; 1 is left.
P2 finishes its last unit (t = 9) without using the whole slice.
All done. Completion times: P3 = 6, P2 = 9, P1 = 10.
Simulator: Round-Robin Scheduler (adjustable quantum and processes).
Applications · Step through the code
Code Trace: roundRobin(q, 2)
roundRobin(q, 2): three processes with 5, 3, and 2 units left (Process: name, remaining). t = 0.
Not empty → p = q.poll() = P1 (5 left).
run = min(2, 5) = 2; P1 runs for 2 units → t = 2, 3 left.
remaining 3 > 0 → q.offer(p): P1 rejoins at the rear.
Not empty → p = q.poll() = P2 (3 left).
run = min(2, 3) = 2; P2 runs for 2 units → t = 4, 1 left.
remaining 1 > 0 → q.offer(p): P2 rejoins at the rear.
Not empty → p = q.poll() = P3 (2 left).
run = min(2, 2) = 2; P3 runs for 2 units → t = 6, 0 left.
remaining 0 → P3 is done (t = 6); it does not rejoin.
Not empty → p = q.poll() = P1 (3 left).
run = min(2, 3) = 2; P1 runs for 2 units → t = 8, 1 left.
remaining 1 > 0 → q.offer(p): P1 rejoins at the rear.
Not empty → p = q.poll() = P2 (1 left).
run = min(2, 1) = 1; P2 runs for 1 unit → t = 9, 0 left. The slice is not used up.
remaining 0 → P2 is done (t = 9); it does not rejoin.
Not empty → p = q.poll() = P1 (1 left).
run = min(2, 1) = 1; P1 runs for 1 unit → t = 10, 0 left. The slice is not used up.
remaining 0 → P1 is done (t = 10); it does not rejoin.
The queue is empty → the loop ends. Completion: P3 = 6, P2 = 9, P1 = 10. Each round is one poll plus at most one offer: O(1).
void roundRobin(Queue<Process> q, int quantum) { int t = 0; while (!q.isEmpty()) { Process p = q.poll(); int run = Math.min(quantum, p.remaining); t += run; p.remaining -= run; if (p.remaining > 0) q.offer(p); else System.out.println(p.name + " t=" + t); } }
Applications · Step through the code
Code Trace: The Josephus Problem (n = 5, k = 3)
Five people stand in a circle. Each round, the first k − 1 = 2 move to the rear and the 3rd is eliminated. A queue rotates the circle naturally.
i = 1: q.offer(q.poll()) → A moves to the rear.
i = 2: q.offer(q.poll()) → B moves to the rear.
C leaves the front and is eliminated. 4 left.
i = 1: q.offer(q.poll()) → D moves to the rear.
i = 2: q.offer(q.poll()) → E moves to the rear.
A leaves the front and is eliminated. 3 left.
i = 1: q.offer(q.poll()) → B moves to the rear.
i = 2: q.offer(q.poll()) → D moves to the rear.
E leaves the front and is eliminated. 2 left.
i = 1: q.offer(q.poll()) → B moves to the rear.
i = 2: q.offer(q.poll()) → D moves to the rear.
B leaves the front and is eliminated. 1 left.
One person is left: the winner is D. n people, k steps: O(n·k) queue operations.
String josephus(Queue<String> q, int k) { while (q.size() > 1) { for (int i = 1; i < k; i++) q.offer(q.poll()); // front to rear String out = q.poll(); System.out.println(out + " eliminated"); } return q.poll(); }
Applications
Example: Binary Numbers from 1 to n
Use a queue to generate the binary representations of the numbers from 1 to n.
x + "0" (2x) and x + "1" (2x + 1). Dequeue a number, output it, and enqueue its successors: the numbers come out level by level, in increasing order (breadth-first).Applications · Step through the code
Code Trace: generateBinary(4)
result: an array with 4 empty slots.
An empty queue is created (a LinkedList behind the Queue interface).
The root “1” is enqueued.
i = 0 < 4 → loop body.
result[0] = q.poll() = “1”: the smallest number, at the front of the queue.
“10” and “11” are enqueued: the successors of “1”, to come out in turn.
i = 1 < 4 → loop body.
result[1] = q.poll() = “10”: the smallest number, at the front of the queue.
“100” and “101” are enqueued: the successors of “10”, to come out in turn.
i = 2 < 4 → loop body.
result[2] = q.poll() = “11”: the smallest number, at the front of the queue.
“110” and “111” are enqueued: the successors of “11”, to come out in turn.
i = 3 < 4 → loop body.
result[3] = q.poll() = “100”: the smallest number, at the front of the queue.
“1000” and “1001” are enqueued: the successors of “100”, to come out in turn.
i = 4 < 4 is false → the loop ends.
return {“1”, “10”, “11”, “100”}. n polls + 2n offers → O(n) queue operations (the strings have length O(log n), so the total character work is O(n log n)).
String[] generateBinary(int n) { String[] result = new String[n]; Queue<String> q = new LinkedList<>(); q.offer("1"); for (int i = 0; i < n; i++) { result[i] = q.poll(); q.offer(result[i] + "0"); q.offer(result[i] + "1"); } return result; }
Applications · Step through the code
Code Trace: A Queue from Two Stacks
inbox takes insertions and outbox serves removals (stack tops are drawn at the top).
enqueue(1): inbox.push(1) → O(1).
enqueue(2): inbox.push(2) → O(1).
enqueue(3): inbox.push(3) → O(1).
dequeue: outbox is empty → move everything from inbox to outbox.
3: pop from inbox, push onto outbox. The transfer reverses the order; the oldest item ends up on top.
2: pop from inbox, push onto outbox. The transfer reverses the order; the oldest item ends up on top.
1: pop from inbox, push onto outbox. The transfer reverses the order; the oldest item ends up on top.
outbox.pop() → 1 (FIFO: oldest first).
enqueue(4): inbox.push(4) → O(1).
dequeue: outbox is not empty → no transfer.
outbox.pop() → 2 (FIFO: oldest first).
dequeue: outbox is not empty → no transfer.
outbox.pop() → 3 (FIFO: oldest first).
dequeue: outbox is empty → move everything from inbox to outbox.
4: pop from inbox, push onto outbox. The transfer reverses the order; the oldest item ends up on top.
outbox.pop() → 4. Output 1, 2, 3, 4 matches the input order. Each item is transferred at most once → dequeue is amortized O(1) (a single call is O(n) in the worst case).
Deque<Integer> inbox = new ArrayDeque<>(); Deque<Integer> outbox = new ArrayDeque<>(); void enqueue(int x) { inbox.push(x); } int dequeue() { if (outbox.isEmpty()) while (!inbox.isEmpty()) outbox.push(inbox.pop()); return outbox.pop(); }
Summary
Common Mistakes
front = null; without rear = null, rear points to a removed node.front == rear both when empty and when full; add a size counter or leave one slot empty.Arrays.copyOf when growingIt scrambles a circular queue that has wrapped around; copy the items in (front + k) % N order.add, remove, and element throw exceptions; offer, poll, and peek return false / null.PriorityQueue is not FIFOQueue<Integer> q = new PriorityQueue<>() hands out items by priority, not by arrival order.ArrayDeque and nullArrayDeque rejects null items (NullPointerException); a null would be confused with the “empty” result.Summary
Summary: Costs of Queue Implementations
| Implementation | Insert | Remove | Front | Note |
|---|---|---|---|---|
| Simple array (with shifting) | O(1) | O(n) | O(1) | shifts on every removal |
| Circular array | O(1) | O(1) | O(1) | fixed capacity, % N |
| Linked list | O(1) | O(1) | O(1) | front and rear references |
| Deque (doubly linked list) | O(1) both ends | O(1) both ends | O(1) | prev reference |
| Queue from two stacks | O(1) | O(1) amortized | O(1) amortized | each item is transferred once |
size counter or one empty slot.rear = null when the last item leaves.Summary
Interactive Simulators
Summary
Sample Problems
- Starting from an empty queue: enqueue(4), enqueue(7), dequeue(), enqueue(1), enqueue(9), dequeue(). What is first(), and what is left in the queue?
- N = 6, front = 4, size = 3: which indices hold the items, and where does the next enqueue write?
- With the “one empty slot” method and N = 10, how many items fit at most? What is the full condition?
- What happens if
if (front == null) rear = null;is removed from dequeue? - How do you build a queue from two stacks? What is the amortized cost of dequeue?
- How can a deque check whether a word is a palindrome?
Show answers
- first() = 1; the queue (front to rear): 1, 9.
- Indices 4, 5, 0. Next write: (4 + 3) % 6 = 1.
- 9 items; full when (rear + 1) % 10 == front.
- In an empty queue, rear still points to the old node: rear() returns a wrong value, and the node cannot be collected (an inconsistent state).
- Enqueue: push onto inbox. Dequeue: if outbox is empty, move inbox into outbox, then pop from outbox. Each item is transferred at most once → amortized O(1).
- Put the letters in a deque; repeatedly remove one from each end and compare. O(n).
Summary
Exercises
- Reverse a queue using only queue operations and one stack.
- Build a stack from one queue: how do you write push, and at what cost?
- Josephus: people numbered 1..7, k = 3. In what order are they eliminated, and who wins?
- A circular queue with N = 5, front = 3, size = 5 is grown. In what order do the items appear in the new array?
- Reverse the first k items of a queue without changing the order of the rest.
- What changes in the deque algorithm for the sliding window minimum?
Hints / answers
- Dequeue everything and push it onto the stack, then pop and enqueue → O(n).
- push(x): enqueue(x), then move the size − 1 items in front of x to the rear with dequeue+enqueue → push O(n), pop O(1).
- Eliminated in the order 3, 6, 2, 7, 5, 1; the winner is 4.
- data[3], data[4], data[0], data[1], data[2] → temp[0..4]; front = 0.
- Push the first k onto a stack and enqueue them back, then rotate the remaining n − k items to the rear.
- The comparison flips: indices with
a[d.peekLast()] >= a[i]are dropped.
Summary
Further Reading
- Goodrich, Tamassia, and Goldwasser, Data Structures and Algorithms in Java, 6th ed., 2014.
- Lafore, Data Structures and Algorithms in Java, 2nd ed., 2002.
- Oracle, Java SE 17 API documentation for
Queue,Deque,ArrayDeque, andPriorityQueue.