DSADeque

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:

Text
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.

JS
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.

JS
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 }
}
Tip
The circular array deque stores elements in contiguous memory, which means the CPU cache works in your favour. For hot inner loops — like the sliding window maximum problem — this is noticeably faster than a linked list despite identical asymptotic complexity.
Deque as a Stack or Queue

Because a deque supports operations on both ends, it can trivially emulate a stack or a queue:

JS
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'  <- FIFO
Applications

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.

  1. Expire old indices: if the front index is outside the current window, pop from the front.

  2. 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.

  3. Insert: push i to the back.

  4. Record result: once the window is full (i >= k-1), the front index holds the window maximum.

JS
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:

Text
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]
...
Note
Each index is pushed exactly once and popped at most once, giving **O(n) total** regardless of k. This is a dramatic improvement over O(n·k) brute force — on an array of 10 million elements with k=1000, that is 10 billion operations vs 10 million.
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.

JS
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'))  // true
true
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).

JS
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.
Tip
0-1 BFS is a pattern that appears in competitive programming and system design problems involving grids, graphs with two types of edges, or toggle costs. Recognizing that the deque replaces a heap here is the key insight.
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.

Warning
In JavaScript, Array.unshift() (prepend) and Array.shift() (remove first) are O(n) because they shift every element in memory. Never use them in a hot loop on large arrays. Implement the circular deque shown above whenever you need O(1) front operations.
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

Note
Up next: **Priority Queue and Binary Heap** — when you need to always dequeue the minimum (or maximum) element efficiently, the heap gives O(log n) for both enqueue and dequeue, making it the engine behind Dijkstra, A*, Huffman coding, and event-driven simulation.