Data Structures · Lecture Notes
Chapter 8: Queues and Deques
A queue is a waiting line in software: first come, first served. Items enter at one end and leave at the other, and none may cut in. The rule appears everywhere, from processor sharing and print jobs to network buffers and breadth-first search. We start with the queue ADT and Java's Queue interface, build a queue on an array, see why the naive version wastes space, and fix it with a circular queue whose indices wrap with mod. Linked queues, kinds of queues, and the deque, which works at both ends, follow, with the sliding window maximum. Applications traced in code close the chapter: round-robin scheduling, the Josephus problem, binary number generation, and a queue from two stacks.
Learning outcomes
- The FIFO model
- Explain first-in, first-out behavior; find a queue's contents and return values after a sequence of operations.
- Implementation
- Implement a queue with an array and a linked list; wrap circular indices with mod, tell empty from full, and grow a full queue without breaking its order.
- Kinds of queues
- Tell the circular queue from the deque; implement a deque with a doubly linked list and use it where it fits.
- Applications
- Pick the right queue for round-robin scheduling, the Josephus problem, binary number generation, sliding window maximum, and a queue from two stacks, and justify each cost.
8.1The queue ADT
A data structure can be described at two levels: what it does (its operations, what each changes and returns) and how (how the data sits in memory and what steps each operation takes). A description at the first level is an abstract data type (ADT). This section treats the queue at that level, apart from any implementation.
8.1.1What is a queue?
Think of a supermarket checkout. Newcomers join at the back, the cashier serves the person at the front, and everyone behind moves up; nobody cuts in or jumps ahead. Ticket windows, bank tellers, and toll booths follow the same rule: first come, first served.
A queue imitates this. Its items form a line whose ends have separate jobs: new items join at the rear, and removal happens at the front. The item that leaves is always the one that has waited longest.
A queue is an abstract data type that keeps items in a sequence, with insertion only at the rear and removal only at the front. This discipline is called FIFO (first-in, first-out).
A stack inserts and removes at the same end, so the last item in is the first out (LIFO); a queue uses opposite ends, so the first in is the first out. Push 1, 2, 3 into each and remove three times: the stack returns 3, 2, 1, the queue 1, 2, 3. A queue exists to hold work in order of arrival and hand it back in that order.
Each end of a queue is used in one direction only. The queue is a linear structure: every item but the first has one predecessor, every item but the last one successor, and nothing branches.
Mixing up the two ends. New items join at the rear, and the item removed is the oldest, not the newest. Thinking "the last one added comes out first" means using the queue as a stack.
8.1.2The basic operations
A queue has two basic operations, whose names are used throughout the course:
- Insertion (
enqueue) adds an item at the rear — joining the line. - Removal (
dequeue) removes and returns the front item, the first one in.
In a good implementation both run in O(1) time, however many items the queue holds. That requires reaching both ends directly, without searching, and the whole design of the array and linked versions serves this goal.
Items in the middle cannot be reached. This restriction is deliberate: client code cannot break the order or let an item skip ahead, and the implementation only has to make two ends fast.
8.1.3Operations and interface of the queue ADT
A few helper operations complete the queue ADT (Table 8.1).
| 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() | Returns the number of items. |
isEmpty() | Is the queue empty? |
isFull() | Is the queue full? (Fixed-capacity implementations only.) |
Only enqueue and dequeue change the queue; the rest just report. first() differs from dequeue() as top() differs from pop() on a stack: both return the front item, but first() leaves it in place, which is what you want when you must look before deciding whether to remove.
isFull() makes sense only for a fixed capacity. A linked queue, or one that grows when full, is never "full" while memory lasts.
In Java an ADT is naturally written as an interface, which gives method signatures without bodies. Here is a shortened form of the queue interface from the textbook by Goodrich, Tamassia, and Goldwasser:
public interface Queue<E> {
int size();
boolean isEmpty();
void enqueue(E e);
E first(); // null if empty
E dequeue(); // null if empty
}E is a type parameter fixed when the queue is created: Queue<Integer> holds integers, Queue<String> strings. Classes and interfaces with type parameters are generic. The interface omits rear(), which basic use doesn't need, and deliberately omits isFull() so that it also covers unbounded queues.
The comments record a contract: on an empty queue, first() and dequeue() return null. Removing from an empty queue is underflow; adding to a full fixed-capacity queue is overflow. How to report these errors is a design choice. Goodrich's interface returns null, other implementations throw; in these notes the circular queue and the linked queue throw and the deque returns null. The caller feels the difference: an unchecked null lets the error travel silently, while an exception stops the program at once.
Calling dequeue() without checking for an empty queue. If the implementation returns null, using the result directly (int x = q.dequeue();) tries to unbox null into an int and throws NullPointerException. Ask isEmpty() first.
8.1.4Queue operations step by step
Apply enqueue(20), enqueue(10), enqueue(15), and three dequeue() calls to an empty queue whose end references are named front and rear.
- Start. The queue is empty;
frontandrearare bothnull. enqueue(20): As the only item, 20 is both front and rear; both references point to it. Size 1.enqueue(10): 10 joins at the rear;rearmoves to it,frontstays at 20. Size 2.enqueue(15): The queue is 20, 10, 15 from front to rear. Size 3.dequeue(): 20 is removed and returned;frontmoves to 10. Size 2.dequeue(): 10 leaves;frontandrearboth point to 15. Size 1.dequeue(): 15 leaves; the queue is empty and both references arenullagain.
The items came out as 20, 10, 15, the order they went in — the FIFO rule.
front = rear = null; items enter at "In" on the right and leave at "Out" on the left. (b) After three enqueue calls: 20, 10, 15, with front on 20 and rear on 15. (c) The first dequeue() has returned 20; front is on 10. (d) The last item has left, and both references are null.Two edge cases matter. The first insertion into an empty queue must update both references, since that item is both front and rear; removing the last item must reset rear to null along with front. Most queue bugs come from these two cases.
A tip for exercises: after each operation, write the contents on one line from front to rear with the returned value beside them. Writing always in one direction keeps you from removing at the wrong end.
8.1.5Queues in Java: java.util.Queue
Java provides the java.util.Queue<E> interface, in which each operation comes in two forms: one throws an exception on failure, the other returns a special value (Table 8.2).
| Operation | Throws exception | Returns special value |
|---|---|---|
| Insert | add(e) | offer(e) → false |
| Remove | remove() | poll() → null |
| Examine | element() | peek() → null |
On an empty queue, remove() and element() throw NoSuchElementException, while poll() and peek() return null. When a bounded queue is full, add(e) throws IllegalStateException and offer(e) returns false; on unbounded queues, such as the two classes below, offer always returns true.
An interface has no objects of its own; two common classes implement it:
ArrayDequeuses a circular array and is usually the fastest; it is a growable version of the circular queue in these notes.LinkedListuses a linked list; it also supports list operations but creates a node object per item.
The code below runs the basic operations on an ArrayDeque. Declaring the variable as Queue<Integer> limits the code to queue operations and makes the implementing class easy to swap.
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)| Call | Returns | Queue (after) |
|---|---|---|
offer(20) | true | [20] |
offer(10) | true | [20, 10] |
offer(15) | true | [20, 10, 15] |
peek() | 20 | [20, 10, 15] |
poll() | 20 | [10, 15] |
poll() | 10 | [15] |
size() | 1 | [15] |
poll() | 15 | [] |
poll() | null | [] |
peek() returns 20 without changing the queue, so the next poll() returns 20 again. The last poll(), on an empty queue, returns null; remove() there would have thrown NoSuchElementException.
Our own classes use the ADT names enqueue/dequeue; the Java library does the same work with offer/poll/peek. The names differ, the behavior does not.
Expecting peek() to throw on an empty queue. It belongs to the special-value group and returns null; its throwing twin is element(). Rule of thumb: add, remove, element throw; offer, poll, peek return false or null.
8.1.6Where queues are used
A queue fits wherever work should be done in arrival order. Four common cases:
- Operating system schedulers. Ready processes wait in a ready queue and get the CPU in turn. Round-robin scheduling, the simplest form of this idea, is covered in Section 8.5.1.
- Print queues. A printer prints the documents sent to it in order of arrival; later ones wait.
- Buffers. Keyboard input, network packets, and video streams wait in a buffer, which evens out the speed difference between producer and consumer: data piles up while the producer is ahead and drains while the consumer catches up.
- Breadth-first search (BFS). Nodes discovered in a graph or tree are queued, so nearby nodes are visited before distant ones and the traversal goes level by level. Binary number generation in Section 8.5.4 is a small instance.
enqueue); the consumer (processor, printer) takes the front job (dequeue). The buffer queue absorbs their speed difference. Job 1 is the oldest and goes first.What these cases share is fairness and preserved order. A printer must print the first document first; packets handled out of order make a video show scrambled frames; a scheduler must not starve a process. A queue guarantees all this by structure: once an item is in, its turn comes as soon as the items ahead of it have left.
8.2Array-based queues
The obvious way to implement a queue is with an array, whose cells are reached by index in O(1); the queue only has to track its two ends. We try the simplest idea first, see why it wastes space, and fix it with a circular queue.
8.2.1The array representation
The items live in an array, and two indices mark the ends:
front: the index of the first item, which the next removal takes.rear: the index of the last item, the most recently added.
To insert, increment rear and write to data[rear]; to remove, take data[front] and increment front. Items never move, so both operations are O(1). The method is simple and fast, but the capacity is fixed.
front = 1 and rear = 3. The item at index 0 was removed earlier; its slot is free, but the simple array queue cannot reuse it.Figure 8.4 already shows the weak spot. Removals push the front rightward, and the slots freed behind it are never used again. rear creeps toward the end of the array according to the number of insertions so far, not the number of items held.
8.2.2The trouble with a simple array queue
Take a simple array queue of capacity N = 6 with front = 0 and rear = −1 (nothing added yet; the first insertion raises rear to 0 and writes at index 0). We insert 10, 20, 30, 40, remove three times, then try to insert 50, 60, 70.
| Operation | front | rear | Array | Returns |
|---|---|---|---|---|
| (start) | 0 | −1 | [_, _, _, _, _, _] | — |
enqueue(10) | 0 | 0 | [10, _, _, _, _, _] | — |
enqueue(20) | 0 | 1 | [10, 20, _, _, _, _] | — |
enqueue(30) | 0 | 2 | [10, 20, 30, _, _, _] | — |
enqueue(40) | 0 | 3 | [10, 20, 30, 40, _, _] | — |
dequeue() | 1 | 3 | [_, 20, 30, 40, _, _] | 10 |
dequeue() | 2 | 3 | [_, _, 30, 40, _, _] | 20 |
dequeue() | 3 | 3 | [_, _, _, 40, _, _] | 30 |
enqueue(50) | 3 | 4 | [_, _, _, 40, 50, _] | — |
enqueue(60) | 3 | 5 | [_, _, _, 40, 50, 60] | — |
enqueue(70) | 3 | 5 | [_, _, _, 40, 50, 60] | "full" |
Four insertions raise rear to 3. Three removals return 10, 20, 30 and move front to 3, so front and rear both point at 40, the only item left. Inserting 50 and 60 brings rear to 5, the last index (N − 1). For enqueue(70), rear + 1 = 6 = N: there is no room at the end, and the queue reports "full."
Yet it holds only 40, 50, and 60, while indices 0–2 sit empty: half the array is wasted because freed slots cannot be reused. Worse, even if each insertion were followed at once by a removal, rear would hit the end after six insertions. A simple array queue's lifetime is bounded by its total insertions, not by its contents.
front = 0, rear = −1. (b) After four insertions, rear = 3. (c) After three removals, front = rear = 3; the freed slots are lost. (d) After 50 and 60, rear = 5 = N − 1, and enqueue(70) reports "full" although the three orange slots are free.There are two ways out.
Solution 1: shifting. After each removal, shift the remaining items one slot left. The front then stays at data[0] and needs no index; size counts the items:
public int dequeue() { // simple array queue with shifting
if (size == 0)
throw new IllegalStateException("empty");
int result = data[0]; // the front is always data[0]
for (int i = 1; i < size; i++)
data[i - 1] = data[i]; // everyone moves one slot left
size--;
return result;
}With n items, a removal makes n − 1 assignments and costs O(n); emptying a queue of 1,000 items takes 999 + 998 + … + 1 + 0 = 499,500. The waste is gone, and so is the speed.
Solution 2: a circular array. Indices wrap from the end of the array to the start. Nothing is shifted, freed slots are reused, and every operation stays O(1). This is the circular queue.
Treating rear == N − 1 as "the queue is full." It only means the end of the array has been reached. In the last row of Table 8.4 the queue holds 3 items with 3 slots free, yet no insertion is possible.
8.2.3The circular queue
In a simple array queue the end of the array is a wall. A circular queue removes it: after the last slot comes the first, so the two ends join logically into a ring. In memory the array is still a flat row; the ring exists only in how indices are computed.
A circular queue stores its items in an array of N slots and advances its indices with mod, as in (i + 1) % N, so index N − 1 is followed by 0. It is also called a ring buffer.
A circular queue is still FIFO; only its "next index" changes, from i + 1 to (i + 1) % N. The two agree while i < N − 1; at i = N − 1, (N − 1 + 1) % N = N % N = 0, and the index wraps. Freed slots are reused and nothing is shifted.
Our circular queue keeps two fields: front, the index of the first item, and size, the item count. The rear is not stored; the free slot for the next item is (front + size) % N.
front is 0. Here rear is the free slot for the next item: (front + size) % N = (0 + 5) % 8 = 5. Slot 7 is followed by slot 0.The ring has no "end": slot 7 neighbors both 6 and 0. Insertion advances the rear clockwise and removal moves the front clockwise, so the queue can start and end anywhere on the ring.
8.2.4Computing indices with mod
Every index is computed from front, size, and N. For non-negative a, the remainder a % N lies between 0 and N − 1, always a valid index. Table 8.5 evaluates the four basic formulas for N = 8, front = 6, size = 4.
| What? | Formula | Example |
|---|---|---|
| Next free slot (insertion point) | (front + size) % N | (6 + 4) % 8 = 2 |
| Last item | (front + size − 1) % N | (6 + 3) % 8 = 1 |
| Item k from the front (k = 0, 1, …) | (front + k) % N | k = 2 → (6 + 2) % 8 = 0 |
front after a removal | (front + 1) % N | (6 + 1) % 8 = 7 |
front = 6, the last item is at index 1, and the next free slot is 2. After index 7 comes 0.All the formulas rest on one idea: item k lies k steps past the front. In a plain array that is front + k; in a circular one the sum may run past the end, and % N wraps it around. C is item 2: 6 + 2 = 8 is outside the array, but 8 % 8 = 0 is C's real place.
The next free slot is simply the case k = size: positions 0 to size − 1 are occupied, so the last item is at size − 1. Mod handles the jump from end to start in one expression, with no if asking whether we have reached the end.
Since front < N and size ≤ N, these sums stay below 2N, so % wraps at most once. Stepping backward needs care: in Java the result of % takes the sign of the dividend, so for front = 0, (front − 1) % N gives −1 % N = −1. The correct expression is (front − 1 + N) % N, which is exactly what inserting at the front of an array-based deque uses.
Forgetting the mod. Written as front + size, the insertion point runs past N once the queue has wrapped, and ArrayIndexOutOfBoundsException follows; in the example 6 + 4 = 10 is no index of an 8-slot array.
8.2.5Full or empty?
Two indices look sufficient at first: front for the first item and rear for the next write position. But then empty and full look alike. In an empty queue both point to the same slot; after N insertions rear has circled the ring back to front. Either way, front == rear.
front == rear. Middle, full (B, C, D, E from front to rear): again front == rear. Right, the "one empty slot" method: with B, C, D stored, slot 0 stays empty and the full test is (rear + 1) % N == front.Two common remedies exist.
1. Keep a counter. A size field makes the tests size == 0 (empty) and size == N (full), and all N slots are used. These notes and the slides use this method; with the counter, the rear index need not be stored, since it is (front + size) % N.
2. Leave one slot empty. The queue holds at most N − 1 items. It is empty when front == rear and full when (rear + 1) % N == front, with rear just behind front and one empty slot between. No counter is needed, but a slot is always wasted.
A circular queue with N = 7 using the "one empty slot" method starts at front = rear = 3. How many items fit, and what is rear when full?
- Start:
front == rear == 3; the queue is empty. - Insertions:
rearadvances 3 → 4 → 5 → 6 → 0 → 1 → 2, wrapping from 6 to 0 on the fourth insertion. - Full check: After six insertions
rear = 2and (2 + 1) % 7 = 3 = front: full, so a seventh insertion is refused.
The queue stores N − 1 = 6 items and ends with rear = 2. A seventh item would move rear onto front, making full indistinguishable from empty.
The methods differ little. The counter costs one integer field but uses every slot, and size == 0 and size == N say plainly what is tested. Leaving a slot empty pays off where a shared counter is costly or risky: when a producer and a consumer use one buffer concurrently, the producer changes only rear and the consumer only front, so neither must update a common field.
Keeping only front and rear while storing all N items. Then front == rear holds for both empty and full, and the code misreads one of them: it either overwrites the oldest item of a full queue or treats a full queue as empty. Use a counter or an empty slot.
8.2.6A circular queue step by step
Here is a counter-based circular queue with N = 5, starting at front = 0, size = 0. enqueue writes to slot (front + size) % 5 and increments size; dequeue returns data[front], clears the slot, sets front to (front + 1) % 5, and decrements size.
| Operation | Slot written / read | front | size | Array | Returns |
|---|---|---|---|---|---|
| (start) | — | 0 | 0 | [_, _, _, _, _] | — |
enqueue(10) | (0 + 0) % 5 = 0 | 0 | 1 | [10, _, _, _, _] | — |
enqueue(20) | (0 + 1) % 5 = 1 | 0 | 2 | [10, 20, _, _, _] | — |
enqueue(30) | (0 + 2) % 5 = 2 | 0 | 3 | [10, 20, 30, _, _] | — |
dequeue() | 0 | 1 | 2 | [_, 20, 30, _, _] | 10 |
dequeue() | 1 | 2 | 1 | [_, _, 30, _, _] | 20 |
enqueue(40) | (2 + 1) % 5 = 3 | 2 | 2 | [_, _, 30, 40, _] | — |
enqueue(50) | (2 + 2) % 5 = 4 | 2 | 3 | [_, _, 30, 40, 50] | — |
enqueue(60) | (2 + 3) % 5 = 0 | 2 | 4 | [60, _, 30, 40, 50] | — |
enqueue(70) | (2 + 4) % 5 = 1 | 2 | 5 | [60, 70, 30, 40, 50] | — |
dequeue() | 2 | 3 | 4 | [60, 70, _, 40, 50] | 30 |
dequeue() | 3 | 4 | 3 | [60, 70, _, _, 50] | 40 |
dequeue() | 4 | 0 | 2 | [60, 70, _, _, _] | 50 |
dequeue() | 0 | 1 | 1 | [_, 70, _, _, _] | 60 |
dequeue() | 1 | 2 | 0 | [_, _, _, _, _] | 70 |
The first five rows match the simple array queue: three insertions fill slots 0–2, and two removals return 10 and 20 and move front to 2. Only front moves.
The difference appears at enqueue(60): the insertion point (2 + 3) % 5 = 0 has wrapped. Where the simple array queue would say "full," the circular queue reuses slot 0. enqueue(70) goes to slot 1 and size reaches 5; now the queue is truly full, with no slot wasted.
The next write position of the full queue, (2 + 5) % 5 = 2, is front itself; judged by indices alone the queue might look empty, but size == 5 settles it.
Five removals then return 30, 40, 50, 60, 70. On the third, front becomes (4 + 1) % 5 = 0, so the front wraps too. After the last, size = 0. The output order equals the input order: the circular layout keeps FIFO intact.
front; ▲avail marks the next write position. (a) After three insertions. (b) After two removals, front = 2. (c) enqueue(60) has wrapped to slot 0. (d) After enqueue(70) the queue is full and avail meets front. (e) After three removals front has wrapped too: (4 + 1) % 5 = 0.8.2.7Code: the CircularQueue class
The class below holds the fields, constructor, and status queries of a counter-based circular queue; insertion and removal follow in Section 8.2.8.
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()
}Line by line:
dataholds the items,frontis the index of the first, andsizeis the count; both start at 0 because a new queue is empty.- The constructor allocates the array. Java forbids
new E[capacity], becauseEis unknown at run time, so we create anObjectarray and cast it with(E[]).@SuppressWarnings("unchecked")silences the compiler's unchecked-cast warning for the constructor only; the cast is safe as long as the array never leaves the class. size()returns the count;isEmpty()testssize == 0, andisFull()testssize == data.length, the capacity.first()returnsnullfor an empty queue anddata[front]otherwise, changing nothing.? :is the conditional operator: the first value if the condition holds, the second if not.
The rear index is computed, (front + size) % data.length, rather than stored. Storing both rear and size would oblige every method to update both correctly, and a method that forgot one would leave them contradicting each other. Keeping each fact in one place removes that risk.
No method loops, so each is O(1). The array has as many slots as the capacity, so space is O(N) however many items are stored.
8.2.8Code: enqueue() and dequeue()
Here are the class's two basic methods:
public void enqueue(E item) { // add at the rear
if (isFull())
throw new IllegalStateException("full");
int avail = (front + size) % data.length;
data[avail] = item;
size++;
}public E dequeue() { // remove from the front
if (isEmpty())
throw new IllegalStateException("empty");
E item = data[front];
data[front] = null; // drop the reference
front = (front + 1) % data.length;
size--;
return item;
}enqueue first checks for a full queue and throws IllegalStateException("full"), the code's way of reporting overflow. Otherwise it computes the insertion point into avail, writes the item, and increments size. front is untouched: adding at the rear does not affect the front.
dequeue throws IllegalStateException("empty") on underflow. Otherwise it saves the front item in item, writes null into the slot, advances front, decrements size, and returns the saved item.
data[front] = null; is not needed for correctness: dequeue returns the right item without it, and the slot is overwritten on the next lap anyway. It is about memory. The garbage collector frees only objects no reference points to, so an object that has left the queue but still sits in the array stays alive. With large items (images, file buffers) this becomes a real memory leak.
Line order matters too. item = data[front] must come before the null assignment, or the return value is lost, and front must advance after it, or the wrong slot is cleared.
The two formulas at their boundary cases:
- Insertion point: With N = 5,
front = 3,size = 2, the items are in slots 3 and 4, andavail = (3 + 2) % 5 = 5 % 5 = 0: the new item wraps to slot 0. - Advancing the front: With N = 5 and
front = 4, the front item is in the last slot; after a removalfront = (4 + 1) % 5 = 0, so the front wraps too.
Each is one addition and one mod, with no loop or shifting: both methods are O(1).
8.2.9Code trace: enqueue()
We trace enqueue through a wrap. Initially data.length = 5, front = 2, size = 2, with 30 in slot 2 and 40 in slot 3; we add 50, 60, 70, and 80 using this method:
public void enqueue(E item) {
if (isFull())
throw new IllegalStateException("full");
int avail = (front + size) % data.length;
data[avail] = item;
size++;
}| Call | isFull() | avail | size (after) | Array (after) |
|---|---|---|---|---|
| (start) | — | — | 2 | [_, _, 30, 40, _] |
enqueue(50) | 2 == 5? no | (2 + 2) % 5 = 4 | 3 | [_, _, 30, 40, 50] |
enqueue(60) | 3 == 5? no | (2 + 3) % 5 = 0 | 4 | [60, _, 30, 40, 50] |
enqueue(70) | 4 == 5? no | (2 + 4) % 5 = 1 | 5 | [60, 70, 30, 40, 50] |
enqueue(80) | 5 == 5? yes | — | 5 | unchanged; exception: "full" |
For enqueue(50), size (2) is below the capacity (5). The insertion point is (2 + 2) % 5 = 4 — here mod changes nothing — so data[4] = 50 and size becomes 3. front stays at 2.
For enqueue(60) the point is (2 + 3) % 5 = 5 % 5 = 0: the index wraps, and 60, logically last in the queue, sits physically first in the array. enqueue(70) writes to (2 + 4) % 5 = 1 and raises size to 5.
For enqueue(80) the check gives 5 == 5: IllegalStateException("full") is thrown and nothing changes. The capacity ran out at exactly 5 items, none wasted.
enqueue trace. (a) Start: 30 and 40, front = 2, avail = 4. (b) enqueue(60): avail = (2 + 3) % 5 = 0, a wrap. (c) After enqueue(70): full, size = 5, and avail lands on front.8.2.10Code trace: dequeue()
Starting from the full queue left by the previous trace (front = 2; 30, 40, 50, 60, 70 from front to rear), we call dequeue() three times.
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;
}| Call | item | Slot cleared | front (after) | size (after) | Array (after) |
|---|---|---|---|---|---|
| (start) | — | — | 2 | 5 | [60, 70, 30, 40, 50] |
1st dequeue() | 30 | 2 | (2 + 1) % 5 = 3 | 4 | [60, 70, _, 40, 50] |
2nd dequeue() | 40 | 3 | (3 + 1) % 5 = 4 | 3 | [60, 70, _, _, 50] |
3rd dequeue() | 50 | 4 | (4 + 1) % 5 = 0 | 2 | [60, 70, _, _, _] |
Each call takes the same five steps. isEmpty() is false, so item = data[front] takes the oldest item (data[2] = 30 on the first call); data[2] = null clears the slot; front advances to (2 + 1) % 5 = 3; size drops to 4; and 30 is returned.
On the third call the front item is in the last slot, data[4] = 50, and front wraps: (4 + 1) % 5 = 5 % 5 = 0. Left are 60 (slot 0) and 70 (slot 1), now at the front of the queue both physically and logically.
No item moves and no loop runs: each call is O(1).
dequeue trace. (a) First call: item = 30 taken and data[2] = null done; front and size not yet updated. (b) End of the first call: front = 3, size = 4, 30 returned. (c) After three calls: front = 0, size = 2; 60 and 70 remain.8.2.11Growing a full circular queue
A fixed-capacity queue fails when full. If the capacity is unknown in advance, an insertion into a full queue can instead allocate an array twice as large and move the items over:
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++;
}resize copies the items to the start of the new array in logical order: item k, at old slot (front + k) % data.length, goes to new slot k. Then the old array is dropped and front becomes 0, since the front is now at the start. On a full queue, enqueue doubles the capacity first and then writes as usual.
Take a full queue of capacity 4 with front = 2: logically A, B, C, D, but physically C, D, A, B, since it has wrapped. Now enqueue(E) is called.
| Step | Computation | New array (after) |
|---|---|---|
| Full? | size == data.length → 4 == 4, full | — |
| New array | new Object[8] | [_, _, _, _, _, _, _, _] |
k = 0 | temp[0] = data[(2 + 0) % 4] = data[2] = A | [A, _, _, _, _, _, _, _] |
k = 1 | temp[1] = data[(2 + 1) % 4] = data[3] = B | [A, B, _, _, _, _, _, _] |
k = 2 | temp[2] = data[(2 + 2) % 4] = data[0] = C | [A, B, C, _, _, _, _, _] |
k = 3 | temp[3] = data[(2 + 3) % 4] = data[1] = D | [A, B, C, D, _, _, _, _] |
data = temp; front = 0 | the front is now in slot 0 | [A, B, C, D, _, _, _, _] |
| Write | (0 + 4) % 8 = 4 → data[4] = E; size = 5 | [A, B, C, D, E, _, _, _] |
At k = 2 the mod kicks in, (2 + 2) % 4 = 0: the copy wraps around the old array and places C and D right behind A and B. The queue now lies in order from slot 0 with front = 0, and E goes to slot (0 + 4) % 8 = 4.
front = 2, logical order A, B, C, D. (b) Step k = 2: C, from data[0], goes to slot 2 of the new array. (c) After the resize and the write: A, B, C, D, E; front = 0, size = 5, N = 8.Growing with Arrays.copyOf(data, 2 * data.length). It copies each item to the same index, which works only if the queue has not wrapped. Above, it would give [C, D, A, B, _, _, _, _] with front = 2: after A and B the queue would take the empty slots 4 and 5 for items, and the wrapped C and D would drop out. Copy in (front + k) % N order and reset front to 0.
A resize copies every item and so costs O(n), but doubling makes resizes rare, so it is more telling to look at the total cost of a long sequence of operations. This is amortized analysis.
If a circular queue doubles its capacity whenever it is full, n insertions into an initially empty queue make fewer than 3n writes and copies in total, so enqueue costs O(1) amortized.
Let the initial capacity be 1; a larger one only means fewer copies. Resizes occur when the queue holds 1, 2, 4, …, 2k items, where 2k is the largest power of two below n, so the copies total 1 + 2 + 4 + … + 2k = 2k+1 − 1. Since 2k < n, this is under 2n − 1, hence under 2n. Adding the n writes, the cost stays below 2n + n = 3n: fewer than 3 per insertion, a constant. Interleaved removals shrink the queue and so cannot add copies. □
For example, add 17 items to an empty queue of capacity 4 with no removals. Resizes at the 5th, 9th, and 17th insertions copy 4 + 8 + 16 = 28 items; with 17 writes the total is 45, below 3 · 17 = 51. The theorem does not promise that every insertion is cheap: the 17th alone made 16 copies, and a single insertion can take O(n). Real-time systems, where every operation must be quick, need a worst-case bound, not an amortized one.
Growing by a fixed amount (say, 10 slots) instead of doubling. Resizes then come at 10, 20, 30, … items, the copies over n insertions add up to about n2/20, and the total is O(n2), O(n) per insertion. Amortized O(1) requires growing by a constant factor.
8.3Linked queues
An array-based queue has a preset capacity and can grow only by copying. A linked list creates a node per arriving item and never copies. This section builds a queue on a singly linked list and compares the two implementations.
8.3.1The linked representation
Each item is a node holding the item's value (data) and a reference to the next node (next). The queue keeps front (first node), rear (last node), and the count size.
class Node {
int data;
Node next;
Node(int data) {
this.data = data;
}
}
// fields:
Node front, rear;
int size;Insertion happens at the end of the list and removal at its head. Each insertion allocates a node and each removal releases one, so there is no capacity limit and no "full queue" while memory lasts. The price is an extra reference per node and a new object per insertion.
front points to the first node (10), rear to the last (15), whose next is null.Why not the other way round? In a singly linked list, deleting the head is a single O(1) assignment, front = front.next, and adding at the end is easy thanks to rear: link the new node to rear.next and move rear to it. Deleting the last node, however, means finding the node before it, the new last node, and with no backward links that takes a walk from the head, O(n). With the ends swapped, dequeue would be O(n).
Keeping no rear reference. Every enqueue then walks the list to find the last node, costing O(n), and filling a queue with n items costs O(n2). Two O(1) ends need two references.
8.3.2The enqueue and dequeue algorithms
In any implementation the two operations follow the same steps:
enqueue(x)
- Is the queue full? (Asked only for a fixed-capacity array.)
- If so, report overflow and stop.
- Otherwise, write
xat the rear position. - Advance the rear to the next position.
- Increment the item count and return successfully.
dequeue()
- Is the queue empty?
- If so, report underflow and stop.
- Otherwise, take the front item.
- Advance the front to the next item.
- Decrement the item count and return the item taken.
Table 8.10 shows the corresponding code in the two implementations.
| 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 (as long as memory lasts) |
In the circular array, advancing the rear is not a separate step: incrementing size moves the next write position. In the linked list, writing and advancing are two assignments: link the old last node to the new one, then move rear.
8.3.3Code trace: enqueue
The linked queue's enqueue:
public void enqueue(int data) {
Node temp = new Node(data);
if (isEmpty()) {
front = temp;
} else {
rear.next = temp;
}
rear = temp;
size++;
}new Node(data) returns an unlinked node holding the value, with next equal to null. If the queue is empty, the new node is also the first, so front points to it; otherwise the old last node's next is set to it. Either way it is now the last node, which is why rear = temp stands outside the if/else. Finally size grows by one.
Apply enqueue(10), enqueue(15), and enqueue(20) to an empty queue:
| Call | isEmpty() | Branch taken | List (after) | front | rear | size |
|---|---|---|---|---|---|---|
| (start) | — | — | empty | null | null | 0 |
enqueue(10) | true | front = temp | 10 | 10 | 10 | 1 |
enqueue(15) | false | rear.next = temp | 10 → 15 | 10 | 15 | 2 |
enqueue(20) | false | rear.next = temp | 10 → 15 → 20 | 10 | 20 | 3 |
On the first call the queue is empty: front = temp and rear = temp make node 10 both front and rear.
On the second call the else branch links the old last node (10) to the new one (15) with rear.next = temp. This must happen before rear changes, since rear is the only way to reach the old last node. Then rear = temp moves the rear to 15; front stays. The third call repeats this for 20.
No call walks the list: each creates one node, assigns at most two references, and increments a counter, O(1).
enqueue trace. (a) After the first call, node 10 is both front and rear. (b) Second call, rear.next = temp: 10 links to 15, but rear is still on 10. (c) rear = temp: the rear moves to 15. (d) After three calls: 10 → 15 → 20.Putting rear = temp only in the else branch, or leaving it out. After the first insertion front points to the node but rear stays null, and the second insertion's rear.next throws NullPointerException. The new node is last in both cases, so the assignment belongs after the if/else.
8.3.4Code trace: dequeue
The linked queue's dequeue:
public int dequeue() {
if (isEmpty())
throw new NoSuchElementException();
int result = front.data;
front = front.next;
if (front == null)
rear = null;
size--;
return result;
}An empty queue throws NoSuchElementException. Otherwise the front value goes into result and front moves to the next node; the old first node is now unreferenced and the garbage collector reclaims it. if (front == null) rear = null; handles the edge case: if the removed node was the only one, the queue is now empty and rear must be cleared too. Finally size drops and result is returned.
Call dequeue() three times on the queue 10 → 15 → 20 from the previous trace:
| Call | result | front (after) | front == null? | rear (after) | size | List (after) |
|---|---|---|---|---|---|---|
| (start) | — | 10 | — | 20 | 3 | 10 → 15 → 20 |
1st dequeue() | 10 | 15 | false | 20 | 2 | 15 → 20 |
2nd dequeue() | 15 | 20 | false | 20 | 1 | 20 |
3rd dequeue() | 20 | null | true | null | 0 | empty |
The first two calls take the front value and move front one node on; since front is not null, rear is untouched. Afterward both point to node 20.
The third call is the edge case. result = 20, and front = front.next makes front null while rear still points to the deleted node 20. front == null holds, so rear = null runs. Without it, an empty queue would keep rear on a deleted node: rear() would return a value not in the queue, and the node could not be freed.
The calls return 10, 15, 20, the insertion order, each in O(1).
dequeue trace. (a) First call, front = front.next: the front moves to 15 and node 10 is detached. (b) Third call: front is null, but rear still points to the deleted node 20. (c) After rear = null both ends are null: the queue is consistently empty.Not resetting rear when the last item leaves. The next enqueue sees an empty queue and resets both ends, so the bug often goes unnoticed — until rear() on an empty queue returns a wrong value. Bugs that appear only sometimes are the hardest to find.
8.3.5Circular array or linked list?
Both implementations have O(1) basic operations; memory use and practical speed decide between them.
A circular array stores items contiguously. The processor reads memory in blocks, so contiguous data is faster to access than scattered nodes; it is cache-friendly, and it needs no per-item reference. But the capacity is fixed: when it runs out, the queue reports overflow or copies into a bigger array, a copy that is O(n) alone and O(1) amortized. The array also occupies its full capacity even when nearly empty.
A linked list has no capacity limit, uses memory in proportion to its items, and makes every operation O(1) in the worst case, since nothing is ever copied in bulk. But each node carries an extra reference and each insertion creates an object, and allocation plus later garbage collection cost more than writing an array slot.
| 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 item count) |
In practice the circular array usually wins, which is why Java's ArrayDeque uses it. A linked list pays off when every operation must be guaranteed fast or when the queue's length swings widely.
8.4Kinds of queues and the deque
The basic queue fixes one end for insertion and one for removal. Other kinds of queues relax or change that rule; this section surveys them and studies the most general, the double-ended queue.
8.4.1Kinds 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 |
The circular queue defines no new behavior: it is an implementation of the simple queue, and its users see no difference. The other kinds change the ADT.
The double-ended queue, which inserts and removes at both ends, is the most general; input-restricted and output-restricted queues limit it. An input-restricted queue inserts only at the rear but removes at either end, so it can act as a queue (removing at the front) or a stack (removing at the rear). An output-restricted queue accepts items at both ends but removes only at the front: an urgent job can jump to the front, an ordinary one joins at the back.
A priority queue ignores arrival order: removal always yields the item of highest priority, as an emergency room sees patients by urgency rather than arrival. It is usually built on a heap and studied separately; despite its name, it is not FIFO.
Confusing deque with dequeue. Deque, short for double-ended queue and pronounced "deck," is a data structure; dequeue is an operation.
Taking Java's PriorityQueue for FIFO. In Queue<Integer> q = new PriorityQueue<>(); the variable's type is Queue, but the object is a priority queue: after q.offer(5); q.offer(1); q.offer(3);, poll() returns 1, then 3, because under natural ordering the smallest item has the highest priority.
8.4.2The double-ended queue (deque)
A double-ended queue (deque) is an abstract data type that keeps items in a sequence and allows insertion and removal at both the front and the rear.
A deque is not bound to FIFO; its behavior depends on the ends its client uses. Adding and removing at the rear only makes it a stack (LIFO); adding at the rear and removing at the front makes it a queue (FIFO). It subsumes both, which is why Java recommends ArrayDeque for either role.
Figure 8.16 shows the flexibility. If 20 came first, then 21 and 22 may have gone to the front, 23 and 24 to the rear, 26 and 27 to the front, and 25 to the rear. The result is neither arrival order nor its reverse; each item's place depends on the end it entered.
Table 8.15 lists our operation names and their ArrayDeque counterparts.
| 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) |
ArrayDeque also has stack operations: push(e) adds at the front (addFirst) and pop() removes there (removeFirst); its queue operations offer and poll are offerLast and pollFirst. Mixing them on one object confuses: push puts an item at the front, offer at the rear.
A deque can use a circular array — inserting at the front steps back with front = (front − 1 + N) % N and writes there, and removing at the rear just decrements size — as ArrayDeque does. These notes use the other option, a doubly linked list.
A deque can test whether a word is a palindrome, reading the same backward: add the letters at the rear, then remove one from each end at a time and compare.
boolean isPalindrome(String s) {
Deque<Character> d = new ArrayDeque<>();
for (char c : s.toCharArray())
d.offerLast(c);
while (d.size() > 1)
if (!d.pollFirst().equals(d.pollLast()))
return false;
return true;
}"kayak": The deque holds k, a, y, a, k. Round one removes k and k, round two a and a; both match. Only y remains,size() > 1fails, and the result istrue."karma": Round one removes k and a, which differ, so the method returnsfalseat once.
Each letter is added once and removed at most once: O(n) time and O(n) extra space. The comparison uses equals, because pollFirst() and pollLast() return Character objects and != would compare identities, not values.
8.4.3Deque operations step by step
Our deque is a doubly linked list: each node has prev and next references, and the deque keeps head (first node), tail (last node), and size.
Apply addLast(5), addLast(8), addFirst(3), addLast(9), removeLast(), and removeFirst() to an empty deque:
| Operation | References changed | List (after) | size | Returns |
|---|---|---|---|---|
| (start) | — | empty; head = tail = null | 0 | — |
addLast(5) | head = tail = node | 5 | 1 | — |
addLast(8) | node.prev = tail; tail.next = node; tail = node | 5 ⇄ 8 | 2 | — |
addFirst(3) | node.next = head; head.prev = node; head = node | 3 ⇄ 5 ⇄ 8 | 3 | — |
addLast(9) | (add at the rear) | 3 ⇄ 5 ⇄ 8 ⇄ 9 | 4 | — |
removeLast() | tail = tail.prev; tail.next = null | 3 ⇄ 5 ⇄ 8 | 3 | 9 |
removeFirst() | head = head.next; head.prev = null | 5 ⇄ 8 | 2 | 3 |
The first insertion is special: in an empty list the new node is both head and tail. Later a new node is linked to the end node in both directions; adding at the rear sets the new node's prev to the old tail and the old tail's next to the new node, and adding at the front mirrors this.
With both ends referenced, every insertion is O(1). The removals show what prev is worth: removeLast() finds the new tail in one step, tail.prev, where a singly linked list would need an O(n) walk from the head to the next-to-last node. The price is one more reference per node.
addLast calls: 5 ⇄ 8, each link two-way. (b) After addFirst(3) and addLast(9): 3 ⇄ 5 ⇄ 8 ⇄ 9, size = 4. (c) removeLast() has returned 9; tail is back on 8.8.4.4Code: adding at either end
The node class:
class Node<E> {
E data;
Node<E> prev, next;
Node(E data) { this.data = data; }
}The insertion methods:
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++;
}Both methods create a node with prev and next set to null. In an empty deque it becomes head and tail at once: head = tail = node; runs right to left, assigning tail first and then head. Otherwise three assignments follow.
In addFirst the new node points to the old head (node.next = head), the old head points back (head.prev = node), and the head moves (head = node). The order matters: doing head = node first would lose the old head before it could be linked.
Swapping head with tail and prev with next turns addFirst into addLast. Neither loops; both are O(1).
8.4.5Code trace: addFirst()
Apply addFirst(5) and then addFirst(3) to an empty deque; 3 should end up in front.
| Line | Effect | head | tail | size |
|---|---|---|---|---|
addFirst(5) | ||||
new Node<>(5) | prev = next = null | null | null | 0 |
isEmpty() | true | null | null | 0 |
head = tail = node | a single node forms both ends | 5 | 5 | 0 |
size++ | 5 | 5 | 1 | |
addFirst(3) | ||||
new Node<>(3) | prev = next = null | 5 | 5 | 1 |
isEmpty() | false | 5 | 5 | 1 |
node.next = head | the new node points to the old head (5) | 5 | 5 | 1 |
head.prev = node | back link: 5 points to the new node | 5 | 5 | 1 |
head = node | the head reference moves to the new node | 3 | 5 | 1 |
size++ | 3 | 5 | 2 | |
The list is now 3 ⇄ 5, read 3, 5 from head to tail and 5, 3 from tail to head. Two-way traversal relies on both node.next = head and head.prev = node. Nothing is traversed: O(1).
addFirst(3). (a) node.next = head: the new node points to 5, which does not point back yet. (b) head.prev = node: the link is two-way; head is still on 5. (c) After head = node and size++: 3 ⇄ 5, size = 2.8.4.6Code: 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;
}On an empty deque both methods return null, as in Goodrich's interface. Otherwise the end value goes into data and the end reference moves one node inward: head = head.next or tail = tail.prev.
If that reference is now null, the removed node was the only one, so the other end is set to null too. Otherwise the new end's outward link is cut (head.prev = null or tail.next = null), so the list no longer refers to the removed node. Then size drops and the value is returned.
Not clearing both ends when the last item goes. If removeLast omits head = null, head still points to the deleted node while tail is null: the node cannot be freed, and first() returns a value not in the deque. It is the linked queue's rear = null bug again.
8.4.7Code trace: removeLast()
Call removeLast() twice on the deque 3 ⇄ 5: 5 comes out, then 3, leaving it empty.
| Line | Effect | head | tail | size |
|---|---|---|---|---|
1st removeLast() | ||||
isEmpty() | false (size 2) | 3 | 5 | 2 |
data = tail.data | data = 5 | 3 | 5 | 2 |
tail = tail.prev | the tail steps back to 3 | 3 | 3 | 2 |
tail == null? | false: the deque is not empty | 3 | 3 | 2 |
tail.next = null | the link to 5 is cut | 3 | 3 | 2 |
size--; return | returns 5 | 3 | 3 | 1 |
2nd removeLast() | ||||
isEmpty() | false (size 1) | 3 | 3 | 1 |
data = tail.data | data = 3 | 3 | 3 | 1 |
tail = tail.prev | there was a single node: tail = null | 3 | null | 1 |
tail == null? | true: the deque is now empty | 3 | null | 1 |
head = null | both ends null | null | null | 1 |
size--; return | returns 3 | null | null | 0 |
On the first call, tail = tail.prev finds the next-to-last node without searching. tail is not null, so the else branch cuts the link to node 5 with tail.next = null, detaching it.
On the second call the single node's prev is null, so tail becomes null while head still points to node 3. tail == null holds and head = null runs; without it head would keep pointing to the deleted node. Both calls are O(1).
removeLast trace. (a) First call, tail = tail.prev: head and tail on 3, node 5 still linked. (b) tail.next = null: the link to 5 is cut. (c) Second call: tail is null but head still points to 3; the next line clears it.8.4.8Sliding window maximum
A classic problem that needs both ends of a deque: slide a window of width k across an array a from left to right and report the largest value at each position. This is a sliding window problem. For a = {1, 3, −1, −3, 5, 3, 6} and k = 3 the five windows {1, 3, −1}, {3, −1, −3}, {−1, −3, 5}, {−3, 5, 3}, {5, 3, 6} have maxima 3, 3, 5, 5, 6.
Brute force scans k items in each of the n − k + 1 windows, O(n·k), which reaches O(n2) for k = n/2. Doing better means reusing earlier work. The key fact: an item with a greater or equal item to its right can never again be a window maximum, since it leaves the window first while the larger item stays. Such items can be discarded at once.
The algorithm keeps candidate indices in a deque whose values decrease from front to rear, so the maximum is always in front. For each new index i:
- Drop from the front the front index if it has left the window (≤ i − k).
- Drop from the rear every index whose value is at most
a[i]; it is no longer a candidate. - Add i at the rear. From the first full window on (i ≥ k − 1), the value at the front index is the window's maximum.
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(); // outside the window
while (!d.isEmpty() && a[d.peekLast()] <= a[i])
d.pollLast(); // no longer a candidate
d.offerLast(i);
if (i >= k - 1) maxes[i - k + 1] = a[d.peekFirst()];
}
return maxes;
}d holds indices, and values are read as a[d.peekFirst()] and a[d.peekLast()]. Whether an item has left the window depends on its position; with values alone we could not tell which of several equal values had left. maxes holds the n − k + 1 results, the window ending at i writing to maxes[i − k + 1].
| i | a[i] | Dropped | d (after) | maxes |
|---|---|---|---|---|
| 0 | 1 | — | 0:1 | — |
| 1 | 3 | from the rear: 0:1 | 1:3 | — |
| 2 | −1 | — | 1:3, 2:−1 | [3] |
| 3 | −3 | — | 1:3, 2:−1, 3:−3 | [3, 3] |
| 4 | 5 | from the front: 1:3 (outside the window); from the rear: 3:−3, 2:−1 | 4:5 | [3, 3, 5] |
| 5 | 3 | — | 4:5, 5:3 | [3, 3, 5, 5] |
| 6 | 6 | from the rear: 5:3, 4:5 | 6:6 | [3, 3, 5, 5, 6] |
The key step is i = 4. The window covers indices 2–4, so the front index 1 (1 ≤ 4 − 3 = 1) is dropped. The incoming 5 beats −3 and −1 at the rear, which can never be maxima again, so they go too, leaving 4:5. At i = 3, by contrast, −3 was added and nothing dropped: small as it is, −3 could still become the maximum once 3 and −1 left.
d = 1:3, 2:−1, 3:−3, values decreasing. (b) i = 4: 1 dropped from the front, 3 and 2 from the rear; only 4:5 remains. (c) i = 6: 6 has dropped both rear candidates; result [3, 3, 5, 5, 6].On an array of n items, windowMax runs in O(n) time with O(k) extra space.
The inner while loop may run often for one i, so counting per loop misleads. Count per index instead: each enters the deque exactly once (offerLast) and leaves at most once, from the front (pollFirst) or the rear (pollLast). That is n insertions and at most n removals, each O(1), so O(n) in all. Only indices inside the window can be in the deque, so the extra space is at most k. □
This counting trick — each item enters once and leaves at most once — recurs throughout queue and stack algorithms; the amortized analysis of the two-stack queue uses it too.
Forgetting to flip the comparison when adapting the code for the sliding window minimum. Then the values must increase from front to rear, and the rear-drop test becomes a[d.peekLast()] >= a[i].
8.5Applications of queues
In the first three problems below, a queue cycles through work fairly or walks a structure level by level; the last builds a queue from stacks.
8.5.1A round-robin scheduler
Many processes want to run, but a processor core runs one at a time. The scheduler shares the processor among them, and one of the simplest and fairest methods is round robin.
Ready processes wait in a FIFO ready queue. The front process runs for at most q time units, the time slice or quantum. If it finishes, it leaves; if not, it returns to the rear with its remaining time. No process can monopolize the processor, and each runs again within one round.
The ready queue holds P1, P2, P3 with 5, 3, and 2 units left, and q = 2. When does each finish?
- t = 0–2: P1 runs 2 units; 3 left, back to the rear. Queue: P2, P3, P1.
- t = 2–4: P2 runs 2 units; 1 left. Queue: P3, P1, P2.
- t = 4–6: P3's 2 units fit in one slice; it finishes at t = 6. Queue: P1, P2.
- t = 6–8: P1 runs 2 more units; 1 left. Queue: P2, P1.
- t = 8–9: P2's last unit; it finishes at t = 9, and the processor moves on without waiting out the slice. Queue: P1.
- t = 9–10: P1 finishes at t = 10; the queue is empty.
Completion times: P3 = 6, P2 = 9, P1 = 10. The shortest process finished first and the longest last, though P1 had entered first.
Performance hinges on q. Too large, and every process finishes in one slice: the method becomes first-come, first-served, with short processes stuck behind long ones. Too small, and the processor switches constantly; each context switch has a cost, so much of its time goes to switching instead of work. In practice q is long enough for most processes to finish an interactive task in one slice.
A process's waiting time is its completion time minus its running time: 10 − 5 = 5 for P1, 9 − 3 = 6 for P2, 6 − 2 = 4 for P3, an average of 15/3 = 5. Run back to back (P1, P2, P3), they would finish at 5, 8, and 10 and wait 0, 5, and 8, an average of 13/3 ≈ 4.33. So round robin does not always lower the average wait; what it buys is that no process waits long for its first turn — all three ran within the first 6 units.
8.5.2Code trace: roundRobin()
The scheduler's code is short. A process is represented by this class:
class Process {
String name; // process name (P1, P2, …)
int remaining; // remaining running time
Process(String name, int remaining) { this.name = name; this.remaining = remaining; }
}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);
}
}t is the elapsed time, and the loop runs until the queue is empty. Each round takes the front process with poll() and runs it for run = Math.min(quantum, p.remaining), so a process about to finish runs only for what it needs. t grows and p.remaining shrinks by run; a process with time left returns to the rear with offer(p), and a finished one has its completion time printed.
| Round | p | run | t | p.remaining | Outcome | Queue (after) |
|---|---|---|---|---|---|---|
| (start) | — | — | 0 | — | — | P1:5, P2:3, P3:2 |
| 1 | P1 | min(2, 5) = 2 | 2 | 3 | back to the rear | P2:3, P3:2, P1:3 |
| 2 | P2 | min(2, 3) = 2 | 4 | 1 | back to the rear | P3:2, P1:3, P2:1 |
| 3 | P3 | min(2, 2) = 2 | 6 | 0 | "P3 t=6" | P1:3, P2:1 |
| 4 | P1 | min(2, 3) = 2 | 8 | 1 | back to the rear | P2:1, P1:1 |
| 5 | P2 | min(2, 1) = 1 | 9 | 0 | "P2 t=9" | P1:1 |
| 6 | P1 | min(2, 1) = 1 | 10 | 0 | "P1 t=10" | empty |
The code repeats the hand computation. In rounds 5 and 6, run is 1 because the remaining time is shorter than the slice. The output is "P3 t=6", "P2 t=9", "P1 t=10".
A round makes one poll and at most one offer, so it is O(1). A process of length s takes ⌈s/q⌉ rounds; here ⌈5/2⌉ + ⌈3/2⌉ + ⌈2/2⌉ = 3 + 2 + 1 = 6.
roundRobin trace. (a) End of round 3: P3 done (t = 6); P1 and P2 remain. (b) End of the loop: the queue is empty, and the timeline reads P1, P2, P3, P1, P2, P1.Advancing t by a full slice (t += quantum) even when less time remains. Every round then counts 2 units: P3 still ends at 6, but P2 seems to end at 10 instead of 9 and P1 at 12 instead of 10, because idle processor time is counted as work. The increment must be Math.min(quantum, p.remaining).
8.5.3The Josephus problem
The Josephus problem comes from an old puzzle. n people stand in a circle and count off from a given person; every kth person leaves the circle, and counting resumes with the next one. Who is the last one left?
A queue rotates the circle naturally: moving the front person to the rear advances the count by one. Each round moves k − 1 people to the rear and then eliminates the person at the front, the kth.
String josephus(Queue<String> q, int k) {
while (q.size() > 1) {
for (int i = 1; i < k; i++)
q.offer(q.poll()); // front to the rear
String out = q.poll();
System.out.println(out + " eliminated");
}
return q.poll();
}The for loop runs q.offer(q.poll()) for i = 1 to k − 1, moving the front person to the rear each time. Then poll() removes the front person for good and prints the name. The outer while runs while more than one person remains, and the survivor is returned.
Here is the trace for n = 5 (A, B, C, D, E) and k = 3.
| Round | Moved to the rear | Queue (before removal) | Eliminated | Queue (after) |
|---|---|---|---|---|
| (start) | — | — | — | A, B, C, D, E |
| 1 | A, B | C, D, E, A, B | C | D, E, A, B |
| 2 | D, E | A, B, D, E | A | B, D, E |
| 3 | B, D | E, B, D | E | B, D |
| 4 | B, D | B, D | B | D |
The elimination order is C, A, E, B; D survives. In round 4 both remaining people move to the rear, the count comes back to B, and B goes. As the circle shrinks the count may lap it several times, and the queue handles this with no special case.
Of n people, n − 1 are eliminated, each round costing k − 1 moves and one removal, k queue operations. The total is (n − 1) · k, or O(n·k); the example used 4 · 3 = 12.
If only the survivor matters, a recurrence suffices. Numbering from 0, J(1) = 0 and J(n) = (J(n − 1) + k) % n; for k = 3, J(2) = 1, J(3) = 1, J(4) = 0, J(5) = 3, and person 3 is the fourth, D. This runs in O(n) but gives no elimination order, which the queue plays out step by step.
8.5.4Binary numbers from 1 to n
The task: list the binary representations of 1 to n in increasing order — {"1", "10", "11"} for n = 3, {"1", "10", "11", "100", "101"} for n = 5.
Converting each number separately works, but a queue is more elegant. Appending "0" to the binary form of x gives 2x, appending "1" gives 2x + 1: "10" (2) yields "100" (4) and "101" (5). This arranges the numbers in a tree rooted at "1," each node's children being the node with "0" and "1" appended.
A queue walks a tree level by level: take a string, write it to the result, add its two children at the rear. First in is first out, so each level is finished before the next begins, left to right. This is breadth-first traversal. Numbers on one level have equal length and grow left to right, and deeper ones are longer and thus larger, so the output is exactly increasing.
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;
}8.5.5Code trace: generateBinary(4)
The method creates a result array of n slots and an empty queue — a LinkedList, used only through Queue operations — and adds the root "1." Each of the n rounds removes the front string, writes it to the result, and adds its two children.
i | poll() | Added at the rear | Queue (after) | result |
|---|---|---|---|---|
| (start) | — | "1" | 1 | [_, _, _, _] |
| 0 | "1" | "10", "11" | 10, 11 | [1, _, _, _] |
| 1 | "10" | "100", "101" | 11, 100, 101 | [1, 10, _, _] |
| 2 | "11" | "110", "111" | 100, 101, 110, 111 | [1, 10, 11, _] |
| 3 | "100" | "1000", "1001" | 101, 110, 111, 1000, 1001 | [1, 10, 11, 100] |
The front string is always the smallest waiting. At i = 4, 4 < 4 fails, and the method returns {"1", "10", "11", "100"}. The five strings left in the queue go unused, a small waste that a counter limiting the insertions could avoid.
generateBinary(4) trace. (a) After round one: "1" is in the result, its children "10" and "11" in the queue. (b) End: result = {1, 10, 11, 100}; 101, 110, 111, 1000, 1001 remain queued.In queue operations the cost is n poll and 2n offer calls, O(n). But each operation builds a string of about log2 n characters, and concatenation is linear in length, so the character-level work is O(n log n). The queue holds at most about n + 1 strings.
8.5.6A queue from two stacks
A design question to finish: can stacks alone make a queue? One stack reverses order, so it cannot; but reversing twice restores the order, and that is the idea behind two stacks.
Insertions go onto inbox; removals come from outbox. enqueue pushes onto inbox. dequeue first checks outbox: if it is empty, every item in inbox is popped and pushed onto outbox, which reverses the order and puts the oldest item on top; then outbox is popped.
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();
}ArrayDeque serves as the stack, with push and pop at its head. On an empty queue both stacks are empty, the transfer moves nothing, and outbox.pop() throws NoSuchElementException: underflow is reported by exception.
Apply enqueue(1), enqueue(2), enqueue(3), dequeue(), enqueue(4), and three dequeue() calls; stacks are written bottom to top.
| Operation | Transfer | inbox (after) | outbox (after) | Returns |
|---|---|---|---|---|
enqueue(1) | — | 1 | empty | — |
enqueue(2) | — | 1, 2 | empty | — |
enqueue(3) | — | 1, 2, 3 | empty | — |
dequeue() | 3, 2, 1 moved (3 items) | empty | 3, 2 | 1 |
enqueue(4) | — | 4 | 3, 2 | — |
dequeue() | none (outbox not empty) | 4 | 3 | 2 |
dequeue() | none | 4 | empty | 3 |
dequeue() | 4 moved (1 item) | empty | empty | 4 |
On the first dequeue, outbox is empty. The 3 on top of inbox moves first, landing at the bottom of outbox, then 2, then 1, which ends on top and is popped. FIFO holds: the first item in is the first out.
enqueue(4) puts 4 into inbox and leaves 2 and 3 in outbox. The next two dequeue calls find outbox non-empty, so 2 and 3 come out without a transfer; 4 rightly waits, having arrived after them. The last dequeue finds outbox empty and transfers and removes 4. The output 1, 2, 3, 4 matches the input; 4 items were transferred in all.
enqueue calls, inbox holds 1, 2, 3 bottom to top. (b) First dequeue: the transfer reversed the order, 1 was popped, and 3, 2 remain in outbox. (c) enqueue(4): 4 goes into inbox; outbox is untouched.One dequeue can be costly: with outbox empty and n items in inbox, the transfer makes n pops and n pushes, O(n) in the worst case. But such calls cannot be frequent.
Starting from an empty queue, any sequence of m enqueue/dequeue operations costs O(m), so both operations are O(1) amortized.
Follow one item. It is pushed onto inbox once; in a transfer it is popped from inbox and pushed onto outbox once, and it never returns to inbox, so it is transferred at most once; finally it is popped from outbox once. That is at most 4 stack operations per item, and m operations add at most m items, so the total is at most 4m. □
The 8 operations of Table 8.23 made 4 pushes onto inbox, 4 transfers (a pop and a push each), and 4 pops from outbox: 4 + 8 + 4 = 16 stack operations, exactly the bound 4 · 4 = 16, since each item was transferred once.
Transferring on every dequeue, even when outbox is not empty. This is slow and wrong: in the example, 4 transferred right after enqueue(4) would sit on top of outbox and leave before 2. Transfer only when outbox is empty.
Conversely, one queue can make a stack. push(x) adds x and then moves every earlier item from the front to the rear, one at a time, bringing x to the front, so pop removes from the front. Here pop is O(1) and push O(n), and amortizing does not help, because every push rotates the whole queue.
8.6Common mistakes
Most queue bugs fall into a few types. The table collects the mistakes noted throughout the chapter; use it as a checklist when reviewing queue code.
| Mistake | What happens? | The fix |
|---|---|---|
Forgetting rear on the last item | In a linked queue, removing the last node makes front = null, but rear keeps pointing to the deleted node. | if (front == null) rear = null; |
| Full / empty confusion | With only front and rear, front == rear holds for both the empty and the full queue. | A size counter or one empty slot. |
| Forgetting the mod | In a wrapped queue the index runs off the array: ArrayIndexOutOfBoundsException. | (front + size) % N; going backward, (front − 1 + N) % N. |
Arrays.copyOf when growing | In a wrapped circular queue the logical order is scrambled and items are lost. | Copy items in (front + k) % N order and set front = 0. |
Mixing up add / offer, remove / poll | add, remove, and element throw; offer, poll, and peek return false or null. | If an empty queue is an expected case, use poll/peek and check for null. |
Taking PriorityQueue for FIFO | Queue<Integer> q = new PriorityQueue<>() returns items by priority, not by arrival. | For FIFO, use ArrayDeque or LinkedList. |
ArrayDeque and null | ArrayDeque rejects null items (NullPointerException); a null item would be confused with poll's "empty" result. | Never put null in a queue; if needed, use a separate "empty" object. |
The last row is a design decision: poll() returns null when empty, so a null item would make "empty" and "front item is null" indistinguishable. ArrayDeque forbids null; LinkedList allows it, so its poll results need more care.
The chapter's problems share a pattern. A queue is the right tool when the question is "Which waiting item has waited longest?": the process waiting longest for the processor, the next person in the Josephus count, the smallest number whose children are not yet generated. A stack asks the opposite: "Which unresolved item was seen most recently?" If items must be handled in arrival order, think queue; if both ends matter, as in the sliding window, think deque.
8.7Summary
- A queue inserts at the rear and removes at the front: FIFO. A good implementation makes
enqueue,dequeue,first/peek,size, andisEmptyall O(1). - Removing from an empty queue is underflow; adding to a full fixed-capacity one, overflow. In Java's
Queue,add/remove/elementthrow, whileoffer/poll/peekreturnfalseornull.ArrayDequeis array-based,LinkedListlinked. - A simple array queue cannot reuse slots freed at the front and can report "full" while nearly empty. Shifting fixes this but makes removal O(n).
- A circular queue advances indices with (i + 1) % N and reuses slots. With a counter, the insertion point is
(front + size) % N, empty issize == 0, and full issize == N; with one empty slot it holds at most N − 1 items and is full when(rear + 1) % N == front. - A full circular queue grows by copying into a double-size array in
(front + k) % Norder: one resize is O(n), but n insertions cost under 3n, amortized O(1) each.Arrays.copyOfscrambles a wrapped queue. - A linked queue inserts at the end and removes at the head through
frontandrear, both in worst-case O(1); the first insertion must setfront, and removing the last item must clearrear. - A deque inserts and removes at both ends: one end only makes it a stack, one end in and the other out a queue. In a doubly linked list,
prevmakes rear removal O(1); removing the last item must clear both ends. - Sliding window maximum takes O(n) with a deque of indices whose values decrease front to rear; each index enters once and leaves at most once.
- Round robin runs each process for at most q units and requeues unfinished ones at the rear. Josephus takes O(n·k), moving k − 1 people to the rear and eliminating the kth. Binary numbers come out in increasing order by queueing the children "x0" and "x1."
- A queue from two stacks pushes onto one stack and transfers to the other when it is empty. Each item moves at most once, so operations are amortized O(1), though one
dequeuecan be O(n).
| Implementation | Insert | Remove | Front item | Note |
|---|---|---|---|---|
| Simple array (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 + 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 transferred once |
8.8Key terms
Each term links to the course glossary, which gives its Turkish equivalent and its use in other chapters.
- queue
- FIFO (first-in, first-out)
- front
- rear
- enqueue
- dequeue
- abstract data type (ADT)
- interface
- generic
- overflow
- underflow
- array
- capacity
- circular queue
- amortized analysis
- singly linked list
- doubly linked list
- node
- deque (double-ended queue)
- priority queue
- sliding window
- scheduler
- round robin
- time slice (quantum)
- breadth-first search (BFS)
- stack
- time complexity
- space complexity
8.9Test Yourself
Starting empty, a queue receives
enqueue(5),enqueue(8),dequeue(),enqueue(2),first(),enqueue(6),dequeue(),dequeue(). What do the value-returning calls return, and what remains?Answer
dequeue()returns 5, leaving 8. Afterenqueue(2)the queue is 8, 2, andfirst()returns 8 without removing it. Afterenqueue(6)it is 8, 2, 6; the twodequeue()calls return 8 and 2. Returned: 5, 8, 8, 2; only 6 remains.A counter-based circular queue has N = 8,
front = 5,size = 6. Where are the items, where is the last one, and where does the next insertion go?Answer
The items are at (5 + k) % 8 for k = 0, …, 5: 5, 6, 7, 0, 1, 2. The last is at (5 + 6 − 1) % 8 = 2, and the next insertion goes to (5 + 6) % 8 = 3. Two more insertions fit (slots 3 and 4) before
size == 8.An empty counter-based circular queue of capacity 4 receives
enqueue(1),enqueue(2),enqueue(3),dequeue(),dequeue(),enqueue(4),enqueue(5),enqueue(6). What are the array,front, andsizeat the end, and what does a furtherenqueue(7)do?Answer
Slots 0–2 fill; the removals return 1 and 2, giving
front = 2,size = 1. Then 4 goes to (2 + 1) % 4 = 3, 5 to (2 + 2) % 4 = 0, 6 to (2 + 3) % 4 = 1. The array is[5, 6, 3, 4],front = 2,size = 4: the queue 3, 4, 5, 6 is full, andenqueue(7)throwsIllegalStateException("full").A full circular queue of capacity 3 has
data = [R, P, Q]andfront = 1. What are the array,front, andsizeafter a growingenqueue(S)?Answer
The logical order is P, Q, R. Since
size == 3,resize(6)runs:temp[0] = data[1] = P,temp[1] = data[2] = Q,temp[2] = data[(1 + 2) % 3] = data[0] = R, andfront = 0. S goes to (0 + 3) % 6 = 3, giving[P, Q, R, S, _, _],front = 0,size = 4.Suppose
if (front == null) rear = null;is deleted from the linked queue'sdequeue. Traceenqueue(7),dequeue(),enqueue(9)from an empty queue. When does the bug become visible?Answer
After
enqueue(7)both ends point to node 7.dequeue()returns 7 and nullsfront, butrearstays on node 7 withsize = 0.enqueue(9)finds the queue empty and moves bothfrontand (viarear = temp)rearto 9, so the final state is correct. The bug shows only ifrearis read while the queue is empty:rear()would return 7, which is not in the queue, and node 7 cannot be freed.On
Deque<Integer> d = new ArrayDeque<>();we calld.offerLast(1),d.offerFirst(2),d.push(3),d.offer(4),d.pollLast(),d.pop(). What do the last two calls return, and what does printingdshow?Answer
offerLast(1)→ [1];offerFirst(2)→ [2, 1];push(3)adds at the front → [3, 2, 1];offer(4)adds at the rear → [3, 2, 1, 4].pollLast()returns 4 → [3, 2, 1];pop()returns 3 from the front → [2, 1].System.out.println(d)prints[2, 1].What does
windowMax({2, 7, 3, 1, 5, 4}, 2)return, and how many times ispollLast()called?Answer
The result is
[7, 7, 3, 5, 5]. At i = 1, 7 drops 0:2 from the rear (1stpollLast); at i = 2, 3 is added; at i = 3, 1:7 leaves from the front and 1 is added; at i = 4, 2:3 leaves from the front and 5 drops 3:1 (2ndpollLast); at i = 5, 4 is added. TwopollLastand twopollFirstcalls in all, fewer than the 6 insertions.The ready queue holds P1 (3 units), P2 (4 units), and P3 (1 unit), and q = 2. When does each process finish under round robin, and what is the average waiting time?
Answer
0–2: P1 (1 left), queue P2, P3, P1. 2–4: P2 (2 left), queue P3, P1, P2. 4–5: P3 finishes. 5–6: P1 finishes. 6–8: P2 finishes. Completion: P3 = 5, P1 = 6, P2 = 8. Waiting times 6 − 3 = 3, 8 − 4 = 4, 5 − 1 = 4; average 11/3 ≈ 3.67 units.
For six people numbered 1 to 6, in what order does
josephus(q, 3)eliminate them, and whom does it return? Check the result with J(n) = (J(n − 1) + k) % n.Answer
The order is 3, 6, 4, 2, 5, and 1 survives. Numbering from 0: J(1) = 0, J(2) = 1, J(3) = 1, J(4) = 0, J(5) = 3, J(6) = (3 + 3) % 6 = 0; position 0 is person 1, so the methods agree. The queue performs 5 · 3 = 15 operations.
A two-stack queue receives
enqueue(a),enqueue(b),dequeue(),enqueue(c),enqueue(d),dequeue(),dequeue(),dequeue(). What do the removals return, which ones transfer, and how many items move in total?Answer
1st
dequeue:outboxis empty, so b and a move (2 items) and a is returned. c and d go intoinbox. 2nd:outboxholds b, no transfer; b is returned. 3rd:outboxis empty, d and c move (2 items), and c is returned. 4th: d. Output a, b, c, d; transfers on the 1st and 3rd calls, 4 items in all.
8.10Further reading
- M. T. Goodrich, R. Tamassia, and M. H. Goldwasser, Data Structures and Algorithms in Java (6th ed., 2014), Sections 6.2 and 6.3: the queue ADT, circular-array and linked implementations, and the deque, with a Java interface close to ours.
- R. Sedgewick and K. Wayne, Algorithms (4th ed., 2011), Section 1.3: linked and resizing-array queues in short, clear code, with iterators and generics.
- T. H. Cormen, C. E. Leiserson, R. L. Rivest, and C. Stein, Introduction to Algorithms (3rd ed., 2009), Section 10.1: a formal array implementation of the queue; the exercises include the two-stack queue and the deque.
- A. Silberschatz, P. B. Galvin, and G. Gagne, Operating System Concepts (10th ed., 2018), Chapter 5: CPU scheduling, including round robin, the choice of quantum, and criteria such as waiting time.
8.11References
- T. H. Cormen, C. E. Leiserson, R. L. Rivest, C. Stein, Introduction to Algorithms, 3rd ed., MIT Press, 2009.
- M. T. Goodrich, R. Tamassia, M. H. Goldwasser, Data Structures and Algorithms in Java, 6th ed., Wiley, 2014.
- R. Lafore, Data Structures and Algorithms in Java, 2nd ed., Sams, 2002.
- Oracle, Java SE 17 API Specification: documentation for
java.util.Queue,java.util.Deque,java.util.ArrayDeque, andjava.util.PriorityQueue. - R. Sedgewick, K. Wayne, Algorithms, 4th ed., Addison-Wesley, 2011.
- A. Silberschatz, P. B. Galvin, G. Gagne, Operating System Concepts, 10th ed., Wiley, 2018.
Related pages: Chapter 8 slides · Study questions · Glossary · © Sercan Külcü. All rights reserved.