On This Page

What Is a Queue?

A queue is a collection where elements are processed in the order they arrived. The first item added is the first one removed โ€” this is called FIFO (First In, First Out).

Think about a line at a coffee shop. The person who arrived first gets served first. New people join at the back. Nobody cuts. That's a queue.

A Brief History

Queues in computing trace back to early work on operating systems in the late 1950s and early 1960s. As computers evolved from single-task machines to multi-program systems (where multiple jobs needed to share the CPU), engineers needed a fair way to order tasks. The queue was the natural answer โ€” processes that arrived first got executed first.

The earliest formal treatment came from queueing theory, a branch of mathematics pioneered by Agner Krarup Erlang at the Copenhagen Telephone Exchange around 1909. Erlang was trying to figure out how many telephone lines were needed to handle call volume. His models โ€” which used queues of incoming calls โ€” became the foundation for modern network design, traffic engineering, and any system where requests line up for service.

Three Flavors

Regular Queue โ€” strict FIFO. Items enter at the rear (back), leave from the front. That's it.

Priority Queue โ€” each item has a priority (a numeric value that determines its importance). Instead of serving in arrival order, the highest-priority item gets served first. Hospital emergency rooms work this way: a broken arm waits while a heart attack goes straight in, regardless of who showed up first. Under the hood, priority queues usually use a heap (a tree-shaped data structure stored in an array), not an actual queue.

Deque (pronounced "deck") โ€” short for double-ended queue. You can add or remove items from both the front and the back. It's a queue and a stack at the same time. Useful when you need flexibility, like in a sliding window algorithm where elements can expire from the front and new ones arrive at the back.

Where You'll See Them

How It Works

Regular Queue (Array-Based)

The simplest implementation uses an array with two pointers: front (the index of the next element to dequeue) and rear (the index where the next element will be enqueued).

When you enqueue (add an item), it goes at the rear position, and rear moves forward. When you dequeue (remove an item), you take from the front position, and front moves forward.

The problem: both pointers only move right. After enough operations, you'll waste the left side of the array even though there's space. The fix is a circular queue (also called a ring buffer) โ€” when a pointer reaches the end of the array, it wraps around to position 0 using the modulo operator: rear = (rear + 1) % capacity. Think of it like a clock โ€” after 12, you go back to 1.

๐Ÿ’ก Why Not Just Use an Array?

You can use a regular array as a queue by using shift() in JavaScript or pop(0) in Python. But both are O(n) because every remaining element has to scoot left to fill the gap. A proper queue (using a circular buffer or linked list) gives you O(1) for both enqueue and dequeue.

Priority Queue (Heap-Based)

A priority queue doesn't maintain FIFO order โ€” it maintains priority order. The standard implementation uses a binary min-heap: a complete binary tree (every level is fully filled except possibly the last, which fills left to right) where every parent node's value is smaller than (or equal to) its children's values.

Insert: add the new element at the bottom-right position (end of the array), then "bubble up" by swapping with its parent until the heap property holds.

Remove: take the root (the minimum), move the last element to the root position, then "sift down" by swapping with the smaller child until the heap property is restored.

Both operations are O(log n) because the tree height is log n.

Deque (Linked List or Circular Buffer)

A deque supports four operations: add/remove from front, add/remove from back. A doubly linked list (where each node has pointers to both its next and previous neighbors) makes this trivial โ€” just update the head or tail pointer. Python's collections.deque uses a doubly linked list of fixed-size blocks, giving you the cache-friendly performance of arrays with the flexibility of linked lists.

Circular Queue Details

The circular queue is worth understanding deeply because it shows up in systems programming everywhere โ€” network buffers, audio processing, producer-consumer patterns.

Key implementation details:

Interactive Visualization

Try it yourself. Enqueue items, dequeue them, and toggle priority mode to see how ordering changes.

Queue Explorer
Press Enter or click Enqueue

Operations & Complexity

Regular Queue

OperationTimeSpaceNotes
EnqueueO(1)O(1)Add to rear
DequeueO(1)O(1)Remove from front
Peek / FrontO(1)O(1)Look without removing
SearchO(n)O(1)Must check each element
isEmptyO(1)O(1)Compare front/rear pointers

Priority Queue (Binary Heap)

OperationTimeSpaceNotes
InsertO(log n)O(1)Bubble up after adding
Extract Min/MaxO(log n)O(1)Bubble down after removing root
PeekO(1)O(1)Root is always the min/max
Build HeapO(n)O(1)Heapify from bottom โ€” not O(n log n)!
SearchO(n)O(1)Heap isn't ordered for search

Deque

OperationTimeSpaceNotes
Push Front / BackO(1)O(1)Amortized for array-based
Pop Front / BackO(1)O(1)Amortized for array-based
Peek Front / BackO(1)O(1)Direct access to both ends
SearchO(n)O(1)Linear scan required

Implementation

Regular Queue

Python
from collections import deque

class Queue:
    def __init__(self):
        # deque gives us O(1) popleft โ€” a plain list would be O(n)
        self._data = deque()

    def enqueue(self, val):
        self._data.append(val)

    def dequeue(self):
        if not self._data:
            raise IndexError("dequeue from empty queue")
        return self._data.popleft()

    def peek(self):
        if not self._data:
            raise IndexError("peek from empty queue")
        return self._data[0]

    def is_empty(self):
        return len(self._data) == 0

    def __len__(self):
        return len(self._data)


# Usage
q = Queue()
q.enqueue(10)
q.enqueue(20)
q.enqueue(30)
print(q.dequeue())  # 10 โ€” first in, first out
print(q.peek())     # 20 โ€” next in line, but stays
JavaScript
class Queue {
  constructor() {
    // Use a map with numeric keys instead of an array
    // so dequeue doesn't shift every element (O(n) with arrays)
    this._data = {};
    this._head = 0;
    this._tail = 0;
  }

  enqueue(val) {
    this._data[this._tail] = val;
    this._tail++;
  }

  dequeue() {
    if (this.isEmpty()) throw new Error("Queue is empty");
    const val = this._data[this._head];
    delete this._data[this._head];
    this._head++;
    return val;
  }

  peek() {
    if (this.isEmpty()) throw new Error("Queue is empty");
    return this._data[this._head];
  }

  isEmpty() {
    return this._tail === this._head;
  }

  get size() {
    return this._tail - this._head;
  }
}

// Usage
const q = new Queue();
q.enqueue(10);
q.enqueue(20);
q.enqueue(30);
console.log(q.dequeue()); // 10
console.log(q.peek());    // 20

Circular Queue (Ring Buffer)

Python
class CircularQueue:
    """Fixed-size circular queue (ring buffer).
    
    Used in: network packet buffers, audio processing,
    producer-consumer patterns, kernel I/O buffers.
    
    Wraps around using modulo arithmetic โ€” when the pointer
    reaches the end of the array, it jumps back to index 0.
    """
    def __init__(self, capacity):
        self._data = [None] * capacity
        self._capacity = capacity
        self._front = 0
        self._rear = 0
        self._size = 0

    def enqueue(self, val):
        if self._size == self._capacity:
            raise OverflowError("queue is full")
        self._data[self._rear] = val
        self._rear = (self._rear + 1) % self._capacity  # wrap around
        self._size += 1

    def dequeue(self):
        if self._size == 0:
            raise IndexError("queue is empty")
        val = self._data[self._front]
        self._data[self._front] = None  # help GC
        self._front = (self._front + 1) % self._capacity  # wrap around
        self._size -= 1
        return val

    def peek(self):
        if self._size == 0:
            raise IndexError("queue is empty")
        return self._data[self._front]

    def is_full(self):
        return self._size == self._capacity

    def is_empty(self):
        return self._size == 0

Priority Queue (Min-Heap)

Python
import heapq

# Python's heapq is a min-heap โ€” smallest value comes out first
pq = []
heapq.heappush(pq, (3, "low priority"))
heapq.heappush(pq, (1, "high priority"))
heapq.heappush(pq, (2, "medium priority"))

# Items come out in priority order, not insertion order
while pq:
    priority, task = heapq.heappop(pq)
    print(f"Priority {priority}: {task}")
# Output:
#   Priority 1: high priority
#   Priority 2: medium priority
#   Priority 3: low priority


# For a max-heap, negate the priority
heapq.heappush(pq, (-5, "important"))
heapq.heappush(pq, (-1, "trivial"))
priority, task = heapq.heappop(pq)
print(-priority, task)  # 5, "important"
JavaScript
class MinHeap {
  constructor() {
    this._heap = [];
  }

  push(val) {
    this._heap.push(val);
    this._bubbleUp(this._heap.length - 1);
  }

  pop() {
    if (!this._heap.length) throw new Error("Heap is empty");
    const min = this._heap[0];
    const last = this._heap.pop();
    if (this._heap.length) {
      this._heap[0] = last;
      this._bubbleDown(0);
    }
    return min;
  }

  peek() {
    return this._heap[0];
  }

  _bubbleUp(i) {
    // Keep swapping with parent until heap property holds
    while (i > 0) {
      const parent = Math.floor((i - 1) / 2);
      if (this._heap[i] >= this._heap[parent]) break;
      [this._heap[i], this._heap[parent]] = [this._heap[parent], this._heap[i]];
      i = parent;
    }
  }

  _bubbleDown(i) {
    const n = this._heap.length;
    while (true) {
      let smallest = i;
      const left = 2 * i + 1;
      const right = 2 * i + 2;
      // Find the smallest among node and its children
      if (left < n && this._heap[left] < this._heap[smallest]) smallest = left;
      if (right < n && this._heap[right] < this._heap[smallest]) smallest = right;
      if (smallest === i) break;
      [this._heap[i], this._heap[smallest]] = [this._heap[smallest], this._heap[i]];
      i = smallest;
    }
  }

  get size() { return this._heap.length; }
}

Common Mistakes

โš ๏ธ Mistakes That Trip Up Beginners
  1. Using list.pop(0) in Python as a queue. It's O(n) because every remaining element shifts left. Use collections.deque which gives O(1) popleft(). This is probably the most common Python performance mistake.
  2. Using Array.shift() in JavaScript. Same problem โ€” O(n) per call. Use an object with head/tail indices, or a linked list. For small queues (under ~1000 elements) it doesn't matter, but for BFS on a large graph it's devastating.
  3. Forgetting that Python's heapq is a min-heap. For max-heap behavior, negate all values: push -x, pop and negate back. Or wrap items in a tuple: (-priority, item).
  4. Not handling the empty queue case. Always check before dequeue or peek. BFS loops should check while queue: not assume the queue has elements.
  5. Confusing queue with stack for BFS. BFS requires a queue (FIFO). Using a stack gives you DFS instead. The difference is which neighbor gets explored first โ€” the oldest one (queue โ†’ BFS โ†’ shortest path) or the newest one (stack โ†’ DFS โ†’ deep exploration).

Real-World Example: Task Scheduler

Here's a simplified version of how OS task schedulers and job queues work. Tasks arrive with priorities and get processed in priority order, not arrival order.

Python
import heapq
import time

class TaskScheduler:
    """Priority-based task scheduler.
    
    Tasks with lower priority numbers run first.
    Ties are broken by arrival order (FIFO within same priority).
    
    This is the basic pattern behind: OS process schedulers,
    Celery/RQ job queues, hospital triage systems, Dijkstra's algorithm.
    """
    def __init__(self):
        self._queue = []
        self._counter = 0  # Tiebreaker for same-priority tasks

    def add_task(self, name, priority=5):
        # Tuple comparison: first by priority, then by arrival order
        # The counter ensures FIFO ordering within the same priority
        heapq.heappush(self._queue, (priority, self._counter, name))
        self._counter += 1
        print(f"  + Queued: '{name}' (priority {priority})")

    def run_next(self):
        if not self._queue:
            print("  No tasks in queue")
            return None
        priority, _, name = heapq.heappop(self._queue)
        print(f"  โ–ถ Running: '{name}' (priority {priority})")
        return name

    def run_all(self):
        print("Running all tasks in priority order:")
        while self._queue:
            self.run_next()


# Simulate a server processing requests
scheduler = TaskScheduler()
scheduler.add_task("Send weekly report email", priority=3)
scheduler.add_task("Process payment", priority=1)        # urgent!
scheduler.add_task("Generate thumbnails", priority=5)
scheduler.add_task("Send password reset", priority=2)
scheduler.add_task("Clean temp files", priority=5)

print()
scheduler.run_all()
# Output:
#   โ–ถ Running: 'Process payment' (priority 1)
#   โ–ถ Running: 'Send password reset' (priority 2)
#   โ–ถ Running: 'Send weekly report email' (priority 3)
#   โ–ถ Running: 'Generate thumbnails' (priority 5)
#   โ–ถ Running: 'Clean temp files' (priority 5)

This same pattern powers Kubernetes pod scheduling, airline boarding (priority groups), and network packet QoS (Quality of Service). The priority queue ensures that critical work gets done first, even if lower-priority tasks arrived earlier.

Interview Patterns

๐Ÿ’ก BFS = Queue, Always

If a problem asks for shortest path in an unweighted graph, level-order traversal, or "minimum steps" โ€” reach for a queue. BFS explores all nodes at distance d before any at distance d+1, which is exactly what gives you the shortest path guarantee.

๐Ÿ’ก "Top K" = Priority Queue

Anytime you see "K largest", "K most frequent", "K closest" โ€” that's a heap. Keep a min-heap of size K. For each new element, push it and pop if the heap exceeds K. You end up with the K largest in O(n log k), which beats sorting at O(n log n) when K is small.

๐Ÿ’ก Sliding Window Maximum = Monotonic Deque

The classic hard problem: find the maximum in every window of size K. A deque that keeps elements in decreasing order gives you O(n) total. When a new element arrives, pop everything smaller from the back (they'll never be the max). Pop from the front if the element has left the window. The front of the deque is always the current max.

๐Ÿ’ก Implement Queue Using Stacks

Use two stacks. Push into stack A. When you need to dequeue, dump stack A into stack B (reversing the order). Now stack B pops in FIFO order. Only dump when B is empty โ€” this gives amortized O(1) per operation. Each element gets pushed and popped at most twice total.

๐Ÿ’ก Multi-Source BFS

When there are multiple starting points (like "distance from nearest 0 in a matrix"), start BFS from ALL sources simultaneously by adding them all to the queue before the first iteration. This avoids running separate BFS from each source. Problems: 01 Matrix (#542), Rotting Oranges (#994).

๐Ÿ’ก Topological Sort (Kahn's Algorithm)

Process nodes in dependency order. Start by queuing all nodes with zero dependencies (in-degree 0). Process each node, reduce the in-degree of its neighbors, and add newly zero-in-degree nodes to the queue. The processing order is a valid topological sort. If you can't process all nodes, there's a cycle.

Problems: Course Schedule (#207), Course Schedule II (#210), Alien Dictionary.

Practice Problems

Ordered roughly from easy to hard. Try to identify the pattern before looking at solutions.

#ProblemDifficultyPattern
232Implement Queue using StacksEasyTwo-stack trick
225Implement Stack using QueuesEasyQueue rotation
933Number of Recent CallsEasySliding window queue
622Design Circular QueueMediumCircular buffer
347Top K Frequent ElementsMediumHeap / bucket sort
621Task SchedulerMediumPriority queue + cooldown
994Rotting OrangesMediumMulti-source BFS
239Sliding Window MaximumHardMonotonic deque
23Merge K Sorted ListsHardMin-heap merge