Module 5: Stacks, Queues & Monotonic Structures·Tutorial 8 of 23 (Step 5.1)
All Tutorials·DSA Roadmap·2 Problems · ~7 hrs

5.1 Stacks (LIFO) & Queues (FIFO)

Learn what Stacks and Queues are from scratch, why restricting access to LIFO or FIFO makes code cleaner and faster, and how to solve bracket and Min-Stack problems.

01 Read the story: What & Why
02 Follow the drawing: Visual stepper
03 Practice code: 4 languages + problems

1. What Are Stacks & Queues?

  • Stack (LIFO — Last-In, First-Out): Picture a stack of cafeteria trays. You can only put a new tray on the very top (push) and take the tray from the very top (pop). The last item you put in is always the first item that comes out.
  • Queue (FIFO — First-In, First-Out): Picture a line of people waiting for coffee. New people join at the back (enqueue), and the person at the front gets served first (dequeue). The first item in is the first item out.
Visual: Stack (LIFO: Last-In, First-Out) vs. Queue (FIFO: First-In, First-Out)
Stack (LIFO): Push & Pop at TopTop: 30 (newest)2010 (oldest)Queue (FIFO): Pop Front ◄── Push BackFront: 1020Back: 30

2. Why Do We Need Stacks & Queues?

If a normal Array lets you read and write anywhere, why restrict yourself to only touching the top or ends?

  1. Stacks Remember "The Most Recent Unfinished Business": When you read nested brackets like "{ [ ( ) ] }" or nested function calls, which bracket must close first? The ( ) that opened most recently! A Stack naturally holds opening brackets so the top of the stack is always the exact bracket waiting to be closed next.
  2. Queues Guarantee Fair Order & Level-by-Level Exploration: When exploring a tree or graph floor by floor (Breadth-First Search), a Queue ensures every node at distance 1 is processed before any node at distance 2.
  3. Every Operation Is O(1): Pushing, popping, and peeking at the top of a Stack or ends of a Deque never shifts elements in memory (O(1) time). (Warning: In Python, never use list.pop(0) for a queue—that shifts the whole list in O(N) time! Always use collections.deque.)

3. Basic Operations & Walkthroughs (All 4 Languages)

from collections import deque

# 1. Stack (LIFO) — Use a normal Python list
stack = []
stack.append(10)        # push 10 -> [10]
stack.append(20)        # push 20 -> [10, 20]
top_val = stack[-1]     # peek top -> 20 (O(1))
popped = stack.pop()    # pop top  -> 20 (O(1))

# 2. Queue (FIFO) — Always use collections.deque!
q = deque()
q.append("A")           # enqueue back  -> ["A"]
q.append("B")           # enqueue back  -> ["A", "B"]
front_val = q[0]        # peek front    -> "A" (O(1))
out = q.popleft()       # dequeue front -> "A" (O(1))

3B. More Operations: Circular Queue & Implementing a Queue Using Two Stacks

Before jumping into LeetCode problems, top product companies (Amazon, Microsoft, Apple) frequently ask how Stacks and Queues are built under the hood:

1. Circular Buffer Queue (Fixed-Size Array with Wrap-Around)

How do operating systems build a fast O(1) Queue inside a fixed-size array of capacity K without ever shifting elements? They keep head and tail indices and wrap around the end of the array using the modulo operator (index + 1) % K!

2. Building a FIFO Queue Using Two LIFO Stacks (Implement Queue using Stacks)

If you push [1, 2, 3] into an in_stack, 3 sits on top (LIFO). How do you get 1 out first (FIFO)? When out_stack is empty, pop everything from in_stack and push it into out_stack ([3, 2, 1]). Pouring from one stack into another reverses the order twice, putting 1 right on top of out_stack in amortized O(1) time!

Warm-Up Example Before Interview Problems: Remove Adjacent Duplicates ("abbaca" -> "ca")

Let's trace a simple beginner problem before tackling nested brackets:

  • Read 'a' -> stack is ['a']
  • Read 'b' -> stack is ['a', 'b']
  • Read 'b' -> matches top of stack ('b')! Pop 'b' -> stack is back to ['a']!
  • Read 'a' -> matches top of stack ('a')! Pop 'a' -> stack is []!
  • Read 'c', then 'a' -> final stack is ['c', 'a'] -> "ca" in one O(N) pass!
# Warm-Up: Remove Adjacent Duplicates in a String -> O(N) time, O(N) space
def remove_duplicates(s: str) -> str:
  stack = []
  for ch in s:
      if stack and stack[-1] == ch:
          stack.pop()      # Duplicate pair cancels out!
      else:
          stack.append(ch)
  return "".join(stack)

# Classic Under-the-Hood Operation: FIFO Queue Using Two LIFO Stacks -> O(1) amortized
class MyQueue:
  def __init__(self):
      self.in_st, self.out_st = [], []

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

  def pop(self) -> int:
      self.peek()
      return self.out_st.pop()

  def peek(self) -> int:
      if not self.out_st:
          while self.in_st:
              self.out_st.append(self.in_st.pop())
      return self.out_st[-1]

4. Core Interview Patterns

Pattern A: Bracket & Expression Matching (Valid Parentheses)

Push every opening bracket (, [, { onto the stack. Whenever a closing bracket arrives, check if the stack is non-empty and its top matches. At the very end, the stack must be completely empty!

The Hand-Drawn Step-by-Step Walkthrough
Step 1 / 3

Interactive Step-by-Step Trace: Valid Parentheses with a LIFO Stack

Trace s = "([{}])" using a Stack of expected closing brackets:

Step 1: Push Opening Brackets '(', '[', '{' onto the Stack
bottom(
middle[
TOP{Top
stack.push('('); stack.push('['); stack.push('{');

The most recently opened bracket '{' sits on TOP of the stack because it must be closed first!

✎ Remember this: Last-In, First-Out (LIFO) matches nested structures naturally.

Pattern B: Prefix-Min / Auxiliary State Stack (Min Stack)

How do you support getMin() in O(1) time even as items are popped off a stack? Instead of storing just val on the stack, push a pair (val, current_min_so_far) at every slot! That way, every item on the stack remembers what the minimum of the stack was at the moment it arrived.

Pattern: Min Stack (Store Value + Running Minimum)

Use whenever you need O(1) min/max queries alongside standard O(1) stack push and pop operations.

class MinStack:
  def __init__(self):
      self.st = []  # stores (val, min_at_this_depth)
  def push(self, val: int) -> None:
      cur_min = val if not self.st else min(val, self.st[-1][1])
      self.st.append((val, cur_min))
  def pop(self) -> None:
      self.st.pop()
  def getMin(self) -> int:
      return self.st[-1][1]
Operation / PatternTimeSpaceNotes
Stack Push / Pop / TopO(1)O(N)LIFO ordering
Queue Enqueue / DequeueO(1)O(N)FIFO ordering via deque
Min Stack (push, pop, getMin)O(1)O(N)Each entry stores (val, running_min)

5. Practice On Your Own (Curated Submodule Problems)

0 / 2 Solved
StatusProblemDifficultyComplexityHint?Links & Notes
Valid Parentheses
LIFO Matching & Expression Parsing
Easy
T: O(N)
S: O(N)
Solve
Min Stack
LIFO Matching & Expression Parsing
Medium
T: O(1)
S: O(N)
Solve