Master the deque (double-ended queue) — a data structure allowing O(1) insertion and deletion at both ends. Learn complete implementation using doubly linked list and circular array, all operations, monotonic deque applications, and classic sliding window maximum problem.
Introduction
A deque (pronounced "deck") stands for Double-Ended Queue. Unlike a regular queue (enqueue at rear, dequeue at front), a deque allows insertion and deletion at both ends — the front and the rear — all in O(1).
A deque is more general than both a stack and a queue. It can simulate a stack (add/remove from same end) or a queue (add to one end, remove from other end).
Python's collections.deque
Python provides a highly optimized deque in collections.deque, implemented as a doubly-linked list of fixed-size blocks:
from collections import deque
d = deque()
# Add to ends
d.append(3) # Right: [3]
d.appendleft(1) # Left: [1, 3]
d.append(5) # Right: [1, 3, 5]
d.appendleft(0) # Left: [0, 1, 3, 5]
print(d) # deque([0, 1, 3, 5])
# Remove from ends
d.pop() # Remove right: 5, deque = [0, 1, 3]
d.popleft() # Remove left: 0, deque = [1, 3]
# Peek
print(d[0]) # Left end: 1
print(d[-1]) # Right end: 3
# Bounded deque (automatically discards from other end when full)
d_bounded = deque(maxlen=3)
d_bounded.append(1); d_bounded.append(2); d_bounded.append(3)
d_bounded.append(4) # 1 is automatically removed from left
print(d_bounded) # deque([2, 3, 4], maxlen=3)
Custom Deque Implementation — Doubly Linked List
class _DequeNode:
__slots__ = ('data', 'prev', 'next')
def __init__(self, data):
self.data = data
self.prev = None
self.next = None
class Deque:
"""
Double-ended queue using a doubly linked list.
All operations guaranteed O(1).
"""
def __init__(self):
# Sentinel nodes (dummy head and tail) simplify edge cases
self._left = _DequeNode(None) # Dummy left sentinel
self._right = _DequeNode(None) # Dummy right sentinel
self._left.next = self._right
self._right.prev = self._left
self._size = 0
def _insert_between(self, data, before, after):
"""Insert new node between two existing nodes. O(1)."""
node = _DequeNode(data)
node.prev = before
node.next = after
before.next = node
after.prev = node
self._size += 1
return node
def _remove_node(self, node):
"""Remove a specific node. O(1)."""
node.prev.next = node.next
node.next.prev = node.prev
self._size -= 1
return node.data
def append(self, data) -> None:
"""Add to right end. O(1)."""
self._insert_between(data, self._right.prev, self._right)
def appendleft(self, data) -> None:
"""Add to left end. O(1)."""
self._insert_between(data, self._left, self._left.next)
def pop(self):
"""Remove from right end. O(1)."""
if self.is_empty():
raise IndexError("pop from empty deque")
return self._remove_node(self._right.prev)
def popleft(self):
"""Remove from left end. O(1)."""
if self.is_empty():
raise IndexError("popleft from empty deque")
return self._remove_node(self._left.next)
def peek_right(self):
if self.is_empty():
raise IndexError("peek_right from empty deque")
return self._right.prev.data
def peek_left(self):
if self.is_empty():
raise IndexError("peek_left from empty deque")
return self._left.next.data
def is_empty(self) -> bool:
return self._size == 0
def __len__(self) -> int:
return self._size
def __repr__(self) -> str:
elements = []
cur = self._left.next
while cur != self._right:
elements.append(str(cur.data))
cur = cur.next
return "Deque(left→right): [" + ", ".join(elements) + "]"
Deque as Stack and Queue
# Use as Stack (LIFO):
stack = Deque()
stack.append(1) # push
stack.append(2)
print(stack.pop()) # pop from right: 2
# Use as Queue (FIFO):
queue = Deque()
queue.append(1) # enqueue at right
queue.append(2)
print(queue.popleft()) # dequeue from left: 1
Monotonic Deque — The Sliding Window Maximum
The most important algorithmic use of a deque: maintaining a monotonic deque for sliding window problems.
Problem: Given an array and window size k, find the maximum in every window of size k.
[1, 3, -1, -3, 5, 3, 6, 7], k=3
Windows: [1,3,-1] [3,-1,-3] [-1,-3,5] [-3,5,3] [5,3,6] [3,6,7]
Maximums: 3 3 5 5 6 7
Naive approach: O(n×k) — recompute max for each window
Optimal with monotonic deque: O(n)
from collections import deque
def sliding_window_maximum(nums, k):
"""
Monotonic decreasing deque: maintains potential maximums.
Front of deque = maximum of current window.
Invariant: Deque stores indices in decreasing order of their values.
Elements that can never be the maximum are discarded from the back.
Time: O(n), Space: O(k)
"""
result = []
dq = deque() # Stores INDICES, not values
for i in range(len(nums)):
# Remove indices outside the current window
while dq and dq[0] < i - k + 1:
dq.popleft()
# Remove indices whose values are smaller than nums[i]
# (they can never be the max while nums[i] is in the window)
while dq and nums[dq[-1]] < nums[i]:
dq.pop()
dq.append(i)
# Window has k elements starting from index k-1
if i >= k - 1:
result.append(nums[dq[0]]) # Front = maximum
return result
print(sliding_window_maximum([1,3,-1,-3,5,3,6,7], 3))
# [3, 3, 5, 5, 6, 7]
Trace:
| i=0, nums[0]=1 | dq=[0] |
| i=1, nums[1]=3 | 1<3, pop 0. dq=[1] |
| i=2, nums[2]=-1 | dq=[1,2]. Window=[1,2]: max=nums[1]=3. result=[3] |
| i=3, nums[3]=-3 | dq=[1,2,3]. Window=[2,3]: max=nums[1]=3. result=[3,3] |
| i=4, nums[4]=5 | -3<5 pop 3, -1<5 pop 2, 3<5 pop 1. dq=[4]. Window=[3,4]: max=5. result=[3,3,5] |
| i=5, nums[5]=3 | dq=[4,5]. Window=[4,5]: max=nums[4]=5. result=[3,3,5,5] |
| i=6, nums[6]=6 | 3<6 pop 5. dq=[4,6]. Index 4 still in window (6-3+1=4). max=6. result=[3,3,5,5,6] |
| i=7, nums[7]=7 | 6<7 pop 6, 5<7 pop 4. dq=[7]. max=7. result=[3,3,5,5,6,7] |
First Negative in Every Window
def first_negative_in_window(arr, k):
"""
Find the first negative number in each window of size k.
Use deque to track indices of negative numbers.
Time: O(n), Space: O(k)
"""
result = []
dq = deque() # Indices of negative numbers
for i in range(len(arr)):
if arr[i] < 0:
dq.append(i)
# Remove indices outside window
while dq and dq[0] < i - k + 1:
dq.popleft()
if i >= k - 1:
result.append(arr[dq[0]] if dq else 0)
return result
print(first_negative_in_window([-8, 2, 3, -6, 10], 2)) # [-8, 0, -6, -6]
Deque Applications Summary
| Application | Deque Usage |
|---|
| Sliding window maximum | Monotonic decreasing deque |
| Sliding window minimum | Monotonic increasing deque |
| BFS with bidirectional search | Both-end access |
| 0-1 BFS (weighted: 0 or 1) | Prepend weight-0 edges, append weight-1 |
| Palindrome check (iterative) | Compare front and back |
| Undo-redo with history limit | Bounded deque (maxlen) |
| LRU cache (combined with hash map) | Doubly linked list = deque |
| Text editor cursor (left/right) | Two stacks ~ two halves of deque |
Summary
- Deque = double-ended queue: O(1) add/remove from both ends
- More general than stack or queue — can simulate both
- Python's
collections.deque: The standard, optimized implementation — use this by default - Custom implementation: Doubly linked list with sentinel nodes
- Monotonic deque: The key algorithmic pattern — maintains potential extremes for sliding window
maxlen deque: Automatically drops oldest elements — perfect for rolling windows
*Next Lesson: Priority Queue — Elements processed by priority, not arrival order — implemented with heaps.*