Toolmingo
Guides13 min read

Queue Data Structure Explained: Complete Guide with Examples

Master the queue data structure. Learn FIFO principle, array vs linked-list implementations in Python, JavaScript, Java, Go, circular queues, deques, priority queues, and 6 classic interview problems with solutions.

Queue Data Structure Explained

Quick Reference

Operation Array (amortised) Linked List Circular Array
enqueue O(1) amortised O(1) O(1)
dequeue O(n) naïve / O(1) circular O(1) O(1)
peek O(1) O(1) O(1)
isEmpty O(1) O(1) O(1)
Space O(n) O(n)+ptr overhead O(n)

What Is a Queue?

A queue is a linear data structure that follows the FIFO principle — First In, First Out. The first element added is the first one removed, just like a line at a checkout counter.

Two ends:

  • Front (head) — where elements leave (dequeue)
  • Rear (tail) — where elements enter (enqueue)
  enqueue ──▶  [ A | B | C | D ]  ──▶ dequeue
               rear               front

Queue vs Stack

Property Queue (FIFO) Stack (LIFO)
Insert end Rear Top
Remove end Front Top
Analogy Checkout line Plate stack
BFS ✅ core data structure
DFS ✅ core data structure
Undo/redo
Print spool

Implementation 1 — Naive Array (O(n) dequeue)

The simplest approach: append to enqueue, pop(0) to dequeue. Dequeue is O(n) because every element shifts left.

class NaiveQueue:
    def __init__(self):
        self._data = []

    def enqueue(self, val):
        self._data.append(val)          # O(1) amortised

    def dequeue(self):
        if self.is_empty():
            raise IndexError("dequeue from empty queue")
        return self._data.pop(0)        # O(n) — every element shifts

    def peek(self):
        if self.is_empty():
            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)

Never use this in performance-critical code. Use collections.deque instead.


Implementation 2 — Python collections.deque (O(1) both ends)

Python's built-in deque (double-ended queue) uses a doubly-linked list of fixed-size blocks. Both append and popleft are O(1).

from collections import deque

class Queue:
    def __init__(self):
        self._data = deque()

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

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

    def peek(self):
        if self.is_empty():
            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(1)
q.enqueue(2)
q.enqueue(3)
print(q.dequeue())  # 1
print(q.peek())     # 2
print(len(q))       # 2

Implementation 3 — Circular Array (fixed capacity, O(1) all ops)

When capacity is known in advance, a circular array wastes no space and has O(1) enqueue and dequeue with no shifting.

index: 0   1   2   3   4
       [D | E | _ | B | C]
              ↑         ↑
             rear      front

Wrap-around: (index + 1) % capacity
class CircularQueue:
    def __init__(self, capacity: int):
        self._cap = capacity
        self._data = [None] * capacity
        self._front = 0
        self._size = 0

    @property
    def _rear(self):
        return (self._front + self._size) % self._cap

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

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

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

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

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

    def __len__(self):
        return self._size

Implementation 4 — Linked List Queue (O(1), unbounded)

Linked list queues have O(1) enqueue and dequeue with no capacity limit. Extra memory per node (pointer overhead).

class _Node:
    __slots__ = ("val", "next")
    def __init__(self, val):
        self.val = val
        self.next = None

class LinkedQueue:
    def __init__(self):
        self._head = None   # front — dequeue here
        self._tail = None   # rear  — enqueue here
        self._size = 0

    def enqueue(self, val):
        node = _Node(val)
        if self._tail:
            self._tail.next = node
        self._tail = node
        if self._head is None:
            self._head = node
        self._size += 1

    def dequeue(self):
        if self._head is None:
            raise IndexError("dequeue from empty queue")
        val = self._head.val
        self._head = self._head.next
        if self._head is None:
            self._tail = None
        self._size -= 1
        return val

    def peek(self):
        if self._head is None:
            raise IndexError("peek from empty queue")
        return self._head.val

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

    def __len__(self):
        return self._size

JavaScript Implementation

class Queue {
  #data = [];
  #head = 0;       // pointer avoids O(n) shift

  enqueue(val) {
    this.#data.push(val);
  }

  dequeue() {
    if (this.isEmpty()) throw new Error("Queue is empty");
    const val = this.#data[this.#head];
    this.#head++;
    // Compact when wasted space > half the array
    if (this.#head > this.#data.length / 2) {
      this.#data = this.#data.slice(this.#head);
      this.#head = 0;
    }
    return val;
  }

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

  isEmpty() { return this.#head >= this.#data.length; }
  get size() { return this.#data.length - this.#head; }
}

const q = new Queue();
q.enqueue("a");
q.enqueue("b");
q.enqueue("c");
console.log(q.dequeue()); // "a"
console.log(q.peek());    // "b"
console.log(q.size);      // 2

Java Implementation

import java.util.ArrayDeque;
import java.util.Deque;
import java.util.NoSuchElementException;

public class Queue<T> {
    private final Deque<T> deque = new ArrayDeque<>();

    public void enqueue(T val) {
        deque.addLast(val);
    }

    public T dequeue() {
        if (deque.isEmpty()) throw new NoSuchElementException();
        return deque.removeFirst();
    }

    public T peek() {
        if (deque.isEmpty()) throw new NoSuchElementException();
        return deque.peekFirst();
    }

    public boolean isEmpty() { return deque.isEmpty(); }
    public int size() { return deque.size(); }
}

Java tip: Use java.util.ArrayDeque as a queue — it is faster than LinkedList due to better cache locality.


Go Implementation

package queue

import "errors"

type Queue[T any] struct {
    data []T
    head int
}

func (q *Queue[T]) Enqueue(val T) {
    q.data = append(q.data, val)
}

func (q *Queue[T]) Dequeue() (T, error) {
    var zero T
    if q.IsEmpty() {
        return zero, errors.New("queue is empty")
    }
    val := q.data[q.head]
    q.data[q.head] = zero // help GC
    q.head++
    // Compact slice periodically
    if q.head > len(q.data)/2 {
        q.data = append([]T(nil), q.data[q.head:]...)
        q.head = 0
    }
    return val, nil
}

func (q *Queue[T]) Peek() (T, error) {
    var zero T
    if q.IsEmpty() {
        return zero, errors.New("queue is empty")
    }
    return q.data[q.head], nil
}

func (q *Queue[T]) IsEmpty() bool { return q.head >= len(q.data) }
func (q *Queue[T]) Len() int      { return len(q.data) - q.head }

Types of Queues

Type Description Key Feature
Simple Queue Basic FIFO Standard enqueue/dequeue
Circular Queue Array wraps around No wasted slots, fixed capacity
Deque (double-ended) Insert/remove at both ends Both appendleft and pop are O(1)
Priority Queue Element with highest priority dequeued first Backed by a heap
Blocking Queue Thread-safe; blocks when empty/full Used in producer-consumer
Monotonic Queue Maintains sorted monotone invariant Sliding window maximum

Deque (Double-Ended Queue)

A deque allows enqueue and dequeue at both ends.

from collections import deque

dq = deque()
dq.append(1)         # enqueue rear   → [1]
dq.appendleft(0)     # enqueue front  → [0, 1]
dq.append(2)         # enqueue rear   → [0, 1, 2]
print(dq.popleft())  # dequeue front  → 0, dq = [1, 2]
print(dq.pop())      # dequeue rear   → 2, dq = [1]

Use deque when you need:

  • Sliding window operations (monotonic deque)
  • BFS with bidirectional expansion
  • LRU cache (deque of keys + dict)

Priority Queue

Elements are dequeued in priority order, not arrival order.

import heapq

class MinPriorityQueue:
    def __init__(self):
        self._heap = []

    def enqueue(self, priority, val):
        heapq.heappush(self._heap, (priority, val))

    def dequeue(self):
        if not self._heap:
            raise IndexError("empty")
        return heapq.heappop(self._heap)   # (priority, val)

    def peek(self):
        return self._heap[0]

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

pq = MinPriorityQueue()
pq.enqueue(3, "low")
pq.enqueue(1, "high")
pq.enqueue(2, "medium")
print(pq.dequeue())  # (1, 'high')
print(pq.dequeue())  # (2, 'medium')

For a max-priority queue, negate the priority: heappush(heap, (-priority, val)).


BFS — The Core Queue Use Case

Breadth-First Search visits all nodes at depth d before visiting depth d+1. A queue is essential — it preserves order.

from collections import deque

def bfs(graph: dict, start: int) -> list[int]:
    visited = {start}
    queue   = deque([start])
    order   = []

    while queue:
        node = queue.popleft()
        order.append(node)
        for neighbour in graph[node]:
            if neighbour not in visited:
                visited.add(neighbour)
                queue.append(neighbour)
    return order

graph = {1: [2, 3], 2: [4, 5], 3: [6], 4: [], 5: [], 6: []}
print(bfs(graph, 1))  # [1, 2, 3, 4, 5, 6]

Why a queue? Nodes enqueued earlier are visited earlier → level-by-level traversal.


6 Classic Interview Problems

1. Binary Tree Level-Order Traversal (LeetCode 102)

Return nodes level by level.

from collections import deque

def level_order(root):
    if not root:
        return []
    result, queue = [], deque([root])
    while queue:
        level_size = len(queue)
        level = []
        for _ in range(level_size):
            node = queue.popleft()
            level.append(node.val)
            if node.left:  queue.append(node.left)
            if node.right: queue.append(node.right)
        result.append(level)
    return result

Key pattern: Snapshot len(queue) at start of each iteration to know how many nodes belong to the current level.


2. Rotting Oranges (LeetCode 994)

Multi-source BFS — all rotten oranges start simultaneously.

from collections import deque

def oranges_rotting(grid):
    rows, cols = len(grid), len(grid[0])
    fresh = 0
    queue = deque()
    for r in range(rows):
        for c in range(cols):
            if grid[r][c] == 2:
                queue.append((r, c, 0))   # (row, col, minutes)
            elif grid[r][c] == 1:
                fresh += 1

    minutes = 0
    while queue:
        r, c, t = queue.popleft()
        for dr, dc in [(-1,0),(1,0),(0,-1),(0,1)]:
            nr, nc = r + dr, c + dc
            if 0 <= nr < rows and 0 <= nc < cols and grid[nr][nc] == 1:
                grid[nr][nc] = 2
                fresh -= 1
                minutes = t + 1
                queue.append((nr, nc, t + 1))

    return minutes if fresh == 0 else -1

3. Walls and Gates (LeetCode 286)

Fill each empty room with distance to nearest gate. Multi-source BFS from all gates at once.

from collections import deque

def walls_and_gates(rooms):
    INF = 2**31 - 1
    rows, cols = len(rooms), len(rooms[0])
    queue = deque()
    for r in range(rows):
        for c in range(cols):
            if rooms[r][c] == 0:           # gate
                queue.append((r, c))
    while queue:
        r, c = queue.popleft()
        for dr, dc in [(-1,0),(1,0),(0,-1),(0,1)]:
            nr, nc = r + dr, c + dc
            if 0 <= nr < rows and 0 <= nc < cols and rooms[nr][nc] == INF:
                rooms[nr][nc] = rooms[r][c] + 1
                queue.append((nr, nc))

4. Implement Queue Using Two Stacks (LeetCode 232)

class MyQueue:
    def __init__(self):
        self._inbox  = []    # push here
        self._outbox = []    # pop from here

    def push(self, x: int) -> None:
        self._inbox.append(x)

    def _transfer(self):
        if not self._outbox:
            while self._inbox:
                self._outbox.append(self._inbox.pop())

    def pop(self) -> int:
        self._transfer()
        return self._outbox.pop()

    def peek(self) -> int:
        self._transfer()
        return self._outbox[-1]

    def empty(self) -> bool:
        return not self._inbox and not self._outbox

Amortized O(1) per operation: each element is moved at most once from inbox to outbox.


5. Sliding Window Maximum (LeetCode 239)

Find max in every window of size k. Uses a monotonic deque (decreasing order).

from collections import deque

def max_sliding_window(nums: list[int], k: int) -> list[int]:
    dq = deque()          # stores indices; front = current max
    result = []

    for i, num in enumerate(nums):
        # Remove indices outside window
        if dq and dq[0] < i - k + 1:
            dq.popleft()
        # Maintain decreasing order — pop smaller values from rear
        while dq and nums[dq[-1]] < num:
            dq.pop()
        dq.append(i)
        if i >= k - 1:
            result.append(nums[dq[0]])

    return result

print(max_sliding_window([1,3,-1,-3,5,3,6,7], 3))
# [3, 3, 5, 5, 6, 7]

Time: O(n) — each element enters and leaves deque at most once.


6. First Non-Repeating Character in a Stream (LeetCode 387 variant)

Process characters one by one; at each step report the first non-repeating char.

from collections import deque, Counter

def first_unique_char_stream(stream: str) -> list[str]:
    count = Counter()
    queue = deque()         # candidates; front = first unique so far
    result = []

    for ch in stream:
        count[ch] += 1
        queue.append(ch)
        # Evict front if it's no longer unique
        while queue and count[queue[0]] > 1:
            queue.popleft()
        result.append(queue[0] if queue else "#")

    return result

print(first_unique_char_stream("aabcbc"))
# ['a', '#', 'b', 'b', 'c', 'b']

Queue vs Stack vs Deque

Feature Queue (FIFO) Stack (LIFO) Deque
Insert Rear only Top only Front or Rear
Remove Front only Top only Front or Rear
BFS
DFS
Sliding window ✅ (monotonic)
Undo/redo

When to Use a Queue

Scenario Why Queue
BFS (graphs, trees) Process nodes level by level
Print spooler First submitted, first printed
CPU / network scheduling Fair round-robin
Rate limiting (token bucket) Fixed processing rate
Producer-consumer pipeline Decouple producers from consumers
Event loop (Node.js, browser) Events processed in arrival order
Sliding window (with deque) O(n) window max/min

When NOT to Use a Queue

Scenario Better choice
DFS traversal Stack
Ordered access by priority Priority queue (heap)
Random access by index Array / list
Last-item-first processing Stack

Real-World Analogy

Real World Queue Operation
Checkout line enqueue = join rear, dequeue = serve front
Print spooler Jobs print in submission order
CPU task scheduling Round-robin time slices
HTTP request queue Requests processed in arrival order
Breadth-first web crawler Crawl nearest pages first

Common Mistakes

Mistake Fix
Using list.pop(0) for dequeue in Python Use collections.deque + popleft()
Forgetting head pointer causes O(n) dequeue in JS Track head index or use a proper deque
Checking if queue: after popleft() instead of before Always guard with is_empty() before dequeue/peek
Confusing queue with stack in BFS — using pop() instead of popleft() BFS needs queue (FIFO); stack gives DFS
Mutating queue while iterating Snapshot len(queue) for level-order BFS
Not clearing evicted slots in circular queue Set to None to help GC
Using PriorityQueue in Java over ArrayDeque for plain FIFO ArrayDeque is faster for plain FIFO
Infinite loop when all elements are equal in monotonic deque Use <= vs < carefully based on problem

Queue vs Related Structures

Structure Order Guarantee O(1) Enqueue O(1) Dequeue Notes
Queue FIFO Standard
Stack LIFO ✅ (push) ✅ (pop) Same end
Deque Both ends Most flexible
Priority Queue Priority order O(log n) O(log n) Heap-backed
Circular Buffer FIFO Fixed capacity
Blocking Queue FIFO (thread-safe) O(1) O(1) Blocks on empty/full

FAQ

Q: Which Python collection should I use as a queue?
collections.deque. Never use a plain list — pop(0) is O(n). deque.popleft() is O(1).

Q: When is a circular array better than a linked-list queue?
When capacity is bounded and you want better cache performance. No pointer overhead, contiguous memory. Use when you know max size upfront (e.g., fixed-size ring buffer).

Q: How is a priority queue different from a regular queue?
A regular queue dequeues in FIFO order. A priority queue dequeues the element with the highest (or lowest) priority, regardless of insertion order. Backed by a heap — O(log n) enqueue/dequeue instead of O(1).

Q: Can a queue be used for DFS?
No. DFS requires a stack (LIFO). Using a queue gives you BFS. A common interview mistake is accidentally getting BFS instead of DFS by choosing the wrong data structure.

Q: What is a monotonic deque?
A deque that maintains elements in increasing or decreasing order by removing dominated elements on enqueue. Used in sliding window maximum/minimum problems to achieve O(n) time instead of O(n·k).

Q: Queue using two stacks — what is the time complexity?
Amortized O(1) per operation. Each element is pushed to inbox once and popped to outbox at most once, giving O(1) amortized over a sequence of operations even though individual dequeue calls may cost O(n) when outbox is empty.

Keep reading

All Toolmingotools are free & run in your browser

No sign-up, no upload, no watermark. Your files never leave your device.

Browse all tools