Stacks, Queues, and Deques: Sequential Access Data Structures
Master LIFO, FIFO, and double-ended queue operations with implementations, time complexity analysis, and practical applications.
Stacks, Queues, and Deques: Sequential Access Data Structures
Stacks, queues, and deques enforce specific access patterns rather than allowing arbitrary inserts and removes. This constraint is a feature—by limiting what you can do, they make certain operations efficient and predictable. A stack follows Last-In-First-Out (LIFO), a queue follows First-In-First-Out (FIFO), and a deque allows insertion and removal from both ends.
Introduction
Stacks, queues, and deques are restricted-access data structures that enforce specific orderings on insertions and removals. A stack follows Last-In-First-Out (LIFO)—you can only access the most recently added element. A queue follows First-In-First-Out (FIFO)—you can only access the oldest element. A deque (double-ended queue) allows insertion and removal from both ends, combining capabilities of both structures.
These data structures are everywhere in software. Stacks power undo/redo systems, expression evaluation, and depth-first search. Queues handle task scheduling, breadth-first search, and message passing between services. Deques excel at sliding window problems and implementing efficient ring buffers. Understanding their implementation details matters because subtle choices—like whether to implement a stack with an array or linked list—affect cache performance and memory usage in production systems.
You’ll implement each structure from scratch, analyze time complexity of all operations, and see how Python’s collections.deque achieves O(1) performance at both ends through chunk-based memory management. We’ll cover balanced parentheses checking, shortest path finding, and the monotonic deque pattern for sliding window maximum problems.
Stack (LIFO - Last In First Out)
class Stack:
"""Array-based stack with O(1) push, pop, peek."""
def __init__(self, capacity=10):
self.capacity = capacity
self.data = [None] * capacity
self.size = 0
def push(self, val):
"""O(1) push - amortized if resizing."""
if self.size == self.capacity:
self._resize()
self.data[self.size] = val
self.size += 1
def pop(self):
"""O(1) pop."""
if self.size == 0:
raise IndexError("Stack underflow")
val = self.data[self.size - 1]
self.size -= 1
return val
def peek(self):
"""O(1) view top element."""
if self.size == 0:
raise IndexError("Stack is empty")
return self.data[self.size - 1]
def is_empty(self):
return self.size == 0
def _resize(self):
new_data = [None] * (self.capacity * 2)
for i in range(self.size):
new_data[i] = self.data[i]
self.data = new_data
self.capacity *= 2
class StackLinkedList:
"""Linked list based stack - O(1) all operations, no resize needed."""
def __init__(self):
self.top = None
self.size = 0
def push(self, val):
self.top = ListNode(val, self.top)
self.size += 1
def pop(self):
if not self.top:
raise IndexError("Stack underflow")
val = self.top.val
self.top = self.top.next
self.size -= 1
return val
def peek(self):
if not self.top:
raise IndexError("Stack is empty")
return self.top.val
Queue (FIFO - First In First Out)
class Queue:
"""Array-based circular queue with O(1) enqueue and dequeue."""
def __init__(self, capacity=10):
self.capacity = capacity
self.data = [None] * capacity
self.front = 0
self.rear = -1
self.size = 0
def enqueue(self, val):
"""O(1) add to rear."""
if self.size == self.capacity:
self._resize()
self.rear = (self.rear + 1) % self.capacity
self.data[self.rear] = val
self.size += 1
def dequeue(self):
"""O(1) remove from front."""
if self.size == 0:
raise IndexError("Queue underflow")
val = self.data[self.front]
self.front = (self.front + 1) % self.capacity
self.size -= 1
return val
def peek(self):
"""O(1) view front element."""
if self.size == 0:
raise IndexError("Queue is empty")
return self.data[self.front]
def _resize(self):
new_data = [None] * (self.capacity * 2)
for i in range(self.size):
new_data[i] = self.data[(self.front + i) % self.capacity]
self.data = new_data
self.front = 0
self.rear = self.size - 1
self.capacity *= 2
from collections import deque as PythonDeque
# For most use cases, Python's deque is optimal:
# O(1) append, appendleft, pop, popleft
queue = PythonDeque()
queue.append(1) # enqueue
queue.append(2) # enqueue
queue.popleft() # dequeue -> returns 1
Deque (Double-Ended Queue)
class Deque:
"""Deque supporting O(1) insert/remove at both ends."""
def __init__(self):
self.front = []
self.rear = []
def push_front(self, val):
"""O(1) insert at front."""
self.front.append(val)
def push_back(self, val):
"""O(1) insert at rear."""
self.rear.append(val)
def pop_front(self):
"""O(1) remove from front."""
if not self.front:
if not self.rear:
raise IndexError("Deque empty")
# Move last element of rear to front
self.rear.reverse()
self.front = self.rear
self.rear = []
return self.front.pop()
def pop_back(self):
"""O(1) remove from rear."""
if not self.rear:
if not self.front:
raise IndexError("Deque empty")
self.front.reverse()
self.rear = self.front
self.front = []
return self.rear.pop()
def peek_front(self):
if self.front:
return self.front[-1]
if self.rear:
return self.rear[0]
raise IndexError("Deque empty")
def peek_back(self):
if self.rear:
return self.rear[-1]
if self.front:
return self.front[0]
raise IndexError("Deque empty")
# Sliding Window Maximum using deque
from collections import deque
def max_sliding_window(arr, k):
"""
Find maximum in each sliding window of size k.
O(n) time using monotonic deque.
"""
if not arr or k == 0:
return []
result = []
dq = deque() # Stores indices
for i in range(len(arr)):
# Remove indices outside current window
while dq and dq[0] < i - k + 1:
dq.popleft()
# Remove indices whose values are smaller than current
# (they'll never be the maximum in this window)
while dq and arr[dq[-1]] <= arr[i]:
dq.pop()
dq.append(i)
# Start recording results once we have a full window
if i >= k - 1:
result.append(arr[dq[0]])
return result
Monotonic Deque Behavior
graph LR
subgraph Window1["Window: [1, 3, 2]"]
direction LR
W1A["1"] --> W1B["3"] --> W1C["2"]
end
subgraph Deque1["Deque (decreasing)"]
direction LR
D1A["3"] --> D1B["2"]
end
Window1 --> Deque1
Deque1 --> Max1["max = 3"]
For sliding window [1, 3, 2] with k=3: we add 1 (deque: [1]), then 3 (remove 1, deque: [3]), then 2 (deque: [3, 2]). The front always holds the maximum. When 3 exits the window, 2 becomes the new maximum.
Stack Applications
| Problem | Stack Usage |
|---|---|
| Undo/Redo | Push operations, pop to undo |
| Expression evaluation | Parentheses matching, postfix conversion |
| Function call stack | Recursion, call hierarchy |
| DFS traversal | Explicit stack for iterative depth-first |
| Nearest greater element | Maintain decreasing stack while traversing |
# Balanced parentheses check
def is_balanced(s):
stack = []
matching = {')': '(', ']': '[', '}': '{'}
for char in s:
if char in '([{':
stack.append(char)
elif char in ')]}':
if not stack or stack[-1] != matching[char]:
return False
stack.pop()
return len(stack) == 0
# Nearest greater element to the right
def nearest_greater_right(arr):
result = [-1] * len(arr)
stack = [] # Stack of indices with decreasing values
for i in range(len(arr)):
while stack and arr[stack[-1]] < arr[i]:
result[stack.pop()] = arr[i]
stack.append(i)
return result
Queue Applications
| Problem | Queue Usage |
|---|---|
| BFS traversal | FIFO order for level-order processing |
| Task scheduling | Round-robin, print queue |
| Buffering | Network packets, I/O operations |
| Message queues | Producer-consumer patterns |
# BFS shortest path
from collections import deque
def bfs_shortest_path(graph, start, end):
if start == end:
return [start]
queue = deque([(start, [start])])
visited = {start}
while queue:
node, path = queue.popleft()
for neighbor in graph[node]:
if neighbor == end:
return path + [neighbor]
if neighbor not in visited:
visited.add(neighbor)
queue.append((neighbor, path + [neighbor]))
return None # No path exists
When to Use Each Structure
| Structure | Access Pattern | Use When |
|---|---|---|
| Stack | LIFO | Undo, recursion, DFS, matching problems |
| Queue | FIFO | BFS, scheduling, breadth-first traversal |
| Deque | Both ends | Sliding window max, LRU cache, double-ended operations |
Production Failure Scenarios and Mitigations
Stack Overflow from Unbounded Recursion
The call stack has a fixed size. Deep recursion on large inputs causes stack overflow. A recursive binary tree traversal on a tree with depth > 10,000 crashes the process.
Mitigation: Convert recursive algorithms to iterative using explicit stacks. Set a recursion depth guard that throws before overflowing. Profile stack usage under max expected input size.
Queue Overflow Under Load
Bounded queues reject new items when full. Under traffic spikes, producers block or drop messages. A print queue accepting jobs from 1,000 concurrent users with a 100-job capacity loses work.
Mitigation: Choose bounded vs unbounded based on requirements. Use dead-letter queues for rejected messages. Monitor queue utilization and scale consumers horizontally. Set alerts at 80% capacity.
Deadlock from Lock Ordering in Concurrent Queues
Multiple queues with shared resources create circular dependencies. Thread A holds Queue 1 and waits for Queue 2; Thread B holds Queue 2 and waits for Queue 1.
Mitigation: Enforce consistent lock ordering across all threads (always acquire Queue 1 before Queue 2). Use lock-free data structures when possible. Add deadlock detection in test environments.
Priority Inversion in Priority Queues
Low-priority tasks hold resources needed by high-priority tasks. A background sync task (low priority) holds a lock; a user-facing request (high priority) waits indefinitely.
Mitigation: Implement priority inheritance — temporarily elevate the low-priority task’s priority when it holds a contended lock. Use separate queues per priority level with weighted scheduling.
Trade-Off Table
| Property | Stack | Queue | Deque |
|---|---|---|---|
| Push/Insert | O(1) | O(1) | O(1) at both ends |
| Pop/Remove | O(1) | O(1) | O(1) at both ends |
| Peek | O(1) front only | O(1) front only | O(1) at both ends |
| Search | O(n) | O(n) | O(n) |
| Memory Layout | Contiguous (array) or heap (linked list) | Contiguous via circular buffer | Usually linked chunks or two arrays |
| Thread Safety | Single-threaded unless synchronized | Single-threaded unless synchronized | Single-threaded unless synchronized |
| Best Use Case | Undo history, DFS, recursion | BFS, task scheduling, message passing | Sliding window, LRU cache, double-ended operations |
| Cache Performance | Excellent (contiguous) | Good (circular buffer) | Moderate (often linked) |
| Overflow Risk | Resize doubles memory | Resize or reject | Resize or reject |
Observability Checklist
- Monitor queue depth in real-time; alert at 80% of capacity
- Track overflow/rejection counts per queue
- Measure enqueue and dequeue latency — spikes indicate backpressure
- Log stack trace on stack overflow exceptions with input size context
- Monitor consumer lag: time between enqueue and dequeue
- Track resize events — frequent resizes suggest capacity misprediction
- Alert on dequeue rate dropping to zero (consumer stall)
- Log dead-letter queue item counts for bounded queues
- Trace queue operations across service boundaries using correlation IDs
- Set up dashboards for depth, latency p50/p95/p99, and overflow rate
Security Notes
Stack Buffer Overflow Propagation
Malicious input oversized for a fixed stack buffer corrupts return addresses. Attackers inject shellcode into stack-allocated buffers. Production C/C++ services with manually managed buffers face this risk.
Mitigation: Use memory-safe languages or sanitizers (ASan, MSan). Enable stack canaries. Validate input length before write. Prefer heap allocation with bounds checking.
Resource Exhaustion via Unbounded Queues
An attacker floods a service with messages that accumulate in an unbounded queue. Memory grows until the process or container OOMs.
Mitigation: Use bounded queues with explicit overflow handling (reject, dead-letter, or block with timeout). Authenticate producers. Rate-limit ingress. Monitor memory per queue.
Denial of Service via Queue Flooding
High-volume producers overwhelm consumers. A single misconfigured client sends 10,000 messages/second; the consumer processes 100/second. Backpressure builds until service degrades for all users.
Mitigation: Enforce per-client rate limits at the queue ingress point. Use priority queues to ensure critical traffic gets through. Set consumer scaling policies that react to queue depth.
Race Conditions in Lock-Free Queue Implementations
Concurrent producers and consumers on a shared queue without proper synchronization cause torn reads/writes, item duplication, or use-after-free bugs. Lock-free does not mean wait-free.
Mitigation: Prefer battle-tested queue implementations (Java ConcurrentLinkedQueue, Go channels, Python queue module). Review memory ordering semantics. Stress-test with tools like ThreadSanitizer.
Quick Recap Checklist
- Stack: push/pop O(1), use for LIFO, DFS, undo operations
- Queue: enqueue/dequeue O(1), use for BFS, FIFO scheduling
- Deque: O(1) at both ends, use for sliding window problems
- Monotonic deque maintains decreasing/increasing values for efficiency
- Python’s collections.deque is optimized for both stack and queue operations
Deque Memory Model: Chunk-Based Implementation
While the two-array deque in the implementation section demonstrates the concept, production-grade deques (like Python’s collections.deque) use a chunk-based design for predictable O(1) performance at both ends.
# Conceptual model of a chunk-based deque
# Each block holds up to BLOCK_SIZE elements
BLOCK_SIZE = 64 # Typical value
class ChunkDeque:
"""Simplified conceptual model of Python's collections.deque."""
def __init__(self):
self._blocks = [] # List of fixed-size arrays
self._left_block = 0 # Index of first block
self._left_pos = 0 # Position within first block
self._right_block = 0
self._right_pos = -1
self._size = 0
def append(self, val):
"""Add to right end."""
if self._right_pos == BLOCK_SIZE - 1:
# Need a new block
self._blocks.append([None] * BLOCK_SIZE)
self._right_block += 1
self._right_pos = 0
else:
self._right_pos += 1
self._blocks[self._right_block][self._right_pos] = val
self._size += 1
def appendleft(self, val):
"""Add to left end."""
if self._left_pos == 0:
# Insert a block at the front
self._blocks.insert(0, [None] * BLOCK_SIZE)
self._right_block += 1 # Shift right index
self._left_pos = 0
else:
self._left_pos -= 1
self._blocks[self._left_block][self._left_pos] = val
self._size += 1
def pop(self):
"""Remove from right end."""
val = self._blocks[self._right_block][self._right_pos]
self._blocks[self._right_block][self._right_pos] = None
self._size -= 1
if self._right_pos == 0 and self._size > 0:
# Block is empty, free it
self._blocks.pop()
self._right_block -= 1
self._right_pos = BLOCK_SIZE - 1
else:
self._right_pos -= 1
return val
def popleft(self):
"""Remove from left end."""
val = self._blocks[self._left_block][self._left_pos]
self._blocks[self._left_block][self._left_pos] = None
self._size -= 1
self._left_pos += 1
if self._left_pos == BLOCK_SIZE and self._size > 0:
self._blocks.pop(0)
self._right_block -= 1
self._left_pos = 0
return val
Key advantages of the chunk-based approach:
| Property | Benefit |
|---|---|
| No full-structure resizing | Unlike dynamic arrays, never need to copy all elements |
| Bounded per-operation cost | Each append/pop touches at most one block |
| Cache-friendly | Elements within a block are contiguous |
| Memory efficient | Empty blocks are freed immediately |
| Predictable growth | Block allocation is O(1) amortized |
This design is the reason Python’s collections.deque is the recommended choice for both stack and queue use cases in production Python code.
Interview Questions
Use enqueue stack and dequeue stack. On enqueue, always push to enqueue stack. On dequeue, if dequeue stack is empty, transfer all elements from enqueue stack (pop from enqueue, push to dequeue), then pop from dequeue stack. This achieves amortized O(1) for both operations. The transfer reverses the order, converting FIFO order (first in enqueue → first out dequeue). This is how some message queue implementations work internally.
A monotonic deque maintains elements in sorted order (increasing or decreasing). For sliding window maximum, we maintain a decreasing deque of indices: the front always has the maximum, and smaller elements are removed from the back. When a new element arrives, we remove all smaller elements (they'll never be the maximum while the new element is in window). When the window moves, we discard indices outside the window from the front. This achieves O(n) for the entire sliding window problem.
Recursion implicitly uses a stack (the call stack). Each recursive call pushes a frame onto the stack; returning pops frames. This is why deep recursion can cause stack overflow. Iteration can use an explicit stack to convert recursive algorithms to iterative. Conversely, BFS uses a queue for level-order traversal—recursion naturally handles depth-first patterns, while queues handle breadth-first patterns. Understanding this helps you convert between recursive and iterative solutions.
Use a second stack that tracks the minimum. When pushing, if the new value is ≤ current minimum, push it to the min-stack too. When popping, if the popped value equals the current minimum, pop from min-stack as well. This gives O(1) getMin() while maintaining O(1) push and pop. Space trade-off is O(n) worst case if all pushed elements are decreasing. Alternatively, store (value, min_at_that_point) pairs in a single stack—though this doubles memory per element.
Array-based stack: Contiguous memory provides excellent cache locality and predictable access times. Push/pop are amortized O(1) but occasional resizing (O(n) copy) can cause latency spikes. Memory overhead is low (just the array).
Linked-list-based stack: No resizing overhead; every operation is O(1). However, per-node heap allocation and pointer overhead (~16–24 bytes per element) degrades cache performance. Better for unpredictable workloads where resizing cost is unacceptable.
Trade-off: Use array-based when you can bound the size or need throughput; use linked-list when memory fragmentation or predictable latency is critical.
Use one primary queue and one auxiliary queue. For push: enqueue the new element to the auxiliary queue, then dequeue all elements from the primary queue and enqueue them to the auxiliary queue. Swap the queues. This makes the newest element always at the front of the primary queue. For pop: simply dequeue from the primary queue.
This gives O(n) push and O(1) pop. Alternatively, make push O(1) and pop O(n) by keeping elements in insertion order and rotating for pop. Choose based on which operation needs to be faster.
Maintain a decreasing deque of indices (not values). For each new element in the array:
- Remove indices outside the current window from the front (popleft).
- Remove indices whose values are ≤ the new value from the back (pop) — they can never be the maximum in this or future windows.
- Append the new index to the back.
- The front index always points to the maximum of the current window.
Each element is pushed and popped at most once, yielding O(n) total time and O(k) space.
A circular queue (ring buffer) uses a fixed-size array with two pointers:
front and rear. Pointers wrap around using modulo arithmetic:
rear = (rear + 1) % capacity. This reuses array slots without shifting
elements, giving O(1) enqueue and dequeue.
Efficiency: No element shifting (unlike a naive array queue), single
contiguous allocation for excellent cache locality, and no per-node allocation overhead.
Used extensively in operating systems (I/O ring buffers), networking (packet queues), and
Python's collections.deque (chunk-based variant).
Bounded queues have a fixed capacity. They provide backpressure, prevent memory exhaustion, and enable predictable resource usage. The downside: producers must handle rejection (retry, drop, dead-letter) when full, which adds complexity to the producer logic.
Unbounded queues can grow indefinitely. They simplify producer logic (never reject), but risk OOM under traffic spikes. Consumers can fall behind indefinitely, leading to stale data and unbounded memory growth.
Recommendation: Prefer bounded queues with monitoring at 80% capacity. Use unbounded queues only when you can guarantee bounded input rates and have consumer auto-scaling in place.
Python's collections.deque uses a doubly-linked list of fixed-size
blocks (chunks), typically holding 64 elements each. This design achieves:
- O(1) append/pop at both ends — no shifting or resizing of the entire structure.
- Good cache locality within each block (contiguous memory).
- Memory efficiency — blocks are allocated on demand and freed when empty.
- Predictable performance — no amortized costs from occasional full resizes (unlike a dynamic array).
The block size is a power of two, enabling fast index calculations via bit masking rather than modulo.
Given an array, find the nearest greater element to the right (NGR) for each position — the first larger element appearing after it.
Stack-based solution: Iterate left to right, maintaining a
monotonic decreasing stack of indices. For each element, while the
stack is not empty and arr[stack.top] < arr[i], pop the top index and
set its NGR to arr[i]. Push the current index. Each element is pushed
and popped at most once, giving O(n) time and O(n) space.
The same technique extends to nearest greater to the left, nearest smaller to the right, and nearest smaller to the left by changing traversal direction and comparison operator.
Use two stacks: an undo stack and a redo stack.
- Perform action: push the action (or a reverse action) onto the undo stack. Clear the redo stack (redo history is invalidated by new actions).
- Undo: pop from the undo stack, reverse the action, and push the forward action onto the redo stack.
- Redo: pop from the redo stack, re-apply the action, and push the reverse action onto the undo stack.
This pattern appears in text editors, image editors, and command-line tools. Some implementations cap the undo stack to bound memory usage (e.g., max 1000 undo levels).
In a circular queue with a fixed-size array, use the size
field or track the difference between front and rear pointers. Three common approaches:
- Size counter: Maintain a
sizevariable. Ifsize == 0, the queue is empty; ifsize == capacity, it's full. This is the clearest approach. - Gaps between pointers: Leave one slot empty to distinguish empty
from full. If
(rear + 1) % capacity == front, it's full; iffront == rear, it's empty. - Flag variable: Use a
is_fullboolean flag in addition to pointer comparison.
Python's collections.deque handles this internally and always maintains O(1)
operations with no capacity limit by default (configurable via maxlen).
A deque (double-ended queue) allows O(1) insertion and removal at both ends, but the ordering within the deque depends on where you insert. Elements are ordered by insertion sequence.
A double-ended priority queue allows O(1) removal of the minimum OR maximum (but not both) from either end, while elements are ordered by priority (heap property). The internal structure is typically a min-max heap or two heaps.
- Deque: Use when you need fast access to both ends with insertion order preserved.
- Double-ended priority queue: Use when elements have priorities and you need to extract extreme values from either end (e.g., scheduling with both high and low priority tasks).
Use a monotonic auxiliary stack that tracks minimums. The auxiliary
stack stores pairs of (value, current_min) or uses a separate stack that
mirrors operations.
- On push(x): If the auxiliary stack is empty or
x <= current_min, pushxonto the auxiliary stack. Then pushxonto the main stack. - On pop(): Pop from the main stack. If the popped value equals the value at the top of the auxiliary stack, pop from the auxiliary stack too.
- On findMin(): Return the top of the auxiliary stack in O(1).
Space trade-off: O(n) worst case when all pushed elements are in decreasing order. This is optimal — any structure that supports O(1) findMin must use at least O(n) extra space in the worst case.
The stack height problem occurs when recursive algorithms create call stacks deeper than available memory. For example, recursively processing a linked list with 100,000 nodes on a stack with 1MB limit can overflow.
Solutions:
- Convert to iteration: Replace the call stack with an explicit stack data structure. This gives you control over memory allocation and size.
- Tail recursion optimization: If the recursive call is the last operation, the compiler may reuse the current stack frame. Not guaranteed in all languages (Python doesn't optimize tail recursion).
- Increase stack size: In C/C++, use
ulimit -sorpthread_attr_setstacksize(). In Java, set-Xsssize. This is a workaround, not a solution. - Recursion with checkpoints: Process in chunks, saving state to disk or heap, then resume.
Best practice: Default to iterative solutions when depth is unbounded or unknown.
In BFS, the queue holds nodes to explore, paired with their path from the source (or just the parent pointer for path reconstruction).
Algorithm:
- Initialize a queue with
(start_node, [start_node]). - Mark
start_nodeas visited. - While the queue is not empty: dequeue a node. For each unvisited neighbor, mark
it visited and enqueue
(neighbor, path + [neighbor]). - When the target node is dequeued, return its path.
The queue processes nodes in FIFO order, which naturally explores all nodes at distance k before nodes at distance k+1. This guarantees the shortest path in unweighted graphs.
Memory: The queue holds all nodes at the current frontier plus one level ahead — never the entire graph. Space complexity is O(width) not O(depth).
Linked list implementation:
- Advantages: No capacity limit; O(1) enqueue and dequeue with no occasional O(n) resize; works well for unpredictable queue sizes.
- Disadvantages: Per-node heap allocation overhead (slower, fragmentation); each node stores a pointer (extra memory); poor cache locality.
Array implementation (circular buffer):
- Advantages: Contiguous memory → excellent cache performance; O(1) operations with no allocation overhead; simpler implementation.
- Disadvantages: Fixed capacity (or costly dynamic resizing); full queue requires rejection or blocking; front and rear pointer management.
Recommendation: Use Python's collections.deque (chunked
linked list) for production — it combines the benefits without the downsides.
Use array-based circular queue in embedded systems where memory is fixed and
predictable.
A regular queue follows FIFO: first inserted is first removed. A priority queue removes elements based on priority (highest or lowest first), regardless of insertion order.
Common implementations:
- Binary heap: O(log n) insert and extract; O(1) peek. Most common. Min-heap for smallest-first, max-heap for largest-first.
- Balanced BST (AVL, Red-Black): O(log n) all operations; supports duplicate priorities naturally.
- Unsorted array/linked list: O(1) insert, O(n) extract-max/min. Use when priority queue size is small or insertions vastly outnumber removals.
- Fibonacci heap: O(1) amortized insert, O(log n) extract-min. Theoretically optimal but high constant factors make it rarely used in practice.
In Python, use heapq for a min-heap or negate priorities for max-heap
behavior. Java provides PriorityQueue and PriorityBlockingQueue.
Consider a task scheduler that supports:
- Adding high-priority tasks to the front (preempt current task).
- Adding normal tasks to the back.
- Removing the current task from the front.
- Returning to a previously blocked task (add to front).
A queue can only add to back and remove from front — it can't inject high-priority tasks at the front. A stack only supports LIFO which breaks normal task ordering. A deque handles both access patterns.
Other ideal deque scenarios:
- Web browser history: Back button (pop front) and forward button (push front from history) with new pages appending to back.
- Sliding window algorithms: Need to add new elements at one end and remove expired elements from the other.
- Undo/redo with skip: Can undo (pop front), redo (pop from auxiliary front), or jump to a checkpoint (insert at specific position).
The deque's flexibility comes from not committing to a single access pattern.
Further Reading
Books
- Introduction to Algorithms (CLRS) — Chapters 10: “Elementary Data Structures” covers stacks, queues, linked lists, and their analysis.
- Algorithm Design Manual (Skiena) — Chapter 3: “Data Structures” includes practical applications and interview-style problems.
- Python Cookbook (Beazley & Jones) — Chapter 1 contains idiomatic deque patterns for real-world Python.
Online Resources
- CPython collections.deque source — Reference implementation in C, showing block management and growth strategy.
- Python’s deque documentation — Official API reference with usage examples.
Practice Problems by Topic
| Topic | Problem | Platform |
|---|---|---|
| Stack | Valid Parentheses | LeetCode 20 |
| Stack | Min Stack | LeetCode 155 |
| Stack | Evaluate Reverse Polish Notation | LeetCode 150 |
| Queue | Implement Stack Using Queues | LeetCode 225 |
| Queue | Design Circular Queue | LeetCode 622 |
| Deque | Sliding Window Maximum | LeetCode 239 |
| Deque | Design Circular Deque | LeetCode 641 |
| Monotonic Stack | Daily Temperatures | LeetCode 739 |
| Monotonic Stack | Largest Rectangle in Histogram | LeetCode 84 |
Related Data Structures to Explore
- Priority Queue (Heap) — For tasks where items have urgency levels and the highest-priority item should be dequeued first.
- LRU Cache — Combines a doubly-linked list with a hash map; often implemented using a deque pattern.
- Monotonic Queue — A deque variant that maintains sorted order; used in sliding window and range query optimizations.
- Ring Buffer — A fixed-size circular queue used extensively in low-level systems (audio, networking, kernel I/O).
Conclusion
Category
Related Posts
Arrays vs Linked Lists: Understanding the Trade-offs
Compare arrays and linked lists in terms of access time, insertion/deletion efficiency, memory usage, and cache performance.
Arrays: 1D, 2D, and Multi-dimensional Mastery
Master array operations, traversal, search, common patterns like two-pointer and sliding window, and when to use multi-dimensional arrays.
AVL Trees: Self-Balancing Binary Search Trees
Master AVL tree rotations, balance factors, and rebalancing logic. Learn when to use AVL vs Red-Black trees for your use case.