Queue & Deque — The Complete Guide
Master queues and deques from first principles. Learn to recognize when FIFO ordering is the key insight, use monotonic deques for sliding window problems, and confidently tackle every queue-based interview question.
Table of Contents
Intuition from Scratch
Why does this pattern exist?
A queue is the opposite of a stack. Instead of Last-In, First-Out, a queue is First-In, First-Out (FIFO). You add to the back and remove from the front — exactly like a line at a coffee shop. The person who arrived first gets served first.
This sounds trivial, but FIFO ordering is the backbone of some of the most important algorithms in computer science. Breadth-First Search (BFS) — the algorithm that finds shortest paths in unweighted graphs, explores trees level by level, and powers everything from social network friend suggestions to maze solving — is built entirely on a queue. Without FIFO ordering, BFS doesn't work.
A deque (double-ended queue) is the queue's more powerful cousin. It lets you add and remove from both ends in O(1). This makes it perfect for problems where you need to maintain a sliding window of values — you add new elements at one end and expire old elements from the other. The monotonic deque pattern (maintaining sorted order in a deque) is one of the most elegant O(n) solutions in all of DSA.
Analogies to Build Intuition
The Coffee Shop Line
People join at the back and get served from the front. The first person in line is the first to leave. That's FIFO — the fundamental property of a queue. In BFS, nodes are 'served' (explored) in the order they were discovered.
Ripples in a Pond
Drop a stone in a pond. The ripple expands outward in concentric circles — the closest points are reached first, then the next ring, then the next. BFS explores a graph exactly like this: level by level, nearest nodes first. The queue ensures this order.
A Sliding Window on a Train
Imagine looking out a train window. As the train moves, new scenery enters from one side and old scenery exits from the other. A deque works the same way — new elements enter from the back, expired elements leave from the front. The monotonic deque keeps track of the 'best view' (min or max) visible through the window at all times.
What kind of problems does it solve?
Queues and deques are your go-to when:
- You need BFS traversal — shortest path in unweighted graphs, level-order tree traversal, multi-source BFS
- You need to process items in arrival order — task scheduling, message queues, round-robin
- You need sliding window min/max — monotonic deque gives O(n) instead of O(n log n)
- You need to simulate a process — circular queues, hot potato, recent counter
- You need 0-1 BFS — shortest path with edge weights 0 and 1 using a deque
- You need to implement other data structures — stack using queues, queue using stacks
The core insight
A queue guarantees FIFO processing — the first item added is the first item removed. This is what makes BFS work: nodes at distance d are always processed before nodes at distance d+1. A deque extends this with O(1) access to both ends, enabling the powerful monotonic deque pattern for sliding window problems.
Pattern Recognition
Recognizing when a queue or deque is the right tool is the most important skill. Here are the signals to look for and the traps to avoid.
Keywords & Signals in the Problem Statement
Use Queue / Deque when you see...
- ✅"Shortest path" in an unweighted graph → BFS with queue
- ✅"Level-order traversal" of a tree → BFS with queue
- ✅"Minimum number of steps/moves" → BFS with queue
- ✅"Rotten oranges" or "walls and gates" → multi-source BFS
- ✅"Sliding window maximum/minimum" → monotonic deque
- ✅"Number of recent calls" or "hit counter" → queue with expiry
- ✅"Implement stack using queues" → queue manipulation
- ✅"Shortest path with 0/1 weights" → 0-1 BFS with deque
- ✅"Process tasks in order" or "round-robin scheduling"
- ✅"Circular buffer" or "design circular queue"
Don't use Queue / Deque when...
- ❌You need LIFO (last-in, first-out) order — use a Stack
- ❌You need DFS traversal — use a Stack or recursion
- ❌You need the Kth largest/smallest — use a Heap
- ❌The graph has weighted edges (not 0/1) — use Dijkstra with a Heap
- ❌You need to match nested structures — use a Stack
- ❌The problem is about next greater/smaller element — use a Monotonic Stack
Queue / Deque vs. Similar Patterns
| Pattern | When to Use | Key Difference |
|---|---|---|
| Queue (FIFO) | BFS, level-order traversal, task scheduling, process in arrival order | First-in, first-out — guarantees processing in discovery order |
| Deque (Double-Ended) | Sliding window min/max, 0-1 BFS, palindrome checks | O(1) add/remove from both ends — more flexible than queue or stack |
| Stack (LIFO) | DFS, bracket matching, undo operations, next greater element | Last-in, first-out — processes most recent item first |
| Heap / Priority Queue | Top-K, shortest path (weighted), merge K sorted lists | Priority-based ordering — not FIFO, but min/max first |
| Monotonic Deque | Sliding window min/max in O(n) | Deque maintaining sorted order within a window — elements expire from front, violators removed from back |
| Monotonic Stack | Next greater/smaller element globally (no window constraint) | Stack maintaining sorted order — no expiry from front, only pops from top |
The queue question to always ask
When you see a graph or grid problem asking for "shortest" or "minimum steps," ask: "Are all edges equal weight?" If yes → BFS with a queue. If edges are 0 or 1 → 0-1 BFS with a deque. If edges have arbitrary weights → Dijkstra with a heap. This single decision tree covers most shortest-path problems.
Brute Force → Optimized Thinking
Let's walk through the thinking evolution using a concrete example: Sliding Window Maximum — given an array and a window size k, return the maximum value in each window as it slides from left to right.
Brute Force — Scan Each Window
For each window position, scan all k elements to find the maximum. When k is large, this is painfully slow — you're re-examining k-1 elements that were already in the previous window.
function maxSlidingWindowBrute(nums: number[], k: number): number[] { const n = nums.length; const result: number[] = []; for (let i = 0; i <= n - k; i++) { let max = nums[i]; for (let j = i + 1; j < i + k; j++) { max = Math.max(max, nums[j]); } result.push(max); } return result; } // Problem: For each of (n-k+1) windows, we scan k elements. // Total: O(n × k). When k ≈ n, this is O(n²).
Better — Heap (Max-Heap)
Use a max-heap to track the current window's maximum. The max is always at the top. But when the window slides, you need to remove the element that left the window — and heap deletion by value is O(n), not O(log n). Lazy deletion helps but doesn't guarantee O(n log k).
// Pseudocode — heap approach // Push (value, index) into max-heap // For each window: // While heap top's index is outside window → pop (lazy delete) // Record heap top as window max // // Time: O(n log n) worst case — each element pushed/popped once // Problem: Lazy deletion means heap can grow to O(n) size. // Not truly O(n log k). Can we do better?
Optimal — Monotonic Deque
The key insight: we don't need ALL elements in the window — only the ones that could potentially be the maximum. If a new element is larger than elements already in the deque, those smaller elements can never be the maximum (the new element will outlast them in the window). Remove them from the back. Expire elements that leave the window from the front.
function maxSlidingWindow(nums: number[], k: number): number[] { const deque: number[] = []; // stores indices, front = max const result: number[] = []; for (let i = 0; i < nums.length; i++) { // 1. Remove elements outside the window from the front while (deque.length > 0 && deque[0] <= i - k) { deque.shift(); } // 2. Remove smaller elements from the back — they can never be max while (deque.length > 0 && nums[deque[deque.length - 1]] <= nums[i]) { deque.pop(); } // 3. Add current element deque.push(i); // 4. Record the maximum (front of deque) once window is full if (i >= k - 1) { result.push(nums[deque[0]]); } } return result; } // Each index is pushed once and removed at most once from each end. // Total operations: O(2n) = O(n). Deque holds at most k elements.
Why Does the Optimization Work?
The deque only keeps 'useful' candidates
An element is useful only if it could be the maximum of some future window. If a newer, larger element exists, the older smaller element can never win — it will leave the window before the larger one. So we remove it immediately.
The front is always the current maximum
Because we remove smaller elements from the back and expired elements from the front, the deque is always sorted in decreasing order. The front element is the largest in the current window — reading the max is O(1).
Each element enters and leaves at most once
Every index is pushed to the back exactly once and removed (from either front or back) at most once. That's O(2n) = O(n) total operations, regardless of k. This amortized analysis is the same insight as monotonic stacks.
The thinking process matters more than the code
In interviews, walk through this progression: "Brute force scans each window — O(nk). A heap gives O(n log n) but has lazy deletion issues. The key insight is that smaller elements in the window are useless once a larger element arrives. A monotonic deque maintains only useful candidates in decreasing order, giving O(n)."
Core Templates
Queue and deque problems follow a few recurring templates. Memorize these structures — most problems are variations of one of them.
Template 1: BFS — Level-Order Traversal
The most common queue pattern. Enqueue the starting node(s), then process level by level. Track the level size to know when one level ends and the next begins.
function bfs(root: TreeNode | null): number[][] { if (!root) return []; const result: number[][] = []; const queue: TreeNode[] = [root]; while (queue.length > 0) { const levelSize = queue.length; // snapshot current level size const level: number[] = []; for (let i = 0; i < levelSize; i++) { const node = queue.shift()!; level.push(node.val); if (node.left) queue.push(node.left); if (node.right) queue.push(node.right); } result.push(level); } return result; } // When to use: level-order traversal, zigzag traversal, // right side view, average of levels, minimum depth
Template 2: BFS — Shortest Path in Grid / Graph
Enqueue the start position with distance 0. For each cell, explore all neighbors. The first time you reach the target is the shortest path. Use a visited set to avoid cycles.
function shortestPath(grid: number[][], start: [number, number], end: [number, number]): number { const rows = grid.length, cols = grid[0].length; const queue: [number, number, number][] = [[start[0], start[1], 0]]; // [row, col, dist] const visited = new Set<string>(); visited.add(`${start[0]},${start[1]}`); const dirs = [[0, 1], [0, -1], [1, 0], [-1, 0]]; while (queue.length > 0) { const [r, c, dist] = queue.shift()!; if (r === end[0] && c === end[1]) return dist; for (const [dr, dc] of dirs) { const nr = r + dr, nc = c + dc; const key = `${nr},${nc}`; if (nr >= 0 && nr < rows && nc >= 0 && nc < cols && grid[nr][nc] === 0 && !visited.has(key)) { visited.add(key); queue.push([nr, nc, dist + 1]); } } } return -1; // unreachable } // When to use: shortest path in maze, word ladder, // minimum knight moves, open the lock
Template 3: Multi-Source BFS
Instead of starting from one source, enqueue ALL sources at once with distance 0. BFS then expands outward from all sources simultaneously. This finds the shortest distance from any source to every cell.
function multiSourceBFS(grid: number[][]): number[][] { const rows = grid.length, cols = grid[0].length; const dist = Array.from({ length: rows }, () => new Array(cols).fill(-1)); const queue: [number, number][] = []; // Enqueue ALL sources at once for (let r = 0; r < rows; r++) { for (let c = 0; c < cols; c++) { if (grid[r][c] === SOURCE) { queue.push([r, c]); dist[r][c] = 0; } } } const dirs = [[0, 1], [0, -1], [1, 0], [-1, 0]]; while (queue.length > 0) { const [r, c] = queue.shift()!; for (const [dr, dc] of dirs) { const nr = r + dr, nc = c + dc; if (nr >= 0 && nr < rows && nc >= 0 && nc < cols && dist[nr][nc] === -1) { dist[nr][nc] = dist[r][c] + 1; queue.push([nr, nc]); } } } return dist; } // When to use: rotting oranges, walls and gates, // 01 matrix (nearest 0), map of highest peak
Template 4: Monotonic Deque — Sliding Window Min/Max
Maintain a deque of indices in monotonic order (decreasing for max, increasing for min). Remove expired elements from the front. Remove elements that can never win from the back. The front is always the answer.
function slidingWindowMax(nums: number[], k: number): number[] { const deque: number[] = []; // indices in decreasing value order const result: number[] = []; for (let i = 0; i < nums.length; i++) { // Expire: remove indices outside the window from front while (deque.length > 0 && deque[0] <= i - k) { deque.shift(); } // Maintain monotonic order: remove smaller from back while (deque.length > 0 && nums[deque[deque.length - 1]] <= nums[i]) { deque.pop(); } deque.push(i); // Window is full — record the max (front of deque) if (i >= k - 1) { result.push(nums[deque[0]]); } } return result; } // For sliding window MINIMUM: flip the comparison to >= // When to use: sliding window maximum, shortest subarray with sum >= k, // constrained subsequence sum, longest continuous subarray with abs diff <= limit
Which template to pick?
Ask yourself: (1) Am I exploring a graph/tree level by level? → Template 1 or 2. (2) Am I finding shortest distance from multiple sources? → Template 3. (3) Am I finding min/max in a sliding window? → Template 4. (4) Am I processing items in arrival order? → Basic queue.
Variations & Sub-Patterns
Queues and deques aren't a single trick — they're a family of techniques. Here are the most common variations and how the approach changes for each.
| Variation | Queue/Deque Strategy | Classic Example |
|---|---|---|
| BFS Level-Order | Queue with level-size snapshot per iteration | Binary Tree Level Order Traversal, Zigzag Traversal |
| BFS Shortest Path | Queue with (node, distance), visited set, first arrival wins | Word Ladder, Open the Lock, Shortest Path in Grid |
| Multi-Source BFS | Enqueue all sources at distance 0, expand outward simultaneously | Rotting Oranges, Walls and Gates, 01 Matrix |
| 0-1 BFS | Deque: push 0-weight edges to front, 1-weight edges to back | Shortest Path in Binary Matrix (with 0/1 weights), Minimum Cost to Make at Least One Valid Path |
| Monotonic Deque (Max) | Decreasing deque — remove smaller from back, expire from front | Sliding Window Maximum, Constrained Subsequence Sum |
| Monotonic Deque (Min) | Increasing deque — remove larger from back, expire from front | Shortest Subarray with Sum at Least K, Jump Game VI |
| Circular Queue | Fixed-size array with head/tail pointers wrapping around | Design Circular Queue, Design Circular Deque |
| Queue Simulation | Model real-world FIFO processes — task scheduling, hit counters | Number of Recent Calls, Task Scheduler, Dota2 Senate |
Problems mentioned above
Deep Dive: Multi-Source BFS — Rotting Oranges
A grid has fresh oranges (1) and rotten oranges (2). Each minute, rotten oranges rot their 4-directional neighbors. Return the minimum minutes until all oranges are rotten, or -1 if impossible. This is the classic multi-source BFS problem.
function orangesRotting(grid: number[][]): number { const rows = grid.length, cols = grid[0].length; const queue: [number, number][] = []; let fresh = 0; // Enqueue ALL rotten oranges as sources for (let r = 0; r < rows; r++) { for (let c = 0; c < cols; c++) { if (grid[r][c] === 2) queue.push([r, c]); if (grid[r][c] === 1) fresh++; } } if (fresh === 0) return 0; const dirs = [[0, 1], [0, -1], [1, 0], [-1, 0]]; let minutes = 0; while (queue.length > 0 && fresh > 0) { const size = queue.length; minutes++; for (let i = 0; i < size; i++) { const [r, c] = queue.shift()!; for (const [dr, dc] of dirs) { const nr = r + dr, nc = c + dc; if (nr >= 0 && nr < rows && nc >= 0 && nc < cols && grid[nr][nc] === 1) { grid[nr][nc] = 2; // rot it fresh--; queue.push([nr, nc]); } } } } return fresh === 0 ? minutes : -1; } // Time: O(rows × cols) — each cell processed at most once // Space: O(rows × cols) — queue can hold all cells // Key: ALL rotten oranges start in the queue at time 0. // BFS expands one "ring" per minute — exactly like ripples in a pond.
Deep Dive: 0-1 BFS — Shortest Path with 0/1 Weights
When edge weights are only 0 or 1, you don't need Dijkstra. Use a deque: push 0-weight edges to the front (they don't increase distance) and 1-weight edges to the back (they do). This gives O(V + E) instead of O((V + E) log V).
function zeroOneBFS(graph: Map<number, [number, number][]>, start: number, n: number): number[] { const dist = new Array(n).fill(Infinity); dist[start] = 0; const deque: number[] = [start]; while (deque.length > 0) { const u = deque.shift()!; for (const [v, weight] of graph.get(u) ?? []) { const newDist = dist[u] + weight; if (newDist < dist[v]) { dist[v] = newDist; if (weight === 0) { deque.unshift(v); // 0-weight → push to FRONT } else { deque.push(v); // 1-weight → push to BACK } } } } return dist; } // Time: O(V + E) — same as regular BFS // Space: O(V) for distance array + deque // Key: 0-weight edges go to front (processed immediately), // 1-weight edges go to back (processed later). // This maintains the BFS invariant: deque is sorted by distance.
Deep Dive: Monotonic Deque — Jump Game VI
Given an array and a jump length k, start at index 0 and reach the last index. At each step, you can jump 1 to k positions forward. Your score is the sum of values at visited indices. Maximize the score. This combines DP with a monotonic deque.
function maxResult(nums: number[], k: number): number { const n = nums.length; const dp = new Array(n).fill(0); dp[0] = nums[0]; const deque: number[] = [0]; // indices, decreasing dp values for (let i = 1; i < n; i++) { // Expire: remove indices outside the jump window while (deque.length > 0 && deque[0] < i - k) { deque.shift(); } // dp[i] = nums[i] + max(dp[j]) for j in [i-k, i-1] dp[i] = nums[i] + dp[deque[0]]; // Maintain decreasing order: remove smaller dp values from back while (deque.length > 0 && dp[deque[deque.length - 1]] <= dp[i]) { deque.pop(); } deque.push(i); } return dp[n - 1]; } // Time: O(n) — each index pushed/popped at most once // Space: O(n) for dp array, O(k) for deque // Key: the deque maintains the maximum dp value within the jump window. // This is the "DP + monotonic deque" pattern — very common in hard problems.
Visual Walkthrough
Let's trace through three classic queue/deque problems step by step to see the pattern in action.
BFS Level-Order Traversal — Step by Step
Tree: 3 / \ 9 20 / \ 15 7 queue = [3] Level 0: levelSize = 1 Dequeue 3 → level = [3] Enqueue children: 9, 20 queue = [9, 20] result = [[3]] Level 1: levelSize = 2 Dequeue 9 → level = [9] (no children) Dequeue 20 → level = [9, 20] Enqueue children: 15, 7 queue = [15, 7] result = [[3], [9, 20]] Level 2: levelSize = 2 Dequeue 15 → level = [15] (no children) Dequeue 7 → level = [15, 7] (no children) queue = [] result = [[3], [9, 20], [15, 7]] Queue empty → done. Answer: [[3], [9, 20], [15, 7]] Key: The "levelSize = queue.length" snapshot at the start of each iteration tells you exactly how many nodes belong to the current level. Everything enqueued during this iteration belongs to the NEXT level.
Rotting Oranges (Multi-Source BFS) — Step by Step
Grid: 2 1 1 1 1 0 0 1 1 Initial: queue = [(0,0)], fresh = 6 Minute 1: process (0,0) Rot (0,1) and (1,0) queue = [(0,1), (1,0)], fresh = 4 Grid: 2 2 1 2 1 0 0 1 1 Minute 2: process (0,1) and (1,0) (0,1) rots (0,2) and (1,1) (1,0) has no fresh neighbors (already rotten or wall) queue = [(0,2), (1,1)], fresh = 2 Grid: 2 2 2 2 2 0 0 1 1 Minute 3: process (0,2) and (1,1) (0,2) has no fresh neighbors (1,1) rots (2,1) queue = [(2,1)], fresh = 1 Grid: 2 2 2 2 2 0 0 2 1 Minute 4: process (2,1) (2,1) rots (2,2) queue = [(2,2)], fresh = 0 Grid: 2 2 2 2 2 0 0 2 2 fresh = 0 → done! Answer: 4 minutes Key: ALL rotten oranges start in the queue simultaneously. Each "minute" is one BFS level. The answer is the number of levels.
Sliding Window Maximum (Monotonic Deque) — Step by Step
Input: nums = [1, 3, -1, -3, 5, 3, 6, 7], k = 3 deque = [] (stores indices), result = [] i=0: num=1 deque empty → push 0 deque = [0] (values: [1]) window not full yet i=1: num=3 nums[0]=1 ≤ 3 → pop 0 from back push 1 deque = [1] (values: [3]) window not full yet i=2: num=-1 nums[1]=3 > -1 → keep push 2 deque = [1, 2] (values: [3, -1]) window full → result.push(nums[1]) = 3 result = [3] i=3: num=-3 deque[0]=1, 1 > 3-3=0 → not expired nums[2]=-1 > -3 → keep push 3 deque = [1, 2, 3] (values: [3, -1, -3]) result.push(nums[1]) = 3 result = [3, 3] i=4: num=5 deque[0]=1, 1 ≤ 4-3=1 → EXPIRE, shift nums[2]=-1 ≤ 5 → pop from back nums[3]=-3 ≤ 5 → pop from back push 4 deque = [4] (values: [5]) result.push(nums[4]) = 5 result = [3, 3, 5] i=5: num=3 nums[4]=5 > 3 → keep push 5 deque = [4, 5] (values: [5, 3]) result.push(nums[4]) = 5 result = [3, 3, 5, 5] i=6: num=6 nums[5]=3 ≤ 6 → pop from back nums[4]=5 ≤ 6 → pop from back push 6 deque = [6] (values: [6]) result.push(nums[6]) = 6 result = [3, 3, 5, 5, 6] i=7: num=7 nums[6]=6 ≤ 7 → pop from back push 7 deque = [7] (values: [7]) result.push(nums[7]) = 7 result = [3, 3, 5, 5, 6, 7] Answer: [3, 3, 5, 5, 6, 7] Key: The deque always holds indices in DECREASING value order. Front = current window max. Back = where new elements enter. Smaller elements are removed from back (they can never be max). Expired elements are removed from front (they left the window).
Practice Problems
These 7 problems are carefully ordered from easy to hard. Each one teaches a different aspect of queue/deque-based problem solving. Solve them in order.
LC 933. Number of Recent Calls
Why this pattern:
Basic queue with expiry. Each ping adds a timestamp to the queue. Remove timestamps older than 3000ms from the front. The queue size is the answer. This is the 'hello world' of queues.
Key idea: Push the new timestamp to the back. While the front timestamp is older than t - 3000, dequeue it. Return queue.length. The queue naturally maintains a sliding window of recent timestamps.
LC 102. Binary Tree Level Order Traversal
Why this pattern:
BFS with level-size snapshot. The foundational BFS-on-trees problem. Enqueue the root, then process level by level using the 'levelSize = queue.length' trick to separate levels.
Key idea: At the start of each level, snapshot the queue size. Process exactly that many nodes (they're all on the same level). Their children go into the queue for the next level. This cleanly separates levels without extra markers.
LC 994. Rotting Oranges
Why this pattern:
Multi-source BFS. All rotten oranges are sources at time 0. BFS expands one ring per minute. The number of BFS levels is the answer. Count fresh oranges to detect the impossible case.
Key idea: Enqueue all rotten oranges at once. Process level by level (each level = 1 minute). For each rotten orange, rot its fresh neighbors and enqueue them. Track fresh count — if it's not 0 at the end, return -1.
LC 752. Open the Lock
Why this pattern:
BFS shortest path in an implicit graph. Each lock state is a node. Each turn of a wheel is an edge. BFS finds the minimum turns from '0000' to the target, avoiding deadends.
Key idea: Start BFS from '0000'. For each state, generate all 8 neighbors (4 wheels × 2 directions). Skip deadends and visited states. The first time you reach the target is the shortest path. Use a Set for O(1) visited checks.
LC 239. Sliding Window Maximum
Why this pattern:
Monotonic deque (decreasing). The deque holds indices in decreasing value order. The front is always the current window's maximum. Remove expired indices from front, remove smaller values from back.
Key idea: Three operations per element: (1) expire from front if outside window, (2) remove from back if smaller than current (they can never be max), (3) push current to back. Front of deque = window max. Each element enters and leaves at most once → O(n).
LC 1696. Jump Game VI
Why this pattern:
DP + monotonic deque. dp[i] = nums[i] + max(dp[j]) for j in [i-k, i-1]. The monotonic deque maintains the maximum dp value within the jump window, turning an O(nk) DP into O(n).
Key idea: For each index, the deque front gives the best dp value within the jump range. Compute dp[i], then maintain the deque: expire old indices from front, remove smaller dp values from back, push i. This is the 'DP optimization with monotonic deque' pattern.
LC 862. Shortest Subarray with Sum at Least K
Why this pattern:
Prefix sums + monotonic deque (increasing). The deque maintains indices with increasing prefix sums. For each index, check if prefix[i] - prefix[deque.front] >= K (found a valid subarray — pop front and update answer). Remove from back if prefix sum is not increasing.
Key idea: Compute prefix sums. The deque holds indices in increasing prefix-sum order. For each i: (1) while prefix[i] - prefix[front] >= K, update answer and pop front (shorter subarrays might exist further right). (2) Remove from back if prefix[back] >= prefix[i] (they're dominated). This handles negative numbers correctly.
Practice strategy
For each problem: (1) Identify whether it's BFS, multi-source BFS, or monotonic deque before coding. (2) Ask "what am I enqueuing and when am I dequeuing?" (3) After solving, write one sentence: "The queue holds [what] and dequeues when [condition]."
Common Mistakes
These are the traps that catch people on queue/deque problems. Learn them here so you don't learn them in an interview.
Not snapshotting the level size in BFS
You process nodes in a while loop without tracking how many belong to the current level. New children get mixed into the same iteration, and you can't separate levels.
✅Always snapshot: const levelSize = queue.length at the START of each level iteration. Then loop exactly levelSize times. Everything enqueued during this loop belongs to the next level. This is the #1 BFS mistake.
Using Array.shift() in JavaScript for large queues
Array.shift() is O(n) because it re-indexes every element. For BFS on large graphs, this turns your O(V + E) algorithm into O(V² + E). The queue becomes the bottleneck.
✅Use an index pointer instead of shift(): let front = 0; and access queue[front++] instead of queue.shift(). Or use a proper deque implementation. In interviews, mention this optimization — it shows you understand JS internals.
Forgetting to mark visited BEFORE enqueuing in BFS
You mark nodes as visited when you dequeue them instead of when you enqueue them. This causes the same node to be enqueued multiple times by different parents, wasting time and potentially giving wrong shortest-path distances.
✅Mark visited immediately when you enqueue, not when you dequeue. This prevents duplicate entries. The rule: 'visit on enqueue, not on dequeue.' This is especially critical in grid BFS where 4 neighbors might all try to enqueue the same cell.
Confusing monotonic deque direction
You use a decreasing deque when you need increasing (or vice versa). Sliding window MAX needs a decreasing deque (largest at front). Sliding window MIN needs an increasing deque (smallest at front).
✅Think about what the front should hold: for MAX, the front should be the largest → remove smaller elements from back → decreasing deque. For MIN, the front should be the smallest → remove larger elements from back → increasing deque.
Off-by-one in window expiry
You expire elements from the deque front using deque[0] < i - k instead of deque[0] <= i - k (or vice versa). This either keeps expired elements or removes valid ones, giving wrong answers.
✅For a window of size k ending at index i, valid indices are [i-k+1, i]. So expire when deque[0] < i - k + 1, which is the same as deque[0] <= i - k. Draw out a small example with k=3 to verify your condition.
Using BFS when edges have different weights
You use a regular BFS queue for a graph with weighted edges. BFS only guarantees shortest path when all edges have equal weight. With different weights, BFS can find a longer path first.
✅Equal weights → BFS (queue). Weights 0 and 1 → 0-1 BFS (deque). Arbitrary non-negative weights → Dijkstra (min-heap). Negative weights → Bellman-Ford. Always check the edge weight constraint before choosing your algorithm.
Interview Strategy
Knowing when and how to use a queue or deque is half the battle. Here's how to present it in an interview setting to maximize your score.
The 5-Step Interview Flow
Clarify & Identify the Pattern
'This is asking for the minimum number of steps in an unweighted graph — that's BFS with a queue.' Or: 'This needs the maximum in a sliding window — that's a monotonic deque.' Name the pattern early.
State the Brute Force
'The brute force would check every element in each window — O(nk). But I can maintain a deque of candidates in decreasing order, so the max is always at the front — O(n).' Show you understand the baseline.
Explain What the Queue/Deque Holds
'The queue holds (node, distance) pairs. Nodes are enqueued when discovered and dequeued when processed. BFS guarantees the first time we reach a node is the shortest path.' This is the key insight interviewers want.
Code It Clean
Use descriptive variable names (levelSize, not n). Comment the three deque operations (expire, maintain order, push). Handle edge cases (empty graph, single node, window size 1) explicitly.
Test & Analyze
Trace through a small example. For BFS: 'Starting from node A, we enqueue B and C at distance 1, then D at distance 2...' State complexity: 'O(V + E) time for BFS, O(n) for monotonic deque since each element is pushed and popped at most once.'
Common Follow-Up Questions
| Follow-Up | How to Handle |
|---|---|
| "Can you do BFS without a queue?" | Technically yes — you can use two arrays (current level and next level) and swap them. But a queue is cleaner and more standard. Mention this as an alternative if asked. |
| "What if the graph has weighted edges?" | BFS only works for equal-weight edges. For 0/1 weights → 0-1 BFS with deque. For arbitrary non-negative weights → Dijkstra with min-heap. For negative weights → Bellman-Ford. |
| "Why not use DFS for shortest path?" | DFS doesn't guarantee shortest path in unweighted graphs — it might find a longer path first. BFS explores level by level, so the first time it reaches a node is always the shortest path. DFS is for exhaustive search, BFS is for shortest path. |
| "Can you optimize the Array.shift() in JavaScript?" | Yes — use a front pointer: let front = 0, then queue[front++] instead of shift(). This avoids O(n) re-indexing. Or use a linked list. In production, use a proper deque library. Mention this to show you understand JS performance. |
| "What's the difference between monotonic stack and monotonic deque?" | Monotonic stack: elements only leave from the top. Used for 'next greater/smaller' globally. Monotonic deque: elements can leave from both ends. Used for 'min/max within a sliding window.' The deque adds expiry from the front. |
| "How would you handle bidirectional BFS?" | Start BFS from both source and target simultaneously. Alternate expanding the smaller frontier. When the two frontiers meet, the sum of their distances is the shortest path. This can reduce time from O(b^d) to O(b^(d/2)). |
The meta-skill
Interviewers love queue problems because they test your understanding of BFS guarantees and your ability to model problems as graphs. The key sentence to say is: "BFS with a queue guarantees shortest path because it processes nodes in order of discovery distance — all nodes at distance d are processed before any node at distance d+1." For deque problems: "Each element is pushed and popped at most once from each end, so the total time is O(n)."
Summary & Cheat Sheet
Pin this section. Review it before mock interviews and contests.
Quick Revision Cheat Sheet
Queue core idea: FIFO — first item added is the first removed. This is what makes BFS work: nodes at distance d are processed before distance d+1.
Deque core idea: O(1) add/remove from both ends. Enables monotonic deque for sliding window min/max and 0-1 BFS.
BFS guarantee: In an unweighted graph, BFS finds the shortest path. The queue ensures level-by-level exploration.
Level-order trick: Snapshot levelSize = queue.length at the start of each level. Process exactly that many nodes. Children go to the next level.
Multi-source BFS: Enqueue ALL sources at distance 0. BFS expands outward from all sources simultaneously. Used for 'nearest distance from any source.'
0-1 BFS: Deque: 0-weight edges → push to front. 1-weight edges → push to back. O(V+E) instead of Dijkstra's O((V+E) log V).
Monotonic deque (max): Decreasing order. Remove smaller from back (they can never be max). Expire from front (left the window). Front = current max.
Monotonic deque (min): Increasing order. Remove larger from back (they can never be min). Expire from front (left the window). Front = current min.
Visit on enqueue: Mark nodes visited when you ENQUEUE them, not when you dequeue. Prevents duplicate entries and wrong distances.
JS shift() trap: Array.shift() is O(n). Use a front pointer or proper deque for large inputs. Mention this in interviews.
Amortized O(n): Monotonic deque: each element pushed once, popped at most once from each end → O(2n) = O(n) total.
DP + deque: When a DP recurrence needs max/min over a sliding window of previous states, monotonic deque optimizes from O(nk) to O(n).
Mental trigger: "Shortest path" or "minimum steps" → BFS queue. "Sliding window min/max" → monotonic deque. "Level by level" → BFS queue.
Decision Flowchart
Does the problem ask for shortest path or minimum steps? ├── YES → Are all edge weights equal? │ ├── YES → BFS with a Queue (Template 2) │ └── NO → Are weights only 0 and 1? │ ├── YES → 0-1 BFS with a Deque │ └── NO → Dijkstra with a Heap (not a queue problem) └── NO → Does the problem ask for level-by-level processing? ├── YES → BFS Level-Order with a Queue (Template 1) └── NO → Does the problem involve multiple starting points expanding outward? ├── YES → Multi-Source BFS (Template 3) └── NO → Does the problem need min/max in a sliding window? ├── YES → Monotonic Deque (Template 4) └── NO → Does a DP recurrence need max/min over a range of previous states? ├── YES → DP + Monotonic Deque └── NO → Does the problem involve FIFO processing or simulation? ├── YES → Basic Queue └── NO → Queue/Deque probably isn't the right pattern → Consider Stack, Heap, or Two Pointers
Queue & Deque Operations Quick Reference
| Operation | Queue | Deque | Common Pitfall |
|---|---|---|---|
| Add to back | enqueue / push — O(1) | pushBack — O(1) | N/A |
| Remove from front | dequeue / shift — O(1)* | popFront — O(1) | JS shift() is O(n) on arrays |
| Add to front | N/A | pushFront / unshift — O(1) | JS unshift() is O(n) on arrays |
| Remove from back | N/A | popBack / pop — O(1) | Forgetting this is what makes deque special |
| Peek front | O(1) | O(1) | Peeking without checking empty |
| Peek back | N/A | O(1) | Only available on deque, not queue |
| Size / isEmpty | O(1) | O(1) | Off-by-one in size checks |
One last thing
The queue is the unsung hero of graph algorithms. BFS — powered by a simple FIFO queue — solves shortest path, level-order traversal, connected components, and topological sort. The deque elevates this further: monotonic deques turn O(nk) sliding window problems into O(n), and 0-1 BFS handles binary-weighted graphs without a heap. Master the queue and deque, and you've unlocked an entire category of interview problems that many candidates struggle with.