3.2.8 · D3Linear Data Structures

Worked examples — Deque (double-ended queue) — operations, use cases

2,672 words12 min readBack to topic

Before anything else, three words we will lean on constantly — defined in plain English so no symbol sneaks in unearned:

The picture above is our mental model for the array deque: a ring of slots. front is an arrow pointing at the first live slot; the live region sweeps clockwise to rear. Nothing ever "shifts" — only the arrows move.


The scenario matrix

Every deque exercise falls into one of these cells. Our examples below are labelled with the cell they cover so you can see the whole space is filled.

# Cell (case class) What makes it tricky
A Plain trace, both ends keeping front→back order straight
B Circular index, no wrap when nothing crosses the edge
C Circular index, front wraps left (the guard) push_front when front = 0
D Circular index, rear wraps right push_back crossing slot
E Degenerate: empty deque popping when size = 0 (error/underflow)
F Degenerate: single element front == rear, both pops target it
G Full deque → resize (amortized doubling) copy into a fresh, un-wrapped array
H Word problem — palindrome (peel both ends) loop condition size > 1
I Exam twist — Sliding Window Maximum monotonic deque of indices
J Exam twist — 0-1 BFS which end for weight 0 vs weight 1

We now hit every cell.


Example 1 — Cell A: plain both-ends trace

Steps:

  1. push_back(7)[7]. Why this step? First element; front and rear are both 7.
  2. push_front(4)[4, 7]. Why? push_front inserts before everything, so 4 lands left of 7.
  3. push_back(9)[4, 7, 9]. Why? push_back appends on the right.
  4. push_front(1)[1, 4, 7, 9]. Why? Again inserts at the far left.
  5. pop_back() removes the rightmost → returns 9, leaves [1, 4, 7].
  6. pop_front() removes the leftmost → returns 1, leaves [4, 7].

Verify: count check — 4 pushes minus 2 pops = 2 elements remaining, matches [4, 7]. The two survivors 4 and 7 were the middle insertions, exactly the elements furthest from both ends. ✓


Example 2 — Cell B: circular index, no wrap

  1. Recall the formula from the parent: the -th element from the front sits at . Why this formula? The live region marches to the right by one slot per step; only matters if we cross the edge.
  2. Plug in: . Why no wrap? , so we never reach the edge; the leaves it untouched.

Verify: slot 3 is indeed R, the 3rd live element from the front. Direct read-off agrees. ✓


Example 3 — Cell C: push_front wraps left (the guard)

  1. Move front left: . Why the ? push_front needs a fresh slot to the left of the current front.
  2. Compute: . Why the ? Without it we'd have , an illegal index. Adding before the turns "one step left of slot 0" into "slot ", the wrap the ring picture demands (look at the red arrow in the figure jumping from slot 0 to slot 4).
  3. Store Z at slot 4, set size = 3.

Verify: array becomes [A, B, _, _, Z], and reading from the new front=4 gives logical order Z, A, B. The naive would have crashed; rescued it into the valid range . ✓


Example 4 — Cell D: push_back wraps right

  1. The next free slot at the rear is one past the last live slot. The last live slot is the -th from front: . The new slot is one further, i.e. . Why ? Front plus how many live elements = the first empty slot on the rear side.
  2. Compute: . Why does it wrap? equals the edge value ; the remainder sends us back to the very first physical slot (see the mint arrow curving from slot 4 to slot 0 in the figure).
  3. Store K at slot 0, set size = 3.

Verify: array [K, _, _, M, N], reading from front=3: logical order M, N, K. K correctly sits at the rear even though it physically wrapped to slot 0. ✓


Example 5 — Cell E: degenerate empty (underflow)

  1. Check the guard before touching any index: if size == 0, there is no front. Why check first? If you computed an index like you'd read a stale slot — old data still physically sitting there. That would return a ghost value.
  2. Signal underflow: raise an error / return a sentinel (e.g. None), and do not decrement size below 0. Why not decrement? size = -1 corrupts every later index computation.

Verify: invariant 0 ≤ size ≤ C must always hold. Popping an empty deque with a guard keeps size = 0; without the guard it drops to -1, breaking the invariant. The guard is the fix. ✓


Example 6 — Cell F: single element (front == rear)

  1. Rear index . Why? With a single element the rear is the front — both point at slot 2.
  2. Return the value at slot 2 = S; set size = 0. Why not move front? pop_back removes from the rear, so front stays; only size shrinks. (Had we done pop_front, we'd advance front instead and also reach size = 0.)

Verify: after the pop, size = 0 → the empty case of Example 5. Front and rear formulas both gave slot 2, confirming they coincide for one element. ✓


Example 7 — Cell G: full deque → resize (amortized )

  1. Detect full: size == C (4 == 4). Must grow. Why grow now? Every physical slot is live; there is nowhere to put e.
  2. Allocate a new array of capacity . Why double, not ? Doubling makes copies rare: pushes trigger only resizes whose copy costs sum to , so the amortized per-push cost is (see Dynamic Array Amortized Analysis).
  3. Copy in logical order, starting the new front at 0: read old logical order a, b, c, d and place at new slots 0,1,2,3. Set new front = 0. Why re-lay flat? The copy is the perfect moment to un-wrap the ring, so the new array is contiguous and simple.
  4. Now push_back(e): new rear slot . Store e, size = 5.

Verify: new array [a, b, c, d, e, _, _, _], front = 0, size = 5, capacity 8. Logical order a, b, c, d, e is preserved and no data was lost. ✓


Example 8 — Cell H: word problem, palindrome peel

  1. Deque: [L, E, V, E, L], size = 5. Loop while size > 1. Why > 1? A single leftover middle character has nothing to compare against — it's automatically fine.
  2. Compare pop_front() vs pop_back(): L vs L → equal. Now [E, V, E], size = 3.
  3. Compare again: E vs E → equal. Now [V], size = 1.
  4. Loop stops (size = 1). All comparisons matched → palindrome. Why a deque? You need to grab the outermost pair each time — front and rear together — which only a deque does in per side.

Verify: length 5 ⇒ comparisons expected; we did exactly 2. String reversed is LEVEL = original, confirming palindrome. ✓


Example 9 — Cell I: exam twist, Sliding Window Maximum

We hold indices; the front index always points to the current window's max. Two rules each step :

  • Pop from back while the value at the back index the new value (those can never be the max again).
  • Pop from front if it slid out of the window (front index ).
  1. (val 1): deque [0].
  2. (val 3): back is idx0 (val 1) → pop it; push 1 → [1].
  3. (val -1): back is idx1 (val 3) → keep; push 2 → [1,2]. Window complete → max = value at front idx1 = 3. Why front? Front holds the largest index-value by construction.
  4. (val -3): back idx2 (val -1) → keep; push 3 → [1,2,3]. Front idx1 = ? No, , idx1 stays. Window → max = value at idx1 = 3.
  5. (val 5): pop back while : idx3(-3), idx2(-1), idx1(3) all → all popped; push 4 → [4]. Front idx4 = i-k? , idx4 stays. Window → max = idx4 = 5.
  6. (val 3): back idx4 (val 5) → keep; push 5 → [4,5]. Window → max = front idx4 value = 5.

Verify: brute force windows: , , , . Deque produced [3, 3, 5, 5]. Match. Each index is pushed once and popped once → total work . ✓ (See Sliding Window Maximum.)


Example 10 — Cell J: exam twist, 0-1 BFS end choice

  1. Pop S from the front (distance 0). Relax its edges. Why a deque, not a Priority Queue? With only two possible weights, we never need a full heap — the deque keeps distances sorted for free.
  2. Weight-0 edge to A: push_front(A) (its distance equals S's, so it belongs at the same front level). Why front? A 0-weight move doesn't increase distance, so A must be processed before anything one step farther.
  3. Weight-1 edge to B: push_back(B) (distance is one more, process later). Why back? Larger distance ⇒ later ⇒ the rear.
  4. Deque now front→back: [A, B]. Next pop from front → A.

Verify: final distances , . The deque [A, B] is in nondecreasing distance order (0 then 1), which is exactly the invariant 0-1 BFS needs; popping the front yields A (distance 0) first. ✓ (See 0-1 BFS.)


Coverage check

Recall Did we hit every cell?

A→Ex1, B→Ex2, C→Ex3, D→Ex4, E→Ex5, F→Ex6, G→Ex7, H→Ex8, I→Ex9, J→Ex10. Every row of the matrix has a fully worked, verified example.

Cell A covered?
Yes — Example 1, plain both-ends trace.
Which cells test modular wrap and which direction?
C (front wraps left, the +C guard) and D (rear wraps right past C-1 to 0).
Which example enforces the size invariant on underflow?
Example 5 (empty pop) keeps 0 ≤ size ≤ C.
Why does the resize example re-lay the array flat?
The copy un-wraps the ring so the new capacity-2C array is contiguous with front = 0.

Connections

  • Parent (Hinglish) — the concept these examples drill
  • Stack · Queue — restricted deques the traces mimic
  • Circular Buffer / Ring Buffer — the ring picture behind Cells B–D, G
  • Doubly Linked List — alternate backing
  • Dynamic Array Amortized Analysis — why doubling in Example 7 is
  • Sliding Window Maximum — Example 9
  • 0-1 BFS — Example 10
  • Priority Queue — what the deque replaces in 0-1 BFS