Learn/DSA/Queues
Data StructuresBeginner8 min

Queues

A FIFO line: enqueue at the back, dequeue from the front in O(1). The engine behind BFS and buffering.

QueuesFIFOBFSDeque

A queue is a line at a coffee shop. People join at the back and are served from the front — whoever waited longest goes first. This is First In, First Out (FIFO), the mirror image of a stack’s LIFO. Fairness is the mental model: nothing jumps the line.

Two operations run the show: enqueue adds to the back, dequeue removes from the front (with peek to read the front without removing). Both should be O(1). That "should" hides a classic trap — do it naively on a plain array and one of them becomes O(n).

Queue (FIFO)
front
1
6
rear
3
First in, first out — enqueue at rear, dequeue at front. Both O(1).
Enqueue adds at the back and dequeue removes from the front — elements are served in arrival order (FIFO).

The naive-array trap

If you back a queue with an array and dequeue by removing index 0, every remaining element shifts left by one — that’s O(n) per dequeue. The fix is to either use a linked list (with head and tail pointers) or a circular buffer that moves an index instead of shifting data.

OperationTime

O(1) both ends — as long as you avoid shifting elements on every dequeue.

The circular buffer

A circular buffer (ring buffer) gives you a fixed-size array where the front and back indices wrap around using modulo arithmetic. When the tail hits the end of the array, it loops back to index 0 — reusing freed slots instead of growing. This is how bounded queues, streaming buffers, and producer/consumer pipelines avoid endless allocation.

A fixed-capacity circular queue — O(1) enqueue and dequeue
class CircularQueue {
  constructor(capacity) {
    this.buf = new Array(capacity)
    this.cap = capacity
    this.head = 0
    this.size = 0
  }
  enqueue(x) {
    if (this.size === this.cap) return false // full
    const tail = (this.head + this.size) % this.cap
    this.buf[tail] = x
    this.size++
    return true
  }
  dequeue() {
    if (this.size === 0) return undefined // empty
    const x = this.buf[this.head]
    this.head = (this.head + 1) % this.cap
    this.size--
    return x
  }
}

Queues power BFS

The reason queues matter for algorithms: breadth-first search is a queue in disguise. You explore a graph or tree level by level — enqueue a node’s unvisited neighbors, dequeue the next node to process. FIFO order guarantees you finish everything at distance k before touching distance k+1, which is exactly why BFS finds shortest paths in unweighted graphs.

Level-order BFS over a graph — O(V + E)
function bfs(start, graph) {
  const visited = new Set([start])
  const queue = [start]
  const order = []
  while (queue.length) {
    const node = queue.shift() // dequeue (use a real queue in prod)
    order.push(node)
    for (const next of graph[node]) {
      if (!visited.has(next)) {
        visited.add(next)
        queue.push(next) // enqueue
      }
    }
  }
  return order
}

Interview reflex

See "shortest path in an unweighted grid/graph" or "level by level"? Reach for BFS with a queue. See "sliding window maximum"? Reach for a deque — a double-ended queue you can push and pop from both ends in O(1).

When to reach for a queue

  • You need FIFO fairness — process items in the order they arrived.
  • You’re running BFS or any level-order traversal.
  • You’re buffering a stream between a fast producer and a slower consumer — a circular buffer bounds memory.
  • You need both ends — a deque generalizes a queue and a stack, handy for sliding-window problems.

Next up

What if the "next" item isn’t the oldest, but the smallest or largest? That’s a priority queue, and the structure that makes it efficient is the heap.

Section navigation