← Back to DevBytes

Interview Guide: Queues Problems and Solutions

Understanding Queues: The Interview-Ready Guide

A queue is a linear data structure that follows the First-In-First-Out (FIFO) principle — the element inserted first is the one that gets removed first. Think of a line of people waiting at a coffee shop: the person who arrives first gets served first. In technical interviews, queues appear everywhere, from breadth-first search to task scheduling, and mastering them is essential for landing a software engineering role.

What Makes a Queue?

At its core, a queue exposes two primary operations:

Additional utility operations often include:

Here's the simplest possible queue implementation using Python's built-in list (not efficient for production, but great for understanding the mechanics):

class SimpleQueue:
    def __init__(self):
        self.items = []

    def enqueue(self, value):
        self.items.append(value)  # Add to the back

    def dequeue(self):
        if self.is_empty():
            raise IndexError("Dequeue from empty queue")
        return self.items.pop(0)  # Remove from the front

    def peek(self):
        if self.is_empty():
            return None
        return self.items[0]

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

    def size(self):
        return len(self.items)

# Usage example
q = SimpleQueue()
q.enqueue(10)
q.enqueue(20)
q.enqueue(30)
print(q.dequeue())  # 10
print(q.peek())     # 20
print(q.size())     # 2

Why is the above naive? The pop(0) operation shifts all remaining elements left by one position, making dequeue an O(n) operation. In interviews, you'll want to discuss this trade-off and then present better alternatives.

Why Queues Matter in Interviews

🚀 Deploy your AI agent in 10 minutes

Managed Hermes hosting. Zero DevOps. 100M tokens/mo included.

Try it free →

Queues test your understanding of several critical concepts:

Interviewers love queue problems because they reveal whether you think about data flow holistically — how elements enter, wait, and exit a system. This mirrors real-world scenarios like message brokers, print job spoolers, and web server request handling.

Queue Variants You Must Know

1. Standard FIFO Queue (via collections.deque)

Python's collections.deque (pronounced "deck," short for double-ended queue) provides O(1) appends and pops from both ends. For a standard queue, you restrict operations to one side for enqueue and the other for dequeue.

from collections import deque

class EfficientQueue:
    def __init__(self):
        self.items = deque()

    def enqueue(self, value):
        self.items.append(value)      # O(1) — add to the right (back)

    def dequeue(self):
        if self.is_empty():
            raise IndexError("Dequeue from empty queue")
        return self.items.popleft()   # O(1) — remove from the left (front)

    def peek(self):
        if self.is_empty():
            return None
        return self.items[0]

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

    def size(self):
        return len(self.items)

# All operations are now O(1)
q = EfficientQueue()
q.enqueue("A")
q.enqueue("B")
q.enqueue("C")
print(q.dequeue())  # "A"
print(q.dequeue())  # "B"

Interview tip: Always mention collections.deque when implementing queues in Python. It shows you know the standard library and care about performance. Under the hood, deque uses a doubly-linked list of blocks, giving you amortized O(1) operations.

2. Circular Queue (Ring Buffer)

A circular queue reuses a fixed-size array by wrapping the front and rear pointers around. When the rear reaches the end of the array, it circles back to index 0 (if space is available). This is the classic "design a circular queue" interview problem.

class CircularQueue:
    def __init__(self, capacity: int):
        self.capacity = capacity
        self.queue = [None] * capacity
        self.front = 0      # Index of the front element
        self.rear = -1      # Index of the rear element
        self.count = 0      # Number of elements currently in the queue

    def enqueue(self, value) -> bool:
        if self.is_full():
            return False
        # Move rear forward (with wrap-around)
        self.rear = (self.rear + 1) % self.capacity
        self.queue[self.rear] = value
        self.count += 1
        return True

    def dequeue(self) -> int:
        if self.is_empty():
            return -1  # Or raise exception based on problem spec
        value = self.queue[self.front]
        self.queue[self.front] = None  # Optional: clear the slot
        # Move front forward (with wrap-around)
        self.front = (self.front + 1) % self.capacity
        self.count -= 1
        return value

    def peek(self) -> int:
        if self.is_empty():
            return -1
        return self.queue[self.front]

    def is_empty(self) -> bool:
        return self.count == 0

    def is_full(self) -> bool:
        return self.count == self.capacity

    def size(self) -> int:
        return self.count

# Example: LeetCode 622 — Design Circular Queue
cq = CircularQueue(3)
print(cq.enqueue(1))   # True
print(cq.enqueue(2))   # True
print(cq.enqueue(3))   # True
print(cq.enqueue(4))   # False (queue full)
print(cq.dequeue())    # 1
print(cq.enqueue(4))   # True (wrapped around)
print(cq.peek())       # 2

Key insight: The modulo operation % capacity is what makes the wrap-around work. Without the count variable, distinguishing between an empty queue and a full queue using only front and rear is tricky — you'd waste one slot or need an extra flag. Using count is the cleanest solution and the one interviewers expect.

3. Priority Queue (Heap-Based)

A priority queue assigns a priority to each element and dequeues the highest (or lowest) priority element first, not necessarily the one that arrived first. This is typically implemented using a binary heap.

import heapq

class PriorityQueue:
    def __init__(self):
        self.heap = []  # Min-heap by default

    def enqueue(self, value, priority):
        # Store as (priority, insertion_order, value) to break ties
        # insertion_order prevents comparing values when priorities are equal
        heapq.heappush(self.heap, (priority, self._next_order(), value))
    
    def dequeue(self):
        if self.is_empty():
            raise IndexError("Dequeue from empty priority queue")
        priority, _, value = heapq.heappop(self.heap)
        return value

    def peek(self):
        if self.is_empty():
            return None
        return self.heap[0][2]  # value of the smallest priority item

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

    def size(self):
        return len(self.heap)

    def _next_order(self):
        # Simple tie-breaker: increment a counter each time
        if not hasattr(self, '_counter'):
            self._counter = 0
        self._counter += 1
        return self._counter

# Usage: lower number = higher priority
pq = PriorityQueue()
pq.enqueue("Critical task", 1)
pq.enqueue("Normal task", 5)
pq.enqueue("Low-priority task", 10)
pq.enqueue("Another critical", 1)
print(pq.dequeue())  # "Critical task" (priority 1, inserted first)
print(pq.dequeue())  # "Another critical" (priority 1, inserted second)
print(pq.dequeue())  # "Normal task" (priority 5)

Time complexity: enqueue is O(log n), dequeue is O(log n), peek is O(1). In interviews, always mention that heapq provides a min-heap; for a max-heap, simply negate priorities.

4. Deque (Double-Ended Queue)

A deque supports insertion and deletion from both ends. It's the Swiss Army knife of queue structures and underpins many sliding-window algorithms.

from collections import deque

dq = deque()
# Add to either end
dq.append(1)        # Right side: [1]
dq.appendleft(2)    # Left side:  [2, 1]
dq.append(3)        # Right side: [2, 1, 3]

# Remove from either end
dq.pop()            # 3 (from right): [2, 1]
dq.popleft()        # 2 (from left):  [1]

# Access both ends in O(1)
print(dq[0])        # First element: 1
print(dq[-1])       # Last element:  1
print(len(dq))      # 1

Common Interview Problems & Solutions

Problem 1: Implement Queue Using Two Stacks

This is arguably the most famous queue interview question. The challenge: simulate a queue's FIFO behavior using two LIFO stacks. The trick is to use one stack for incoming elements and the other for outgoing elements, amortizing the cost.

class QueueUsingStacks:
    def __init__(self):
        self.stack_in = []   # For enqueue operations
        self.stack_out = []  # For dequeue/peek operations

    def enqueue(self, value):
        # Always push to the in-stack — O(1)
        self.stack_in.append(value)

    def _transfer(self):
        # When stack_out is empty, dump all elements from stack_in
        # into stack_out, reversing their order
        if not self.stack_out:
            while self.stack_in:
                self.stack_out.append(self.stack_in.pop())

    def dequeue(self):
        self._transfer()
        if not self.stack_out:
            raise IndexError("Dequeue from empty queue")
        return self.stack_out.pop()

    def peek(self):
        self._transfer()
        if not self.stack_out:
            return None
        return self.stack_out[-1]  # Top of stack_out is the front

    def is_empty(self):
        return not self.stack_in and not self.stack_out

    def size(self):
        return len(self.stack_in) + len(self.stack_out)

# Demonstration
q = QueueUsingStacks()
q.enqueue(1)
q.enqueue(2)
q.enqueue(3)
print(q.dequeue())  # 1
q.enqueue(4)
print(q.peek())     # 2
print(q.dequeue())  # 2
print(q.dequeue())  # 3
print(q.dequeue())  # 4

Amortized analysis: Each element is pushed exactly once onto stack_in and popped exactly once from stack_in (during transfer) and then popped exactly once from stack_out. So each element incurs O(1) pushes and O(1) pops total. The amortized cost per dequeue is O(1), even though an individual dequeue that triggers a transfer costs O(n). This is a beautiful example of amortized analysis that interviewers love to discuss.

Problem 2: Sliding Window Maximum

Given an array and a window size k, find the maximum value in each sliding window as it moves from left to right. This is LeetCode 239 and a quintessential deque problem. The naive O(n*k) approach won't pass; you need a monotonic deque.

from collections import deque

def sliding_window_maximum(nums, k):
    """
    Returns a list of the maximum value in each sliding window of size k.
    Uses a deque that stores indices, maintaining decreasing order of values.
    """
    dq = deque()  # Will store indices, not values directly
    result = []

    for i, num in enumerate(nums):
        # 1. Remove indices that are out of the current window
        #    The window is [i - k + 1, i]
        if dq and dq[0] <= i - k:
            dq.popleft()

        # 2. Maintain decreasing order: remove from back all elements
        #    smaller than the current number
        while dq and nums[dq[-1]] <= num:
            dq.pop()

        # 3. Add current index
        dq.append(i)

        # 4. Once we've processed at least k elements, record the maximum
        #    The front of the deque holds the index of the current max
        if i >= k - 1:
            result.append(nums[dq[0]])

    return result

# Example
nums = [1, 3, -1, -3, 5, 3, 6, 7]
k = 3
print(sliding_window_maximum(nums, k))
# Output: [3, 3, 5, 5, 6, 7]

# Walk-through for first few steps:
# i=0, num=1:  dq=[0], window not yet formed
# i=1, num=3:  dq=[1]  (1 popped because nums[0]=1 < 3), window not yet formed
# i=2, num=-1: dq=[1,2], window formed: max=nums[1]=3  ✓
# i=3, num=-3: index 1 still in window, dq=[1,2,3], max=nums[1]=3 ✓
# i=4, num=5:  index 1 out of window (popleft), all smaller popped, dq=[4], max=5 ✓

Why the deque stores indices and not values: You need to know when elements fall out of the window. By storing indices, you can check dq[0] <= i - k to evict expired elements. The decreasing monotonic property ensures the front always holds the maximum for the current window. Time complexity: O(n) — each element enters and leaves the deque at most once.

Problem 3: BFS on a Binary Tree (Level-Order Traversal)

Breadth-first search is the canonical queue application. Here's a complete level-order traversal that groups nodes by level — a very common interview request.

from collections import deque

class TreeNode:
    def __init__(self, val=0, left=None, right=None):
        self.val = val
        self.left = left
        self.right = right

def level_order_traversal(root):
    """
    Returns a list of lists, where each inner list contains
    the values of nodes at that level (left to right).
    """
    if not root:
        return []

    result = []
    queue = deque([root])

    while queue:
        level_size = len(queue)
        current_level = []

        for _ in range(level_size):
            node = queue.popleft()
            current_level.append(node.val)

            if node.left:
                queue.append(node.left)
            if node.right:
                queue.append(node.right)

        result.append(current_level)

    return result

# Build a sample tree:
#         1
#       /   \
#      2     3
#     / \   /
#    4   5 6
root = TreeNode(1)
root.left = TreeNode(2, TreeNode(4), TreeNode(5))
root.right = TreeNode(3, TreeNode(6))

print(level_order_traversal(root))
# Output: [[1], [2, 3], [4, 5, 6]]

The key pattern here is level_size = len(queue) before processing a level. This "freezes" the number of nodes at the current depth, so you know exactly when to start a new level list. This technique extends to graph BFS, shortest-path problems, and word ladder puzzles.

Problem 4: Generate Binary Numbers Using a Queue

A clever queue application: generate the first N binary numbers in sequence (1, 10, 11, 100, 101...). The algorithm is elegant — enqueue "1", then repeatedly dequeue, print, and enqueue that number + "0" and that number + "1".

from collections import deque

def generate_binary_numbers(n):
    """
    Generates the first n binary numbers as strings.
    Uses the classic queue-based approach.
    """
    if n <= 0:
        return []

    result = []
    queue = deque(["1"])  # Start with "1"

    for _ in range(n):
        current = queue.popleft()
        result.append(current)

        # Append current + "0" and current + "1" to the queue
        queue.append(current + "0")
        queue.append(current + "1")

    return result

# Generate first 10 binary numbers
print(generate_binary_numbers(10))
# Output: ['1', '10', '11', '100', '101', '110', '111', '1000', '1001', '1010']

This demonstrates that queues aren't just for storing things — they can generate sequences in a controlled order. The pattern generalizes to any problem where you need to process items in arrival order while continuously adding new derived items.

Problem 5: First Unique Character in a Stream

You're receiving a stream of characters and must be able to query the first non-repeating character at any time. A queue combined with a frequency map solves this elegantly.

from collections import deque

class FirstUnique:
    def __init__(self):
        self.queue = deque()          # Maintains candidates in insertion order
        self.frequency = {}           # Tracks count of each character seen

    def add_character(self, char):
        self.frequency[char] = self.frequency.get(char, 0) + 1
        self.queue.append(char)

        # Remove from front any characters that are now repeating
        while self.queue and self.frequency[self.queue[0]] > 1:
            self.queue.popleft()

    def first_unique(self):
        if not self.queue:
            return None  # Or '#', depending on problem spec
        return self.queue[0]

# Example usage
fu = FirstUnique()
stream = ['a', 'b', 'c', 'a', 'c', 'b', 'd']
for char in stream:
    fu.add_character(char)
    print(f"After adding '{char}', first unique: {fu.first_unique()}")
# Output:
# After adding 'a', first unique: a
# After adding 'b', first unique: a
# After adding 'c', first unique: a
# After adding 'a', first unique: b   (a repeats, deque pops 'a')
# After adding 'c', first unique: b   (c repeats)
# After adding 'b', first unique: d   (b repeats, deque pops 'b', then 'c')
# After adding 'd', first unique: d

The lazy deletion strategy — removing non-unique elements from the front only when we need to check — ensures that each element enters and leaves the deque at most once, giving O(1) amortized per operation.

Time Complexity Cheat Sheet

Here's a quick reference for queue operations across different implementations. Memorize this for your interview:

┌─────────────────────┬──────────────┬──────────────┬───────────┬───────────┐
│ Implementation      │ Enqueue      │ Dequeue      │ Peek      │ Search    │
├─────────────────────┼──────────────┼──────────────┼───────────┼───────────┤
│ List (pop(0))       │ O(1)*        │ O(n)         │ O(1)      │ O(n)      │
│ collections.deque   │ O(1)         │ O(1)         │ O(1)      │ O(n)      │
│ Circular Queue      │ O(1)         │ O(1)         │ O(1)      │ O(n)      │
│ PriorityQueue(heap) │ O(log n)     │ O(log n)     │ O(1)      │ O(n)      │
│ Two Stacks          │ O(1)         │ Amortized O(1)│ Amort. O(1)│ O(n)   │
│ LinkedList (custom) │ O(1)         │ O(1)         │ O(1)      │ O(n)      │
└─────────────────────┴──────────────┴──────────────┴───────────┴───────────┘

*List append is amortized O(1), but occasional resizing makes it O(n) in the worst case for a single append.

Best Practices for Queue Interview Problems

1. Always Clarify the Queue Type

Before writing code, ask: "Is this a standard FIFO queue, a deque, a priority queue, or a circular queue?" The problem description often hints at this — "sliding window" screams deque, "scheduling" suggests priority queue, "fixed buffer" points to circular queue.

2. Choose the Right Data Structure

3. Handle Edge Cases Explicitly

Every queue problem should handle:

4. Talk About Amortized Complexity

When using the two-stack approach or lazy deletion in a deque, explicitly mention amortized analysis. Interviewers light up when you say "each element is moved at most once, so the amortized cost is O(1)." It shows depth of understanding beyond just Big-O memorization.

5. Draw Before You Code

For circular queues and two-stack implementations, sketch the state transitions. Drawing [front=2, rear=0, count=3] on a whiteboard prevents off-by-one errors that plague circular buffer code. For deque-based sliding windows, draw the deque contents at each step.

6. Consider Thread Safety Only If Asked

Most algorithm interviews assume single-threaded execution. Don't introduce locks unless the problem explicitly involves concurrency (e.g., "design a thread-safe bounded buffer"). In Python, queue.Queue is thread-safe; collections.deque is not.

7. Test with a Dry Run

After writing your solution, walk through it with a small concrete example out loud. This catches logic errors and demonstrates your debugging methodology. For the circular queue, test the wrap-around case specifically.

Advanced Pattern: Monotonic Queue

The monotonic queue pattern (used in the sliding window maximum) deserves special attention because it appears in many hard LeetCode problems. The core idea: maintain a deque where elements are in strictly increasing or decreasing order, popping from the back any element that violates the monotonic property. This gives you O(1) access to the extreme value at the front while processing a stream.

from collections import deque

def monotonic_increasing_example(nums):
    """
    Demonstrates building a monotonic increasing deque.
    At each step, the front holds the smallest valid element.
    """
    dq = deque()
    for num in nums:
        # Remove from back all elements GREATER than current
        # to maintain increasing order
        while dq and dq[-1] > num:
            dq.pop()
        dq.append(num)
        print(f"After {num}: deque = {list(dq)}")

nums = [5, 2, 8, 1, 9, 3]
monotonic_increasing_example(nums)
# Output:
# After 5: deque = [5]
# After 2: deque = [2]        (5 popped, 2 is smaller)
# After 8: deque = [2, 8]     (8 is larger, keep at back)
# After 1: deque = [1]        (2 and 8 popped, 1 is smallest)
# After 9: deque = [1, 9]     (9 is larger, keep)
# After 3: deque = [1, 3]     (9 popped, 3 is smaller)

The monotonic queue pattern is your secret weapon for problems involving "next greater element," "daily temperatures," "sliding window maximum/minimum," and "shortest subarray with sum at least K."

Putting It All Together: A Mock Interview Solution

Let's solve a realistic interview problem that combines queues with real-world constraints: Design a Hit Counter that counts hits in the past 5 minutes (300 seconds). This is LeetCode 362 and tests your ability to choose the right queue variant.

from collections import deque

class HitCounter:
    """
    Records hits (timestamps) and returns the number of hits
    in the past 300 seconds.
    Uses a deque to store timestamps in insertion order.
    Lazy cleanup removes expired hits on each query.
    """
    def __init__(self):
        self.hits = deque()

    def hit(self, timestamp: int):
        """Record a hit at the given timestamp (in seconds)."""
        self.hits.append(timestamp)

    def get_hits(self, timestamp: int) -> int:
        """
        Return the number of hits in the past 300 seconds,
        i.e., hits with timestamp > timestamp - 300.
        """
        # Remove all hits that are too old
        threshold = timestamp - 300
        while self.hits and self.hits[0] <= threshold:
            self.hits.popleft()

        return len(self.hits)

# Example usage
hc = HitCounter()
hc.hit(1)
hc.hit(2)
hc.hit(3)
print(hc.get_hits(4))    # 3 (all hits at 1, 2, 3 are within 300s)
hc.hit(300)
print(hc.get_hits(300))  # 4 (hits at 1, 2, 3, 300)
print(hc.get_hits(301))  # 3 (hit at 1 expires: 301-300=1, so timestamp 1 is removed)
hc.hit(302)
print(hc.get_hits(302))  # 4 (hits at 2, 3, 300, 302)
print(hc.get_hits(600))  # 1 (only hit at 302 remains: 600-300=300)

Why this works: Timestamps arrive in monotonically increasing order (a guarantee stated in the problem). The deque stores them in insertion order. On each get_hits call, we lazily evict expired timestamps from the front. Each timestamp is added once and removed at most once, giving O(1) amortized per operation. The space complexity is O(M) where M is the maximum number of hits in a 300-second window.

Conclusion

Queues are far more than a simple FIFO tube — they are a fundamental abstraction that models sequential processing, buffering, and ordered workflows. In interviews, your ability to recognize when a queue is the right tool, choose the appropriate variant (FIFO, deque, circular, or priority), and implement it with clean edge-case handling will set you apart from candidates who reach for arrays out of habit. Master the patterns in this guide: the two-stack queue for amortized analysis, the monotonic deque for sliding windows, the heap-based priority queue for ordered retrieval, and the circular buffer for fixed-capacity scenarios. Practice writing each from scratch, drawing the pointer movements, and articulating the time complexity with confidence. With these skills, queue problems will become one of your strongest assets in the interview room.

🚀 Need a reliable AI agent for your project?

Deploy Hermes Agent in 10 minutes. Managed hosting, zero DevOps.

Get Started — $23.99/mo
← Back to all articles