Filter concepts by levelShowing all levels.

Data Structures & Algorithms · Section 12

Queues and Deques

Level
beginner
Read
70 min
Concepts
7

The FIFO principle and why a plain list makes a slow queue, collections.deque as the O(1)-both-ends fix, the queue that drives breadth-first search, the circular buffer a fixed-capacity queue needs, the monotonic deque pattern behind sliding-window maximum, and why a priority queue is a genuinely different contract from a FIFO queue.

What is true here

  1. A queue serves the oldest item first (FIFO) — the mirror image of a stack's LIFO.
  2. list.pop(0) is O(n); collections.deque makes both ends O(1), which is what a real queue needs.
  3. BFS is "queue instead of stack" applied to the same traversal shape a stack gives DFS.
  4. A monotonic deque answers "what is the max of the current window?" in O(1) per step, O(n) total, by evicting from both ends.

What you will be able to do

  • Explain why a Python list is a fine stack but a poor queue, and fix it with collections.deque
  • Implement BFS using a queue and explain why that specific container gives breadth-first order
  • Build a fixed-capacity circular queue using modulo-wrapped indices
  • Apply the monotonic deque pattern to compute a sliding window maximum in O(n)

Concepts

From the FIFO principle and why a list makes a poor queue, through deque as the fix, its use in BFS and circular buffers, to the monotonic deque pattern and how a priority queue differs from all of it.

FIFO principle

referencebeginner

A queue is First-In-First-Out: the item that has been waiting longest is always the first one served — the exact opposite of a stack's LIFO order.

fifo.pypython
from collections import deque

queue = deque()
queue.append('a')
queue.append('b')
queue.append('c')

print(queue.popleft())   # 'a' — the earliest-enqueued item, out first
print(queue)               # deque(['b', 'c']) — 'c' arrived last and waits behind 'b'

Remember: FIFO means the first thing enqueued is the first thing dequeued — picture a line of people, not a stack of plates.

Was this helpful?

Why list.pop(0) is O(n) — never use a plain list as a queue

referencebeginner

list.pop(0) and list.insert(0, x) both shift every remaining element over by one — O(n) per operation — so a plain list makes a correct but slow queue.

list_as_queue.pypython
queue = []
queue.append('a')
queue.append('b')
queue.append('c')

queue.pop(0)   # O(n) — 'b' and 'c' both shift left by one to fill the gap

Remember: A plain list can play queue, but every dequeue costs O(n) — reach for collections.deque the moment front-of-structure removal matters.

Was this helpful?

collections.deque — O(1) append/pop from both ends

corebeginner

deque is a double-ended queue: append/appendleft add to either end, and pop/popleft remove from either end, all in O(1) — unlike a list, which is only fast at one end.

Think of it as

Think of a deque as a row of people who can join or leave from EITHER end of the line, each in O(1) — unlike a Python list, which can only fast-join or fast-leave from the right. append/pop touch the right end; appendleft/popleft touch the left end. Which pair you use is what turns the same structure into a stack (one end only) or a queue (opposite ends).

What we're doing: Add and remove values from both ends of a deque, all in O(1).

deque_both_ends.pypython
from collections import deque

dq = deque([2, 3, 4])
dq.appendleft(1)
dq.append(5)
print(dq)
print(dq.popleft())
print(dq.pop())
print(dq)
4
appendleft(1) adds to the LEFT end — 1 becomes the new front, in O(1), with nothing shifted.
5
append(5) adds to the RIGHT end — 5 becomes the new back, also O(1).
Output
deque([1, 2, 3, 4, 5])
1
5
deque([2, 3, 4])

Why this works: appendleft/append built [1, 2, 3, 4, 5] by growing both ends outward from [2, 3, 4]. popleft() then removes 1 (the front), and pop() removes 5 (the back), leaving the original [2, 3, 4] untouched in the middle.

Calling pop() out of list habit, when a queue needs popleft()

Wrong

python
from collections import deque

def process_queue_wrong(tasks):
    queue = deque(tasks)
    order = []
    while queue:
        order.append(queue.pop())  # habit from list.pop() — wrong end for a queue
    return order


print(process_queue_wrong(['a', 'b', 'c']))

Better

python
from collections import deque

def process_queue(tasks):
    queue = deque(tasks)
    order = []
    while queue:
        order.append(queue.popleft())
    return order


print(process_queue(['a', 'b', 'c']))

What you see: process_queue_wrong(['a', 'b', 'c']) returns ['c', 'b', 'a'] — the exact reverse of the FIFO order a queue promises.

Why: deque.pop() removes from the RIGHT end — the same end append() just added to, which is LIFO (stack) behavior. popleft() is the operation that actually removes from the opposite end, giving first-in-first-out order.

Remember: append/pop work the right end, appendleft/popleft work the left end, all O(1) — using pop() where a queue needs popleft() silently turns FIFO into LIFO.

Was this helpful?

Queue via deque for BFS

standardintermediate

Breadth-first search visits everything at the current distance before anything farther away — a plain FIFO queue is exactly what enforces that order: nodes discovered earlier get processed before nodes discovered later.

Think of it as

BFS explores in growing rings — distance 0, then 1, then 2. A queue enforces that order for free: append a neighbor the moment it is discovered, and popleft() always serves the earliest-discovered node still waiting, so every node at the current distance is processed before any node at the next distance gets a turn.

What we're doing: Visit every node in a small graph breadth-first, using a deque as the queue.

bfs_queue.pypython
from collections import deque

def bfs(graph, start):
    visited = {start}
    order = []
    queue = deque([start])
    while queue:
        node = queue.popleft()
        order.append(node)
        for neighbor in graph[node]:
            if neighbor not in visited:
                visited.add(neighbor)
                queue.append(neighbor)
    return order


graph = {'a': ['b', 'c'], 'b': ['d'], 'c': ['d'], 'd': []}
print(bfs(graph, 'a'))
6
'a' starts as both visited and the only item in the queue.
9
popleft() always takes the earliest-discovered node still waiting — the FIFO order that makes this breadth-first.
11
A neighbor is marked visited the moment it is discovered, before it is ever dequeued — this is what stops it from being queued a second time.
Output
['a', 'b', 'c', 'd']

Why this works: 'a' is visited first, discovering 'b' and 'c'. Both are one step away, so both are fully processed — in the order they were discovered — before 'd', which is two steps away, gets its turn. That is exactly the breadth-first guarantee.

Remember: A queue is what makes a traversal breadth-first — enqueue neighbors as you discover them, mark them visited immediately, and popleft() always serves the earliest-discovered one first.

Was this helpful?

Circular queue concept

standardintermediate

A circular queue reuses a fixed-size block of memory by wrapping its front and rear indices with modulo arithmetic, instead of ever shifting elements or growing without bound.

Think of it as

Picture a fixed number of seats arranged in a circle. `front` points at the next seat to leave, and `front + size` points at the next empty seat to fill — both wrap back to seat 0 the moment they walk past the last seat, which is exactly what `index % capacity` computes.

What we're doing: Enqueue and dequeue from a fixed-capacity circular queue, and watch a new value reuse a slot without shifting anything.

circular_queue.pypython
class CircularQueue:
    def __init__(self, capacity):
        self.capacity = capacity
        self.data = [None] * capacity
        self.front = 0
        self.size = 0

    def enqueue(self, val):
        if self.size == self.capacity:
            raise OverflowError("queue is full")
        index = (self.front + self.size) % self.capacity
        self.data[index] = val
        self.size += 1

    def dequeue(self):
        val = self.data[self.front]
        self.front = (self.front + 1) % self.capacity
        self.size -= 1
        return val


cq = CircularQueue(3)
cq.enqueue('a')
cq.enqueue('b')
cq.enqueue('c')
print(cq.dequeue())
cq.enqueue('d')
print(cq.data)
10
(front + size) % capacity computes where the next value goes — it wraps back to index 0 once the array end is passed.
15
(front + 1) % capacity advances the front pointer the same way, so it wraps too.
Output
a
['d', 'b', 'c']

Why this works: Dequeuing 'a' frees slot 0 without moving 'b' or 'c'. Enqueuing 'd' then computes index (front=1 + size=2) % 3 = 0 — 'd' lands directly in the freed slot 0, reusing it in place instead of appending to a growing array.

Remember: A circular queue reuses a fixed block of memory by wrapping indices with modulo, instead of shifting elements or growing without bound — the same result deque(maxlen=n) gives for free.

Was this helpful?

Monotonic deque pattern (sliding window maximum)

coreintermediate

Keep a deque of indices whose values are decreasing, front to back. The front is always the current window's maximum — before reading it, evict any front index that has slid outside the window; before pushing a new index, evict any back index whose value it beats.

Think of it as

Two separate evictions keep the deque honest. From the BACK: while the new value beats the value at the back index, pop it — a smaller value sitting in front of a bigger, more recent one can never be the max again, so it is dead weight. From the FRONT: if the front index has slid outside the current k-wide window, pop it — it is too old to count, however large its value is. What survives is always decreasing, front to back, so the front is the answer.

What we're doing: Find the maximum of every size-k window as it slides across an array, in one O(n) pass.

sliding_window_max.pypython
from collections import deque

def max_sliding_window(nums, k):
    dq = deque()  # stores indices, values decreasing front to back
    for i, n in enumerate(nums):
        if dq and dq[0] <= i - k:
            dq.popleft()
        while dq and nums[dq[-1]] < n:
            dq.pop()
        dq.append(i)
        if i >= k - 1:
            yield nums[dq[0]]


print(list(max_sliding_window([1, 3, -1, -3, 5, 3, 6, 7], 3)))
6
dq[0] <= i - k means the front index is at or before i - k — too old to be inside the current window [i - k + 1, i], so it is dropped.
8
Every back index with a smaller value than the incoming one is popped — it can never be reported as a max again, since something bigger and newer is right behind it.
Output
[3, 3, 5, 5, 6, 7]

Why this works: The window [1, 3, -1] has max 3; sliding to [3, -1, -3] keeps max 3; [-1, -3, 5], [−3, 5, 3], [5, 3, 6] and [3, 6, 7] give 5, 5, 6, 7 in turn — six windows for an 8-element array with k=3, each read straight off the front of the deque.

Forgetting the front eviction — an out-of-window index never leaves

Wrong

python
from collections import deque

def max_sliding_window_wrong(nums, k):
    dq = deque()
    result = []
    for i, n in enumerate(nums):
        while dq and nums[dq[-1]] < n:
            dq.pop()
        dq.append(i)
        if i >= k - 1:
            result.append(nums[dq[0]])  # never checks whether dq[0] is still inside the window
    return result


print(max_sliding_window_wrong([5, 3, 2, 1], 2))

Better

python
from collections import deque

def max_sliding_window(nums, k):
    dq = deque()
    result = []
    for i, n in enumerate(nums):
        if dq and dq[0] <= i - k:
            dq.popleft()
        while dq and nums[dq[-1]] < n:
            dq.pop()
        dq.append(i)
        if i >= k - 1:
            result.append(nums[dq[0]])
    return result


print(max_sliding_window([5, 3, 2, 1], 2))

What you see: max_sliding_window_wrong([5, 3, 2, 1], 2) returns [5, 5, 5] — the correct answer is [5, 3, 2].

Why: 5 at index 0 is never beaten by anything later, so the back-eviction check never removes it — it sits at the front forever. Without the front-eviction check, every later window keeps reporting 5 as its max, even the window [2, 1], which 5 left two steps ago.

  • Sliding Window MaximumhardLeetCode

    Exactly the example above — a deque of indices, decreasing front to back, with both evictions applied on every step.

  • Number of Recent CallseasyLeetCode

    Different shape — a plain FIFO queue trimmed from the front as time passes — but the same amortized O(1) argument for why the trimming stays cheap overall.

Remember: A monotonic deque needs BOTH evictions — back for "beaten by a newer value", front for "aged out of the window" — dropping either one produces a stale answer, not a crash.

Was this helpful?

Priority queue as a distinct structure from a plain queue

referencebeginner

A priority queue always serves the item with the highest (or lowest) priority next, regardless of insertion order. A plain queue always serves the oldest item, regardless of any priority. They solve different problems, even though both have 'queue' in the name.

priority_vs_fifo.pypython
tasks = [(3, 'write report'), (1, 'fix outage'), (2, 'reply to email')]

# a priority queue serves the LOWEST number first, not the first inserted —
# 'fix outage' (priority 1) jumps ahead of 'write report', enqueued first
next_task = min(tasks)
print(next_task)

Remember: A plain queue answers "what came first?"; a priority queue answers "what matters most?" — different contracts, and a priority queue usually costs O(log n) instead of O(1) to get that ordering.

Was this helpful?

Spotted a mistake on this page? Tell us.