Deques
Both ends
A stack works at one end. A queue adds at one end and removes at the other. A deque — short for double-ended queue, pronounced “deck” — does everything at both ends.
That makes it a superset. Any stack or queue can be built from a deque by simply choosing which methods you call. This is why the two previous lessons both used ArrayDeque.
Deque methods
| Front | Back | Behaviour when empty or absent |
|---|---|---|
addFirst | addLast | Throws if capacity-limited |
offerFirst | offerLast | Returns false |
removeFirst | removeLast | Throws |
pollFirst | pollLast | Returns null |
getFirst | getLast | Throws |
peekFirst | peekLast | Returns null |
Six operations in two families: the throwing kind and the returning kind. Choose one family per program.
Using one
import java.util.ArrayDeque;
import java.util.Deque;
Deque<String> deque = new ArrayDeque<>();
deque.addLast("middle");
deque.addFirst("start");
deque.addLast("end");
System.out.println(deque); // [start, middle, end]
System.out.println(deque.peekFirst()); // start
System.out.println(deque.peekLast()); // end
deque.removeFirst();
deque.removeLast();
System.out.println(deque); // [middle]
Adding at the front costs the same as adding at the back — both instant. That is the thing an ArrayList cannot do, where inserting at index 0 shifts every element.
As a stack, as a queue
// stack: add and remove at the same end
Deque<Integer> stack = new ArrayDeque<>();
stack.addFirst(1);
stack.addFirst(2);
System.out.println(stack.removeFirst()); // 2 — last in, first out
// queue: add at one end, remove at the other
Deque<Integer> queue = new ArrayDeque<>();
queue.addLast(1);
queue.addLast(2);
System.out.println(queue.removeFirst()); // 1 — first in, first out
Same class, same object type, different method pairing.
push and pop are front operations
Deque also provides push, pop, and peek for stack-style code. Be aware of what they map to:
push(x)isaddFirst(x)pop()isremoveFirst()peek()ispeekFirst()
So push adds at the front, not the back. If you mix push with addLast on the same deque you will get results that look random. Decide whether you are treating the object as a stack or a queue and use one vocabulary throughout.
Which implementation
| Class | Use when |
|---|---|
ArrayDeque | Default choice — fastest, least memory |
LinkedList | Only if you need null elements or List methods too |
ArrayBlockingQueue | Passing work between threads safely |
ArrayDeque stores elements in an array used as a circular buffer. Instead of shifting elements when the front is removed, it moves an internal index and wraps around at the end. That is how both ends stay instant while keeping array-like memory layout.
It rejects null, which is deliberate: the poll and peek methods use null to mean “empty”, so allowing null elements would make that signal ambiguous.
Where deques show up
A sliding window. Keeping the maximum of the last K values needs removal from both ends — new values push in at the back, stale ones drop off the front.
// indices of candidates for the maximum in each window of width k
Deque<Integer> window = new ArrayDeque<>();
for (int i = 0; i < values.length; i++) {
// drop indices that have fallen out of the window
while (!window.isEmpty() && window.peekFirst() <= i - k) {
window.removeFirst();
}
// drop values smaller than the incoming one — they can never be the max
while (!window.isEmpty() && values[window.peekLast()] <= values[i]) {
window.removeLast();
}
window.addLast(i);
if (i >= k - 1) {
System.out.println("window max: " + values[window.peekFirst()]);
}
}
Both ends are used for different purposes: the front discards expired entries, the back discards values that are now irrelevant. No other structure does this cleanly.
Undo and redo. Two deques, moving items between them.
Browser history. Back and forward, from either end.
A bounded buffer of recent readings. Add at the back, and drop from the front once you exceed the limit.
Deque<Double> recent = new ArrayDeque<>();
void record(double reading) {
recent.addLast(reading);
if (recent.size() > 50) {
recent.removeFirst(); // discard the oldest
}
}
That last pattern is genuinely common in robot code, where you want a rolling average of the last N sensor values without letting the buffer grow forever.
Common mistakes
- Mixing
push/popwithaddLast/removeLaston the same object. - Mixing the throwing and returning method families.
- Adding
nullto anArrayDeque. - Assuming iteration order matches insertion order at both ends. Iterating a
Dequegoes front to back, so items added withaddFirstappear in reverse of the order you added them. - Reaching for
LinkedListwhenArrayDequeis faster.
Practice
- Add the values 1 through 5 alternately to the front and back of a deque, then print it. Predict the output before running it.
- Write a method that checks whether a
Stringis a palindrome by comparing characters from both ends of a deque. - Write a class holding the last 20 readings, dropping the oldest when a twenty-first arrives, with a method returning their average.
- Implement undo and redo with two deques:
doAction,undo, andredo. - Given an
int[]and a window widthk, print the maximum of every window of that width.
Hints
- Work it out on paper.
addFirstreverses the order those items appear in. - Push every character, then repeatedly
removeFirstandremoveLastand compare. Stop when fewer than two remain — an odd-length string leaves a middle character that needs no partner. addLastthen trim from the front. For the average, iterate and divide bysize().doActionpushes onto the undo deque and clears redo.undomoves one item from undo to redo.redomoves it back. Clearing redo on a new action is the part people miss — it is also what real editors do.- The sliding-window maximum above. Each index is added once and removed once, so the whole pass is linear despite the inner loops.