Deque — Double-Ended Queue
A deque (pronounced "deck", short for Double-Ended Queue) is a generalization of both stacks and queues. It supports inserting and removing elements from either end in O(1) time. A deque is strictly more powerful than a queue or a stack: any algorithm that works with either can be implemented with a deque, plus you unlock a whole family of algorithms that need both ends simultaneously.
Core Operations
Operation | Description | Complexity |
|---|---|---|
pushFront(x) | Insert x at the front | O(1) |
pushBack(x) | Insert x at the back | O(1) |
popFront() | Remove and return the front element | O(1) |
popBack() | Remove and return the back element | O(1) |
peekFront() | Read front without removing | O(1) |
peekBack() | Read back without removing | O(1) |
isEmpty() | Check if deque has no elements | O(1) |
size() | Return element count | O(1) |
Visualized, a deque is a tunnel open at both ends:
pushBack(10), pushBack(20), pushBack(30): front -> [10, 20, 30] <- back pushFront(5): front -> [5, 10, 20, 30] <- back popBack() -> 30: front -> [5, 10, 20] <- back popFront() -> 5: front -> [10, 20] <- back peekFront() -> 10 (no removal) peekBack() -> 20 (no removal)
Doubly Linked List Implementation
The natural implementation of a deque is a doubly linked list. Each node holds a value plus pointers to both its predecessor and successor. Maintaining head and tail pointers gives O(1) access to both ends with no shifting, no wrapping, and no capacity limit.
class Node {
constructor(value) {
this.value = value
this.prev = null
this.next = null
}
}
class Deque {
constructor() {
this.head = null // front node
this.tail = null // back node
this.count = 0
}
// O(1) — prepend a new node before head
pushFront(value) {
const node = new Node(value)
if (this.head === null) {
this.head = node
this.tail = node
} else {
node.next = this.head
this.head.prev = node
this.head = node
}
this.count++
}
// O(1) — append a new node after tail
pushBack(value) {
const node = new Node(value)
if (this.tail === null) {
this.head = node
this.tail = node
} else {
node.prev = this.tail
this.tail.next = node
this.tail = node
}
this.count++
}
// O(1) — remove and return head
popFront() {
if (this.isEmpty()) throw new Error('Deque is empty')
const value = this.head.value
this.head = this.head.next
if (this.head !== null) {
this.head.prev = null
} else {
this.tail = null // became empty
}
this.count--
return value
}
// O(1) — remove and return tail
popBack() {
if (this.isEmpty()) throw new Error('Deque is empty')
const value = this.tail.value
this.tail = this.tail.prev
if (this.tail !== null) {
this.tail.next = null
} else {
this.head = null // became empty
}
this.count--
return value
}
peekFront() {
if (this.isEmpty()) throw new Error('Deque is empty')
return this.head.value
}
peekBack() {
if (this.isEmpty()) throw new Error('Deque is empty')
return this.tail.value
}
isEmpty() { return this.count === 0 }
size() { return this.count }
// Print all elements front to back
toArray() {
const result = []
let curr = this.head
while (curr !== null) {
result.push(curr.value)
curr = curr.next
}
return result
}
}
// Demo
const dq = new Deque()
dq.pushBack(10)
dq.pushBack(20)
dq.pushBack(30)
dq.pushFront(5)
dq.pushFront(1)
console.log(dq.toArray()) // [1, 5, 10, 20, 30]
console.log(dq.popFront()) // 1
console.log(dq.popBack()) // 30
console.log(dq.toArray()) // [5, 10, 20]
console.log(dq.peekFront()) // 5
console.log(dq.peekBack()) // 20[1, 5, 10, 20, 30] 1 30 [5, 10, 20] 5 20
Circular Array Implementation
For cache-friendly performance and fixed-capacity use cases (e.g., OS kernel ring buffers), a circular array beats a linked list. Two pointers, front and back, track the logical ends. Both move inward on pushFront/popBack and outward on pushBack/popFront, wrapping with modulo arithmetic.
class CircularDeque {
constructor(capacity) {
this.capacity = capacity
this.data = new Array(capacity)
// front points ONE STEP ahead of the actual front element
// so we can pushFront by decrementing front first.
this.front = Math.floor(capacity / 2)
this.back = Math.floor(capacity / 2) - 1 // back is "behind" front initially
this.count = 0
}
_mod(i) {
// JavaScript's % can return negative values for negative i
return ((i % this.capacity) + this.capacity) % this.capacity
}
// O(1)
pushFront(value) {
if (this.isFull()) throw new Error('Deque is full')
this.front = this._mod(this.front - 1)
this.data[this.front] = value
this.count++
}
// O(1)
pushBack(value) {
if (this.isFull()) throw new Error('Deque is full')
this.back = this._mod(this.back + 1)
this.data[this.back] = value
this.count++
}
// O(1)
popFront() {
if (this.isEmpty()) throw new Error('Deque is empty')
const value = this.data[this.front]
this.front = this._mod(this.front + 1)
this.count--
return value
}
// O(1)
popBack() {
if (this.isEmpty()) throw new Error('Deque is empty')
const value = this.data[this.back]
this.back = this._mod(this.back - 1)
this.count--
return value
}
peekFront() {
if (this.isEmpty()) throw new Error('Deque is empty')
return this.data[this.front]
}
peekBack() {
if (this.isEmpty()) throw new Error('Deque is empty')
return this.data[this.back]
}
isEmpty() { return this.count === 0 }
isFull() { return this.count === this.capacity }
size() { return this.count }
}Deque as a Stack or Queue
Because a deque supports operations on both ends, it can trivially emulate a stack or a queue:
const dq = new Deque()
// Use as a STACK (LIFO) — push and pop from same end
dq.pushBack(1)
dq.pushBack(2)
dq.pushBack(3)
dq.popBack() // 3 <- LIFO
// Use as a QUEUE (FIFO) — push to back, pop from front
dq.pushBack('a')
dq.pushBack('b')
dq.pushBack('c')
dq.popFront() // 'a' <- FIFOApplications
Application | How the deque is used |
|---|---|
Sliding Window Maximum / Minimum | Maintain a monotonic deque of indices; pop from back when a larger element arrives, pop from front when the window slides past the index |
Palindrome checking | Push all characters; compare front and back pairs, popping both as you go |
Undo / Redo stack | Two ends represent undo history (front) and redo history (back) |
Browser history (Back / Forward) | Current page in middle; back pages to the left, forward pages to the right |
Steal scheduling (work-stealing queues) | Owner pushes/pops from back; thieves steal from front — reduces contention |
A* / BFS with 0-1 weights | Push weight-0 edges to front, weight-1 edges to back (0-1 BFS trick) |
Rate limiter sliding window | Pop expired timestamps from the front, push new ones to the back |
Classic Problem: Sliding Window Maximum
Given an array and a window size k, find the maximum value in each window as it slides from left to right. A brute-force O(n·k) scan per window is too slow for large inputs. A deque reduces this to O(n) — each element is pushed and popped at most once.
The key idea is to maintain a monotonically decreasing deque of indices. The deque stores array indices, not values. Its invariant is: the values at those indices are in decreasing order from front to back. The front of the deque always holds the index of the maximum element in the current window.
Expire old indices: if the front index is outside the current window, pop from the front.
Maintain the monotone property: before inserting index i, pop all indices from the back whose values are less than or equal to arr[i]. They can never be the maximum while arr[i] is in the window.
Insert: push i to the back.
Record result: once the window is full (i >= k-1), the front index holds the window maximum.
function slidingWindowMax(arr, k) {
const n = arr.length
if (n === 0 || k === 0) return []
const result = []
// Deque stores array INDICES, not values.
// Invariant: arr[deque[front]] >= arr[deque[back]] (decreasing values)
const deque = [] // using a plain array as deque for brevity
for (let i = 0; i < n; i++) {
// Step 1: remove indices that have fallen outside the window
while (deque.length > 0 && deque[0] < i - k + 1) {
deque.shift() // pop from front
}
// Step 2: remove indices from back whose values are <= arr[i]
// They are useless — arr[i] is both newer and at least as large.
while (deque.length > 0 && arr[deque[deque.length - 1]] <= arr[i]) {
deque.pop() // pop from back
}
// Step 3: add current index to back
deque.push(i)
// Step 4: once window is full, record the maximum (front of deque)
if (i >= k - 1) {
result.push(arr[deque[0]])
}
}
return result
}
// Example
const arr = [3, 1, 2, 5, 4, 6, 3, 7]
const k = 3
console.log(slidingWindowMax(arr, k))
// Windows: [3,1,2]=3 [1,2,5]=5 [2,5,4]=5 [5,4,6]=6 [4,6,3]=6 [6,3,7]=7[3, 5, 5, 6, 6, 7]
Let us trace through the first few steps to see the deque in action:
Array: [3, 1, 2, 5, 4, 6, 3, 7], k = 3
Indices: 0 1 2 3 4 5 6 7
i=0 arr[0]=3: deque=[] -> push 0 -> deque=[0] window not full yet
i=1 arr[1]=1: arr[0]=3 > 1 -> push 1 -> deque=[0,1] window not full yet
i=2 arr[2]=2: arr[1]=1 <=2 -> pop 1; push 2 -> deque=[0,2] window=[0..2] max=arr[0]=3 -> result=[3]
i=3 arr[3]=5: arr[2]=2 <=5 -> pop 2
arr[0]=3 <=5 -> pop 0; push 3 -> deque=[3] window=[1..3] max=arr[3]=5 -> result=[3,5]
i=4 arr[4]=4: arr[3]=5 > 4 -> push 4 -> deque=[3,4] window=[2..4] max=arr[3]=5 -> result=[3,5,5]
i=5 arr[5]=6: arr[4]=4 <=6 -> pop 4
arr[3]=5 <=6 -> pop 3; push 5 -> deque=[5] window=[3..5] max=arr[5]=6 -> result=[3,5,5,6]
...Palindrome Check Using a Deque
A deque elegantly solves palindrome checking without index arithmetic. Push every character into the deque, then repeatedly compare and pop the front and back. If they ever differ, it is not a palindrome.
function isPalindrome(str) {
// Normalize: lowercase, keep only alphanumeric characters
const clean = str.toLowerCase().replace(/[^a-z0-9]/g, '')
const dq = new Deque()
for (const ch of clean) dq.pushBack(ch)
while (dq.size() > 1) {
const front = dq.popFront()
const back = dq.popBack()
if (front !== back) return false
}
return true
}
console.log(isPalindrome('racecar')) // true
console.log(isPalindrome('A man a plan a canal Panama')) // true
console.log(isPalindrome('hello')) // false
console.log(isPalindrome('Was it a car or a cat I saw')) // truetrue true false true
0-1 BFS — Deque Enables a Clever Optimization
In a graph where edge weights are only 0 or 1, a deque replaces Dijkstra entirely. When traversing an edge of weight 0, push the neighbor to the front (it costs nothing extra). When traversing a weight-1 edge, push to the back. This maintains a sorted-by-cost frontier without a priority queue, running in O(V + E) instead of O((V + E) log V).
function bfs01(graph, source, n) {
// graph[u] = [ { to: v, weight: 0 or 1 }, ... ]
const dist = new Array(n).fill(Infinity)
dist[source] = 0
const dq = new Deque()
dq.pushFront(source)
while (!dq.isEmpty()) {
const u = dq.popFront()
for (const { to: v, weight: w } of graph[u]) {
if (dist[u] + w < dist[v]) {
dist[v] = dist[u] + w
if (w === 0) {
dq.pushFront(v) // free edge — goes to front
} else {
dq.pushBack(v) // cost-1 edge — goes to back
}
}
}
}
return dist
}
// Grid maze: 0 = free passage (cost 0), 1 = door that costs 1 to open
// Use 0-1 BFS to find minimum doors to open from top-left to bottom-right.Complexity Reference
Operation | Doubly Linked List | Circular Array |
|---|---|---|
pushFront | O(1) | O(1) |
pushBack | O(1) | O(1) |
popFront | O(1) | O(1) |
popBack | O(1) | O(1) |
peekFront | O(1) | O(1) |
peekBack | O(1) | O(1) |
Space | O(n) — plus pointer overhead per node | O(capacity) — fixed allocation |
Cache behaviour | Poor — nodes scattered in heap | Excellent — contiguous memory |
Built-In Deques in Other Languages
Language | Built-in deque | Notes |
|---|---|---|
Python | collections.deque | O(1) appendleft/popleft, O(1) append/pop; backed by a doubly-linked list of fixed-size blocks |
Java | ArrayDeque<E> | Circular array; faster than LinkedList for most uses; also implements Stack and Queue interfaces |
C++ | std::deque<T> | Segmented array — O(1) both ends, O(1) random access (unlike Java/Python) |
Rust | VecDeque<T> | Circular buffer; O(1) push/pop at both ends |
JavaScript | (none built-in) | Use array with push/shift/unshift/pop — but shift/unshift are O(n). Implement CircularDeque for production use. |
When to Use a Deque
You need O(1) operations at both ends — this is the defining reason
Sliding window maximum/minimum — the most common deque interview problem
Implementing both a stack and a queue using a single structure
Palindrome checking without index arithmetic
0-1 BFS on graphs with binary edge weights
Work-stealing thread pool schedulers
Any algorithm that needs a monotonic structure (increasing or decreasing) over a sliding range
Summary
A deque allows O(1) insertion and removal from both front and back
Doubly linked list: dynamic capacity, O(1) all-end operations, poor cache locality
Circular array: fixed capacity, O(1) all-end operations, excellent cache locality
A deque generalizes both stacks (use one end) and queues (use opposite ends)
The sliding window maximum is the canonical deque algorithm — O(n) via a monotonic deque
Palindrome check with a deque avoids index math entirely
0-1 BFS uses pushFront for free edges and pushBack for unit-cost edges