Exercises — Deque (double-ended queue) — operations, use cases
The one rule you need for everything below, restated in plain words: a deque is a line you can poke from both sides. Adding/removing happens only at the two ends — front (leftmost) and back (rightmost) — never the middle.
Level 1 — Recognition
(Can you name the right operation / read the picture?)
L1.1 — Name the operations
Start empty. You want the deque to end as [7, 4, 9] (front→back), where 4 was inserted first. List a valid sequence of operations.
Recall Solution
4 is in the middle and inserted first, so it is the seed. Then 7 must land to its left (front) and 9 to its right (back).
push_back(4)→[4]push_front(7)→[7, 4]push_back(9)→[7, 4, 9]✅ Why:push_frontalways inserts before everything,push_backafter everything. So to grow on both sides of a seed you use one of each.
L1.2 — Restricted deque identification
You are told a structure only ever calls push_back and pop_front. Which classic structure is this, and what ordering does it enforce?
Recall Solution
push_back adds at the rear, pop_front removes from the front → the first thing you put in is the first thing out. This is a Queue (FIFO).
Contrast: push_back+pop_back would be a Stack (LIFO), because you remove from the same end you added.
L1.3 — Peek vs pop
Deque is [3, 5, 8]. You call front() then back(). What is returned, and what does the deque look like afterward?
Recall Solution
front() returns the leftmost = 3. back() returns the rightmost = 8. Peek does not remove, so the deque is unchanged: [3, 5, 8].
Why it matters: pop_front() would have returned 3 and left [5, 8]. Peek is look-only.
Level 2 — Application
(Run the machine correctly.)
L2.1 — Full operation trace
Start empty. Apply in order:
push_front(2), push_back(6), push_front(1), pop_back(), push_back(9), pop_front().
Give the deque (front→back) after each step and every returned value.
Recall Solution
Walk it one poke at a time, always asking "which tip does this touch?"
- Step 1 —
push_front(2): the deque is empty, so front and back are the same slot. The result is[2]. Nothing returned — pushes never return. - Step 2 —
push_back(6):push_backglues6onto the right tip, after the existing2. So6lands to the right of2→[2, 6]. This is why order matters: had we calledpush_front(6)instead, we'd have gotten[6, 2]. - Step 3 —
push_front(1):push_frontglues1onto the left tip, before everything. So1lands left of2→[1, 2, 6]. - Step 4 —
pop_back(): removes and returns the rightmost element. The rightmost is6, so it returns 6 and the deque shrinks from the right →[1, 2]. - Step 5 —
push_back(9): glues9onto the right tip again →[1, 2, 9]. Nothing returned. - Step 6 —
pop_front(): removes and returns the leftmost element. The leftmost is1, so it returns 1 and the deque shrinks from the left →[2, 9].
Collecting the picture into a table (each row is just the sentence above):
| Step | Action | Which tip? | Deque (front→back) | Returns |
|---|---|---|---|---|
| 1 | push_front(2) | left tip (empty) | [2] |
— |
| 2 | push_back(6) | right tip, after 2 | [2, 6] |
— |
| 3 | push_front(1) | left tip, before all | [1, 2, 6] |
— |
| 4 | pop_back() | remove right tip | [1, 2] |
6 |
| 5 | push_back(9) | right tip, after 2 | [1, 2, 9] |
— |
| 6 | pop_front() | remove left tip | [2, 9] |
1 |
Final deque: [2, 9]. Returned in order: 6, then 1.
The one habit to internalize: the word front always means the left tip and back always means the right tip — decide which tip an operation touches first, and the row writes itself.
L2.2 — Circular array: move front
Capacity C = 6. Currently front = 0, size = 3 (slots 0,1,2 filled). You call push_front(X). Compute the new front index and the resulting slot layout.

Recall Solution
The formula from the parent note (move front left, guard the wrap):
Store X at index 5, set size = 4.
Layout (_ = empty), indices 0..5: [a, b, c, _, _, X].
Logical order front→back starts at index 5: X, a, b, c.
Why +C? Without it, 0 - 1 = -1, an illegal index. Adding C before the mod sends front off the left edge and wraps it to the right edge — exactly the red arrow in the figure above, which lands on slot 5.
L2.3 — Index of the k-th element
Circular deque, C = 8, front = 6, size = 5. Which physical array index holds the element that is 3rd from the front (i.e. , counting the front as )?
Recall Solution
So the element sits at physical index 1. It wrapped: positions walked are → the third step lands past the edge onto slot 1. Sanity: the 5 elements occupy indices . Element index-1 is indeed the 4th listed = (zero-based). ✅
Level 3 — Analysis
(Explain WHY, spot the cost, diagnose.)
L3.1 — Why doubly, not singly?
A student implements a deque with a singly linked list (each node has only a next pointer, plus a tail pointer). Which single operation is not , and why exactly?
Recall Solution
pop_back() is , not .
Why: removing the last node requires updating the second-to-last node's next to null. With only next pointers you cannot jump backward from tail; you must walk from the head until you find the node whose next is tail — that walk is steps = .
Fix: give each node a prev pointer (Doubly Linked List). Then tail.prev is the predecessor instantly → . This is the whole reason the parent note insists on doubly linked.
L3.2 — Plain array cost blow-up
You (wrongly) implement a deque as a plain array where push_front shifts every element one slot right. You perform push_front operations on an initially empty array. What is the total time, in Big-O, and show the sum.
Recall Solution
The -th push_front shifts the elements already present. Total work:
So front-inserts cost , not .
Why this kills the deque's promise: a deque exists to give at both ends. Shifting turns one end into per op, defeating the purpose. Fix: Circular Buffer / Ring Buffer — move the front pointer instead of the data.
L3.3 — Amortized reasoning
A dynamic-array deque doubles capacity when full. Starting from capacity 1, you do push_backs. How much total copying work is done by the resizes, and why is each push still amortized?
Recall Solution
Capacity doubles at sizes (the copies happen right before growing past these). Elements copied at each resize: Total copy work . In general the geometric sum is , i.e. total copying across all pushes. Amortized meaning: spread that total over pushes = average per push, even though one individual push (the one triggering a resize) is . See Dynamic Array Amortized Analysis.
Level 4 — Synthesis
(Combine ideas into an algorithm.)
L4.1 — Palindrome check, formalized
Given a string in a deque, write the loop that decides palindrome, and trace it on L E V E L. State the loop invariant.
Recall Solution
while size > 1:
if pop_front() != pop_back():
return False
return True # 0 or 1 element left in the middle
Invariant: everything already popped in matching pairs was equal, and the remaining deque is the untested middle substring.
Trace on L E V E L:
- pop_front
L, pop_backL→ equal, dequeE V E - pop_front
E, pop_backE→ equal, dequeV size == 1→ loop ends → True (palindrome). MiddleVnever needs a partner. Why a deque? You must peel from both ends at once in — a stack alone can't reach the front cheaply, a queue can't reach the back.
L4.2 — Sliding Window Maximum construction
Array A = [1, 3, -1, -3, 5, 3], window size k = 3. Use a deque of indices kept in decreasing-value order. Give the max of every window and show the deque contents when each window completes.
Recall Solution
The deque holds indices, and we maintain one promise: the values they point to strictly decrease from front to back, so the front index is always the current window's maximum. Watch the four rules fire as we advance i one step at a time.
- i=0, A[0]=1: deque empty, just
push_back(0)→[0]. No window yet (need 3 elements). - i=1, A[1]=3: before pushing, check the back:
A[0]=1 <= A[1]=3, so index0can never be a future max (a bigger, newer value shadows it) → pop it. Nowpush_back(1)→[1]. Still no full window. - i=2, A[2]=-1: back is index
1,A[1]=3 > -1, so-1does not shadow it → keep, thenpush_back(2)→[1, 2]. Nowi = 2 = k-1, first window[1,3,-1]is complete. Its max isA[front] = A[1] =3. - i=3, A[3]=-3: back index
2,A[2]=-1 > -3→ keep,push_back(3)→[1, 2, 3]. Window is[3,-1,-3]; before reading the max, check front-expiry: front is index1, is1 <= i-k = 0? No. So max =A[1] =3. - i=4, A[4]=5:
5is huge — pop the back while it's<= 5:A[3]=-3≤5pop,A[2]=-1≤5pop,A[1]=3≤5pop. Deque empties, thenpush_back(4)→[4]. Window[-1,-3,5], max =A[4] =5. - i=5, A[5]=3: back index
4,A[4]=5 > 3→ keep,push_back(5)→[4, 5]. Window[-3,5,3], front4still in window (4 > i-k = 2), max =A[4] =5.
Same walk in table form (each row is one bullet above):
| i | A[i] | back-pop? | deque (indices) | window | max |
|---|---|---|---|---|---|
| 0 | 1 | — | [0] | — | — |
| 1 | 3 | pop 0 (1≤3) | [1] | — | — |
| 2 | -1 | keep | [1, 2] | [1,3,-1] | A[1]=3 |
| 3 | -3 | keep | [1, 2, 3] | [3,-1,-3] | A[1]=3 |
| 4 | 5 | pop 3,2,1 (all ≤5) | [4] | [-1,-3,5] | A[4]=5 |
| 5 | 3 | keep | [4, 5] | [-3,5,3] | A[4]=5 |
Answers (window maxima): 3, 3, 5, 5.
Why ? Each index is pushed once and popped once → total deque operations . The two "why deque" moments are visible above: newer bigger values evict from the back, and expired indices leave from the front — two different ends, both . See Sliding Window Maximum.
L4.3 — 0-1 BFS wiring
In 0-1 BFS you replace a Priority Queue with a deque. State precisely which edges cause push_front vs push_back, and why that keeps the deque "sorted by distance".
Recall Solution
- Edge of weight 0 →
push_front(neighbor): same distance as the current node, so it belongs at the cheapest end, processed next. - Edge of weight 1 →
push_back(neighbor): distance is one greater, so it belongs behind everything at the current distance. Invariant maintained: the deque always holds nodes of at most two distinct distances, (front region) and (back region), non-decreasing front→back. Popping from the front therefore always yields the smallest tentative distance — exactly what a priority queue would, but in instead of .
Level 5 — Mastery
(Design / prove / push to the edge.)
L5.1 — Two stacks make a deque? (proof-ish)
Can you build a deque from two Stacks so that all four operations are amortized ? Sketch the design and the cost argument for pop_front when the front stack is empty.
Recall Solution
Keep two stacks, L (its top = the deque's front) and R (its top = the deque's back).
push_front→ push ontoL;push_back→ push ontoR.pop_front→ popL. IfLis empty, rebalance: move half ofRintoL(reversing so tops line up), then pop. Cost of the empty-Lcase: a rebalance touches elements, but it only happens after at least cheap pops emptiedL. Charging the rebalance to those pops gives amortized. Edge case: if both stacks are empty,pop_frontis an error (underflow) — must be guarded, just like popping an empty stack. Why it works at all: stacks give at their single top; two of them, tops pointing at opposite ends, cover both deque ends — the reversal cost is amortized away.
L5.2 — Degenerate & boundary cases
For a circular-array deque of capacity C, answer all four:
(a) What does size == C trigger? (b) What is the layout when size == 0 — where is front? (c) After many wraps, can front exceed C-1? (d) Distinguish an empty deque from a full one when both might have front == back region overlap.
Recall Solution
(a) size == C means the ring is full → resize: allocate a new array of capacity 2C, copy the C logical elements in front→back order into the fresh array (typically re-basing front = 0), then continue the insertion. By the doubling argument of L3.3 this is amortized .
(b) size == 0 (empty): there are no elements, so front points at no valid data — its numeric value is meaningless until the first insert. Conventionally it is kept at 0, but any index is equally correct because nothing is readable. The very first push_front/push_back sets the meaningful position.
(c) No. Every update to front (and to any read index) passes through mod C, and mod C always returns a value in the range . So no matter how many times the pointer wraps around the ring, front can never exceed C-1 — the modular arithmetic is the guarantee that keeps indices legal.
(d) You cannot distinguish empty from full using the pointer positions alone — this is the classic circular-buffer ambiguity: a totally empty ring and a totally full ring can both make the front/back regions coincide. Fix: store an explicit size counter (or, alternatively, sacrifice one slot as a permanent gap sentinel). Then the test is unambiguous: size == 0 → empty, size == C → full. This is exactly why the parent note tracks size alongside front rather than trying to derive fullness from pointers.
L5.3 — Design: undo/redo with a bounded history
Design a deque-based undo/redo of at most H actions. When history exceeds H, the oldest action is discarded. Which operations map to which ends, and why is the deque (not a stack) required?
Recall Solution
Use a deque of actions, most-recent at the back.
- New action →
push_back(action). Then ifsize > H→pop_front()to drop the oldest action, keeping history bounded atH. - Undo →
pop_back()on the main deque (takes back the most recent action) andpush_back()it onto a separate redo deque. - Redo →
pop_back()on the redo deque andpush_back()it onto the main deque (re-applies the most recently undone action). - On a fresh action after some undos → clear the redo deque (the redo history is invalidated), exactly like real editors. Why a deque, not a stack? Undo/redo on its own is stack-like: you always take back the most recent action (LIFO at the back). But the bounded-history requirement forces you to also delete the oldest action — that lives at the front. Only a structure with at both ends can simultaneously "add newest at back" and "evict oldest at front". Why a stack fails: a plain stack has cheap access only at its top; dropping the bottom element would require shifting or emptying the whole stack = per eviction. The deque makes eviction . Edge cases: undo on an empty main deque is a no-op (nothing to undo); redo on an empty redo deque is a no-op (nothing to redo); both must be guarded like an empty-stack pop.
Connections
- Deque — the parent this page drills.
- Stack · Queue — the restricted forms (L1.2, L5.1).
- Circular Buffer / Ring Buffer — the array machinery in L2, L5.
- Doubly Linked List — why L3.1 needs
prev. - Dynamic Array Amortized Analysis — the L3.3 doubling proof.
- Sliding Window Maximum — L4.2 flagship.
- 0-1 BFS · Priority Queue — L4.3 replacement trick.