DAY 18 / 30 · WEEK 3

Queues & Deques

Goal of the day

Concept

FIFO vs. LIFO. Day 17's stack is last-in, first-out: whatever you pushed most recently is the first thing that comes back out, and both push and pop happen at the same end. A queue flips that: it's first-in, first-out. Whoever enqueued first is the first one dequeued — think of a checkout line. Enqueue always adds to the back; dequeue always removes from the front. Those are two different ends, which is the key structural difference from a stack.

A JS gotcha worth knowing: a plain array works fine as a queue functionally (push to enqueue, shift to dequeue), but Array.prototype.shift() is O(n) — removing index 0 forces every remaining element to re-index down by one. A real O(1) queue needs a different structure (two stacks, a linked list, or an index-based circular buffer) — that's exactly what today's first two practice problems have you build.

A deque (pronounced "deck") generalizes both: you can push and pop from either end — pushFront/pushBack/popFront/popBack. A stack is a deque restricted to one end; a queue is a deque restricted to push-back/pop-front. Having both ends available is exactly what makes today's main algorithm possible.

Sliding Window Maximum. Given an array and a window size k, report the max of every contiguous window of size k as it slides across the array. The naive approach rescans all k elements for every one of the n - k + 1 windows: O(n·k). A monotonic deque gets this to O(n): keep a deque of indices whose values are strictly decreasing from front to back. The front index is always the current window's max. When a new element arrives, pop any smaller values off the back first — they can never win again, a bigger number just showed up behind them. Then push the new index onto the back. Finally, if the front index has slid outside the window, pop it from the front — even if it's still the largest value ever seen, it's no longer inside the window, so it doesn't count anymore.

Diagram: queue mechanics (FIFO)

Queue (FIFO): dequeue removes from the front, enqueue adds to the back 10 20 30 front dequeue() here back enqueue() here After dequeue() removes 10, then enqueue(40) adds 40: 20 30 40 new front newly enqueued 10 left from the front, 40 joined at the back — order of arrival is preserved.

Diagram: the monotonic deque's invariant

Snapshot mid-algorithm: deque = [index 4, index 6] idx 4 val 9 idx 6 val 7 front = current max back = newest pushed Values decrease strictly front → back (9 > 7). That's the invariant every push must preserve — pop smaller values off the back before pushing. The deque stores indices, not values — that's what lets it check whether the front has slid outside the window, even when that front value is still the largest one ever seen.

Interactive visualization: Sliding Window Maximum

current index i in the deque just popped from the back just expired from the front front of deque = window max

Array nums[i] — colors show what's happening at each index right now

Deque contents after this step (front → back, shown as index:value)

Press Play or Step to begin.

The code: Sliding Window Maximum

function maxSlidingWindow(nums, k) {
  const deque = [];   // holds INDICES; front-to-back values strictly decrease
  const result = [];

  for (let i = 0; i < nums.length; i++) {
    // 1. pop smaller trailing values off the BACK — they can never be the
    //    max again now that a bigger (or equal) number has shown up
    while (deque.length && nums[deque[deque.length - 1]] <= nums[i]) {
      deque.pop();
    }

    // 2. push the current index onto the back
    deque.push(i);

    // 3. pop the FRONT if it has slid outside the window
    if (deque[0] <= i - k) {
      deque.shift();
    }

    // 4. once the window is full (i >= k - 1), the front is the max
    if (i >= k - 1) {
      result.push(nums[deque[0]]);
    }
  }

  return result;
}

Line by line:

Dry run: maxSlidingWindow([5, 3, 8, 2, 9, 1, 7, 4], k = 3)

inums[i]Deque beforeAction(s)Deque afterWindow max
05[]push 0[0]
13[0]3 doesn't evict 5 → push 1[0, 1]
28[0, 1]pop back 1 (3≤8), pop back 0 (5≤8), push 2[2]nums[2] = 8
32[2]2 doesn't evict 8 → push 3[2, 3]nums[2] = 8
49[2, 3]pop back 3 (2≤9), pop back 2 (8≤9), push 4[4]nums[4] = 9
51[4]1 doesn't evict 9 → push 5[4, 5]nums[4] = 9
67[4, 5]pop back 5 (1≤7), 7 doesn't evict 9 → push 6[4, 6]nums[4] = 9
74[4, 6]4 doesn't evict 7 → push 7; front 4 ≤ 7−3=4 → expired, pop front[6, 7]nums[6] = 7

Final result: [8, 8, 9, 9, 9, 7]. Notice i=7: index 4 (value 9) is still the largest value ever seen, but it's outside the window [5, 6, 7] — position, not value, decides eviction from the front.

Complexity

ApproachTimeSpaceWhy
Naive (rescan each window)O(n·k)O(1)For each of the n − k + 1 windows, scan all k elements to find the max.
Monotonic dequeO(n)O(k)Every index is pushed onto the deque exactly once and popped at most once (from either end) — that's ≤ 2n total push/pop operations, not n·k. The deque never holds more than k indices at a time.

Interview corner

How to talk through it out loud: name the invariant before you write a line of code — "I'll keep a deque of indices where the values are strictly decreasing front to back, so the front is always my current window's max." Then narrate the two eviction rules separately: "I pop the back when a new value makes an old one useless, and I pop the front when an old index slides outside the window." Interviewers are listening for whether you can articulate why each end gets popped, not just recite the code from memory.

Practice problems

Note: problem 3 (Sliding Window Maximum) is LeetCode Hard and is today's own core algorithm, not a step up in difficulty from problems 1-2 — same intentional exception to the usual easy→medium ramp used on Day 10 (Merge k Sorted Lists), since it's the natural, direct application of what this lesson just taught.

1. Implement Queue using Stacks (Easy — LeetCode: Implement Queue using Stacks)

Implement a FIFO queue (push, pop, peek, empty) using only two stacks (LIFO structures).

2. Design Circular Deque (Medium — LeetCode: Design Circular Deque)

Implement a fixed-capacity deque with insertFront, insertLast, deleteFront, deleteLast, getFront, getRear, isEmpty, and isFull — all O(1), using a fixed-size array as the backing store (no shifting elements around).

3. Sliding Window Maximum (Hard — intentional exception, see note above — LeetCode: Sliding Window Maximum)

Given an array nums and window size k, return the max of every contiguous window of size k. Today's main algorithm, applied directly.

Quiz

1. In a queue, which end does dequeue() remove from?

2. What does the monotonic deque in Sliding Window Maximum actually store?

3. Front to back, how do the values at the deque's stored indices relate to each other?

4. Why is Sliding Window Maximum's deque solution O(n), not O(n·k), despite the nested while loop?

5. When do we pop from the deque's front vs. its back?