C++ std::deque Guide: Double-Ended Queue Container
C++ std::deque Guide: Double-Ended Queue Container
A comprehensive guide to std::deque, a double-ended queue container that provides efficient insertion and deletion at both ends with random access, covering all essential methods, common use cases, and best practices.
Table of Contents
- Deque Basics
- Element Access Methods
- Modifiers
- Iterator Methods
- Common Use Cases
- Runtime Complexity Analysis
- Best Practices
Deque Basics
std::deque (double-ended queue) is a sequence container that provides efficient insertion and deletion at both ends, along with random access to elements. It’s implemented as a collection of fixed-size arrays.
#include <deque>
#include <iostream>
int main() {
// Default construction
std::deque<int> dq1;
// Construction with size
std::deque<int> dq2(5); // 5 elements, all initialized to 0
// Construction with size and initial value
std::deque<int> dq3(5, 10); // 5 elements, all set to 10
// Initializer list (C++11)
std::deque<int> dq4 = {1, 2, 3, 4, 5};
// Copy construction
std::deque<int> dq5(dq4);
// Range construction
std::vector<int> vec = {1, 2, 3};
std::deque<int> dq6(vec.begin(), vec.end());
// Access elements
dq4[0] = 10; // Random access
int first = dq4.front(); // First element
int last = dq4.back(); // Last element
// Iterate
for (const auto& elem : dq4) {
std::cout << elem << " ";
}
}
Key Characteristics
- Double-ended: Efficient insertion/deletion at both ends (O(1))
- Random access: Supports
operator[]andat()(O(1)) - Segmented storage: Implemented as collection of fixed-size arrays
- Iterator invalidation: More complex than
std::vector - No contiguous storage: Elements may not be stored contiguously
Element Access Methods
Random Access
std::deque<int> dq = {1, 2, 3, 4, 5};
// operator[] - No bounds checking
int elem = dq[2]; // Returns 3
dq[2] = 99; // Modify element
// at() - Bounds checking
try {
int elem = dq.at(2); // Returns 3
int invalid = dq.at(10); // Throws std::out_of_range
} catch (const std::out_of_range& e) {
std::cout << "Index out of range" << std::endl;
}
Front and Back Access
std::deque<int> dq = {1, 2, 3, 4, 5};
// front() - First element
int first = dq.front(); // Returns 1
dq.front() = 10; // Modify first element
// back() - Last element
int last = dq.back(); // Returns 5
dq.back() = 50; // Modify last element
⚠️ Note: front() and back() are undefined if deque is empty. Always check empty() first.
Modifiers
Insertion at Ends
std::deque<int> dq = {1, 2, 3};
// push_front() - Insert at beginning
dq.push_front(0); // {0, 1, 2, 3}
// push_back() - Insert at end
dq.push_back(4); // {0, 1, 2, 3, 4}
// emplace_front() - Construct at front (C++11)
dq.emplace_front(-1); // {-1, 0, 1, 2, 3, 4}
// emplace_back() - Construct at back (C++11)
dq.emplace_back(5); // {-1, 0, 1, 2, 3, 4, 5}
Insertion at Position
std::deque<int> dq = {1, 2, 3, 4, 5};
// insert() - Insert at position
auto it = dq.begin();
std::advance(it, 2);
dq.insert(it, 99); // Insert 99 before position 2
// Result: {1, 2, 99, 3, 4, 5}
// insert() with count
dq.insert(it, 3, 88); // Insert 3 copies of 88
// insert() with range
std::vector<int> vec = {10, 20};
dq.insert(it, vec.begin(), vec.end());
// insert() with initializer list
dq.insert(it, {100, 200, 300});
Erasure
std::deque<int> dq = {1, 2, 3, 4, 5};
// pop_front() - Remove first element
dq.pop_front(); // {2, 3, 4, 5}
// pop_back() - Remove last element
dq.pop_back(); // {2, 3, 4}
// erase() - Remove by iterator
auto it = dq.begin();
std::advance(it, 1);
it = dq.erase(it); // Erase element at position, returns next iterator
// Result: {2, 4}
// erase() - Remove range
auto first = dq.begin();
auto last = dq.end();
dq.erase(first, last); // Clear deque
// clear() - Remove all elements
dq.clear();
Modification
std::deque<int> dq = {1, 2, 3, 4, 5};
// resize() - Change size
dq.resize(3); // {1, 2, 3} (truncate)
dq.resize(5); // {1, 2, 3, 0, 0} (default-constructed)
dq.resize(5, 99); // {1, 2, 3, 99, 99} (with value)
// swap() - Exchange contents
std::deque<int> dq1 = {1, 2, 3};
std::deque<int> dq2 = {4, 5, 6};
dq1.swap(dq2);
// dq1: {4, 5, 6}, dq2: {1, 2, 3}
// assign() - Replace contents
dq.assign(5, 10); // {10, 10, 10, 10, 10}
dq.assign({1, 2, 3}); // {1, 2, 3}
Iterator Methods
std::deque<int> dq = {1, 2, 3, 4, 5};
// Forward iteration
for (auto it = dq.begin(); it != dq.end(); ++it) {
std::cout << *it << " ";
}
// Reverse iteration
for (auto it = dq.rbegin(); it != dq.rend(); ++it) {
std::cout << *it << " ";
}
// Range-based for loop (C++11)
for (const auto& elem : dq) {
std::cout << elem << " ";
}
// Const iterators
for (auto it = dq.cbegin(); it != dq.cend(); ++it) {
// *it is const
}
// Random access iterators
auto it = dq.begin();
it += 3; // Move 3 positions forward
int value = *it; // Access element
Common Use Cases
1. Queue with Random Access
std::deque<int> queue;
// Enqueue at back
queue.push_back(1);
queue.push_back(2);
queue.push_back(3);
// Dequeue from front
while (!queue.empty()) {
int front = queue.front();
queue.pop_front();
// Process front
}
// Random access when needed
int middle = queue[queue.size() / 2];
2. Sliding Window
std::deque<int> window;
std::vector<int> data = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
int k = 3; // Window size
// Maintain sliding window
for (int i = 0; i < data.size(); ++i) {
// Remove elements outside window
while (!window.empty() && window.front() <= i - k) {
window.pop_front();
}
// Remove smaller elements from back
while (!window.empty() && data[window.back()] <= data[i]) {
window.pop_back();
}
window.push_back(i);
// Process window when it's full
if (i >= k - 1) {
int max = data[window.front()];
// Use max value
}
}
3. Both-Ends Operations
std::deque<int> dq;
// Efficient operations at both ends
dq.push_front(1); // O(1)
dq.push_back(2); // O(1)
dq.pop_front(); // O(1)
dq.pop_back(); // O(1)
// Random access
int middle = dq[dq.size() / 2]; // O(1)
4. Palindrome Checker
bool isPalindrome(const std::deque<char>& dq) {
auto front = dq.begin();
auto back = dq.rbegin();
while (front != dq.end() && back != dq.rend()) {
if (*front != *back) {
return false;
}
++front;
++back;
}
return true;
}
5. BFS Queue with Index Access
std::deque<std::pair<int, int>> queue; // {node, level}
queue.push_back({0, 0});
while (!queue.empty()) {
auto [node, level] = queue.front();
queue.pop_front();
// Process node
// Add neighbors
queue.push_back({neighbor, level + 1});
// Can still access by index if needed
if (!queue.empty()) {
int next_level = queue[0].second;
}
}
Runtime Complexity Analysis
Understanding the time and space complexity of std::deque operations is crucial for choosing the right container.
Time Complexity
| Operation | Time Complexity | Notes |
|---|---|---|
| Element Access | ||
operator[], at() |
O(1) | Random access |
front(), back() |
O(1) | Direct access to first/last |
| Iterators | ||
begin(), end(), rbegin(), rend() |
O(1) | Iterator creation |
| Modifiers | ||
push_front(), push_back() |
O(1) | Insert at beginning/end |
pop_front(), pop_back() |
O(1) | Remove from beginning/end |
insert() (at position) |
O(n) | Linear in distance from nearest end |
insert() (count) |
O(n + m) | n = distance, m = count |
insert() (range) |
O(n + m) | n = distance, m = range size |
emplace_front(), emplace_back() |
O(1) | Construct at beginning/end |
emplace() |
O(n) | Linear in distance from nearest end |
erase() (at position) |
O(n) | Linear in distance from nearest end |
erase() (range) |
O(n + m) | n = distance, m = range size |
clear() |
O(n) | Destroys all elements |
resize() |
O(n) | n = difference in size |
swap() |
O(1) | Constant time, swaps internal pointers |
assign() |
O(n) | n = new size |
| Operations | ||
size(), empty(), max_size() |
O(1) | Constant time |
Space Complexity
- Storage: O(n) where n is the number of elements
- Overhead: Collection of fixed-size arrays (chunks)
- Total: Typically similar to
std::vector, but with additional overhead for chunk management
Internal Structure
std::deque is implemented as a collection of fixed-size arrays:
- Segmented storage: Elements stored in multiple fixed-size chunks
- Map of chunks: Central map points to chunks
- Growth: New chunks added at either end as needed
- No reallocation: Unlike
std::vector, doesn’t need to move all elements
Comparison with Other Containers
| Operation | std::deque |
std::vector |
std::list |
|---|---|---|---|
| Insert at beginning | O(1) | O(n) | O(1) |
| Insert at end | O(1) | O(1) amortized | O(1) |
| Insert in middle | O(n) | O(n) | O(1) |
| Erase at beginning | O(1) | O(n) | O(1) |
| Erase at end | O(1) | O(1) | O(1) |
| Erase in middle | O(n) | O(n) | O(1) |
| Random access | ✅ O(1) | ✅ O(1) | ❌ No |
| Contiguous storage | ❌ No | ✅ Yes | ❌ No |
| Iterator invalidation | Complex | Frequent | Stable |
Performance Tips Based on Complexity
- Use
std::dequefor both-end operations → O(1) at both ends - Prefer
std::vectorfor middle insertions → Both O(n), but vector is cache-friendly - Use random access when needed → O(1) random access available
- Avoid middle insertions/deletions → O(n) operation, use
std::listif frequent - Consider cache performance →
std::vectoris more cache-friendly - Use
reserve()equivalent:std::dequedoesn’t havereserve(), but you can pre-allocate chunks
When to Use std::deque
✅ Use std::deque when:
- Need efficient insertion/deletion at both ends
- Random access is required
- Both queue and stack operations needed
- Sliding window algorithms
- BFS/DFS with index access
❌ Avoid std::deque when:
- Frequent middle insertions/deletions → Use
std::list - Cache performance is critical → Use
std::vector - Contiguous storage needed → Use
std::vector - Simple sequential access →
std::vectoris usually faster
Best Practices
✅ Do’s
- Use for both-end operations
std::deque<int> dq; dq.push_front(1); // O(1) dq.push_back(2); // O(1) - Use random access when needed
int middle = dq[dq.size() / 2]; // O(1) - Check
empty()beforefront()/back()if (!dq.empty()) { int first = dq.front(); } - Use
emplace_front()/emplace_back()for complex typesdq.emplace_front(ComplexType{arg1, arg2}); - Prefer
std::vectorfor cache performance// If cache performance matters more than both-end ops std::vector<int> vec; // More cache-friendly
❌ Don’ts
- Don’t use for frequent middle insertions
// ❌ If frequent middle insertions std::deque<int> dq; // O(n) for middle insertion // ✅ Use list std::list<int> lst; // O(1) for middle insertion - Don’t assume contiguous storage
// ❌ May not be contiguous int* ptr = dq.data(); // Doesn't exist // ✅ Use vector for contiguous storage std::vector<int> vec; int* ptr = vec.data(); // Valid - Don’t ignore iterator invalidation
std::deque<int> dq = {1, 2, 3, 4, 5}; auto it = dq.begin() + 2; // ⚠️ Iterator may be invalidated dq.push_front(0); // May invalidate it dq.push_back(6); // May invalidate it - Don’t use when cache performance is critical
// ❌ Less cache-friendly std::deque<int> dq; // ✅ More cache-friendly std::vector<int> vec;
Performance Tips
- Both-end operations:
std::dequeexcels at O(1) operations at both ends - Random access: O(1) random access available
- Cache performance:
std::vectoris more cache-friendly - Middle operations: Use
std::listfor frequent middle insertions/deletions - Iterator invalidation: More complex than
std::vector, be careful
Summary: std::deque provides efficient O(1) operations at both ends with random access. Use it when you need both queue and stack operations with random access. For cache performance or contiguous storage, prefer std::vector. For frequent middle operations, use std::list.