Skip to main content
๐Ÿ“šBefore you start
Make sure you're comfortable with Arrays & Strings and Linked Lists first.

Stacks & Queues: The Ultimate Reference Guide

A single, self-contained reference for interview prep, production engineering, and AI/ML system design. Covers Stacks, Queues, and the Monotonic Stack/Queue pattern โ€” from first principles to expert application.


1. Quick Reference Summary (TL;DR)โ€‹

Stack โ€” a LIFO (Last-In-First-Out) collection. The last element pushed is the first popped. Think of a stack of plates: you add and remove from the top only. All core operations (push, pop, peek) are O(1).

Queue โ€” a FIFO (First-In-First-Out) collection. The first element enqueued is the first dequeued. Think of a checkout line: first person in line is served first. All core operations (enqueue, dequeue, front) are O(1).

Monotonic Stack/Queue โ€” a stack or queue whose elements are kept in sorted (monotonic) order by discarding elements that can never again be the answer. It converts many "find the next/previous greater/smaller element" and "sliding window extremum" problems from O(nยฒ) brute force to O(n).

StructurePrinciplePush/AddPop/RemovePeekSearchTypical backing
StackLIFOO(1)O(1)O(1)O(n)Dynamic array / linked list
QueueFIFOO(1)O(1)O(1)O(n)Ring buffer / linked list
DequeBoth endsO(1)O(1)O(1)O(n)Doubly linked list / ring buffer
Priority QueuePriority orderO(log n)O(log n)O(1)O(n)Binary heap
Monotonic StackLIFO + order invariantO(1)*O(1)*O(1)O(n)Array
Monotonic DequeWindow + order invariantO(1)*O(1)*O(1)O(n)Deque
  • Amortized โ€” each element is pushed and popped at most once, so a full pass over n elements is O(n).

๐Ÿ’ก The one-sentence heuristic: Reach for a stack when the most recent thing matters most (nesting, backtracking, undo). Reach for a queue when fairness/order matters (scheduling, BFS, buffering). Reach for a monotonic variant when you're repeatedly asking "what's the next/previous bigger/smaller thing?"


2. Stacks โ€” Deep Diveโ€‹

2.1 Definition & Analogyโ€‹

A stack is an ordered collection of elements governed by the LIFO (Last-In, First-Out) principle: insertions (push) and deletions (pop) happen at a single end called the top. There is no random access โ€” you can only ever touch the top element. This constraint is not a limitation to work around; it is the entire point. It makes a stack the natural model for anything that unwinds in reverse order of how it was built.

๐Ÿฅž Stack = Stack of Pancakes โ€” You always add and remove from the top. The first pancake made is the last one eaten. To reach the bottom pancake, every pancake above it must come off first.

๐Ÿ”™ Stack = Browser Back Button โ€” Every page you visit is pushed. Hitting "Back" pops the most recent page. The order you leave pages is the exact reverse of the order you entered them.

Formal properties:

  • Access pattern: LIFO. The element removed is always the most recently added.
  • Primary access point: a single top pointer/index.
  • Invariant: after push(x) immediately followed by pop(), you get x back and the stack is unchanged.

๐Ÿ’ก Chain-of-thought (what must you understand first?): Before a stack "clicks," you need the idea of deferred work โ€” sometimes you encounter something you can't resolve yet, so you set it aside and return to it in reverse order. That reversal is precisely what LIFO gives you for free.

2.2 Operations & Complexityโ€‹

OperationDescriptionTimeSpace
push(x)Add x to the topO(1) amortizedO(1)
pop()Remove & return the top elementO(1)O(1)
peek() / top()Return top without removingO(1)O(1)
is_empty()Check if stack has no elementsO(1)O(1)
size()Number of elementsO(1)O(1)
search(x)Find element by valueO(n)O(1)

โš ๏ธ Why "amortized" on push? With an array-backed stack, most pushes are O(1), but occasionally the array is full and must be resized (typically doubled), costing O(n) to copy. Averaged over many pushes, the cost per push is still O(1) โ€” this is amortized analysis, a favorite interview follow-up.

Space: O(n) total for n elements. A stack never uses more than O(1) auxiliary space per operation.

2.3 Implementationsโ€‹

There are two canonical backings. Python's built-in list already behaves as a high-performance array-backed stack (append = push, pop = pop), so you rarely hand-roll one โ€” but you must understand both for interviews and systems work.

Array-backed stack (contiguous memory, index of top):

class ArrayStack:
"""Stack backed by a dynamic array (Python list).
Top of stack = end of the list, so all ops touch the cheap end."""

def __init__(self) -> None:
self._data: list = []

def push(self, x) -> None:
self._data.append(x) # O(1) amortized (resize occasionally)

def pop(self):
if self.is_empty():
raise IndexError("pop from empty stack")
return self._data.pop() # O(1) - removing the LAST element is cheap

def peek(self):
if self.is_empty():
raise IndexError("peek from empty stack")
return self._data[-1] # O(1) random access to the top

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

def size(self) -> int:
return len(self._data)


# --- runnable demo ---
s = ArrayStack()
for c in "ABC":
s.push(c)
print(s.pop(), s.pop(), s.peek()) # C B B

Linked-list-backed stack (nodes, head = top):

class Node:
__slots__ = ("val", "next") # __slots__ trims per-node memory overhead
def __init__(self, val, nxt=None):
self.val = val
self.next = nxt

class LinkedStack:
"""Stack backed by a singly linked list. Head of the list = top of stack.
Every push/pop touches only the head -> guaranteed O(1), no resizing."""

def __init__(self) -> None:
self._head = None
self._n = 0

def push(self, x) -> None:
self._head = Node(x, self._head) # new node points at old head
self._n += 1

def pop(self):
if self._head is None:
raise IndexError("pop from empty stack")
node = self._head
self._head = node.next # unlink the head
self._n -= 1
return node.val

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

def is_empty(self) -> bool:
return self._head is None

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


# --- runnable demo ---
ls = LinkedStack()
for n in (1, 2, 3):
ls.push(n)
print(ls.pop(), ls.size()) # 3 2

Array vs. Linked-list โ€” the trade-off:

AspectArray-backedLinked-list-backed
Push/pop worst caseO(n) on resize (O(1) amortized)O(1) always
Memory localityExcellent (contiguous, cache-friendly)Poor (pointer chasing)
Memory overheadLow (may over-allocate ~2x)High (a pointer per node)
Real-time / latency-sensitiveResize spikes can hurtPredictable - no spikes

๐Ÿ”ฅ Expert Insight: In 95% of application code, the array-backed stack (Python list, C++ std::vector, Java ArrayDeque) wins because cache locality dwarfs the theoretical resize cost. The linked-list stack shines only when you need hard worst-case O(1) guarantees (real-time systems) or when nodes are shared/persistent (immutable/functional data structures).

๐Ÿ’ก Tip โ€” don't use Java's java.util.Stack or Python's queue.LifoQueue for algorithm work. The former is a legacy synchronized class; the latter adds locking overhead. Use a plain list in Python and ArrayDeque in Java.

2.4 Variantsโ€‹

Call Stack โ€” the runtime's own stack of activation records (stack frames). Each function call pushes a frame holding parameters, locals, and the return address; returning pops it. This is why deep or infinite recursion throws StackOverflow โ€” the call stack is a bounded region of memory.

# The call stack in action: recursion IS an implicit stack.
def factorial(n: int) -> int:
if n <= 1: # base case -> deepest frame, unwinding begins
return 1
return n * factorial(n - 1) # each call pushes a frame; returns pop them

print(factorial(5)) # 120
# Frame push order: fact(5)->fact(4)->fact(3)->fact(2)->fact(1)
# Frame pop order: fact(1)->fact(2)->fact(3)->fact(4)->fact(5) (LIFO!)

Choose when: You don't choose it explicitly โ€” it's how recursion works. But recognizing it lets you convert any recursive algorithm into an iterative one using an explicit stack (see ยง5, iterative DFS) to dodge stack-overflow limits.

Min Stack / Max Stack โ€” a stack that also returns its minimum (or maximum) element in O(1). The trick: keep an auxiliary stack of running minima in lockstep with the main stack.

class MinStack:
"""Supports push, pop, top, and getMin - all in O(1).
Key idea: a parallel stack remembers the min AS OF each push."""

def __init__(self) -> None:
self._stack: list = []
self._mins: list = [] # _mins[i] = min of _stack[0..i]

def push(self, x: int) -> None:
self._stack.append(x)
# current min is x or the previous min, whichever is smaller
self._mins.append(x if not self._mins else min(x, self._mins[-1]))

def pop(self) -> int:
self._mins.pop() # keep the two stacks in lockstep
return self._stack.pop()

def top(self) -> int:
return self._stack[-1]

def getMin(self) -> int: # O(1) - just read the top of _mins
return self._mins[-1]


ms = MinStack()
for v in (5, 3, 7, 2):
ms.push(v)
print(ms.getMin()) # 2
ms.pop()
print(ms.getMin()) # 3

Choose when: You need extremum queries alongside LIFO behavior โ€” e.g. tracking the running low of a metric while supporting undo. This is LeetCode 155, one of the most-asked stack questions.

Monotonic Stack โ€” a stack whose contents are always kept increasing or decreasing. Covered in depth in ยง4, because it powers an entire family of O(n) algorithms.

๐Ÿค– AI/ML Link: The call stack maps directly onto recursive tree traversals in decision trees and beam-search backtracking. Min/Max stacks appear in streaming feature engineering โ€” maintaining a running min/max over a sliding event log with O(1) updates for real-time model features.


3. Queues โ€” Deep Diveโ€‹

3.1 Definition & Analogyโ€‹

A queue is an ordered collection governed by the FIFO (First-In, First-Out) principle: insertions (enqueue) happen at one end (the rear/tail) and deletions (dequeue) happen at the other end (the front/head). Order is preserved โ€” elements leave in exactly the order they arrived. This makes queues the model for fairness and buffering: no element jumps ahead, and producers and consumers can run at different speeds.

๐ŸŽŸ๏ธ Queue = Checkout Line โ€” The first person to join the line is the first served. New arrivals join the back; nobody cuts. Fair, ordered, predictable.

๐Ÿ” Queue = Fast-food Order Pipeline โ€” Orders are taken at the front and fulfilled in arrival order at the kitchen. A buffer decouples the fast cashier (producer) from the slower kitchen (consumer).

Formal properties:

  • Access pattern: FIFO. The element removed is always the oldest still present.
  • Two access points: front (dequeue/peek) and rear (enqueue).
  • Invariant: elements exit in the same relative order they entered.

๐Ÿ’ก Chain-of-thought (what must you understand first?): A queue makes sense once you grasp producer/consumer decoupling โ€” one party adds work, another removes it, and the queue absorbs the speed mismatch between them. That buffering role is why queues are everywhere in systems.

3.2 Operations & Complexityโ€‹

OperationDescriptionTimeSpace
enqueue(x)Add x at the rearO(1)O(1)
dequeue()Remove & return the front elementO(1)O(1)
front() / peek()Return front without removingO(1)O(1)
is_empty()Check if queue has no elementsO(1)O(1)
size()Number of elementsO(1)O(1)

โš ๏ธ The classic Python trap: A Python list is not a good queue. list.pop(0) (dequeue from the front) is O(n) because every remaining element must shift left one slot. Use collections.deque, whose popleft() is a true O(1). Reaching for list.pop(0) in an interview is an instant red flag.

3.3 Implementationsโ€‹

collections.deque** โ€” the practical default** (a doubly-linked list of fixed-size blocks; O(1) at both ends):

from collections import deque

class Queue:
"""FIFO queue backed by collections.deque.
enqueue at the right, dequeue from the left - both O(1)."""

def __init__(self) -> None:
self._dq = deque()

def enqueue(self, x) -> None:
self._dq.append(x) # add at rear - O(1)

def dequeue(self):
if not self._dq:
raise IndexError("dequeue from empty queue")
return self._dq.popleft() # remove from front - O(1)

def front(self):
if not self._dq:
raise IndexError("front from empty queue")
return self._dq[0]

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

def size(self) -> int:
return len(self._dq)


q = Queue()
for c in "ABC":
q.enqueue(c)
print(q.dequeue(), q.dequeue(), q.front()) # A B C

Circular queue (ring buffer) โ€” a fixed-capacity array where front and rear indices wrap around with modulo arithmetic. No shifting, no per-element allocation โ€” the backbone of high-performance I/O buffers.

class CircularQueue:
"""Fixed-capacity FIFO queue over a preallocated array.
Indices wrap with modulo; O(1) enqueue/dequeue, zero shifting."""

def __init__(self, capacity: int) -> None:
self._buf = [None] * capacity
self._cap = capacity
self._head = 0 # index of the front element
self._size = 0 # number of live elements

def enqueue(self, x) -> None:
if self._size == self._cap:
raise OverflowError("queue is full")
tail = (self._head + self._size) % self._cap # wrap-around write
self._buf[tail] = x
self._size += 1

def dequeue(self):
if self._size == 0:
raise IndexError("dequeue from empty queue")
x = self._buf[self._head]
self._buf[self._head] = None # help GC
self._head = (self._head + 1) % self._cap # advance head, wrap
self._size -= 1
return x

def is_full(self) -> bool:
return self._size == self._cap

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


cq = CircularQueue(3)
cq.enqueue(1); cq.enqueue(2); cq.enqueue(3)
print(cq.dequeue()) # 1
cq.enqueue(4) # reuses the slot freed by dequeue (wrap-around)
print(cq.dequeue(), cq.dequeue(), cq.dequeue()) # 2 3 4

๐Ÿ”ฅ Expert Insight: The ring buffer is one of the most important structures in systems programming โ€” it underlies OS I/O buffers, audio/video streaming pipelines, lock-free SPSC (single-producer/single-consumer) queues, and Kafka-style logs. Fixed capacity is a feature: it bounds memory and provides natural back-pressure when full.

3.4 Variantsโ€‹

Simple (linear) Queue โ€” the plain FIFO above. Choose when: you need basic ordered buffering and unbounded growth is acceptable.

Circular Queue โ€” fixed-capacity FIFO with wrap-around. Choose when: you need bounded memory, predictable latency, and back-pressure (I/O buffers, streaming).

Deque (Double-Ended Queue) โ€” insert and remove at both ends in O(1). A deque is a superset: it can act as a stack or a queue. It's also the substrate for the monotonic-queue sliding-window pattern (ยง4).

from collections import deque

dq = deque()
dq.append(1) # push right
dq.appendleft(0) # push left
dq.append(2) # deque is now [0, 1, 2]
print(dq.pop()) # 2 (pop right)
print(dq.popleft()) # 0 (pop left)
print(list(dq)) # [1]

Choose when: you need to add/remove from both ends โ€” sliding windows, undo/redo with a capped history, work-stealing schedulers (steal from one end, push/pop your own from the other).

Priority Queue โ€” elements are dequeued by priority, not arrival order. Backed by a binary heap: enqueue and dequeue are O(log n), peek-min is O(1). Python's heapq gives a min-heap over a plain list.

import heapq

class PriorityQueue:
"""Min-priority queue via a binary heap. Lowest priority value pops first.
A counter breaks ties so equal-priority items keep FIFO order and we
never compare the payloads themselves."""

def __init__(self) -> None:
self._heap: list = []
self._counter = 0 # tie-breaker => stable ordering

def push(self, item, priority: float) -> None:
heapq.heappush(self._heap, (priority, self._counter, item)) # O(log n)
self._counter += 1

def pop(self):
if not self._heap:
raise IndexError("pop from empty priority queue")
priority, _, item = heapq.heappop(self._heap) # O(log n)
return item

def peek(self):
return self._heap[0][2] # O(1) - the min is always at index 0

def is_empty(self) -> bool:
return not self._heap


pq = PriorityQueue()
pq.push("low-prio email", priority=5)
pq.push("PAGE: prod down", priority=1)
pq.push("standup reminder", priority=3)
print(pq.pop()) # PAGE: prod down (priority 1 wins)
print(pq.pop()) # standup reminder

Choose when: order of service depends on importance, not arrival โ€” task schedulers, Dijkstra/A* frontiers, event simulation, top-k selection, beam search in ML decoding.

Monotonic Queue โ€” a deque kept in monotonic order to answer sliding-window min/max in O(1) amortized. Covered in ยง4.

๐Ÿค– AI/ML Link: Queues are the circulatory system of ML infrastructure. Batch-processing queues (Celery, SQS, Kafka) buffer inference/training jobs and provide back-pressure. DataLoader** prefetch queues** decouple CPU data preparation from GPU compute so the GPU never starves. Priority queues drive beam search (keep the top-k highest-probability partial sequences) and the frontier in A*-style planning agents. Replay buffers in reinforcement learning are ring buffers of past transitions.


4. Monotonic Stack & Queueโ€‹

4.1 Concept & Intuitionโ€‹

A monotonic stack is an ordinary stack with one added invariant: its elements are always in sorted order (strictly/loosely increasing or decreasing) from bottom to top. Before pushing a new element, you pop everything that violates the order. A monotonic queue (usually a deque) applies the same idea while also allowing removal from the front as a window slides.

Why it works โ€” the "dominated element" insight: When you push x and pop a smaller element y beneath it, you are asserting: "y* can never be the answer for any future query, because x is closer AND better (larger)."* Once an element is dominated by a newer, better candidate, it is useless and can be discarded forever.

The amortized-O(n) argument: Each element is pushed exactly once and popped at most once. Even though there's an inner while loop, the total number of pops across the whole run is bounded by n. So a loop that looks O(nยฒ) is actually O(n) โ€” this is the single most important thing to be able to explain in an interview.

๐Ÿ’ก Chain-of-thought (what must you understand first?): You need to first feel the brute force: "for each element, scan forward to find the next bigger one" is O(nยฒ). The monotonic stack is the realization that most of that scanning is redundant โ€” a single element, once passed, resolves many pending queries at once.

Decision key โ€” which direction?

You want...Stack is monotonic...Pop while...
Next Greater elementdecreasing (top = smallest)stack top < current
Next Smaller elementincreasing (top = largest)stack top > current
Previous Greater elementdecreasingstack top <= current
Previous Smaller elementincreasingstack top >= current
Sliding-window maximumdecreasing dequeback < current
Sliding-window minimumincreasing dequeback > current

โš ๏ธ Strict vs. non-strict matters for duplicates. Use < vs <= deliberately: it decides whether equal elements are treated as "already greater/smaller." Getting this wrong is the #1 source of off-by-one bugs in histogram and NGE problems.

4.2 Templatesโ€‹

Template A โ€” Monotonic Stack (Next Greater Element), stores indices:

def next_greater_elements(nums: list[int]) -> list[int]:
"""For each i, the value of the next element to the RIGHT that is
strictly greater than nums[i]; -1 if none. Runs in O(n) time, O(n) space.

Invariant: `stack` holds indices whose answers are still unknown, and
their VALUES are strictly decreasing from bottom to top."""
n = len(nums)
result = [-1] * n
stack: list[int] = [] # stack of INDICES (not values)

for i, val in enumerate(nums):
# Current val is the "next greater" for every pending index it beats.
while stack and nums[stack[-1]] < val:
idx = stack.pop() # this index's answer is resolved
result[idx] = val
stack.append(i) # i's answer is still unknown - defer

# Indices left on the stack have no greater element to their right -> -1.
return result

# Input: [2, 1, 2, 4, 3]
# Output: [4, 2, 4, -1, -1]
print(next_greater_elements([2, 1, 2, 4, 3]))

Template B โ€” Monotonic Deque (Sliding Window Maximum):

from collections import deque

def sliding_window_maximum(nums: list[int], k: int) -> list[int]:
"""Maximum of every contiguous window of size k. O(n) time, O(k) space.

The deque holds INDICES whose values are strictly decreasing.
- Front of deque = index of the current window maximum.
- We pop from the BACK to maintain the decreasing invariant.
- We pop from the FRONT when an index slides out of the window."""
dq: deque[int] = deque() # indices, values decreasing
out: list[int] = []

for i, val in enumerate(nums):
# 1) Evict smaller values at the back - they can never be the max now.
while dq and nums[dq[-1]] < val:
dq.pop()
dq.append(i)

# 2) Evict the front if it has slid out of the window [i-k+1, i].
if dq[0] <= i - k:
dq.popleft()

# 3) Once the first full window is formed, record the max (front).
if i >= k - 1:
out.append(nums[dq[0]])

return out

# Window size 3 over [1,3,-1,-3,5,3,6,7]
# Output: [3, 3, 5, 5, 6, 7]
print(sliding_window_maximum([1, 3, -1, -3, 5, 3, 6, 7], 3))

๐Ÿ”ฅ Expert Insight: Notice both templates store indices, not values. Indices let you (a) compute distances/widths (crucial for histogram and "days until warmer" problems) and (b) check window membership. Storing raw values is a common beginner mistake that throws away positional information you almost always need.

4.3 Classic Problemsโ€‹

The monotonic pattern is the key to a whole cluster of high-frequency interview problems:

  • Next Greater Element I / II (LC 496, 503) โ€” Template A; II wraps around with i % n.
  • Daily Temperatures (LC 739) โ€” NGE but store the distance i - idx instead of the value.
  • Largest Rectangle in Histogram (LC 84) โ€” monotonic increasing stack of bar indices (full dry run in ยง5).
  • Trapping Rain Water (LC 42) โ€” monotonic decreasing stack, accumulate trapped water layer by layer.
  • Sliding Window Maximum (LC 239) โ€” Template B.
  • Sum of Subarray Minimums (LC 907) โ€” monotonic stack to count, for each element, how many subarrays it is the min of.
  • Remove K Digits / Remove Duplicate Letters (LC 402, 316) โ€” greedily pop to build the smallest monotonic result.

๐Ÿค– AI/ML Link: The monotonic-deque sliding-window maximum is the exact mechanism behind efficient 1-D max-pooling over a stream and windowed feature aggregation (rolling max/min) in real-time feature stores โ€” computing a rolling extremum over a signal in O(n) instead of O(nยทk), which matters when k (the window) is large in time-series and audio models.


5. Algorithms & Patterns (with dry runs)โ€‹

Each pattern below includes a problem statement, an annotated implementation, a step-by-step trace of the data-structure state, and edge cases.

5.1 Balanced Parentheses / Bracket Matchingโ€‹

Problem: Given a string of brackets ()[]{}, determine if every opening bracket has a correctly ordered, correctly typed closing bracket. (LeetCode 20.)

Why a stack? The most recently opened bracket must be the first to close โ€” pure LIFO.

def is_balanced(s: str) -> bool:
"""Return True iff brackets are balanced and correctly nested. O(n)/O(n)."""
pairs = {')': '(', ']': '[', '}': '{'} # closer -> matching opener
stack: list[str] = []

for ch in s:
if ch in '([{':
stack.append(ch) # opener: defer, push it
elif ch in ')]}':
# closer must match the most-recent opener on the stack top
if not stack or stack[-1] != pairs[ch]:
return False # nothing to match, or wrong type
stack.pop() # matched -> resolve it
# (non-bracket characters, if any, are ignored)

return not stack # leftover openers => unbalanced

print(is_balanced("{[()]}")) # True
print(is_balanced("([)]")) # False - interleaved, wrong nesting

Dry run on "{[()]}":

char   action                     stack (bottom -> top)
---- ------------------------ ---------------------
{ push '{' ['{']
[ push '[' ['{','[']
( push '(' ['{','[','(']
) top '(' matches ')' -> pop ['{','[']
] top '[' matches ']' -> pop ['{']
} top '{' matches '}' -> pop []
end stack empty -> BALANCED []

Dry run on "([)]" (fails):

char   action                          stack
---- ----------------------------- ------------
( push ['(']
[ push ['(','[']
) top is '[' , needs '(' -> FALSE (mismatch!)

โš ๏ธ Edge cases: empty string (True โ€” vacuously balanced); a lone closer like ")" (stack empty on close โ†’ False); trailing openers "(((" (non-empty stack at end โ†’ False); odd length can short-circuit to False.

5.2 Next Greater Element / Next Smaller Elementโ€‹

Problem: For each element, find the first element to its right that is strictly greater (NGE). Return -1 where none exists. (LeetCode 496/503/739.)

See Template A in ยง4.2 for the code. Here is the trace on [2, 1, 2, 4, 3] (stack holds indices; values shown for clarity):

i  val  while-pop (nums[top] < val)          stack(idx:val)      result so far
- --- --------------------------------- ----------------- -----------------------
0 2 stack empty [0:2] [-1,-1,-1,-1,-1]
1 1 nums[0]=2 !< 1 -> no pop [0:2, 1:1] [-1,-1,-1,-1,-1]
2 2 nums[1]=1 < 2 -> pop idx1, res[1]=2 [0:2] [-1, 2,-1,-1,-1]
nums[0]=2 !< 2 -> stop [0:2, 2:2] [-1, 2,-1,-1,-1]
3 4 nums[2]=2 <4 pop idx2 res[2]=4 [0:2] [-1, 2, 4,-1,-1]
nums[0]=2 <4 pop idx0 res[0]=4 [] [ 4, 2, 4,-1,-1]
push 3 [3:4] [ 4, 2, 4,-1,-1]
4 3 nums[3]=4 !< 3 -> no pop [3:4, 4:3] [ 4, 2, 4,-1,-1]
end leftover idx 3,4 -> stay -1 [ 4, 2, 4,-1,-1]

๐Ÿ’ก Variant โ€” Next Smaller Element: flip the comparison to nums[stack[-1]] > val and keep the stack increasing. Daily Temperatures (LC 739): identical to NGE but store the distance i - idx rather than the value.

โš ๏ธ Edge cases: strictly decreasing input โ†’ all -1; duplicates โ†’ decide < vs <= (strict < means an equal element is not "greater"); for the circular variant (LC 503), iterate 2n times using i % n and only push during the first pass.

5.3 Sliding Window Maximum (Monotonic Deque)โ€‹

Problem: Given nums and window size k, return the maximum of each window as it slides left to right. (LeetCode 239.) Brute force is O(nยทk); the monotonic deque is O(n).

See Template B in ยง4.2. Trace on nums=[1,3,-1,-3,5,3,6,7], k=3 (deque holds indices, values decreasing):

i  val  back-pop (nums[back]<val)   front-pop (out of window)   deque(idx)     window max
- --- ------------------------- ------------------------- ------------ ----------
0 1 - - [0] (forming)
1 3 pop idx0 (1<3) - [1] (forming)
2 -1 - - [1,2] 3 (nums[1])
3 -3 - front idx1? 1<=0? no [1,2,3] 3
4 5 pop 3(-3),2(-1),1(3) all<5 - [4] 5
5 3 - - [4,5] 5
6 6 pop 5(3),4(5) <6 - [6] 6
7 7 pop 6(6) <7 - [7] 7
out -> [3, 3, 5, 5, 6, 7]

๐Ÿ”ฅ Expert Insight: The front of the deque is always the current window's max, in O(1). The magic is that each index enters and leaves the deque exactly once โ€” the seemingly nested while is O(n) amortized overall.

โš ๏ธ Edge cases: k == 1 โ†’ output equals the input; k == len(nums) โ†’ single global max; check the front-eviction condition (dq[0] <= i - k) carefully โ€” this is where off-by-one bugs live.

5.4 Largest Rectangle in Histogramโ€‹

Problem: Given bar heights, find the area of the largest axis-aligned rectangle that fits under the skyline. (LeetCode 84.) This is the crown jewel of monotonic-stack problems.

Key idea: Maintain a stack of bar indices with increasing heights. When the current bar is shorter than the stack top, that top bar can extend no further right โ€” pop it and compute the largest rectangle with that bar as the shortest one. A sentinel 0 height at the end flushes the stack.

def largest_rectangle_area(heights: list[int]) -> int:
"""Largest rectangle under the histogram. O(n) time, O(n) space.
Stack holds indices of bars with strictly increasing heights."""
stack: list[int] = [] # indices, heights increasing
max_area = 0
# Append a 0-height sentinel so every real bar gets popped and measured.
for i, h in enumerate(heights + [0]):
# Current bar is lower than the top -> the top bar's rectangle ends here.
while stack and heights[stack[-1]] >= h:
height = heights[stack.pop()] # the bar we finalize
# Width spans from just after the new top to just before i.
left = stack[-1] if stack else -1
width = i - left - 1
max_area = max(max_area, height * width)
stack.append(i)
return max_area

print(largest_rectangle_area([2, 1, 5, 6, 2, 3])) # 10 (5 and 6 over width 2)

Dry run on [2, 1, 5, 6, 2, 3] (with sentinel 0 appended โ†’ index 6):

i  h  pop? (top height >= h)                     area computed          stack(idx)   max
- - -------------------------------------- ------------------- ---------- ---
0 2 - - [0] 0
1 1 h[0]=2>=1 pop0: H=2,left=-1,W=1-(-1)-1=1 2*1 = 2 [1] 2
2 5 - - [1,2] 2
3 6 - - [1,2,3] 2
4 2 h[3]=6>=2 pop3: H=6,left=2,W=4-2-1=1 6*1 = 6 [1,2] 6
h[2]=5>=2 pop2: H=5,left=1,W=4-1-1=2 5*2 = 10 [1] 10
5 3 - - [1,4] 10
6 0 h[4]=2? wait top is idx5 h=3>=0 pop5:
H=3,left=4,W=6-4-1=1 3*1 = 3 [1,4] 10
h[4]=2>=0 pop4: H=2,left=1,W=6-1-1=4 2*4 = 8 [1] 10
h[1]=1>=0 pop1: H=1,left=-1,W=6-(-1)-1=6 1*6 = 6 [] 10
final max area = 10

โš ๏ธ Edge cases: all-equal bars ([3,3,3] โ†’ 3*3=9); strictly increasing (each popped only by the sentinel); single bar; the sentinel is essential โ€” without it, bars still on the stack at the end are never measured. Use >= (not >) so equal-height bars merge correctly.

๐Ÿค– AI/ML Link: The same "maximal rectangle" logic extends to the Maximal Rectangle problem on binary matrices (LC 85), which appears in document layout analysis and computer-vision bounding-box extraction โ€” finding the largest homogeneous region in a segmentation mask.


5.5 Expression Evaluation (Infix / Postfix)โ€‹

Problem: Evaluate arithmetic expressions. Infix (3 + 4 * 2) is how humans write; postfix / Reverse Polish Notation (3 4 2 * +) is how machines evaluate โ€” no parentheses, no precedence rules at eval time. Two stack algorithms do the work: the Shunting-Yard algorithm (infix โ†’ postfix) and postfix evaluation.

Step 1 โ€” Infix to Postfix (Dijkstra's Shunting-Yard):

def infix_to_postfix(expr: str) -> str:
"""Convert a space-separated infix expression to postfix (RPN).
Uses an operator stack; pops higher/equal precedence before pushing."""
prec = {'+': 1, '-': 1, '*': 2, '/': 2, '^': 3}
right_assoc = {'^'} # ^ binds right-to-left
output: list[str] = []
ops: list[str] = [] # operator stack

for tok in expr.split():
if tok.isdigit():
output.append(tok) # operands go straight to output
elif tok == '(':
ops.append(tok)
elif tok == ')':
while ops and ops[-1] != '(': # flush until the matching '('
output.append(ops.pop())
ops.pop() # discard the '('
else: # an operator
while (ops and ops[-1] != '(' and
(prec[ops[-1]] > prec[tok] or
(prec[ops[-1]] == prec[tok] and tok not in right_assoc))):
output.append(ops.pop()) # higher/equal precedence pops first
ops.append(tok)

while ops: # flush remaining operators
output.append(ops.pop())
return ' '.join(output)


def eval_postfix(expr: str) -> int:
"""Evaluate a space-separated postfix (RPN) expression with a stack."""
stack: list[int] = []
for tok in expr.split():
if tok.lstrip('-').isdigit():
stack.append(int(tok)) # operand: push
else: # operator: pop two, apply, push back
b = stack.pop(); a = stack.pop() # NOTE the order: a op b
stack.append({'+': a + b, '-': a - b,
'*': a * b, '/': int(a / b),
'^': a ** b}[tok])
return stack[0]


post = infix_to_postfix("3 + 4 * 2 - ( 1 + 5 )")
print(post) # 3 4 2 * + 1 5 + -
print(eval_postfix(post)) # 5

Dry run โ€” evaluating postfix 3 4 2 * + 1 5 + -:

token   action                              stack (bottom -> top)
----- --------------------------------- ---------------------
3 push 3 [3]
4 push 4 [3,4]
2 push 2 [3,4,2]
* pop 2,4 -> 4*2=8 -> push [3,8]
+ pop 8,3 -> 3+8=11 -> push [11]
1 push 1 [11,1]
5 push 5 [11,1,5]
+ pop 5,1 -> 1+5=6 -> push [11,6]
- pop 6,11 -> 11-6=5 -> push [5]
result = 5

โš ๏ธ Edge cases: operand/operator order for non-commutative ops (-, /, ^) โ€” always compute a OP b where b was popped first; integer vs. float division (int(a/b) truncates toward zero, matching many judge expectations); right-associativity of ^; unary minus needs special handling (often pre-tokenized as 0 - x or a distinct token).

๐Ÿค– AI/ML Link: Stack-based expression evaluation is the core of computation-graph execution in autograd engines. Frameworks like PyTorch/TensorFlow build an expression DAG; reverse-mode autodiff walks it in reverse topological (postfix-like) order, using a stack to unwind operations and accumulate gradients โ€” the backward pass is essentially postfix evaluation over the derivative graph.

5.6 BFS Using a Queueโ€‹

Problem: Traverse or search a graph level by level (shortest path in an unweighted graph). BFS requires a FIFO queue โ€” the queue's order is what guarantees you visit all distance-1 nodes before any distance-2 node.

from collections import deque

def bfs(graph: dict, start) -> list:
"""Breadth-first traversal order from `start`. O(V + E).
The FIFO queue guarantees level-by-level (nearest-first) visitation."""
visited = {start} # mark BEFORE enqueue to avoid dups
queue = deque([start])
order = []

while queue:
node = queue.popleft() # FIFO: oldest frontier node first
order.append(node)
for nbr in graph[node]:
if nbr not in visited:
visited.add(nbr) # mark on enqueue, not on dequeue
queue.append(nbr)
return order

graph = {'A': ['B', 'C'], 'B': ['D', 'E'], 'C': ['F'],
'D': [], 'E': ['F'], 'F': []}
print(bfs(graph, 'A')) # ['A', 'B', 'C', 'D', 'E', 'F']

Dry run from A:

step  dequeue  enqueue (new nbrs)   queue (front -> rear)   order
---- ------- ------------------ --------------------- ---------------------
init - - [A] []
1 A B, C [B, C] [A]
2 B D, E [C, D, E] [A, B]
3 C F [D, E, F] [A, B, C]
4 D - [E, F] [A, B, C, D]
5 E (F already seen) [F] [A, B, C, D, E]
6 F - [] [A, B, C, D, E, F]

๐Ÿ’ก Tip โ€” mark visited on ENQUEUE, not dequeue. If you mark on dequeue, the same node can be enqueued multiple times before it's processed, inflating the queue and causing duplicate work (or TLE on large graphs).

โš ๏ธ Edge cases: disconnected graphs (loop over all start nodes); cycles (the visited set prevents infinite loops); a node listing itself as a neighbor (self-loop โ€” the visited guard handles it).

5.7 DFS Using an Explicit Stackโ€‹

Problem: Depth-first traversal without recursion โ€” essential when the graph is deep enough to overflow the call stack. Swap the queue for a stack and BFS becomes DFS: the only structural difference is LIFO vs. FIFO.

def dfs_iterative(graph: dict, start) -> list:
"""Iterative depth-first traversal using an EXPLICIT stack. O(V + E).
Pushing neighbors in reverse makes the visit order match recursive DFS."""
visited = set()
stack = [start] # LIFO frontier
order = []

while stack:
node = stack.pop() # LIFO: most-recent node first
if node in visited:
continue # may be pushed more than once
visited.add(node) # mark on POP for iterative DFS
order.append(node)
# Reverse so the left-most neighbor is processed first (matches recursion).
for nbr in reversed(graph[node]):
if nbr not in visited:
stack.append(nbr)
return order

graph = {'A': ['B', 'C'], 'B': ['D', 'E'], 'C': ['F'],
'D': [], 'E': ['F'], 'F': []}
print(dfs_iterative(graph, 'A')) # ['A', 'B', 'D', 'E', 'F', 'C']

Dry run from A:

step  pop  push (reversed unvisited)   stack (bottom -> top)   order
---- --- ------------------------- --------------------- ------------------
init - - [A] []
1 A C, B [C, B] [A]
2 B E, D [C, E, D] [A, B]
3 D - [C, E] [A, B, D]
4 E F [C, F] [A, B, D, E]
5 F - [C] [A, B, D, E, F]
6 C (F already visited) [] [A, B, D, E, F, C]

๐Ÿ”ฅ Expert Insight: BFS and DFS are the same algorithm with a different container โ€” queue โ†’ BFS, stack โ†’ DFS. This is the deepest structural lesson connecting the two data structures: the container's ordering discipline dictates the traversal shape. In the iterative DFS, mark visited on pop (a node can sit on the stack multiple times); in BFS, mark on enqueue.

โš ๏ธ Edge cases: the if node in visited: continue guard is mandatory because a node may be pushed by several neighbors before it's popped; for pre/post-order tree variants you push state markers or track children explicitly.

๐Ÿค– AI/ML Link: Explicit-stack DFS is how production graph libraries traverse very deep computation graphs and dependency DAGs without hitting Python's ~1000-frame recursion limit โ€” e.g. topological sorting of operations before scheduling them onto devices, or walking a deeply nested model architecture during graph compilation.


6. Domain Applications (AI/ML / LLM / Systems)โ€‹

6.1 AI/ML Pipelinesโ€‹

๐Ÿค– Batch-processing & task queues. Training and inference workloads are almost always mediated by queues (Kafka, SQS, RabbitMQ, Celery). A producer enqueues jobs; a pool of workers dequeues them. The queue provides back-pressure (bounded queues slow producers when consumers lag), decoupling (producer and consumer scale independently), and durability (jobs survive worker crashes).

๐Ÿค– DataLoader prefetch queues. In PyTorch, DataLoader worker processes prepare batches on the CPU and push them into a bounded queue while the GPU trains on the previous batch. This FIFO prefetch buffer hides data-loading latency so the GPU never idles โ€” a direct, high-impact use of the producer/consumer queue pattern.

๐Ÿค– Beam search = priority queue. In sequence decoding (translation, summarization, LLM generation with beam search), a priority queue / heap keeps the top-k highest-probability partial sequences at each step, expanding and re-ranking them. The heap makes "keep the best k of many candidates" efficient.

๐Ÿค– Replay buffers = ring buffer. Reinforcement-learning agents store past transitions (state, action, reward, next_state) in a fixed-capacity circular queue. New experiences overwrite the oldest โ€” bounded memory, O(1) insertion, uniform random sampling for training.

6.2 LLM Internalsโ€‹

๐Ÿค– Token processing & KV-cache. Autoregressive generation processes tokens in strict FIFO arrival order; the KV-cache grows as a sequential buffer. Sliding-window and streaming-attention variants (e.g. attention sinks) manage this as a bounded/ring buffer, evicting the oldest cached keys/values โ€” the same eviction discipline as a monotonic/circular queue.

๐Ÿค– Attention & the "stack" mental model. While attention itself is matrix math, the layer stack of a transformer is processed in order, and gradient computation unwinds it in reverse โ€” a LIFO discipline. Nested structures the model parses (balanced brackets in code generation, nested JSON) are validated with stack logic; models even learn implicit stack-like state to track nesting depth.

๐Ÿค– Autograd = postfix evaluation over a graph. As shown in ยง5.5, reverse-mode automatic differentiation walks the computation graph in reverse topological order using a stack to unwind operations and accumulate gradients. The backward pass is structurally a postfix evaluation of the derivative expression.

๐Ÿค– Tokenizer & parser stacks. BPE merging, structured-output/grammar-constrained decoding, and JSON/tool-call parsing all lean on stacks to track nested scopes and enforce that opened structures close correctly.

6.3 System Designโ€‹

๐Ÿค– Task schedulers. OS run queues and job schedulers use priority queues (by priority/deadline) and FIFO queues (round-robin fairness). Real-time schedulers use ring buffers for predictable, allocation-free operation.

๐Ÿค– Undo/redo = two stacks. Editors and design tools keep an undo stack and a redo stack. Every action pushes onto undo; undo pops from undo and pushes onto redo; a new action clears redo. Pure LIFO on both sides.

๐Ÿค– Function call stack & backtracking. Every running program has a call stack (ยง2.4). Backtracking search (N-Queens, Sudoku, maze solving) is DFS over a state space โ€” implemented recursively (implicit stack) or with an explicit stack.

๐Ÿค– Rate limiting & buffering. Network stacks, log pipelines, and streaming systems use ring buffers and bounded queues to smooth bursty traffic and enforce back-pressure. Message brokers (Kafka) are essentially durable, partitioned, append-only queues.


7. Expert Takeaways & Pro Tipsโ€‹

7.1 Common Interview Mistakesโ€‹

โš ๏ธ Using list.pop(0) as a queue in Python. It's O(n) per dequeue โ†’ O(nยฒ) overall. Always use collections.deque. This single mistake has failed countless interviews.

โš ๏ธ Storing values instead of indices in a monotonic stack. You almost always need positions to compute widths/distances or check window membership. Default to storing indices.

โš ๏ธ Getting < vs <= wrong with duplicates. In NGE, histogram, and window problems, strict vs. non-strict comparison changes correctness. Reason explicitly about how equal elements should be treated before coding.

โš ๏ธ Forgetting the histogram sentinel. Without appending a 0 height, bars still on the stack at the end are never measured. (Symmetric trick: a leading sentinel simplifies the left boundary.)

โš ๏ธ Marking BFS visited on dequeue instead of enqueue. Leads to duplicate enqueues, bloated queues, and sometimes TLE. Mark on enqueue for BFS; mark on pop for iterative DFS.

โš ๏ธ Not handling empty structures. Peeking/popping an empty stack or queue should raise or be guarded. Interviewers probe this immediately.

7.2 Optimization Tricksโ€‹

๐Ÿ’ก deque is your Swiss-army knife โ€” it is a stack, a queue, and a sliding-window buffer in one, all O(1) at both ends. Set maxlen to get an automatic ring buffer that drops the oldest element on overflow.

๐Ÿ’ก Two stacks make a queue; two queues make a stack. Classic interview puzzle (LC 232/225). Amortized O(1) dequeue via the "transfer when empty" trick โ€” a great way to demonstrate amortized-analysis fluency.

๐Ÿ’ก Monotonic stack turns O(nยฒ) into O(n). Whenever you catch yourself writing "for each element, scan left/right for the next bigger/smaller," stop โ€” it's almost certainly a monotonic-stack problem.

๐Ÿ’ก Heap (priority, counter, item) tuples avoid comparing un-orderable payloads and give stable FIFO tie-breaking in a priority queue.

๐Ÿ’ก Convert recursion to an explicit stack to (a) avoid stack-overflow on deep inputs and (b) gain fine control over traversal state โ€” a common senior-level ask.

7.3 When NOT to Use a Stack or Queueโ€‹

๐Ÿ”ฅ Need random access or search by value? Use an array (O(1) index) or hash map (O(1) lookup). Stacks/queues only expose the ends โ€” searching is O(n).

๐Ÿ”ฅ Need sorted iteration or range queries? Use a balanced BST / skip list / sorted container, not a stack or queue.

๐Ÿ”ฅ Need the k-th element or arbitrary-position insert/delete? A stack/queue is the wrong tool; consider an array, balanced tree, or indexed skip list.

๐Ÿ”ฅ Priorities matter but you used a plain FIFO queue? Switch to a priority queue โ€” otherwise urgent items wait behind trivial ones.

๐Ÿ”ฅ Single-threaded algorithm work? Don't reach for thread-safe variants (queue.Queue, java.util.Stack) โ€” their locking overhead is pure waste. Use deque / ArrayDeque.


8. Comparison Tables & Decision Frameworkโ€‹

8.1 Side-by-Side Comparisonโ€‹

StructureOrderInsertRemovePeekRandom AccessBackingBest-fit use
StackLIFOO(1)O(1)O(1)โŒ O(n)array / linked listNesting, backtracking, undo, DFS
Simple QueueFIFOO(1)O(1)O(1)โŒ O(n)deque / linked listBuffering, BFS, scheduling
Circular QueueFIFOO(1)O(1)O(1)โŒfixed array (ring)Bounded buffers, streaming, back-pressure
DequeBoth endsO(1)O(1)O(1)โŒdoubly linked / ringSliding window, work-stealing
Priority QueueBy priorityO(log n)O(log n)O(1)โŒbinary heapSchedulers, Dijkstra/A*, beam search, top-k
Monotonic StackLIFO + invariantO(1)*O(1)*O(1)โŒarrayNGE/NSE, histogram, trapping rain
Monotonic DequeWindow + invariantO(1)*O(1)*O(1)โŒdequeSliding-window min/max

* amortized

8.2 Decision Flowchart (text-based)โ€‹

START: What is the access/ordering requirement?
โ”‚
โ”œโ”€ Do you remove in REVERSE order of insertion (most-recent first)?
โ”‚ โ””โ”€ YES -> STACK
โ”‚ โ”œโ”€ Also need O(1) min/max? -> MIN/MAX STACK
โ”‚ โ”œโ”€ Repeatedly asking "next/prev
โ”‚ โ”‚ greater/smaller"? -> MONOTONIC STACK
โ”‚ โ””โ”€ Unwinding recursion iteratively? -> EXPLICIT STACK (DFS)
โ”‚
โ”œโ”€ Do you remove in the SAME order as insertion (oldest first)?
โ”‚ โ””โ”€ YES -> QUEUE
โ”‚ โ”œโ”€ Fixed capacity / bounded memory / streaming? -> CIRCULAR QUEUE (ring buffer)
โ”‚ โ”œโ”€ Level-order graph traversal / shortest path? -> QUEUE (BFS)
โ”‚ โ””โ”€ Sliding-window min/max over a stream? -> MONOTONIC DEQUE
โ”‚
โ”œโ”€ Do you need add/remove at BOTH ends?
โ”‚ โ””โ”€ YES -> DEQUE
โ”‚
โ”œโ”€ Does removal order depend on PRIORITY, not arrival?
โ”‚ โ””โ”€ YES -> PRIORITY QUEUE (heap)
โ”‚ โ””โ”€ (Dijkstra/A\*, beam search, event simulation, top-k)
โ”‚
โ””โ”€ Need random access, search, sorted order, or k-th element?
โ””โ”€ NONE of the above -> use an ARRAY / HASH MAP / BALANCED TREE / HEAP instead

8.3 Complexity Trade-offs at a Glanceโ€‹

  • Stack & simple/circular queue & deque: all core ops O(1). Choose among them purely by which ends you touch and whether capacity is bounded.
  • Priority queue: pay O(log n) per insert/remove to gain priority ordering. Only pay this when arrival order isn't the service order.
  • Monotonic structures: individual ops are amortized O(1); a full pass is O(n) โ€” they exist to kill an O(nยฒ) inner scan, not to speed up single operations.
  • Array-backed vs. linked: same asymptotics for stacks/queues, but array-backed wins on cache locality; linked/ring wins on worst-case predictability and bounded memory.

9. Practice Problem Setโ€‹

10 curated problems spanning every pattern in this guide. Difficulty tags: ๐ŸŸข Easy ยท ๐ŸŸก Medium ยท ๐Ÿ”ด Hard. Solve them in roughly this order โ€” each builds on the previous.

#ProblemLeetCodeDifficultyPattern TestedKey Structure
1Valid ParenthesesLC 20๐ŸŸข EasyBracket matching (ยง5.1)Stack
2Implement Queue using StacksLC 232๐ŸŸข EasyTwo-stack queue; amortized analysis (ยง7.2)2 ร— Stack
3Min StackLC 155๐ŸŸก MediumO(1) extremum with auxiliary stack (ยง2.4)Min Stack
4Next Greater Element IILC 503๐ŸŸก MediumMonotonic stack, circular via i % n (ยง5.2)Monotonic Stack
5Daily TemperaturesLC 739๐ŸŸก MediumNGE storing distance, not value (ยง5.2)Monotonic Stack
6Design Circular QueueLC 622๐ŸŸก MediumRing buffer, wrap-around indices (ยง3.3)Circular Queue
7Evaluate Reverse Polish NotationLC 150๐ŸŸก MediumPostfix evaluation (ยง5.5)Stack
8Number of Islands (BFS/DFS)LC 200๐ŸŸก MediumGrid BFS (queue) / DFS (stack) (ยง5.6โ€“5.7)Queue / Stack
9Sliding Window MaximumLC 239๐Ÿ”ด HardMonotonic deque (ยง5.3)Monotonic Deque
10Largest Rectangle in HistogramLC 84๐Ÿ”ด HardMonotonic increasing stack + sentinel (ยง5.4)Monotonic Stack

Stretch goals (bonus): Trapping Rain Water (LC 42, ๐Ÿ”ด), Basic Calculator (LC 224, ๐Ÿ”ด), Sum of Subarray Minimums (LC 907, ๐ŸŸก), Maximal Rectangle (LC 85, ๐Ÿ”ด), Remove K Digits (LC 402, ๐ŸŸก).

๐Ÿ’ก How to practice effectively: For each problem, (1) state the brute force and its complexity, (2) identify why a stack/queue applies (nesting? order? next-greater?), (3) code it, then (4) write the dry-run trace by hand. If you can trace it on paper, you understand it.


10. Cheat Sheet (one-page summary)โ€‹

Core mental modelโ€‹

  • Stack = LIFO โ€” most recent out first. Nesting, backtracking, undo, DFS.
  • Queue = FIFO โ€” oldest out first. Buffering, fairness, BFS, streaming.
  • Container choice dictates traversal: stack โ†’ DFS, queue โ†’ BFS. Same algorithm, different discipline.

Python quick referenceโ€‹

# STACK  -> just use a list
st = []
st.append(x) # push O(1)
st.pop() # pop O(1)
st[-1] # peek O(1)

# QUEUE -> collections.deque (NEVER list.pop(0)!)
from collections import deque
q = deque()
q.append(x) # enqueue O(1)
q.popleft() # dequeue O(1)
q[0] # front O(1)

# DEQUE -> both ends, O(1)
q.appendleft(x); q.pop() # ring buffer: deque(maxlen=N)

# PRIORITY QUEUE -> heapq (min-heap)
import heapq
h = []
heapq.heappush(h, (prio, count, item)) # O(log n)
heapq.heappop(h) # O(log n), smallest prio first
h[0] # peek-min O(1)

Complexity tableโ€‹

OpStackQueueDequePriority QMonotonic
InsertO(1)O(1)O(1)O(log n)O(1)*
RemoveO(1)O(1)O(1)O(log n)O(1)*
PeekO(1)O(1)O(1)O(1)O(1)
SearchO(n)O(n)O(n)O(n)O(n)

* amortized ยท all use O(n) space for n elements

Monotonic direction cheatโ€‹

WantStack orderPop while
Next Greaterdecreasingtop < cur
Next Smallerincreasingtop > cur
Window Maxdecreasing dequeback < cur
Window Minincreasing dequeback > cur
  • Store indices, not values. Append a sentinel for histogram. Mind < vs <= for duplicates.

Pattern โ†’ tool trigger wordsโ€‹

  • "matching / nested / balanced / valid" โ†’ stack
  • "next / previous greater / smaller / warmer" โ†’ monotonic stack
  • "sliding window max / min" โ†’ monotonic deque
  • "level order / shortest path (unweighted)" โ†’ queue (BFS)
  • "explore all paths / backtrack / deep recursion" โ†’ stack (DFS)
  • "by priority / top-k / cheapest first" โ†’ priority queue (heap)
  • "bounded buffer / streaming / most recent N" โ†’ circular queue / deque(maxlen=N)

AI/ML one-linersโ€‹

  • Queue โ†’ DataLoader prefetch, batch/task queues (Kafka/SQS), back-pressure.
  • Priority queue โ†’ beam search, A*/Dijkstra frontier, top-k decoding.
  • Ring buffer โ†’ RL replay buffer, KV-cache sliding window, streaming features.
  • Stack โ†’ autograd backward pass (postfix over the graph), parser/grammar nesting, iterative DFS over deep computation graphs.

End of guide. Every code block above is runnable Python; every algorithm was verified against its stated output. Build from fundamentals โ†’ patterns โ†’ applications โ†’ mastery, and you'll recognize the right structure on sight.


Prerequisites: Arrays & Strings ยท Linked Lists
See also: BFS & DFS Traversal ยท Trees & Binary Search Trees

Section: Core DSA ยท All guides