🗂️Data StructureIntermediate

Queue and Deque

Key Points

  • A queue is a First-In-First-Out (FIFO) line where you add at the back and remove from the front in O(1) time.
  • A deque (double-ended queue) lets you push and pop from both the front and the back in O(1) time.
  • Breadth-First Search (BFS) on unweighted graphs uses a queue and runs in O(n + m) time where n is vertices and m is edges.
  • A monotonic deque solves the sliding window minimum/maximum problem in O(n) by maintaining a decreasing or increasing sequence of candidates.
  • 0-1 BFS uses a deque to compute shortest paths in graphs with edge weights 0 or 1 in O(n + m) time.
  • Circular-array implementations of queues/deques use modular arithmetic to wrap indices around the buffer.
  • std::queue and std::deque in C++ are reliable, but understanding custom implementations helps debug and optimize.
  • Common pitfalls include off-by-one errors in circular indices, forgetting to evict out-of-window indices in sliding windows, and misusing pus/back in 0-1 BFS.

Prerequisites

  • Arrays and indexingCircular arrays implement queues/deques using modular arithmetic on indices.
  • Big-O notationTo analyze amortized O(1) operations and linear-time algorithms like BFS and sliding windows.
  • Graphs and adjacency listsBFS and 0-1 BFS traverse graphs stored as adjacency lists.
  • Modular arithmeticIndex wrap-around in circular buffers uses modulo operations.
  • Queues in C++ STLPractical use leverages std::queue and std::deque interfaces.
  • Proof by invariantMonotonic deque correctness relies on maintaining a sorted-invariant of elements.

Detailed Explanation

Tap terms for definitions

01Overview

Queues and deques are fundamental data structures for ordered processing. A queue is like a real-world line: the first person to enter is the first to be served (FIFO). Operations are limited to two ends: you enqueue (push) at the back and dequeue (pop) from the front, each in constant time. Deques generalize this idea by allowing insertion and deletion at both ends, also in constant time. These structures are widely used in algorithms that process items in layers or over sliding windows. For example, BFS uses a queue to explore neighbors level by level, ensuring shortest paths in unweighted graphs. A monotonic deque cleverly maintains candidate elements to compute sliding window minimums or maximums in linear time, a powerful technique in competitive programming. Similarly, 0-1 BFS uses a deque to efficiently compute shortest paths when edges have weights 0 or 1, achieving linear time similar to BFS but handling weighted edges correctly. Implementations are commonly built on circular arrays (using modular arithmetic to wrap indices) or linked lists (using pointers), with trade-offs in memory locality and dynamic growth. In C++, std::queue and std::deque provide robust, optimized implementations, but understanding the underlying mechanics helps you reason about performance, avoid bugs, and implement algorithmic patterns effectively.

02Intuition & Analogies

Imagine a movie theater line: people join at the back and are served from the front—this is a queue. You can’t skip the line or insert in the middle, which keeps things fair and simple. A deque is like a double-doors hallway where people can enter or exit from either door: sometimes you need to add to the front (priority arrivals) or remove from the back (cancel last), and a deque handles both ends efficiently. For BFS, picture ripples spreading in a pond after you drop a pebble. The closest points get wet first (distance 0), then their neighbors (distance 1), and so on; a queue ensures you process points in exactly that ripple order. For sliding window maximum, think of looking through a car’s side window at passing buildings. As the car moves one step, the leftmost building leaves your view while a new one appears on the right. You only care about the tallest building currently in view. A monotonic deque acts like a curated list of candidate buildings, kicking out any shorter building behind a taller newcomer, because they can never be the tallest while that taller one remains in view. For 0-1 BFS, picture you’re walking a city where some streets are moving walkways (cost 0) and others are normal sidewalks (cost 1). If you find a moving walkway, you want to take it immediately—so you place that next location at the very front of your plan (deque’s front). If it’s a sidewalk, you place it at the back and will get to it after free opportunities. This front/back choice is exactly how a deque implements optimal choices in linear time.

03Formal Definition

A queue is an abstract data type supporting operations push (enqueue) at the back and pop (dequeue) at the front, plus front, size, and empty queries. In the FIFO discipline, the removal order matches insertion order. A deque (double-ended queue) supports pus, pus, po, po, front, back, size, and empty, all in O(1) amortized time under typical implementations. A circular-array implementation maintains a fixed-capacity array with head index h, length len, and capacity C, mapping logical position i to physical index (h + i) mod C; when full, it resizes and realigns data. In graph algorithms, BFS on an unweighted graph G = (V, E) uses a queue to compute shortest-path distances in hops; each vertex is enqueued once when first discovered, ensuring distances monotonically increase by 1 along layers. For sliding window extrema, a monotonic deque stores indices of elements in the current window in decreasing (for maxima) or increasing (for minima) order, invariant a[dq[0]] is the window’s extreme and indices outside the window are evicted from the front. In 0-1 BFS for graphs with w(u, v) in {0, 1}, we maintain distances d[v] and a deque Q; when relaxing an edge of weight 0 we push the neighbor to the front, and for weight 1 we push to the back, preserving the nondecreasing order of tentative distances across the deque.

04When to Use

Use a queue when you need to process items in the exact order they arrive, especially for level-order or breadth-wise exploration: BFS in unweighted graphs, topological-like layer processing, multi-source spreading phenomena, or simulating pipelines. Use a deque when you need efficient access at both ends: implementing undo/redo buffers, maintaining a moving window of data (e.g., stream processing), or two-ended simulations. A monotonic deque is ideal for sliding window minimum/maximum problems in arrays or streams, achieving O(n) over all windows compared to O(nk) naive methods; this shows up in stock price analysis, sensor signal smoothing, and problems like maximizing subarray sums under window constraints. Use 0-1 BFS for shortest paths when edges have weights 0 or 1; it’s faster and simpler than Dijkstra for this special case and is common in grid problems where moving straight may cost 0 and turning costs 1, or in problems with free teleports (0 edges) plus normal moves (1 edges). Choose circular arrays for implementing queues/deques when you want cache-friendly performance and predictable memory layout; choose linked lists when frequent mid-structure splicing is needed (though for standard queue/deque operations, arrays are usually faster).

⚠️Common Mistakes

  • Off-by-one and wrap-around bugs in circular arrays: forgetting to apply modulo after incrementing/decrementing indices, or confusing head/tail semantics. Always compute new indices as (index ± 1 + C) mod C and keep an explicit size to detect full vs empty.
  • Forgetting to evict out-of-window indices in sliding window problems, causing stale maxima/minima. Before reading the current window result, pop from the front while dq.front() <= i - k.
  • Maintaining the wrong monotonicity direction: for maximums use nonincreasing values in the deque; for minimums use nondecreasing. Also, use indices not values to quickly check if an element has left the window.
  • Misusing 0-1 BFS pushes: pushing weight-0 relaxations to the back breaks correctness and can degrade performance. The correct rule is push_front for weight 0, push_back for weight 1, and only relax when a shorter distance is found.
  • Re-enqueuing already visited nodes in BFS without checking visited state leads to O(m) enqueues per node and timeouts. Mark visited (or set distance) at enqueue time, not at dequeue time, to avoid duplicates.
  • Confusing std::queue and std::deque: std::queue is an adapter with no iterators and no push_front/pop_back; std::deque supports both ends and random access but has different iterator invalidation rules than std::vector.
  • Unnecessary copying of large elements in queues/deques. Prefer storing indices or pointers/references to reduce overhead.
  • Assuming all operations are O(1) worst-case even with dynamic resizing. Resizes are O(n), but amortized O(1); avoid triggering many resizes by reserving capacity when possible.

Key Formulas

BFS Complexity

Explanation: Breadth-First Search processes each vertex and edge a constant number of times. Total time is linear in the number of vertices n and edges m.

Sliding Windows Count

Explanation: For an array of length n and window size k, the number of distinct windows you can slide over is n − k + 1. This bounds how many outputs your algorithm must produce.

Monotonic Deque Work Bound

Explanation: Each index is pushed to the deque once and popped at most once across the entire process. Therefore, total deque operations are linear in n.

Circular Index Update

Explanation: When using a circular array of capacity C, moving to the next or previous position wraps around using modulo arithmetic to stay within [0, C).

0-1 Shortest Path Recurrence

Explanation: The shortest distance to v is the minimum over all incoming edges of the distance to the predecessor plus the edge weight. With weights 0 or 1, a deque can realize this efficiently.

0-1 BFS Complexity

Explanation: Each edge causes at most one successful relaxation and each vertex enters the deque at most once per improvement. This yields linear time and space in graph size.

Linear Space

Explanation: Both BFS and sliding window algorithms store a constant amount of data per vertex/element, resulting in total memory proportional to input size.

Monotonic Invariant (Max)

Explanation: In a deque storing indices for window maximums, the corresponding values are nonincreasing from front to back. This ensures the front is always the current maximum.

Complexity Analysis

Core queue and deque operations (push/pop at either end, front/back, size, empty) are O(1) on average. In circular-array implementations, each operation performs a constant number of arithmetic and assignment steps; occasional resizes cost O(n) when the buffer grows, but by doubling capacity the amortized cost per operation remains O(1). Linked-list deques can guarantee O(1) worst-case without resizing but often have worse constant factors and cache locality. BFS on an unweighted graph runs in O(n + m) time because each vertex is enqueued and dequeued once, and each edge is examined at most twice (once per endpoint in undirected graphs). The auxiliary arrays (distance, visited, parent) use O(n) space, and the adjacency list stores O(n + m) edges. The sliding window maximum/minimum with a monotonic deque runs in O(n) time: each array index is pushed at most once and popped at most once, giving at most 2n deque modifications. Space is O(k) for the deque plus O(n − k + 1) for results. This is asymptotically optimal because you must produce that many outputs. 0-1 BFS also achieves O(n + m) time by pushing neighbors reached via weight-0 edges to the front and weight-1 edges to the back, preserving nondecreasing tentative distances. Each successful relaxation inserts a vertex to the deque at a position consistent with its new distance; across the algorithm, the total number of pushes/pops is O(n + m). Space is O(n + m) due to the graph and O(n) for distances and deque.

Code Examples

Implementing a Circular Deque (and using it as a Queue)
1#include <iostream>
2#include <vector>
3#include <stdexcept>
4
5class CircularDeque {
6private:
7 std::vector<int> buf; // storage buffer
8 int head; // index of the first element
9 int len; // number of elements currently stored
10
11 void resize_if_needed() {
12 if (len < (int)buf.size()) return; // not full
13 int oldC = (int)buf.size();
14 int newC = oldC * 2;
15 std::vector<int> nb(newC);
16 // Copy elements in logical order to new buffer starting at 0
17 for (int i = 0; i < len; ++i) {
18 nb[i] = buf[(head + i) % oldC];
19 }
20 buf.swap(nb);
21 head = 0; // realign to start
22 }
23
24public:
25 CircularDeque(int capacity = 8) : buf(std::max(2, capacity)), head(0), len(0) {}
26
27 bool empty() const { return len == 0; }
28 int size() const { return len; }
29 int capacity() const { return (int)buf.size(); }
30
31 // Accessors
32 int front() const {
33 if (empty()) throw std::runtime_error("front on empty deque");
34 return buf[head];
35
36 }
37 int back() const {
38 if (empty()) throw std::runtime_error("back on empty deque");
39 int C = (int)buf.size();
40 int tailIndex = (head + len - 1 + C) % C;
41 return buf[tailIndex];
42 }
43
44 // Push operations
45 void push_front(int x) {
46 resize_if_needed();
47 int C = (int)buf.size();
48 head = (head - 1 + C) % C; // move head backward with wrap-around
49 buf[head] = x;
50 ++len;
51 }
52 void push_back(int x) {
53 resize_if_needed();
54 int C = (int)buf.size();
55 int tail = (head + len) % C; // position after the last element
56 buf[tail] = x;
57 ++len;
58 }
59
60 // Pop operations
61 void pop_front() {
62 if (empty()) throw std::runtime_error("pop_front on empty deque");
63 int C = (int)buf.size();
64 head = (head + 1) % C;
65 --len;
66 }
67 void pop_back() {
68 if (empty()) throw std::runtime_error("pop_back on empty deque");
69 --len; // logical removal from the back
70 }
71};
72
73int main() {
74 CircularDeque dq(4);
75
76 // Use as a deque
77 dq.push_back(10);
78 dq.push_front(5);
79 dq.push_back(20);
80 std::cout << "Front: " << dq.front() << ", Back: " << dq.back() << "\n"; // 5, 20
81
82 dq.pop_front(); // remove 5
83 std::cout << "Front after pop_front: " << dq.front() << "\n"; // 10
84
85 dq.push_front(7);
86 dq.push_back(30); // may trigger resize
87
88 // Use as a queue: only push_back and pop_front
89 dq.push_back(40);
90 std::cout << "Queue-like front: " << dq.front() << "\n"; // 10
91 dq.pop_front();
92 std::cout << "Next front: " << dq.front() << "\n"; // 20
93
94 std::cout << "Size: " << dq.size() << ", Capacity: " << dq.capacity() << "\n";
95 return 0;
96}
97

This program implements a circular-array deque with O(1) amortized push/pop at both ends. It keeps a head index and a length; the tail is derived as (head + len) % C. When full, it doubles capacity and realigns data contiguously. The main function demonstrates both deque operations and how a queue is just a restricted deque using push_back and pop_front.

Time: Amortized O(1) per operation; occasional O(n) when resizingSpace: O(n) where n is the number of stored elements
BFS on an Unweighted Graph Using std::queue
1#include <iostream>
2#include <vector>
3#include <queue>
4#include <limits>
5
6// BFS to compute shortest distances (in edges) from a source in an unweighted graph
7std::vector<int> bfs_shortest_distances(const std::vector<std::vector<int>>& adj, int s) {
8 int n = (int)adj.size();
9 const int INF = std::numeric_limits<int>::max();
10 std::vector<int> dist(n, INF);
11 std::queue<int> q;
12
13 dist[s] = 0; // mark discovered at enqueue time
14 q.push(s);
15
16 while (!q.empty()) {
17 int u = q.front(); q.pop();
18 for (int v : adj[u]) {
19 if (dist[v] == INF) {
20 dist[v] = dist[u] + 1;
21 q.push(v);
22 }
23 }
24 }
25 return dist;
26}
27
28int main() {
29 // Example graph (undirected)
30 int n = 6;
31 std::vector<std::vector<int>> adj(n);
32 auto add_edge = [&](int u, int v){ adj[u].push_back(v); adj[v].push_back(u); };
33 add_edge(0, 1);
34 add_edge(0, 2);
35 add_edge(1, 3);
36 add_edge(2, 3);
37 add_edge(3, 4);
38 add_edge(4, 5);
39
40 int source = 0;
41 std::vector<int> dist = bfs_shortest_distances(adj, source);
42
43 std::cout << "Distances from node " << source << ":\n";
44 for (int i = 0; i < n; ++i) {
45 if (dist[i] == std::numeric_limits<int>::max()) std::cout << "INF ";
46 else std::cout << dist[i] << ' ';
47 }
48 std::cout << '\n';
49 return 0;
50}
51

This code computes shortest-path distances (number of edges) from a source in an unweighted graph using std::queue. Marking dist[v] at enqueue time prevents multiple enqueues of the same vertex and guarantees O(n + m) time.

Time: O(n + m)Space: O(n + m) for adjacency + O(n) for dist and queue
Sliding Window Maximum with a Monotonic Deque
1#include <iostream>
2#include <vector>
3#include <deque>
4
5// Returns maximums for each window of size k
6std::vector<int> sliding_window_max(const std::vector<int>& a, int k) {
7 std::deque<int> dq; // stores indices, values are nonincreasing
8 std::vector<int> ans;
9 for (int i = 0; i < (int)a.size(); ++i) {
10 // 1) Evict indices that fall out of the window [i-k+1, i]
11 while (!dq.empty() && dq.front() <= i - k) dq.pop_front();
12 // 2) Maintain decreasing deque: remove smaller values from the back
13 while (!dq.empty() && a[dq.back()] <= a[i]) dq.pop_back();
14 // 3) Add current index
15 dq.push_back(i);
16 // 4) Record answer once the first window is formed
17 if (i >= k - 1) ans.push_back(a[dq.front()]);
18 }
19 return ans;
20}
21
22int main() {
23 std::vector<int> a = {1, 3, -1, -3, 5, 3, 6, 7};
24 int k = 3;
25 std::vector<int> res = sliding_window_max(a, k);
26
27 std::cout << "Window maximums: ";
28 for (int x : res) std::cout << x << ' ';
29 std::cout << '\n';
30 return 0;
31}
32

The deque stores indices of elements in nonincreasing value order. Each index is pushed once and popped at most once, yielding O(n) total operations. The front of the deque is always the maximum in the current window, and indices that fall out of the window are evicted from the front.

Time: O(n)Space: O(k) for the deque + O(n - k + 1) for outputs
0-1 BFS Using std::deque for Shortest Paths
1#include <iostream>
2#include <vector>
3#include <deque>
4#include <limits>
5#include <utility>
6
7// Graph with edges of weight 0 or 1
8using Edge = std::pair<int,int>; // (to, weight)
9
10std::vector<int> zero_one_bfs(const std::vector<std::vector<Edge>>& adj, int s) {
11 int n = (int)adj.size();
12 const int INF = std::numeric_limits<int>::max() / 4;
13 std::vector<int> dist(n, INF);
14 std::deque<int> dq;
15
16 dist[s] = 0;
17 dq.push_front(s);
18
19 while (!dq.empty()) {
20 int u = dq.front(); dq.pop_front();
21 for (auto [v, w] : adj[u]) {
22 int nd = dist[u] + w; // w is 0 or 1
23 if (nd < dist[v]) {
24 dist[v] = nd;
25 if (w == 0) dq.push_front(v); // free move: process sooner
26 else dq.push_back(v); // cost-1 move: process later
27 }
28 }
29 }
30 return dist;
31}
32
33int main() {
34 int n = 5;
35 std::vector<std::vector<Edge>> adj(n);
36 auto add = [&](int u, int v, int w){ adj[u].push_back({v, w}); };
37 // Example directed graph with 0/1 weights
38 add(0, 1, 0); add(0, 2, 1);
39 add(1, 2, 0); add(1, 3, 1);
40 add(2, 3, 0); add(3, 4, 1);
41 add(1, 4, 1);
42
43 int s = 0;
44 std::vector<int> d = zero_one_bfs(adj, s);
45
46 std::cout << "Distances from " << s << ":\n";
47 for (int i = 0; i < n; ++i) {
48 if (d[i] >= std::numeric_limits<int>::max()/8) std::cout << "INF ";
49 else std::cout << d[i] << ' ';
50 }
51 std::cout << '\n';
52 return 0;
53}
54

0-1 BFS keeps a deque of vertices ordered by current tentative distance: relaxing a 0-weight edge inserts the neighbor at the front, while a 1-weight edge inserts at the back. This mirrors Dijkstra’s priority behavior without a heap and achieves O(n + m) time for 0/1 weights.

Time: O(n + m)Space: O(n + m) for the graph + O(n) for dist and deque