03 — Stacks, Queues, Deques & Bounded Ring Buffers

At a glance

After completing this lesson you can:

Time: ~50 minutes · Prerequisites: 02 — Hash Tables & Sets

Reading Companion

Why this matters in production

When an upstream source fires events faster than your database or workers can process them, an unbounded queue (list or asyncio.Queue()) will quietly consume RAM until the host crashes from an OOM kill. A bounded ring buffer or bounded queue establishes backpressure: it rejects excess requests, sheds non-critical load, or pauses upstream producers safely.

Deep dive

1. Stack (LIFO) vs Queue (FIFO)

2. The Circular Ring Buffer

A ring buffer uses a fixed-size array and two pointer indices: head (write position) and tail (read position), wrapping around using modulo arithmetic: next_pos = (pos + 1) % capacity.

Circular Buffer (Capacity 4):
[ A, B, C, D ]
  ^        ^
 tail     head   (full)
After 1 read: tail moves to index 1.

Hands on

python3 phase1-cs-and-architecture/01-data-structures/03-stacks-queues-and-deques/examples/01_demo.py
python3 phase1-cs-and-architecture/01-data-structures/03-stacks-queues-and-deques/examples/02_trap.py

AI Trap & Code Review

# BAD AI-GENERATED CODE — O(N) QUEUE VIA LIST.POP(0)
class MessageQueue:
    def __init__(self):
        self.queue = []
    def enqueue(self, msg):
        self.queue.append(msg)
    def dequeue(self):
        return self.queue.pop(0)  # O(n) array shift on EVERY message!

Exercises

  1. Rung 1 (validate_balanced_delimiters): Use a stack to check matching brackets/tags ((), [], {}) in a code string.
  2. Rung 2 (drain_queue_batch — Fix the AI Bug): Fix an $O(n^2)$ queue drain that uses list.pop(0) by refactoring to collections.deque.
  3. Rung 3 (CircularRingBuffer): Implement a circular ring buffer with fixed capacity using an internal list and modulo pointers.
  4. Rung 4 (MonotonicMaxQueue): Build a queue supporting push, pop, and get_max() in $O(1)$ amortized time using a monotonic deque.
  5. Rung 5 (BackpressureChannel): Build a bounded producer-consumer channel supporting drop-oldest and drop-newest overflow policies.

Wrapping up

Can you explain?

  1. Why is collections.deque.popleft() $O(1)$ while list.pop(0) is $O(n)$?
  2. What is a monotonic stack and why is it useful for sliding window maximums?
  3. How does modulo arithmetic prevent memory allocation in a ring buffer?

Further Reading & References

Show reference solution notes

Solution & Engineering Notes — 03 Stacks, Queues & Deques

  1. Circular buffer pointer updates: Using (head + 1) % capacity wraps pointers back to 0 when reaching the boundary, eliminating memory reallocations.
  2. Monotonic queue invariant: Maintaining a decreasing deque of candidate maximums guarantees that the current maximum element is always at index 0.

Self-check

Run python3 -m unittest phase1-cs-and-architecture/01-data-structures/03-stacks-queues-and-deques/test_exercises.py -v until green, then record it here. Saved to progress/rungs.json; commit it.

Confidence:

Completion edits PROGRESS.md; commit it with the lesson.