Queue

A queue is a FIFO (first-in, first-out) container with enqueue (add to back) and dequeue (remove from front). Use collections.deque in Python — O(1) at both ends. A list as a queue is O(n) per dequeue and a common performance bug.

Complexity

Average Worst Space
AccessSearchInsertionDeletion AccessSearchInsertionDeletion Worst
Θ(n) Θ(n) Θ(1) Θ(1) O(n) O(n) O(1) O(1) O(n)

How it works

Backed by a circular buffer or doubly-linked list with head and tail pointers. Enqueue advances tail; dequeue advances head. Both are pointer-bumps, so O(1). Indexing or searching by value still requires a scan — O(n).

Python implementation

from collections import deque

q = deque()
q.append("a")          # enqueue
q.append("b")
q.append("c")
print(q.popleft())     # "a"  — dequeue, O(1)
print(q.popleft())     # "b"


# DON'T do this — list.pop(0) is O(n):
# q = []
# q.append("a"); q.pop(0)   # shifts every remaining element


# Explicit class:
class Queue:
    def __init__(self):
        self._d = deque()
    def enqueue(self, x):  # O(1)
        self._d.append(x)
    def dequeue(self):     # O(1)
        if not self._d:
            raise IndexError("dequeue from empty queue")
        return self._d.popleft()
    def peek(self):        # O(1)
        return self._d[0]
    def __len__(self):
        return len(self._d)

Trade-offs

← Back to Algorithms

1. Summary

A queue is an abstract data type that returns elements in the order they arrived: first in, first out. Its two core operations are enqueue, which adds at the back, and dequeue, which removes from the front; most implementations also provide peek and size. Queues model anything that must be processed fairly in arrival order, such as print jobs, network packets, tasks for a worker pool, or the frontier of a breadth-first search. The key implementation requirement is that both ends are O(1). A Python list fails this at the front, because removing index 0 shifts every remaining element; collections.deque and circular buffers meet it.

Operationdeque / circular bufferPython list (pop(0))
enqueueO(1) amortisedO(1) amortised
dequeueO(1)O(n)
peek frontO(1)O(1)
searchO(n)O(n)
spaceO(n)O(n)

Queues appear in two related variants. A double-ended queue (deque) allows insertion and removal at both ends and can act as either a queue or a stack. A priority queue, despite the name, is not FIFO: it always returns the element with the highest priority and is usually implemented with a binary heap. Choosing the wrong one is a common source of subtle bugs, for example using a priority queue when fairness in arrival order was required. Bounded queues add a third consideration: what happens when the queue is full. The options are to block the producer, to reject the new item, or to drop the oldest item, as a deque with maxlen does.

2. Circular buffer implementation

Circular Buffer Queue (capacity 6) enqueue a, b, c a b c dequeue -> a b c enqueue d, e, f b c d e f enqueue g (wraps) g b c d e f Yellow = head (next to dequeue). Green = tail (last enqueued). Indices wrap with (i + 1) % capacity.

A circular buffer stores elements in a fixed array with two indices. The head points to the next element to dequeue; the tail points to where the last element was written. Both advance with modulo arithmetic, so when the tail reaches the end of the array it wraps to index 0 and reuses slots freed by earlier dequeues. No elements are ever shifted. A separate count (or one deliberately empty slot) distinguishes a full buffer from an empty one, because in both cases head and tail can be equal. A growable version doubles the array when full and copies elements in order, giving amortised O(1) enqueue.

class CircularQueue:
    def __init__(self, capacity=8):
        self.buf = [None] * capacity
        self.head = 0          # index of front element
        self.size = 0

    def enqueue(self, x):
        if self.size == len(self.buf):
            self._grow()
        tail = (self.head + self.size) % len(self.buf)
        self.buf[tail] = x
        self.size += 1

    def dequeue(self):
        if self.size == 0:
            raise IndexError("dequeue from empty queue")
        x = self.buf[self.head]
        self.buf[self.head] = None            # release reference
        self.head = (self.head + 1) % len(self.buf)
        self.size -= 1
        return x

    def _grow(self):
        old = self.buf
        self.buf = [old[(self.head + i) % len(old)] for i in range(self.size)]
        self.buf += [None] * len(old)
        self.head = 0

q = CircularQueue(2)
for c in "abcde":
    q.enqueue(c)
print([q.dequeue() for _ in range(5)])   # ['a', 'b', 'c', 'd', 'e']

3. Queues in systems: producer and consumer

Producer / Consumer With a Queue Producerweb handler QueueFIFO buffer Consumerworker The queue decouples rates: producers enqueue bursts, consumers drain at their own pace. A bounded queue applies back-pressure by blocking or rejecting when full.

Outside algorithms, queues are the standard way to connect components running at different speeds. A web request can enqueue a job and return immediately while a worker processes it later. In-process, Python offers queue.Queue for threads and asyncio.Queue for coroutines; across machines, brokers such as RabbitMQ, Amazon SQS or Kafka play the same role with durability. Bounding the queue size is important: an unbounded queue hides overload until memory runs out, while a bounded one pushes back on producers.

4. Worked example: BFS and a thread-safe queue

from collections import deque
import queue, threading

# Breadth-first search: the queue holds the frontier in distance order
graph = {"A": ["B", "C"], "B": ["D"], "C": ["D", "E"], "D": ["F"], "E": ["F"], "F": []}
def bfs(start):
    dist, q = {start: 0}, deque([start])
    while q:
        node = q.popleft()
        for nxt in graph[node]:
            if nxt not in dist:
                dist[nxt] = dist[node] + 1
                q.append(nxt)
    return dist
print(bfs("A"))   # {'A': 0, 'B': 1, 'C': 1, 'D': 2, 'E': 2, 'F': 3}

# Bounded producer/consumer with worker threads
jobs = queue.Queue(maxsize=10)
def worker():
    while (item := jobs.get()) is not None:
        print("processed", item)
        jobs.task_done()
    jobs.task_done()
t = threading.Thread(target=worker); t.start()
for i in range(3):
    jobs.put(i)          # blocks if the queue is full
jobs.put(None)           # sentinel to stop the worker
jobs.join(); t.join()

BFS visits nodes in order of distance precisely because the queue is FIFO; replacing it with a stack turns the same loop into depth-first search.

5. Choosing a queue type

NeedUse
Single-threaded FIFOcollections.deque
Threads sharing workqueue.Queue (with maxsize)
Coroutinesasyncio.Queue
Highest priority firstheapq or queue.PriorityQueue
Both ends, sliding windowdeque with maxlen
Across processes or machinesmultiprocessing.Queue, SQS, RabbitMQ, Kafka

6. Pitfalls