Stacks, Queues, and Deques: Sequential Access Data Structures

Master LIFO, FIFO, and double-ended queue operations with implementations, time complexity analysis, and practical applications.

published: reading time: 26 min read author: GeekWorkBench

Stacks, Queues, and Deques: Sequential Access Data Structures

Stacks, queues, and deques enforce specific access patterns rather than allowing arbitrary inserts and removes. This constraint is a feature—by limiting what you can do, they make certain operations efficient and predictable. A stack follows Last-In-First-Out (LIFO), a queue follows First-In-First-Out (FIFO), and a deque allows insertion and removal from both ends.

Introduction

Stacks, queues, and deques are restricted-access data structures that enforce specific orderings on insertions and removals. A stack follows Last-In-First-Out (LIFO)—you can only access the most recently added element. A queue follows First-In-First-Out (FIFO)—you can only access the oldest element. A deque (double-ended queue) allows insertion and removal from both ends, combining capabilities of both structures.

These data structures are everywhere in software. Stacks power undo/redo systems, expression evaluation, and depth-first search. Queues handle task scheduling, breadth-first search, and message passing between services. Deques excel at sliding window problems and implementing efficient ring buffers. Understanding their implementation details matters because subtle choices—like whether to implement a stack with an array or linked list—affect cache performance and memory usage in production systems.

You’ll implement each structure from scratch, analyze time complexity of all operations, and see how Python’s collections.deque achieves O(1) performance at both ends through chunk-based memory management. We’ll cover balanced parentheses checking, shortest path finding, and the monotonic deque pattern for sliding window maximum problems.

Stack (LIFO - Last In First Out)

class Stack:
    """Array-based stack with O(1) push, pop, peek."""

    def __init__(self, capacity=10):
        self.capacity = capacity
        self.data = [None] * capacity
        self.size = 0

    def push(self, val):
        """O(1) push - amortized if resizing."""
        if self.size == self.capacity:
            self._resize()
        self.data[self.size] = val
        self.size += 1

    def pop(self):
        """O(1) pop."""
        if self.size == 0:
            raise IndexError("Stack underflow")
        val = self.data[self.size - 1]
        self.size -= 1
        return val

    def peek(self):
        """O(1) view top element."""
        if self.size == 0:
            raise IndexError("Stack is empty")
        return self.data[self.size - 1]

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

    def _resize(self):
        new_data = [None] * (self.capacity * 2)
        for i in range(self.size):
            new_data[i] = self.data[i]
        self.data = new_data
        self.capacity *= 2


class StackLinkedList:
    """Linked list based stack - O(1) all operations, no resize needed."""

    def __init__(self):
        self.top = None
        self.size = 0

    def push(self, val):
        self.top = ListNode(val, self.top)
        self.size += 1

    def pop(self):
        if not self.top:
            raise IndexError("Stack underflow")
        val = self.top.val
        self.top = self.top.next
        self.size -= 1
        return val

    def peek(self):
        if not self.top:
            raise IndexError("Stack is empty")
        return self.top.val

Queue (FIFO - First In First Out)

class Queue:
    """Array-based circular queue with O(1) enqueue and dequeue."""

    def __init__(self, capacity=10):
        self.capacity = capacity
        self.data = [None] * capacity
        self.front = 0
        self.rear = -1
        self.size = 0

    def enqueue(self, val):
        """O(1) add to rear."""
        if self.size == self.capacity:
            self._resize()
        self.rear = (self.rear + 1) % self.capacity
        self.data[self.rear] = val
        self.size += 1

    def dequeue(self):
        """O(1) remove from front."""
        if self.size == 0:
            raise IndexError("Queue underflow")
        val = self.data[self.front]
        self.front = (self.front + 1) % self.capacity
        self.size -= 1
        return val

    def peek(self):
        """O(1) view front element."""
        if self.size == 0:
            raise IndexError("Queue is empty")
        return self.data[self.front]

    def _resize(self):
        new_data = [None] * (self.capacity * 2)
        for i in range(self.size):
            new_data[i] = self.data[(self.front + i) % self.capacity]
        self.data = new_data
        self.front = 0
        self.rear = self.size - 1
        self.capacity *= 2


from collections import deque as PythonDeque

# For most use cases, Python's deque is optimal:
# O(1) append, appendleft, pop, popleft
queue = PythonDeque()
queue.append(1)      # enqueue
queue.append(2)      # enqueue
queue.popleft()      # dequeue -> returns 1

Deque (Double-Ended Queue)

class Deque:
    """Deque supporting O(1) insert/remove at both ends."""

    def __init__(self):
        self.front = []
        self.rear = []

    def push_front(self, val):
        """O(1) insert at front."""
        self.front.append(val)

    def push_back(self, val):
        """O(1) insert at rear."""
        self.rear.append(val)

    def pop_front(self):
        """O(1) remove from front."""
        if not self.front:
            if not self.rear:
                raise IndexError("Deque empty")
            # Move last element of rear to front
            self.rear.reverse()
            self.front = self.rear
            self.rear = []
        return self.front.pop()

    def pop_back(self):
        """O(1) remove from rear."""
        if not self.rear:
            if not self.front:
                raise IndexError("Deque empty")
            self.front.reverse()
            self.rear = self.front
            self.front = []
        return self.rear.pop()

    def peek_front(self):
        if self.front:
            return self.front[-1]
        if self.rear:
            return self.rear[0]
        raise IndexError("Deque empty")

    def peek_back(self):
        if self.rear:
            return self.rear[-1]
        if self.front:
            return self.front[0]
        raise IndexError("Deque empty")


# Sliding Window Maximum using deque
from collections import deque

def max_sliding_window(arr, k):
    """
    Find maximum in each sliding window of size k.
    O(n) time using monotonic deque.
    """
    if not arr or k == 0:
        return []

    result = []
    dq = deque()  # Stores indices

    for i in range(len(arr)):
        # Remove indices outside current window
        while dq and dq[0] < i - k + 1:
            dq.popleft()

        # Remove indices whose values are smaller than current
        # (they'll never be the maximum in this window)
        while dq and arr[dq[-1]] <= arr[i]:
            dq.pop()

        dq.append(i)

        # Start recording results once we have a full window
        if i >= k - 1:
            result.append(arr[dq[0]])

    return result

Monotonic Deque Behavior

graph LR
    subgraph Window1["Window: [1, 3, 2]"]
        direction LR
        W1A["1"] --> W1B["3"] --> W1C["2"]
    end

    subgraph Deque1["Deque (decreasing)"]
        direction LR
        D1A["3"] --> D1B["2"]
    end

    Window1 --> Deque1
    Deque1 --> Max1["max = 3"]

For sliding window [1, 3, 2] with k=3: we add 1 (deque: [1]), then 3 (remove 1, deque: [3]), then 2 (deque: [3, 2]). The front always holds the maximum. When 3 exits the window, 2 becomes the new maximum.

Stack Applications

ProblemStack Usage
Undo/RedoPush operations, pop to undo
Expression evaluationParentheses matching, postfix conversion
Function call stackRecursion, call hierarchy
DFS traversalExplicit stack for iterative depth-first
Nearest greater elementMaintain decreasing stack while traversing
# Balanced parentheses check
def is_balanced(s):
    stack = []
    matching = {')': '(', ']': '[', '}': '{'}

    for char in s:
        if char in '([{':
            stack.append(char)
        elif char in ')]}':
            if not stack or stack[-1] != matching[char]:
                return False
            stack.pop()

    return len(stack) == 0


# Nearest greater element to the right
def nearest_greater_right(arr):
    result = [-1] * len(arr)
    stack = []  # Stack of indices with decreasing values

    for i in range(len(arr)):
        while stack and arr[stack[-1]] < arr[i]:
            result[stack.pop()] = arr[i]
        stack.append(i)

    return result

Queue Applications

ProblemQueue Usage
BFS traversalFIFO order for level-order processing
Task schedulingRound-robin, print queue
BufferingNetwork packets, I/O operations
Message queuesProducer-consumer patterns
# BFS shortest path
from collections import deque

def bfs_shortest_path(graph, start, end):
    if start == end:
        return [start]

    queue = deque([(start, [start])])
    visited = {start}

    while queue:
        node, path = queue.popleft()

        for neighbor in graph[node]:
            if neighbor == end:
                return path + [neighbor]
            if neighbor not in visited:
                visited.add(neighbor)
                queue.append((neighbor, path + [neighbor]))

    return None  # No path exists

When to Use Each Structure

StructureAccess PatternUse When
StackLIFOUndo, recursion, DFS, matching problems
QueueFIFOBFS, scheduling, breadth-first traversal
DequeBoth endsSliding window max, LRU cache, double-ended operations

Production Failure Scenarios and Mitigations

Stack Overflow from Unbounded Recursion

The call stack has a fixed size. Deep recursion on large inputs causes stack overflow. A recursive binary tree traversal on a tree with depth > 10,000 crashes the process.

Mitigation: Convert recursive algorithms to iterative using explicit stacks. Set a recursion depth guard that throws before overflowing. Profile stack usage under max expected input size.

Queue Overflow Under Load

Bounded queues reject new items when full. Under traffic spikes, producers block or drop messages. A print queue accepting jobs from 1,000 concurrent users with a 100-job capacity loses work.

Mitigation: Choose bounded vs unbounded based on requirements. Use dead-letter queues for rejected messages. Monitor queue utilization and scale consumers horizontally. Set alerts at 80% capacity.

Deadlock from Lock Ordering in Concurrent Queues

Multiple queues with shared resources create circular dependencies. Thread A holds Queue 1 and waits for Queue 2; Thread B holds Queue 2 and waits for Queue 1.

Mitigation: Enforce consistent lock ordering across all threads (always acquire Queue 1 before Queue 2). Use lock-free data structures when possible. Add deadlock detection in test environments.

Priority Inversion in Priority Queues

Low-priority tasks hold resources needed by high-priority tasks. A background sync task (low priority) holds a lock; a user-facing request (high priority) waits indefinitely.

Mitigation: Implement priority inheritance — temporarily elevate the low-priority task’s priority when it holds a contended lock. Use separate queues per priority level with weighted scheduling.

Trade-Off Table

PropertyStackQueueDeque
Push/InsertO(1)O(1)O(1) at both ends
Pop/RemoveO(1)O(1)O(1) at both ends
PeekO(1) front onlyO(1) front onlyO(1) at both ends
SearchO(n)O(n)O(n)
Memory LayoutContiguous (array) or heap (linked list)Contiguous via circular bufferUsually linked chunks or two arrays
Thread SafetySingle-threaded unless synchronizedSingle-threaded unless synchronizedSingle-threaded unless synchronized
Best Use CaseUndo history, DFS, recursionBFS, task scheduling, message passingSliding window, LRU cache, double-ended operations
Cache PerformanceExcellent (contiguous)Good (circular buffer)Moderate (often linked)
Overflow RiskResize doubles memoryResize or rejectResize or reject

Observability Checklist

  • Monitor queue depth in real-time; alert at 80% of capacity
  • Track overflow/rejection counts per queue
  • Measure enqueue and dequeue latency — spikes indicate backpressure
  • Log stack trace on stack overflow exceptions with input size context
  • Monitor consumer lag: time between enqueue and dequeue
  • Track resize events — frequent resizes suggest capacity misprediction
  • Alert on dequeue rate dropping to zero (consumer stall)
  • Log dead-letter queue item counts for bounded queues
  • Trace queue operations across service boundaries using correlation IDs
  • Set up dashboards for depth, latency p50/p95/p99, and overflow rate

Security Notes

Stack Buffer Overflow Propagation

Malicious input oversized for a fixed stack buffer corrupts return addresses. Attackers inject shellcode into stack-allocated buffers. Production C/C++ services with manually managed buffers face this risk.

Mitigation: Use memory-safe languages or sanitizers (ASan, MSan). Enable stack canaries. Validate input length before write. Prefer heap allocation with bounds checking.

Resource Exhaustion via Unbounded Queues

An attacker floods a service with messages that accumulate in an unbounded queue. Memory grows until the process or container OOMs.

Mitigation: Use bounded queues with explicit overflow handling (reject, dead-letter, or block with timeout). Authenticate producers. Rate-limit ingress. Monitor memory per queue.

Denial of Service via Queue Flooding

High-volume producers overwhelm consumers. A single misconfigured client sends 10,000 messages/second; the consumer processes 100/second. Backpressure builds until service degrades for all users.

Mitigation: Enforce per-client rate limits at the queue ingress point. Use priority queues to ensure critical traffic gets through. Set consumer scaling policies that react to queue depth.

Race Conditions in Lock-Free Queue Implementations

Concurrent producers and consumers on a shared queue without proper synchronization cause torn reads/writes, item duplication, or use-after-free bugs. Lock-free does not mean wait-free.

Mitigation: Prefer battle-tested queue implementations (Java ConcurrentLinkedQueue, Go channels, Python queue module). Review memory ordering semantics. Stress-test with tools like ThreadSanitizer.

Quick Recap Checklist

  • Stack: push/pop O(1), use for LIFO, DFS, undo operations
  • Queue: enqueue/dequeue O(1), use for BFS, FIFO scheduling
  • Deque: O(1) at both ends, use for sliding window problems
  • Monotonic deque maintains decreasing/increasing values for efficiency
  • Python’s collections.deque is optimized for both stack and queue operations

Deque Memory Model: Chunk-Based Implementation

While the two-array deque in the implementation section demonstrates the concept, production-grade deques (like Python’s collections.deque) use a chunk-based design for predictable O(1) performance at both ends.

# Conceptual model of a chunk-based deque
# Each block holds up to BLOCK_SIZE elements
BLOCK_SIZE = 64  # Typical value

class ChunkDeque:
    """Simplified conceptual model of Python's collections.deque."""

    def __init__(self):
        self._blocks = []       # List of fixed-size arrays
        self._left_block = 0    # Index of first block
        self._left_pos = 0      # Position within first block
        self._right_block = 0
        self._right_pos = -1
        self._size = 0

    def append(self, val):
        """Add to right end."""
        if self._right_pos == BLOCK_SIZE - 1:
            # Need a new block
            self._blocks.append([None] * BLOCK_SIZE)
            self._right_block += 1
            self._right_pos = 0
        else:
            self._right_pos += 1
        self._blocks[self._right_block][self._right_pos] = val
        self._size += 1

    def appendleft(self, val):
        """Add to left end."""
        if self._left_pos == 0:
            # Insert a block at the front
            self._blocks.insert(0, [None] * BLOCK_SIZE)
            self._right_block += 1  # Shift right index
            self._left_pos = 0
        else:
            self._left_pos -= 1
        self._blocks[self._left_block][self._left_pos] = val
        self._size += 1

    def pop(self):
        """Remove from right end."""
        val = self._blocks[self._right_block][self._right_pos]
        self._blocks[self._right_block][self._right_pos] = None
        self._size -= 1
        if self._right_pos == 0 and self._size > 0:
            # Block is empty, free it
            self._blocks.pop()
            self._right_block -= 1
            self._right_pos = BLOCK_SIZE - 1
        else:
            self._right_pos -= 1
        return val

    def popleft(self):
        """Remove from left end."""
        val = self._blocks[self._left_block][self._left_pos]
        self._blocks[self._left_block][self._left_pos] = None
        self._size -= 1
        self._left_pos += 1
        if self._left_pos == BLOCK_SIZE and self._size > 0:
            self._blocks.pop(0)
            self._right_block -= 1
            self._left_pos = 0
        return val

Key advantages of the chunk-based approach:

PropertyBenefit
No full-structure resizingUnlike dynamic arrays, never need to copy all elements
Bounded per-operation costEach append/pop touches at most one block
Cache-friendlyElements within a block are contiguous
Memory efficientEmpty blocks are freed immediately
Predictable growthBlock allocation is O(1) amortized

This design is the reason Python’s collections.deque is the recommended choice for both stack and queue use cases in production Python code.

Interview Questions

1. How would you implement a queue using two stacks?

Use enqueue stack and dequeue stack. On enqueue, always push to enqueue stack. On dequeue, if dequeue stack is empty, transfer all elements from enqueue stack (pop from enqueue, push to dequeue), then pop from dequeue stack. This achieves amortized O(1) for both operations. The transfer reverses the order, converting FIFO order (first in enqueue → first out dequeue). This is how some message queue implementations work internally.

2. What is a monotonic deque and why is it useful?

A monotonic deque maintains elements in sorted order (increasing or decreasing). For sliding window maximum, we maintain a decreasing deque of indices: the front always has the maximum, and smaller elements are removed from the back. When a new element arrives, we remove all smaller elements (they'll never be the maximum while the new element is in window). When the window moves, we discard indices outside the window from the front. This achieves O(n) for the entire sliding window problem.

3. How do stacks and queues relate to recursion and iteration?

Recursion implicitly uses a stack (the call stack). Each recursive call pushes a frame onto the stack; returning pops frames. This is why deep recursion can cause stack overflow. Iteration can use an explicit stack to convert recursive algorithms to iterative. Conversely, BFS uses a queue for level-order traversal—recursion naturally handles depth-first patterns, while queues handle breadth-first patterns. Understanding this helps you convert between recursive and iterative solutions.

4. How would you design a stack that supports getMin() in O(1)?

Use a second stack that tracks the minimum. When pushing, if the new value is ≤ current minimum, push it to the min-stack too. When popping, if the popped value equals the current minimum, pop from min-stack as well. This gives O(1) getMin() while maintaining O(1) push and pop. Space trade-off is O(n) worst case if all pushed elements are decreasing. Alternatively, store (value, min_at_that_point) pairs in a single stack—though this doubles memory per element.

5. What is the difference between an array-based stack and a linked-list-based stack?

Array-based stack: Contiguous memory provides excellent cache locality and predictable access times. Push/pop are amortized O(1) but occasional resizing (O(n) copy) can cause latency spikes. Memory overhead is low (just the array).

Linked-list-based stack: No resizing overhead; every operation is O(1). However, per-node heap allocation and pointer overhead (~16–24 bytes per element) degrades cache performance. Better for unpredictable workloads where resizing cost is unacceptable.

Trade-off: Use array-based when you can bound the size or need throughput; use linked-list when memory fragmentation or predictable latency is critical.

6. How would you implement a stack using two queues?

Use one primary queue and one auxiliary queue. For push: enqueue the new element to the auxiliary queue, then dequeue all elements from the primary queue and enqueue them to the auxiliary queue. Swap the queues. This makes the newest element always at the front of the primary queue. For pop: simply dequeue from the primary queue.

This gives O(n) push and O(1) pop. Alternatively, make push O(1) and pop O(n) by keeping elements in insertion order and rotating for pop. Choose based on which operation needs to be faster.

7. Explain the sliding window maximum algorithm using a monotonic deque.

Maintain a decreasing deque of indices (not values). For each new element in the array:

  • Remove indices outside the current window from the front (popleft).
  • Remove indices whose values are ≤ the new value from the back (pop) — they can never be the maximum in this or future windows.
  • Append the new index to the back.
  • The front index always points to the maximum of the current window.

Each element is pushed and popped at most once, yielding O(n) total time and O(k) space.

8. How do you implement a circular queue and why is it efficient?

A circular queue (ring buffer) uses a fixed-size array with two pointers: front and rear. Pointers wrap around using modulo arithmetic: rear = (rear + 1) % capacity. This reuses array slots without shifting elements, giving O(1) enqueue and dequeue.

Efficiency: No element shifting (unlike a naive array queue), single contiguous allocation for excellent cache locality, and no per-node allocation overhead. Used extensively in operating systems (I/O ring buffers), networking (packet queues), and Python's collections.deque (chunk-based variant).

9. What are the trade-offs between bounded and unbounded queues in production systems?

Bounded queues have a fixed capacity. They provide backpressure, prevent memory exhaustion, and enable predictable resource usage. The downside: producers must handle rejection (retry, drop, dead-letter) when full, which adds complexity to the producer logic.

Unbounded queues can grow indefinitely. They simplify producer logic (never reject), but risk OOM under traffic spikes. Consumers can fall behind indefinitely, leading to stale data and unbounded memory growth.

Recommendation: Prefer bounded queues with monitoring at 80% capacity. Use unbounded queues only when you can guarantee bounded input rates and have consumer auto-scaling in place.

10. How does Python's collections.deque work internally?

Python's collections.deque uses a doubly-linked list of fixed-size blocks (chunks), typically holding 64 elements each. This design achieves:

  • O(1) append/pop at both ends — no shifting or resizing of the entire structure.
  • Good cache locality within each block (contiguous memory).
  • Memory efficiency — blocks are allocated on demand and freed when empty.
  • Predictable performance — no amortized costs from occasional full resizes (unlike a dynamic array).

The block size is a power of two, enabling fast index calculations via bit masking rather than modulo.

11. What is the nearest greater element problem and how does a stack solve it?

Given an array, find the nearest greater element to the right (NGR) for each position — the first larger element appearing after it.

Stack-based solution: Iterate left to right, maintaining a monotonic decreasing stack of indices. For each element, while the stack is not empty and arr[stack.top] < arr[i], pop the top index and set its NGR to arr[i]. Push the current index. Each element is pushed and popped at most once, giving O(n) time and O(n) space.

The same technique extends to nearest greater to the left, nearest smaller to the right, and nearest smaller to the left by changing traversal direction and comparison operator.

12. How do you implement undo/redo functionality using stacks?

Use two stacks: an undo stack and a redo stack.

  • Perform action: push the action (or a reverse action) onto the undo stack. Clear the redo stack (redo history is invalidated by new actions).
  • Undo: pop from the undo stack, reverse the action, and push the forward action onto the redo stack.
  • Redo: pop from the redo stack, re-apply the action, and push the reverse action onto the undo stack.

This pattern appears in text editors, image editors, and command-line tools. Some implementations cap the undo stack to bound memory usage (e.g., max 1000 undo levels).

13. How would you detect if a queue is empty or full in a circular queue implementation?

In a circular queue with a fixed-size array, use the size field or track the difference between front and rear pointers. Three common approaches:

  • Size counter: Maintain a size variable. If size == 0, the queue is empty; if size == capacity, it's full. This is the clearest approach.
  • Gaps between pointers: Leave one slot empty to distinguish empty from full. If (rear + 1) % capacity == front, it's full; if front == rear, it's empty.
  • Flag variable: Use a is_full boolean flag in addition to pointer comparison.

Python's collections.deque handles this internally and always maintains O(1) operations with no capacity limit by default (configurable via maxlen).

14. What is the difference between a deque and a double-ended priority queue?

A deque (double-ended queue) allows O(1) insertion and removal at both ends, but the ordering within the deque depends on where you insert. Elements are ordered by insertion sequence.

A double-ended priority queue allows O(1) removal of the minimum OR maximum (but not both) from either end, while elements are ordered by priority (heap property). The internal structure is typically a min-max heap or two heaps.

  • Deque: Use when you need fast access to both ends with insertion order preserved.
  • Double-ended priority queue: Use when elements have priorities and you need to extract extreme values from either end (e.g., scheduling with both high and low priority tasks).
15. How would you implement a stack that supports findMin() to return the minimum element in O(1)?

Use a monotonic auxiliary stack that tracks minimums. The auxiliary stack stores pairs of (value, current_min) or uses a separate stack that mirrors operations.

  • On push(x): If the auxiliary stack is empty or x <= current_min, push x onto the auxiliary stack. Then push x onto the main stack.
  • On pop(): Pop from the main stack. If the popped value equals the value at the top of the auxiliary stack, pop from the auxiliary stack too.
  • On findMin(): Return the top of the auxiliary stack in O(1).

Space trade-off: O(n) worst case when all pushed elements are in decreasing order. This is optimal — any structure that supports O(1) findMin must use at least O(n) extra space in the worst case.

16. What is the "stack height" problem in recursive algorithms and how do you solve it?

The stack height problem occurs when recursive algorithms create call stacks deeper than available memory. For example, recursively processing a linked list with 100,000 nodes on a stack with 1MB limit can overflow.

Solutions:

  • Convert to iteration: Replace the call stack with an explicit stack data structure. This gives you control over memory allocation and size.
  • Tail recursion optimization: If the recursive call is the last operation, the compiler may reuse the current stack frame. Not guaranteed in all languages (Python doesn't optimize tail recursion).
  • Increase stack size: In C/C++, use ulimit -s or pthread_attr_setstacksize(). In Java, set -Xss size. This is a workaround, not a solution.
  • Recursion with checkpoints: Process in chunks, saving state to disk or heap, then resume.

Best practice: Default to iterative solutions when depth is unbounded or unknown.

17. Explain how you would use a queue to implement breadth-first search in a graph. What data does the queue hold at each step?

In BFS, the queue holds nodes to explore, paired with their path from the source (or just the parent pointer for path reconstruction).

Algorithm:

  • Initialize a queue with (start_node, [start_node]).
  • Mark start_node as visited.
  • While the queue is not empty: dequeue a node. For each unvisited neighbor, mark it visited and enqueue (neighbor, path + [neighbor]).
  • When the target node is dequeued, return its path.

The queue processes nodes in FIFO order, which naturally explores all nodes at distance k before nodes at distance k+1. This guarantees the shortest path in unweighted graphs.

Memory: The queue holds all nodes at the current frontier plus one level ahead — never the entire graph. Space complexity is O(width) not O(depth).

18. What are the advantages and disadvantages of using a linked list versus an array to implement a queue?

Linked list implementation:

  • Advantages: No capacity limit; O(1) enqueue and dequeue with no occasional O(n) resize; works well for unpredictable queue sizes.
  • Disadvantages: Per-node heap allocation overhead (slower, fragmentation); each node stores a pointer (extra memory); poor cache locality.

Array implementation (circular buffer):

  • Advantages: Contiguous memory → excellent cache performance; O(1) operations with no allocation overhead; simpler implementation.
  • Disadvantages: Fixed capacity (or costly dynamic resizing); full queue requires rejection or blocking; front and rear pointer management.

Recommendation: Use Python's collections.deque (chunked linked list) for production — it combines the benefits without the downsides. Use array-based circular queue in embedded systems where memory is fixed and predictable.

19. How does a priority queue differ from a regular queue, and what are common implementations?

A regular queue follows FIFO: first inserted is first removed. A priority queue removes elements based on priority (highest or lowest first), regardless of insertion order.

Common implementations:

  • Binary heap: O(log n) insert and extract; O(1) peek. Most common. Min-heap for smallest-first, max-heap for largest-first.
  • Balanced BST (AVL, Red-Black): O(log n) all operations; supports duplicate priorities naturally.
  • Unsorted array/linked list: O(1) insert, O(n) extract-max/min. Use when priority queue size is small or insertions vastly outnumber removals.
  • Fibonacci heap: O(1) amortized insert, O(log n) extract-min. Theoretically optimal but high constant factors make it rarely used in practice.

In Python, use heapq for a min-heap or negate priorities for max-heap behavior. Java provides PriorityQueue and PriorityBlockingQueue.

20. Describe a scenario where using a deque is more appropriate than either a stack or a queue alone.

Consider a task scheduler that supports:

  • Adding high-priority tasks to the front (preempt current task).
  • Adding normal tasks to the back.
  • Removing the current task from the front.
  • Returning to a previously blocked task (add to front).

A queue can only add to back and remove from front — it can't inject high-priority tasks at the front. A stack only supports LIFO which breaks normal task ordering. A deque handles both access patterns.

Other ideal deque scenarios:

  • Web browser history: Back button (pop front) and forward button (push front from history) with new pages appending to back.
  • Sliding window algorithms: Need to add new elements at one end and remove expired elements from the other.
  • Undo/redo with skip: Can undo (pop front), redo (pop from auxiliary front), or jump to a checkpoint (insert at specific position).

The deque's flexibility comes from not committing to a single access pattern.

Further Reading

Books

  • Introduction to Algorithms (CLRS) — Chapters 10: “Elementary Data Structures” covers stacks, queues, linked lists, and their analysis.
  • Algorithm Design Manual (Skiena) — Chapter 3: “Data Structures” includes practical applications and interview-style problems.
  • Python Cookbook (Beazley & Jones) — Chapter 1 contains idiomatic deque patterns for real-world Python.

Online Resources

Practice Problems by Topic

TopicProblemPlatform
StackValid ParenthesesLeetCode 20
StackMin StackLeetCode 155
StackEvaluate Reverse Polish NotationLeetCode 150
QueueImplement Stack Using QueuesLeetCode 225
QueueDesign Circular QueueLeetCode 622
DequeSliding Window MaximumLeetCode 239
DequeDesign Circular DequeLeetCode 641
Monotonic StackDaily TemperaturesLeetCode 739
Monotonic StackLargest Rectangle in HistogramLeetCode 84
  • Priority Queue (Heap) — For tasks where items have urgency levels and the highest-priority item should be dequeued first.
  • LRU Cache — Combines a doubly-linked list with a hash map; often implemented using a deque pattern.
  • Monotonic Queue — A deque variant that maintains sorted order; used in sliding window and range query optimizations.
  • Ring Buffer — A fixed-size circular queue used extensively in low-level systems (audio, networking, kernel I/O).

Conclusion

Category

Related Posts

Arrays vs Linked Lists: Understanding the Trade-offs

Compare arrays and linked lists in terms of access time, insertion/deletion efficiency, memory usage, and cache performance.

#arrays #linked-lists #data-structures

Arrays: 1D, 2D, and Multi-dimensional Mastery

Master array operations, traversal, search, common patterns like two-pointer and sliding window, and when to use multi-dimensional arrays.

#arrays #1d-arrays #2d-arrays

AVL Trees: Self-Balancing Binary Search Trees

Master AVL tree rotations, balance factors, and rebalancing logic. Learn when to use AVL vs Red-Black trees for your use case.

#avl-tree #self-balancing-bst #binary-search-tree