🌍
Why does this topic matter?
Stacks and queues are the two most fundamental sequential data
structures after arrays and linked lists. Every operating system
uses a call stack to manage function invocations — that's why you
see "stack overflow" when recursion goes too deep. Every web browser
uses a history stack for the back button. Every customer service
system uses queues. In interview problems, the
monotonic stack pattern alone appears in dozens of
FAANG questions — Next Greater Element, Daily Temperatures, Largest
Rectangle in Histogram, Trapping Rain Water. The
deque (double-ended queue) unlocks O(n) solutions
to sliding window min/max problems that would otherwise require O(n
log n) with a heap. These are not just data structures — they are
thinking tools.
📖 Before We Start — The Mental Model
Think of a stack as a pile of plates — you can only
add or remove from the top (Last In, First Out = LIFO). Think
of a queue as a line at a coffee shop — first person
in line is first to be served (First In, First Out = FIFO).
These simple behaviours unlock surprisingly powerful patterns: you can
use a stack to reverse order, to match pairs, and to
track history. You can use a queue to process things
level by level — which is exactly what BFS does on graphs and
trees.
🔗
How it connects: Lecture 11 (Linked Lists) showed
you how to build singly/doubly linked lists. A stack is just a
linked list where you only touch the head. A queue is a linked list
where you add at the tail and remove from the head. Lecture 15
(Trees) uses stacks for DFS traversal and queues for BFS level-order
traversal — you'll use everything from this lecture there.
📚 Stack Fundamentals — LIFO
🎯
Core Property: Last In, First Out (LIFO). Only
the top element is accessible. Three operations —
push(x) (add to top), pop() (remove
top), peek() (read top without removing) — all
O(1).
Stack visualisation — push(1), push(2), push(3): After push(1): [1]
← top After push(2): [2] ← top [1] After push(3): [3] ← top [2] [1]
pop() → returns 3. Stack: [2][1] peek()→ returns 2. Stack unchanged:
[2][1]
Stack Implementations in Java
| Implementation |
Backed By |
push |
pop |
Notes |
ArrayDeque<T> (preferred) |
Resizable array |
O(1)* |
O(1) |
Fastest, no synchronisation overhead |
Stack<T> (legacy) |
Vector |
O(1)* |
O(1) |
Synchronized — avoid in single-threaded code |
| LinkedList (as Deque) |
Doubly linked list |
O(1) |
O(1) |
More GC pressure from node allocation |
// ✅ PREFERRED — ArrayDeque used as a stack
Deque<Integer> stack = new ArrayDeque<>();
stack.push(10); // addFirst(10) — add to top
stack.push(20);
stack.push(30);
stack.peek(); // 30 — read top, do NOT remove
stack.pop(); // 30 — removes and returns top
stack.isEmpty(); // false
stack.size(); // 2
// ❌ AVOID in interviews — legacy, synchronized
Stack<Integer> legacyStack = new Stack<>();
legacyStack.push(10); // works, but prefer Deque
Java Stack API Quick Reference
// Push: addFirst / push / offerFirst
stack.push(x); // throws if capacity exceeded (rare)
stack.offerFirst(x); // returns false if capacity exceeded
// Pop: removeFirst / pop / pollFirst
stack.pop(); // throws NoSuchElementException if empty
stack.pollFirst(); // returns null if empty — safer for conditionals
// Peek: peekFirst / peek / getFirst
stack.peek(); // null if empty
stack.getFirst(); // throws if empty
// Check
stack.isEmpty();
stack.size();
// ⚠️ Common interview pattern — check before pop:
if (!stack.isEmpty() && stack.peek() < threshold)
stack.pop();
📈 Monotonic Stack — The FAANG Power Pattern
🎯
What is a Monotonic Stack? A stack that is always
kept in a sorted order (either monotonically increasing or
decreasing). When a new element arrives that violates the
order, we pop elements until the order is restored. Each popped
element has "found its answer" — the new element is the answer it
was waiting for.
Next Greater Element Pattern
For each element, find the first element to its right that
is greater than it. Brute force is O(n²). Monotonic stack solves it
in O(n).
Array: [2, 1, 5, 3, 4] NGE: [5, 5, -1, 4, -1] Walk left→right.
Maintain a DECREASING stack of indices. i=0, val=2: stack empty →
push 0. stack=[0] (vals:[2]) i=1, val=1: 1 ≤ 2 → push 1. stack=[0,1]
(vals:[2,1]) i=2, val=5: 5>1 → pop 1, ans[1]=5 5>2 → pop 0, ans[0]=5
stack empty → push 2. stack=[2] (vals:[5]) i=3, val=3: 3 ≤ 5 → push
3. stack=[2,3] (vals:[5,3]) i=4, val=4: 4>3 → pop 3, ans[3]=4 4 ≤ 5
→ push 4. stack=[2,4] (vals:[5,4]) End: remaining on stack → NGE =
-1. ans[2]=-1, ans[4]=-1 Result: [5, 5, -1, 4, -1] ✓
public int[] nextGreaterElement(int[] nums) {
int[] ans = new int[nums.length];
Arrays.fill(ans, -1); // default: no greater element
Deque<Integer> stack = new ArrayDeque<>(); // stores INDICES
for (int i = 0; i < nums.length; i++) {
// Pop all indices whose element is smaller than current
while (!stack.isEmpty() && nums[stack.peek()] < nums[i]) {
ans[stack.pop()] = nums[i]; // current is the NGE for popped index
}
stack.push(i);
}
return ans;
}
// Time: O(N) — each element pushed/popped at most once
// Space: O(N) — stack in worst case
Largest Rectangle in Histogram
Find the largest rectangle that fits inside a histogram. Each bar
has width 1. This classic problem uses a stack to track left and
right boundaries for each bar height.
Heights: [2, 1, 5, 6, 2, 3] ┌─┐ │6│ ┌─┼─┼─┐ │5│6│ │ │ │ │ │ ┌─┐ ┌─┐
│ │ │2│ │3│ │2│ │ │ │ │ │ │ ┗━┷━━┷━┷━┷━┷━━━┷━┛ Largest rectangle =
10 units (heights 5 and 6, width 2) Key insight: For bar at index i
with height h[i], - Left boundary: first bar to the left that is
SHORTER than h[i] - Right boundary: first bar to the right that is
SHORTER than h[i] Stack maintains indices in INCREASING height
order. When a shorter bar arrives, pop — each popped bar found its
right boundary.
public int largestRectangleArea(int[] heights) {
Deque<Integer> stack = new ArrayDeque<>(); // indices, increasing heights
int maxArea = 0, n = heights.length;
for (int i = 0; i <= n; i++) {
// Use 0 as sentinel at end to flush all remaining bars
int curH = (i == n) ? 0 : heights[i];
while (!stack.isEmpty() && curH < heights[stack.peek()]) {
int h = heights[stack.pop()]; // height of this bar
int w = stack.isEmpty() ? i : i - stack.peek() - 1; // width
maxArea = Math.max(maxArea, h * w);
}
stack.push(i);
}
return maxArea;
}
// [2,1,5,6,2,3]:
// i=0: push 0. stack=[0]
// i=1: h[1]=1 < h[0]=2 → pop 0, h=2, w=1(stack empty), area=2
// push 1. stack=[1]
// i=2,3: increasing → push. stack=[1,2,3]
// i=4: h=2 < h[3]=6 → pop 3, h=6,w=4-2-1=1, area=6
// 2 < h[2]=5 → pop 2, h=5,w=4-1-1=2, area=10 ← max!
// ... continues. Final maxArea = 10 ✓
🚶 Queue Fundamentals — FIFO
📌
Core Property: First In, First Out (FIFO).
Elements enter at the tail (rear) and exit from the
head (front). Operations:
offer(x) (enqueue), poll() (dequeue),
peek() — all O(1).
Queue Implementations in Java
| Implementation |
Use Case |
Notes |
ArrayDeque<T> (preferred) |
General FIFO |
Fastest, no null elements |
LinkedList<T> |
When null needed |
Slower due to GC |
PriorityQueue<T> |
Min/Max ordering |
NOT FIFO — ordering by priority |
ArrayBlockingQueue<T> |
Thread-safe bounded |
For multi-threaded use |
// ArrayDeque used as a QUEUE — add at tail, remove from head
Queue<Integer> queue = new ArrayDeque<>();
queue.offer(1); // enqueue to tail
queue.offer(2);
queue.offer(3);
// queue: [1, 2, 3] (1 is head)
queue.peek(); // → 1 (read head, no remove)
queue.poll(); // → 1 (remove head)
queue.peek(); // → 2
queue.size(); // → 2
queue.isEmpty(); // → false
// BFS template — bread-and-butter queue usage:
while (!queue.isEmpty()) {
int node = queue.poll();
// process node
// add neighbours: queue.offer(neighbour)
}
Deque — Sliding Window Maximum
A deque (double-ended queue) allows O(1) add/remove
from both ends. This enables a powerful pattern: keep a
monotonically decreasing deque of indices for an O(n)
sliding window maximum solution.
nums=[1,3,-1,-3,5,3,6,7], k=3 → max per window Windows: [1,3,-1]→3,
[3,-1,-3]→3, [-1,-3,5]→5, [-3,5,3]→5, [5,3,6]→6, [3,6,7]→7 Deque
(monotonically decreasing, stores indices): i=0, val=1: deque empty
→ addLast(0). deque=[0] (vals:[1]) i=1, val=3: 3>nums[0]=1 →
removeLast, addLast(1). deque=[1] (vals:[3]) i=2, val=-1:-1≤3→
addLast(2). deque=[1,2] (vals:[3,-1]) Window [0..2] done →
ans[0]=nums[deque.peek()]=nums[1]=3 ✓ i=3, val=-3: -3≤-1→
addLast(3). deque=[1,2,3](vals:[3,-1,-3]) i-deque.peek()=3-1=2=k-1 →
deque[0] expired (i-k+1=1, dq[0]=1≥1 ok) Wait: 3-k+1=1 ≤
deque.peek()=1 → still valid. ans[1]=3 ✓ ... and so on
public int[] maxSlidingWindow(int[] nums, int k) {
int[] res = new int[nums.length - k + 1];
Deque<Integer> dq = new ArrayDeque<>(); // stores indices, decreasing values
for (int i = 0; i < nums.length; i++) {
// 1. Remove indices outside the current window
if (!dq.isEmpty() && dq.peekFirst() < i - k + 1)
dq.pollFirst();
// 2. Remove indices with smaller values (they can never be max)
while (!dq.isEmpty() && nums[dq.peekLast()] < nums[i])
dq.pollLast();
dq.offerLast(i);
// 3. Window is full — record the max (front of deque)
if (i >= k - 1) res[i - k + 1] = nums[dq.peekFirst()];
}
return res;
}
// Each element enters and leaves the deque at most once → O(N) total
🔢 Expression Evaluation — Stack Arithmetic
Evaluating arithmetic expressions is a classic stack application.
The key idea: numbers go onto an operand stack; operators control
when to compute. Reverse Polish Notation (RPN / postfix) eliminates
the need for parentheses — operators come after their
operands.
RPN: ["2","1","+","3","*"] → (2+1)*3 = 9 Process token by token: "2"
→ push 2. stack=[2] "1" → push 1. stack=[2,1] "+" → pop 1, pop 2 →
2+1=3 → push 3. stack=[3] "3" → push 3. stack=[3,3] "*" → pop 3, pop
3 → 3*3=9 → push 9. stack=[9] Result = 9 ✓
public int evalRPN(String[] tokens) {
Deque<Integer> stack = new ArrayDeque<>();
for (String t : tokens) {
if ("+-*/".contains(t)) {
int b = stack.pop(), a = stack.pop(); // order matters!
switch (t) {
case "+": stack.push(a + b); break;
case "-": stack.push(a - b); break;
case "*": stack.push(a * b); break;
case "/": stack.push(a / b); break; // truncates toward zero
}
} else {
stack.push(Integer.parseInt(t));
}
}
return stack.pop();
}
🗺 Pattern Decision Guide
Balanced brackets / undo history → Plain Stack
Next greater/smaller, span problems → Monotonic Decreasing Stack
Histogram area, trapping rain water → Monotonic Increasing Stack
BFS / level-order traversal → Queue
Sliding window max/min in O(n) → Monotonic Deque
Priority scheduling, top-K → PriorityQueue (Heap)
Stack using queues / Queue using stacks → Design (two data
structures)
💪 Practice Problems
Problem
Given a string containing only '(',
')', '{', '}',
'[', ']', determine if the input
string is valid. Open brackets must be closed by the same type
bracket and in the correct order.
"()[]{}" → true "([)]" → false "{[]}" → true "[" → false
Think First
Push every open bracket. When you encounter a close bracket,
check if the stack's top is the matching open bracket — if
not, immediately return false. At the end, the stack must be
empty (every open was properly closed).
▶ Solution + Dry Run
public boolean isValid(String s) {
Deque<Character> stack = new ArrayDeque<>();
for (char c : s.toCharArray()) {
if (c == '(' || c == '[' || c == '{') {
stack.push(c);
} else {
if (stack.isEmpty()) return false;
char top = stack.pop();
if (c == ')' && top != '(') return false;
if (c == ']' && top != '[') return false;
if (c == '}' && top != '{') return false;
}
}
return stack.isEmpty();
}
// "{[]}" → push {, push [, ] matches [ pop, } matches { pop, stack empty → true ✓
Problem
Design a stack that supports push,
pop, top, and getMin —
all in O(1) time.
push(5)→push(3)→push(7)→getMin()=3→pop()→getMin()=3→pop()→getMin()=5
Think First
Run two parallel stacks: a main stack for values, and a
min-stack where each level records the
current minimum at that depth. When you push, also
push min(val, minStack.peek()). When you pop, pop
both. getMin() is always the top of the min-stack
— O(1).
▶ Solution + Trace
class MinStack {
Deque<Integer> stack = new ArrayDeque<>();
Deque<Integer> minStack = new ArrayDeque<>();
public void push(int val) {
stack.push(val);
int curMin = minStack.isEmpty() ? val : Math.min(val, minStack.peek());
minStack.push(curMin);
}
public void pop() { stack.pop(); minStack.pop(); }
public int top() { return stack.peek(); }
public int getMin() { return minStack.peek(); }
}
// push(5): stack=[5], min=[5]
// push(3): stack=[3,5], min=[3,5]
// push(7): stack=[7,3,5], min=[3,3,5] ← min stays 3
// getMin()=3 ✓ pop()→ stack=[3,5],min=[3,5] → getMin()=3 ✓
TimeO(1) all ops
SpaceO(N)
Problem
Given daily temperatures, return an array where
answer[i] is the number of days you have to wait
after day i to get a warmer temperature. If no
warmer day exists, put 0.
[73,74,75,71,69,72,76,73] → [1,1,4,2,1,1,0,0]
Think First
This is "Next Greater Element" — the answer for each day is
the distance (in indices) to its next greater temperature. Use
a monotonic decreasing stack of indices. When
a warmer day arrives, pop all cooler days and record the gap.
▶ Solution + Trace
public int[] dailyTemperatures(int[] temps) {
int[] ans = new int[temps.length];
Deque<Integer> stack = new ArrayDeque<>(); // indices
for (int i = 0; i < temps.length; i++) {
while (!stack.isEmpty() && temps[stack.peek()] < temps[i]) {
int j = stack.pop();
ans[j] = i - j; // days waited = index difference
}
stack.push(i);
}
return ans; // remaining on stack → default 0 (no warmer day)
}
// [73,74,...]: i=0 push 0. i=1 temp[1]=74>73 → pop 0, ans[0]=1-0=1 ✓
Problem
nums1 is a subset of nums2. For each
element in nums1, find its Next Great Element in
nums2. Return -1 if none exists.
nums1=[4,1,2], nums2=[1,3,4,2] → [-1,3,-1] (4 has no NGE in
nums2; 1's NGE is 3; 2 has no NGE)
Think First
Pre-compute NGE for every element in nums2 using
a monotonic stack and store the results in a HashMap
val → NGE. Then answer each query in O(1) from
the map. This pattern decouples the "compute" step from the
"query" step.
▶ Solution
public int[] nextGreaterElement(int[] nums1, int[] nums2) {
Map<Integer, Integer> nge = new HashMap<>();
Deque<Integer> stack = new ArrayDeque<>(); // values, decreasing
for (int n : nums2) {
while (!stack.isEmpty() && stack.peek() < n)
nge.put(stack.pop(), n);
stack.push(n);
}
// remaining on stack have no NGE → map will return null → default -1
int[] ans = new int[nums1.length];
for (int i = 0; i < nums1.length; i++)
ans[i] = nge.getOrDefault(nums1[i], -1);
return ans;
}
Problem
Given an array of bar heights (each width = 1), find the area
of the largest rectangle that can be formed within the
histogram.
[2,1,5,6,2,3] → 10 (bars of height 5 and 6, width 2)
Think First
Maintain a
monotonically increasing stack of bar indices. When a shorter bar arrives (the right boundary), pop the
taller bar and compute its maximum rectangle. The width is
determined by: left boundary = current stack top (after pop),
right boundary = current index. Append a sentinel height 0 at
the end to flush all remaining bars.
▶ Solution (see explanation
above)
public int largestRectangleArea(int[] heights) {
Deque<Integer> stack = new ArrayDeque<>();
int maxArea = 0, n = heights.length;
for (int i = 0; i <= n; i++) {
int curH = (i == n) ? 0 : heights[i]; // sentinel
while (!stack.isEmpty() && curH < heights[stack.peek()]) {
int h = heights[stack.pop()];
int w = stack.isEmpty() ? i : i - stack.peek() - 1;
maxArea = Math.max(maxArea, h * w);
}
stack.push(i);
}
return maxArea;
}
Problem
You are given an array and a sliding window of size
k moving from left to right. Return the maximum
value in each window position.
nums=[1,3,-1,-3,5,3,6,7], k=3 → [3,3,5,5,6,7]
Think First
Maintain a
monotonically decreasing deque of indices.
For each new element: ① Remove indices no longer in the window
(from front). ② Remove indices with smaller values from the
back (they can never be max while current is in window). ③ Add
current index to back. ④ The front is always the window
maximum. Each element is added/removed once → O(N).
▶ Solution (see explanation
above)
public int[] maxSlidingWindow(int[] nums, int k) {
int[] res = new int[nums.length - k + 1];
Deque<Integer> dq = new ArrayDeque<>();
for (int i = 0; i < nums.length; i++) {
if (!dq.isEmpty() && dq.peekFirst() < i - k + 1)
dq.pollFirst(); // expired
while (!dq.isEmpty() && nums[dq.peekLast()] < nums[i])
dq.pollLast(); // smaller, useless
dq.offerLast(i);
if (i >= k - 1) res[i - k + 1] = nums[dq.peekFirst()];
}
return res;
}
Problem
Evaluate the value of an arithmetic expression in Reverse
Polish Notation (postfix). Valid operators: +,
-, *, /. Each operand
may be an integer or another expression. Division truncates
toward zero.
["2","1","+","3","*"] → 9 ((2+1)*3) ["4","13","5","/","+"] → 6
(4+(13/5))
Think First
Process tokens left to right. If a number, push to stack. If
an operator, pop two numbers (order matters:
b = pop(), a = pop(), then compute
a op b) and push the result. The final stack
contains the answer.
▶ Solution (see explanation
above)
public int evalRPN(String[] tokens) {
Deque<Integer> stack = new ArrayDeque<>();
for (String t : tokens) {
if ("+-*/".contains(t)) {
int b = stack.pop(), a = stack.pop();
switch (t) {
case "+": stack.push(a+b); break;
case "-": stack.push(a-b); break;
case "*": stack.push(a*b); break;
case "/": stack.push(a/b); break;
}
} else stack.push(Integer.parseInt(t));
}
return stack.pop();
}
Problem
Design a circular queue (ring buffer) supporting
enQueue, deQueue,
Front, Rear, isEmpty,
isFull — all O(1).
Circular buffer of capacity 5: Indices: [0] [1] [2] [3] [4]
[A] [B] [C] [ ] [ ] ↑head ↑tail(next write) After deQueue():
head=1, B is front After enQueue(D): [A][B][C][D][ ], tail=4
After filling and deQueuing, tail wraps: tail = (tail+1) %
capacity
Think First
Use a fixed-size array, a head pointer, a
tail pointer, and a size counter.
Advance pointers using % capacity to wrap around.
isFull when size == capacity,
isEmpty when size == 0.
▶ Solution
class MyCircularQueue {
private int[] buf;
private int head = 0, tail = 0, size = 0, cap;
MyCircularQueue(int k) { buf = new int[k]; cap = k; }
public boolean enQueue(int val) {
if (isFull()) return false;
buf[tail] = val;
tail = (tail + 1) % cap;
size++;
return true;
}
public boolean deQueue() {
if (isEmpty()) return false;
head = (head + 1) % cap;
size--;
return true;
}
public int Front() { return isEmpty() ? -1 : buf[head]; }
public int Rear() { return isEmpty() ? -1 : buf[(tail-1+cap)%cap]; }
public boolean isEmpty() { return size == 0; }
public boolean isFull() { return size == cap; }
}
TimeO(1) all ops
SpaceO(K)
📝 Assignment
📋
Topic 12 Assignment — 25 Problems
Complete the assignment before moving to HashMap & HashSet.
Includes stack design problems, monotonic stack variants (Stock
Span, Trapping Rain Water), and BFS via Queue problems.
📄 Open Assignment →
✅ Lecture Completion Checklist
✓
I can implement a stack using ArrayDeque and know why to prefer it
over Stack<T>
✓
I can solve Valid Parentheses from memory in under 3 minutes
✓
I understand the Monotonic Stack pattern and can identify when to
use increasing vs decreasing
✓
I can solve Daily Temperatures (Next Greater Element pattern) in
O(N)
✓
I understand how the width calculation works in Largest Rectangle
in Histogram
✓
I can implement a Deque-based sliding window maximum solution in
O(N)
✓
I know the difference between Queue.offer() vs add() and
Queue.poll() vs remove()
✓
I can evaluate RPN expressions using a stack
✓
I can design a circular queue with wrap-around using modulo
arithmetic
✓
I can use the Pattern Decision Guide to choose the right
Stack/Queue variant for a problem
🧠
You're ready for Topic 13: HashMap & HashSet Deep
Dive
You've mastered LIFO/FIFO, monotonic patterns, and deque tricks.
HashMap is the most used data structure in FAANG interviews — it
underlies complement lookup, frequency counting, prefix sums, and
grouping. Everything from here gets faster with a HashMap.