Study Notes
Notesapplied

Queues — Five Problems

Five queue problems — queue using stacks (amortized O(1)), moving average from a stream, flatten multilevel linked list via BFS queue, right node of key in binary tree, and sliding window maximum with a monotonic deque. Full walkthroughs + commented Java.

30 min read·verified Content review 2026·reviewed 2026-08-30

After this you should be able to

  • Explain the core idea in Queues — Five Problems
  • Apply Queues — Five Problems in a representative exercise
Monotonic DequeBFSSliding Window

I. Notebook Summary (hand-copy / flashcards)#

Copy-me cards#

1. Queue using Stacks   two stacks / reversal#

Insight: a second stack reverses LIFO into FIFO; move all elements over so the queue-front sits on top.

Complexity: amortized O(1) per op [+]; naive move-every-time O(n) per op. Space O(n).

push(x): while s2 not empty: s1.push(s2.pop()); s1.push(x)
peek():  if s2 empty: while s1: s2.push(s1.pop()); return s2.top
pop():   peek(); return s2.pop()
empty(): s1.empty && s2.empty

star TRICK: shift only when the target stack is empty → amortized O(1). [+]

LeetCode 232 — "Implement Queue using Stacks"

doodle: in→[ s1 ] ⇄ pour ⇄ [ s2 ]→out (top of s2 = front)

2. Moving Average from Stream   sliding window / queue#

Insight: keep a running sum + a queue of window size; on overflow, subtract the popped front.

Complexity: O(1) per next(), O(k) space.

next(v): q.add(v); sum += v
         if q.size > k: sum -= q.poll()
         return sum / q.size()

star TRICK: never re-sum the window — maintain sum incrementally.

! PITFALL: divide by current count (min(seen,k)), not always k.

LeetCode 346 — "Moving Average from Data Stream"

doodle: stream → [ v v v ]→drop sum tracks window

3. Flatten Multilevel Linked List   queue / traversal#

Insight: walk next chain; enqueue each child seen; when next==null, splice the queue's front on.

Complexity: O(n) time, O(n) space (queue).

cur = head
while cur: if cur.child: q.add(cur.child)
          if cur.next == null && q not empty: cur.next = q.poll()
          cur = cur.next
return head

star TRICK: queue turns nested pointers into a flat FIFO order — no messy pointer surgery.

LeetCode 430 — "Flatten a Multilevel Doubly Linked List" (variant) [!]

doodle: A→B→C child↓D ⇒ A→B→C→D… (D from queue)

4. Node to the Right of Key (Tree)   BFS / level order#

Insight: BFS level by level; when node==key, its right neighbor is the next item in the queue — unless it's last in the level.

Complexity: O(n) time, O(n) space.

q=[root]
while q: n=q.size
  for i in 0..n-1: cur=q.poll()
     if cur.data==key: return (i==n-1)? null : q.peek()
     add cur.left, cur.right
return null

! PITFALL: check the key-match inside the per-level loop; peek (don't poll) the right neighbor.

GeeksforGeeks — "Find right node of a given node in binary tree"

doodle: level: [ .. key | RIGHT .. ] right = q.peek()

5. Sliding Window Maximum   monotonic deque #

Insight: store indices in a deque kept decreasing by value; front is always the window max.

Complexity: O(n) time, O(k) space.

for i in 0..n-1:
  if dq.front <= i-k: dq.pollFirst           // out of window
  while dq && a[dq.last] <= a[i]: dq.pollLast // smaller → useless
  dq.addLast(i)
  if i >= k-1: output a[dq.first]

star TRICK: store indices not values → you can detect the window edge i-k.

! PITFALL: pop stale front by index before reading the max.

LeetCode 239 — "Sliding Window Maximum"

doodle: a[..] deque(idx): [big → small] front = max

II. Detailed Study Notes#

0. Foundations#

Queue = FIFO. The element inserted first is removed first. Enqueue at the rear, dequeue from the front. Implementable via array or linked list.

Deque (double-ended queue). Open at both ends: insert or delete at either the front or the rear, and read either end. This flexibility is what makes problem 5 possible.

  • Standard queue operations to implement: push/enqueue, pop/dequeue, peek/front, empty, size.
  • Queue underpins BFS (level-order traversal, shortest path in unweighted graphs), sliding windows, and streaming aggregates.
Tip The throughline: a queue is more flexible than a stack. There's no single rigid "queue pattern" — recognize it whenever you need FIFO ordering, a moving window (drop from the front), or BFS. Stacks solve a narrower, more recognizable class of problems.

1. Implement Queue using Stacks#

two stacks

Restatement. Build a FIFO queue exposing push, pop, peek, empty using only stack (LIFO) operations. No other data structure allowed.

Practice: LeetCode 232 — "Implement Queue using Stacks".

Pattern / family. Data-structure design; stack↔queue conversion via reversal. Sibling of "Implement Stack using Queues."

Why two stacks. One stack alone gives you access only to the most recently pushed element (the top). But the queue's front is the oldest element, buried at the bottom. A single stack can never expose it directly. Pouring one stack into a second reverses the order, so the oldest element rises to the top of the second stack.

star Trick — the reversal insight A stack reverses order. Pour s1 → s2 and the bottom of s1 (the true front) becomes the top of s2. Two stacks = one reversal = FIFO from LIFO. This is the whole problem.

Approach A — costed peek/pop. Keep everything in s1 on push. When you need the front (peek/pop), drain s1 into s2, act on s2.top, then pour back. Correct but O(n) on every read because it moves elements back and forth each time.

star Trick — amortized O(1) [+] Don't pour back. Keep an input stack s1 and an output stack s2. Push always to s1. For peek/pop, refill s2 from s1 only when s2 is empty. Each element is moved at most once in, once out → amortized O(1) per operation. This is the standard optimal version and what interviewers expect.

Complexity. Approach A: O(n) per read op. Amortized version: O(1) amortized per op, O(n) space. [!] The transcript states complexities loosely; the amortized O(1) result is the accepted answer.

// Amortized O(1) queue backed by two stacks.
class MyQueue {
    private Deque<Integer> in  = new ArrayDeque<>();  // newest on top
    private Deque<Integer> out = new ArrayDeque<>();  // oldest on top

    public void push(int x) {          // enqueue at rear
        in.push(x);
    }

    public int pop() {                 // dequeue from front
        peek();                        // ensure 'out' has the front
        return out.pop();
    }

    public int peek() {                // front element
        if (out.isEmpty()) {           // refill ONLY when empty
            while (!in.isEmpty()) out.push(in.pop());
        }
        return out.peek();
    }

    public boolean empty() {
        return in.isEmpty() && out.isEmpty();
    }
}
! Pitfall If you pour back into s1 after every read (Approach A), you pay O(n) each time — a correct but slow answer. Also: never refill out while it still has elements, or you'll break FIFO order.

Edge cases & bugs. Empty on both stacks → empty() true. Calling peek()/pop() on an empty queue is undefined by the problem; guard if needed. Interleaving push and pop is handled automatically because pushes land in in and reads consume out.

Interview follow-ups. Reverse direction: implement a stack using two queues (the cost shifts to push). Discuss amortized vs. worst-case per-operation cost.

Why it matters. The "reverse with a second container" trick recurs whenever you need the opposite access order from what a structure gives you.

2. Moving Average from Data Stream#

sliding window

Restatement. A MovingAverage(int size) object; each next(int val) returns the average of the last size values seen so far. Before size values exist, average over however many have arrived.

Practice: LeetCode 346 — "Moving Average from Data Stream".

Pattern / family. Fixed-size sliding window over a stream, backed by a queue; incremental aggregate.

Worked example (size = 3). next(1)→1.0; next(10)→(1+10)/2 = 5.5; next(3)→(1+10+3)/3 = 4.667; next(5)→ window slides, (10+3+5)/3 = 6.0.

Brute force. Store all values; on each next, re-sum the last size of them. O(k) per call.

star Trick — running sum Keep a sum. Add the new value; if the window overflows, subtract the value you dequeue from the front. Never recompute the sum from scratch → O(1) per next().
! Pitfall — divide by the right count Divide by the current window count, not always by size. Early on (fewer than size elements) the denominator is the number seen so far. Using size too early gives wrong averages. Using queue.size() as the denominator handles both phases automatically.

Complexity. O(1) per next(), O(k) space for the window (k = size).

// Running-sum sliding window; O(1) per next().
class MovingAverage {
    private final Deque<Integer> q = new ArrayDeque<>();
    private final int size;
    private double sum = 0;

    public MovingAverage(int size) { this.size = size; }

    public double next(int val) {
        q.addLast(val);
        sum += val;
        if (q.size() > size) {          // window overflowed
            sum -= q.pollFirst();       // drop oldest, adjust sum
        }
        return sum / q.size();          // divide by current count
    }
}

Edge cases & bugs. First few calls (window not yet full). Integer sum overflow for large streams → use double/long. The variables (count, front) describe the same subtract-on-eviction logic; queue.size() is the clean way to track count.

Interview follow-ups. Weighted or exponential moving average; handling very large windows; concurrent producers.

Why it matters. Incremental aggregation (add-on-enter, subtract-on-exit) is the core of every fixed-size sliding-window problem — sums, counts, distinct-counts, and more.

3. Flatten a Multilevel Linked List#

queue traversal

Restatement. Each node has a next pointer and a child pointer that may head a separate list (which itself may have children). Flatten into one list following the taught traversal order.

Practice: LeetCode 430 — "Flatten a Multilevel Doubly Linked List" [!] (LeetCode's official version uses a doubly linked list and depth-first pre-order flattening; the queue/BFS ordering taught here is the GeeksforGeeks "Flatten a multilevel linked list" variant — search that title to match this exact behavior).

Pattern / family. Linked-list traversal with a helper (auxiliary) data structure; queue used for deferred/level ordering.

The traversal rule. Walk the next chain. Whenever a node has a child, enqueue that child head. When you hit a node whose next is null, dequeue the front of the queue and attach it as the current node's next, then keep walking. Children are appended in FIFO order — hence a queue.

star Trick — queue over pointer surgery You could solve this by manually splicing child lists into the main chain, but that pointer manipulation is error-prone and complex. A queue reduces it to: "see a child → remember it; run out of next → pull the next remembered head." Simple and linear.

Complexity. O(n) time (each node visited once). O(n) space for the queue in the worst case.

// Node has: int val; Node next; Node child;
public static Node flatten(Node head) {
    if (head == null) return null;
    Queue<Node> q = new LinkedList<>();
    Node cur = head;
    while (cur != null) {
        if (cur.child != null) {        // defer the child list
            q.add(cur.child);
            cur.child = null;           // [+] tidy up dangling child ptr
        }
        if (cur.next == null && !q.isEmpty()) {
            cur.next = q.poll();        // splice next remembered head
        }
        cur = cur.next;
    }
    return head;
}
Tip — why interviewers ask this The point isn't the linked list itself — it's whether you reach for a secondary data structure (the queue) to tame a nested structure. Multi-structure problems test exactly that instinct.

Edge cases & bugs. A node with a child but no next (attach child, continue). Deeply nested chains. Empty list. [+] Null out child after enqueuing so the flattened list is clean.

Interview follow-ups. A good exercise: solve it with pure pointer manipulation and no queue — doable but far more complex. Also compare against LeetCode 430's DFS pre-order flatten, which produces a different order.

Why it matters. "Defer with a queue, resume when the current path ends" generalizes to tree/graph traversal and any level-order flattening.

4. Node to the Right of a Given Key (Binary Tree)#

BFS

Restatement. Given a binary tree and a key, return the node immediately to the right of the key's node on the same level. Return null if none (key is rightmost on its level or absent). Expected O(n).

Practice: GeeksforGeeks — "Find right node of a given node in binary tree" (also stated as "right sibling / next right node in same level"). Related: LeetCode 116/117 "Populating Next Right Pointers."

Pattern / family. Level-order traversal (BFS) with a queue; classic BFS template.

star Trick — the right neighbor is just the next dequeue Process one full level at a time (loop size = q.size() times). If the current node matches the key, its right neighbor is simply the next element still in the queue — q.peek() — provided the current node isn't the last one in this level.
! Pitfall — check inside the level loop, and peek don't poll Snapshot n = q.size() before the inner loop so you process exactly one level. Do the key-match test inside that inner loop. If the match is at index i == n-1, it's the last node of the level → return null. Otherwise return q.peek() (the neighbor is still queued) — do not poll() it, or you corrupt the traversal.

Complexity. O(n) time, O(n) space (queue holds up to a full level).

// TreeNode: int data; TreeNode left, right;
public static TreeNode findRight(TreeNode root, int key) {
    if (root == null) return null;
    Queue<TreeNode> q = new LinkedList<>();
    q.add(root);
    while (!q.isEmpty()) {
        int n = q.size();                 // freeze this level's size
        for (int i = 0; i < n; i++) {
            TreeNode cur = q.poll();
            if (cur.data == key) {        // found the key node
                return (i == n - 1) ? null // last on level → no right
                : q.peek(); // next is the right node
            }
            if (cur.left  != null) q.add(cur.left);
            if (cur.right != null) q.add(cur.right);
        }
    }
    return null;                          // key not present
}

Edge cases & bugs. Key is the last node in its level → null. Key absent → null. Single-node tree. Forgetting to freeze n before enqueuing children (would run into the next level). Returning only one node, not an array.

Interview follow-ups. Populate next pointers for every node (LeetCode 116/117). Do it with O(1) extra space using established level links.

Why it matters. The "freeze level size, loop that many times" BFS template is reusable for level averages, right-side view, zigzag order, and connecting siblings.

5. Sliding Window Maximum#

monotonic deque

Restatement. Given array a and window size k, output the maximum of every contiguous subarray of size k.

Practice: LeetCode 239 — "Sliding Window Maximum".

Pattern / family. Monotonic deque (decreasing). The "maintain a useful subset in sorted order" idea mirrors the monotonic-stack technique.

Worked example. a = [10,21,5,18,13,7,6,5,4,9,3], k=3 → maxima: 21, 21, 18, 18, 13→ (per array) 18, 7, 6, ... the running window max for each position. [!] The transcript's spoken numbers drift; trust the mechanism: front of deque = current window max.

Brute force. For each of the n−k+1 windows, scan k elements → O(n·k). Too slow for large n.

star Trick — store indices in a decreasing deque Keep a deque of array indices whose corresponding values are strictly (or non-strictly) decreasing front→back. The front index always holds the current window's maximum. Store indices, not values, so you can tell when the front falls out of the window (front ≤ i - k).

Two maintenance steps per element i:

  • Evict stale front by index: if deque.front ≤ i - k, it's outside the window → pollFirst.
  • Evict smaller from back by value: while a[deque.last] ≤ a[i], those can never be the max again → pollLast. Then addLast(i).
! Pitfall — pop stale front before reading; store indices not values If you stored values you couldn't detect when the max scrolled out of the window — you need the index to compare against i - k. And you must evict the out-of-window front before reading a[deque.first] as the answer, or you'll report a max that already left the window.

Why decreasing order. When the current element is bigger than the back of the deque, that back element is now useless: it's older and smaller, so it can never be a future window's max while the current element is around. Removing it keeps the deque a shrinking list of "still-possible maxima," with the front being the biggest.

Complexity. O(n) time — each index is pushed and popped at most once. O(k) space for the deque.

// Monotonic decreasing deque of indices.
public static int[] maxSlidingWindow(int[] a, int k) {
    int n = a.length;
    int[] res = new int[n - k + 1];
    Deque<Integer> dq = new ArrayDeque<>(); // holds indices, values decreasing
    for (int i = 0; i < n; i++) {
        // 1) drop indices that fell out of the window
        if (!dq.isEmpty() && dq.peekFirst() <= i - k) dq.pollFirst();
        // 2) drop back indices whose values are <= current (useless)
        while (!dq.isEmpty() && a[dq.peekLast()] <= a[i]) dq.pollLast();
        dq.addLast(i);
        // 3) once the first window is complete, front is the max
        if (i >= k - 1) res[i - k + 1] = a[dq.peekFirst()];
    }
    return res;
}
Tip — deque flexibility is the enabler This problem needs removal from both ends: stale maxima leave the front, smaller values leave the back. Only a double-ended queue supports both in O(1). That's why deques come up right before this.

Edge cases & bugs. k = 1 (every element is its own max). k = n (single window). Duplicates — decide ≤ vs < in the back-eviction; ≤ keeps the deque smaller and is standard. Only start writing output once i ≥ k-1.

Interview follow-ups. Sliding window minimum (flip the comparison). First negative in every window. Constant-max stack/queue. Compare with a max-heap solution: O(n log n) and needs lazy deletion — the deque is strictly better.

Why it matters. The monotonic-deque template — "kick out the useless, keep a sorted candidate set, read one end" — solves a whole family of window-extremum problems in O(n).

Patterns Recap#

ProblemTechniqueKey insightTime / Space
Queue using StacksTwo stacks / reversalSecond stack reverses LIFO→FIFO; refill only when emptyO(1) amortized / O(n)
Moving AverageSliding window + queueRunning sum: add on enter, subtract on evict; divide by current count O(1) / O(k)
Flatten Multilevel ListQueue traversalEnqueue children; splice front when next is nullO(n) / O(n)
Right Node of Key (Tree)BFS level orderRight neighbor = next queued node, unless last in levelO(n) / O(n)
Sliding Window MaximumMonotonic dequeDecreasing deque of indices; front is the window maxO(n) / O(k)

Session throughline. A stack is for reversal (LIFO); a queue is for FIFO ordering, moving windows, and BFS. A deque adds two-ended access — the key to O(n) window extremum. When a problem needs the oldest element, a fixed window, level-by-level order, or removal from both ends, reach for a queue or deque.