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.
Assumes you know
After this you should be able to
- Explain the core idea in Queues — Five Problems
- Apply Queues — Five Problems in a representative exercise
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"
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"
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 headstar TRICK: queue turns nested pointers into a flat FIFO order — no messy pointer surgery.
LeetCode 430 — "Flatten a Multilevel Doubly Linked List" (variant) [!]
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"
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"
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.
1. Implement Queue using Stacks#
two stacksRestatement. 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.
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.
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();
}
}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 windowRestatement. 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.
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().
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 traversalRestatement. 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.
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;
}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)#
BFSRestatement. 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.
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.
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 dequeRestatement. 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.
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. ThenaddLast(i).
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;
}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#
| Problem | Technique | Key insight | Time / Space |
|---|---|---|---|
| Queue using Stacks | Two stacks / reversal | Second stack reverses LIFO→FIFO; refill only when empty | O(1) amortized / O(n) |
| Moving Average | Sliding window + queue | Running sum: add on enter, subtract on evict; divide by current count | O(1) / O(k) |
| Flatten Multilevel List | Queue traversal | Enqueue children; splice front when next is null | O(n) / O(n) |
| Right Node of Key (Tree) | BFS level order | Right neighbor = next queued node, unless last in level | O(n) / O(n) |
| Sliding Window Maximum | Monotonic deque | Decreasing deque of indices; front is the window max | O(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.