Stack Fundamentals
Stack = LIFO (Last In, First Out) data structure
Push 10, 20, 30:
┌────┐
│ 30 │ ← top (last pushed)
├────┤
│ 20 │
├────┤
│ 10 │ ← bottom (first pushed)
└────┘
Pop → returns 30 (most recently pushed)
┌────┐
│ 20 │ ← top
├────┤
│ 10 │
└────┘
| Operation | Action | Time |
push | Add to top | O(1) |
pop | Remove from top | O(1) |
peek | View top without removing | O(1) |
Real-world analogies:
Plate stack — take from top, add to top
Browser back — most recent page popped first
Undo (Ctrl+Z) — last action reversed first
Function calls — deepest call returns first
- LIFO: the element added most recently is removed first — the defining property of a stack
- 3 core operations — push, pop, peek — all
O(1)
- The constraint is the strength: restricting access to one end enables elegant solutions
Array-based stack:
Index: [0] [1] [2] [3] [4] [5] [6] [7]
Value: 10 20 30 40 50 _ _ _
↑ top capacity=8, size=5
Linked-list stack:
head → [50] → [40] → [30] → [20] → [10] → null
↑ top push/pop at head = O(1)
| Aspect | Array-based | Linked-list |
| Memory | Compact, contiguous | Extra pointer per node |
| Cache | Excellent locality | Poor (scattered nodes) |
| Resize | Amortized O(1), occasional copy | Never needed |
- Arrays are ideal for stacks — push/pop happen at one end only, no shifting needed
- Array-based push is
O(1) amortized — occasional resize copies all elements
- Excellent cache locality — contiguous memory means CPU prefetching works well
| Type | Backing | Notes |
ArrayDeque | Resizable array | Recommended — Kotlin stdlib, fast |
java.util.ArrayDeque | Resizable array | Java version, also good |
java.util.Stack | Vector (synchronized) | Avoid — legacy, synchronized overhead |
MutableList | ArrayList | Works via add/removeLast |
LinkedList | Doubly linked list | Poor cache locality |
// ⚠ DON'T — java.util.Stack is legacy & synchronized
val bad = java.util.Stack<Int>()
bad.push(1) // synchronized — unnecessary overhead
bad.pop() // Vector-based — allows random access
// ✓ DO — Kotlin ArrayDeque
val stack = ArrayDeque<Int>()
stack.addLast(1) // push
stack.removeLast() // pop
stack.last() // peek
ArrayDeque is the primary choice for stacks in Kotlin — fast, unsynchronized, no legacy baggage
java.util.Stack is legacy — synchronized, extends Vector, allows random access. Never use it
Core Operations
// ── Empty stack ──
val stack = ArrayDeque<Int>()
// ── From a list ──
val stack = ArrayDeque(listOf(1, 2, 3)) // 3 is top
// ── From a range ──
val stack = ArrayDeque((1..5).toList()) // [1,2,3,4,5]
// ── Typed wrapper class ──
class Stack<T> {
private val data = ArrayDeque<T>()
fun push(item: T) = data.addLast(item)
fun pop(): T = data.removeLast()
fun peek(): T = data.last()
fun isEmpty() = data.isEmpty()
val size get() = data.size
}
addLast = push, removeLast = pop, last() = peek
- Wrapping ArrayDeque in a Stack class enforces LIFO discipline — prevents accidental random access
val stack = ArrayDeque<Int>()
// ── Push ──
stack.addLast(10) // [10]
stack.addLast(20) // [10, 20]
stack.addLast(30) // [10, 20, 30]
// ── Peek (view top) ──
stack.last() // 30 — throws if empty
stack.lastOrNull() // 30 — null if empty (safe)
// ── Pop (remove top) ──
stack.removeLast() // 30 — throws if empty
stack.removeLastOrNull() // 20 — null if empty (safe)
// ── Safe patterns ──
if (stack.isNotEmpty()) stack.removeLast()
stack.removeLastOrNull() ?: defaultValue
- Always use null-safe variants (
lastOrNull, removeLastOrNull) or check isEmpty() first
removeLast() and last() throw NoSuchElementException on empty stacks
// ── Drain (pop all elements) ──
while (stack.isNotEmpty()) {
val item = stack.removeLast()
// process item (LIFO order)
}
// ── Iterate without modifying ──
for (item in stack) { } // bottom → top order
// ── Reversed iteration (top → bottom) ──
for (item in stack.reversed()) { }
// ── Convert to list ──
stack.toList() // bottom → top
stack.reversed() // top → bottom
stack.joinToString() // "10, 20, 30"
- Drain pattern (
while isNotEmpty removeLast) processes elements in LIFO order
- Direct iteration goes bottom-to-top; use
reversed() for top-to-bottom
// ── Array-based Stack ──
class ArrayStack<T> {
private var data = arrayOfNulls<Any>(8)
private var size = 0
fun push(item: T) {
if (size == data.size) resize()
data[size++] = item
}
fun pop(): T {
if (size == 0) throw NoSuchElementException()
val item = data[--size] as T
data[size] = null // prevent memory leak
return item
}
fun peek(): T = data[size - 1] as T
private fun resize() {
data = data.copyOf(data.size * 2)
}
}
// ── Linked-list Stack ──
class LinkedStack<T> {
private class Node<T>(val value: T, val next: Node<T>?)
private var head: Node<T>? = null
fun push(item: T) { head = Node(item, head) }
fun pop(): T {
val node = head ?: throw NoSuchElementException()
head = node.next
return node.value
}
fun peek(): T = head?.value ?: throw NoSuchElementException()
}
| Aspect | ArrayStack | LinkedStack |
| push | O(1) amortized | O(1) worst case |
| pop | O(1) | O(1) |
| Memory per element | ~4-8 bytes | ~24-32 bytes |
| Cache locality | Excellent | Poor |
Algorithm Patterns
Monotonic Stack Visualizer → see Arrays page for interactive monotonic stack visualization |
Stack via Linked List → see Linked Lists page for linked-list-based stack implementation
// ── Valid parentheses with multiple bracket types ──
fun isValid(s: String): Boolean {
val pairs = mapOf(')' to '(', ']' to '[', '}' to '{')
val stack = ArrayDeque<Char>()
for (ch in s) {
if (ch in pairs.values) {
stack.addLast(ch)
} else if (ch in pairs) {
if (stack.isEmpty() || stack.removeLast() != pairs[ch])
return false
}
}
return stack.isEmpty()
}
// ── Longest valid parentheses ──
fun longestValidParentheses(s: String): Int {
val stack = ArrayDeque<Int>()
stack.addLast(-1) // sentinel
var maxLen = 0
for (i in s.indices) {
if (s[i] == '(') {
stack.addLast(i)
} else {
stack.removeLast()
if (stack.isEmpty()) stack.addLast(i)
else maxLen = maxOf(maxLen, i - stack.last())
}
}
return maxLen
}
When to Use
bracket matchingWhy: Push openers, pop when closer matches top. If mismatch or stack not empty at end, invalid. Classic stack problem.
longest valid parenthesesWhy: Push indices onto stack. Use a sentinel index (-1) to calculate lengths. When matched, length = i - stack.top.
minimum removalsWhy: Track unmatched positions in a stack. After processing, stack size = number of removals needed.
HTML tag matchingWhy: Push opening tags, pop when closing tag matches. Same principle as bracket matching but with string tags.
nested structure validationWhy: Any nested open/close structure maps to the parentheses pattern. Push on open, pop on close, validate matches.
- Push openers, pop when closer matches top — stack empty at end = valid
- Index-based variant tracks positions for length/removal calculations
- HashMap of pairs makes multi-bracket matching clean and extensible
▶ Visualize
Two parallel stacks:
Main stack: [3, 5, 2, 1, 4]
Min stack: [3, 3, 2, 1, 1]
↑ top
getMin() → min stack top → 1
Pop 4 → min stack also pops → getMin() = 1
Pop 1 → min stack also pops → getMin() = 2
class MinStack {
private val stack = ArrayDeque<Int>()
private val minStack = ArrayDeque<Int>()
fun push(x: Int) {
stack.addLast(x)
val curMin = if (minStack.isEmpty()) x
else minOf(x, minStack.last())
minStack.addLast(curMin)
}
fun pop() {
stack.removeLast()
minStack.removeLast()
}
fun top(): Int = stack.last()
fun getMin(): Int = minStack.last()
}
When to Use
O(1) min retrievalWhy: Parallel min stack tracks the minimum at each level. Push mirrors main stack with min(current, prevMin). Pop both together.
max stack variantWhy: Same pattern with max instead of min. Parallel max stack tracks running maximum at each level.
track running sumWhy: Generalize to any associative operation — sum, GCD, etc. Maintain a parallel stack with the running aggregate.
- Parallel min stack mirrors the main stack — each entry stores the minimum up to that level
- Alternative: store pairs
(value, currentMin) in a single stack
- All operations remain
O(1) — the extra stack doubles space but not time
▶ Visualize
Trace: ["2", "1", "+", "3", "*"]
Token Stack
"2" [2]
"1" [2, 1]
"+" pop 1,2 → 2+1=3 → [3]
"3" [3, 3]
"*" pop 3,3 → 3*3=9 → [9]
Result = 9
fun evalRPN(tokens: Array<String>): Int {
val stack = ArrayDeque<Int>()
for (token in tokens) {
when (token) {
"+", "-", "*", "/" -> {
val b = stack.removeLast()
val a = stack.removeLast()
stack.addLast(when (token) {
"+" -> a + b
"-" -> a - b
"*" -> a * b
else -> a / b // truncate toward zero
})
}
else -> stack.addLast(token.toInt())
}
}
return stack.last()
}
When to Use
postfix evaluationWhy: Numbers push, operators pop two operands and push result. No parentheses or precedence needed — order is encoded in the sequence.
calculator implementationWhy: Convert infix to postfix (Shunting Yard), then evaluate with a stack. Clean separation of parsing and evaluation.
expression parsingWhy: Stack-based evaluation handles arbitrary expression depth naturally. Each operator consumes exactly two operands.
- Numbers push, operators pop two and push result
- Order matters: pop
b then a, compute a op b (not b op a)
- Final stack contains exactly one element — the result
▶ Visualize
Decreasing stack → finds next greater element
nums = [2, 1, 4, 3]
i=0: stack=[] push 0 → [0]
i=1: 1 < 2 push 1 → [0, 1]
i=2: 4 > 1 → pop 1, result[1]=4
4 > 2 → pop 0, result[0]=4
push 2 → [2]
i=3: 3 < 4 push 3 → [2, 3]
result = [4, 4, -1, -1]
Increasing stack → finds next smaller element
// ── Next Greater Element ──
fun nextGreaterElement(nums: IntArray): IntArray {
val result = IntArray(nums.size) { -1 }
val stack = ArrayDeque<Int>() // indices
for (i in nums.indices) {
while (stack.isNotEmpty() && nums[stack.last()] < nums[i]) {
result[stack.removeLast()] = nums[i]
}
stack.addLast(i)
}
return result
}
// ── Next Smaller Element ──
fun nextSmallerElement(nums: IntArray): IntArray {
val result = IntArray(nums.size) { -1 }
val stack = ArrayDeque<Int>()
for (i in nums.indices) {
while (stack.isNotEmpty() && nums[stack.last()] > nums[i]) {
result[stack.removeLast()] = nums[i]
}
stack.addLast(i)
}
return result
}
When to Use
next greater elementWhy: Monotonic stack maintains sorted invariant. When an element violates the invariant, pop and assign — each element pushed and popped at most once → O(n).
daily temperaturesWhy: Decreasing stack of indices. When a warmer day appears, pop all cooler days and compute distance.
stock spanWhy: Decreasing stack tracks previous greater element. Span = distance to that element.
largest rectangle in histogramWhy: Increasing stack finds left and right boundaries for each bar — the first smaller bar on each side.
- Each element pushed and popped at most once =
O(n) total
- Decreasing stack → next greater; increasing stack → next smaller
- Store indices (not values) to compute distances and positions
▶ Visualize
Histogram:
heights = [2, 1, 5, 6, 2, 3]
█
█ █
█ █
█ █ █ █
█ █ █ █ █ █
─────────────
2 1 5 6 2 3
Largest rectangle = 5×2 = 10 (bars at indices 2,3)
fun largestRectangleArea(heights: IntArray): Int {
val stack = ArrayDeque<Int>() // indices
var maxArea = 0
val h = heights + 0 // sentinel triggers final cleanup
for (i in h.indices) {
while (stack.isNotEmpty() && h[stack.last()] > h[i]) {
val height = h[stack.removeLast()]
val width = if (stack.isEmpty()) i
else i - stack.last() - 1
maxArea = maxOf(maxArea, height * width)
}
stack.addLast(i)
}
return maxArea
}
When to Use
largest rectangle in histogramWhy: Monotonic increasing stack. When a shorter bar is encountered, pop taller bars and compute area. Sentinel at end forces final cleanup.
maximal rectangle (row heights)Why: Treat each row as a histogram base. Build heights row by row, apply histogram algorithm per row. O(rows × cols).
- Monotonic increasing stack — pop when a shorter bar appears
- Sentinel 0 appended at end triggers final cleanup of remaining bars
- Width =
i - stack.last() - 1 (distance between current index and new stack top)
▶ Visualize
Bars with trapped water:
height = [0, 1, 0, 2, 1, 0, 1, 3, 2, 1, 2, 1]
█
█ ~ ~ █ █ ~ █
█ ~ █ █ ~ █ █ █ ~ █
────────────────────
0 1 0 2 1 0 1 3 2 1 2 1
Total trapped water = 6
fun trap(height: IntArray): Int {
val stack = ArrayDeque<Int>() // indices
var water = 0
for (i in height.indices) {
while (stack.isNotEmpty() && height[stack.last()] < height[i]) {
val bottom = height[stack.removeLast()]
if (stack.isEmpty()) break
val width = i - stack.last() - 1
val bounded = minOf(height[i], height[stack.last()]) - bottom
water += width * bounded
}
stack.addLast(i)
}
return water
}
When to Use
trapping rain waterWhy: Decreasing stack. When a taller bar appears, pop and calculate water trapped between current bar and the bar below the popped one. Width × bounded height.
container with most waterWhy: Two-pointer approach is more common, but stack-based thinking helps understand the bounding walls concept.
- Decreasing stack — water is trapped between the current bar and the bar below the popped top
- Water per layer =
width × min(left, right heights) - bottom height
- Alternative: two-pointer approach uses
O(1) space but stack approach generalizes to 2D
▶ Visualize
fun dailyTemperatures(temps: IntArray): IntArray {
val result = IntArray(temps.size)
val stack = ArrayDeque<Int>() // indices
for (i in temps.indices) {
while (stack.isNotEmpty() && temps[stack.last()] < temps[i]) {
val j = stack.removeLast()
result[j] = i - j
}
stack.addLast(i)
}
return result
}
When to Use
days until warmerWhy: Classic monotonic decreasing stack. Push indices of unresolved days. When a warmer day appears, pop and compute distance.
next event distanceWhy: Generalization: any "how far until condition X" maps to a monotonic stack with distance calculation.
- Classic monotonic decreasing stack problem
result[j] = i - j — distance between current warmer day and the popped cooler day
- Unresolved indices remain on stack with result 0 (no warmer day exists)
▶ Visualize
fun simplifyPath(path: String): String {
val stack = ArrayDeque<String>()
for (part in path.split("/")) {
when (part) {
".." -> if (stack.isNotEmpty()) stack.removeLast()
".", "" -> { } // skip
else -> stack.addLast(part)
}
}
return "/" + stack.joinToString("/")
}
When to Use
Unix path simplificationWhy: Split by "/", push directory names, ".." pops (go up), "." and "" are no-ops. Join stack with "/" for canonical path.
directory navigationWhy: Stack naturally models directory hierarchy — push to descend, pop to ascend.
- Split by
"/", push directory names, ".." pops, "." and "" skip
- Result =
"/" + stack.joinToString("/")
- Edge case:
".." at root does nothing (stack is already empty)
▶ Visualize
// "3[a2[c]]" → "accaccacc"
fun decodeString(s: String): String {
val countStack = ArrayDeque<Int>()
val stringStack = ArrayDeque<StringBuilder>()
var current = StringBuilder()
var k = 0
for (ch in s) {
when {
ch.isDigit() -> k = k * 10 + (ch - '0')
ch == '[' -> {
countStack.addLast(k)
stringStack.addLast(current)
current = StringBuilder()
k = 0
}
ch == ']' -> {
val decoded = stringStack.removeLast()
val count = countStack.removeLast()
repeat(count) { decoded.append(current) }
current = decoded
}
else -> current.append(ch)
}
}
return current.toString()
}
When to Use
decode encoded stringWhy: Two stacks (counts + strings) handle nesting. '[' pushes current state, ']' pops and repeats. Natural recursive structure flattened to iterative.
nested repetitionWhy: Each '[' starts a new scope. Stack depth = nesting level. Inner patterns resolve first (LIFO matches nesting).
'[' pushes current state (count + string built so far), ']' pops and repeats
- Two stacks: counts (repetition factors) and strings (partial results)
- Handles arbitrary nesting via stack depth — inner patterns resolve first
▶ Visualize
// ── Calculator I: +, -, parentheses ──
fun calculate(s: String): Int {
val stack = ArrayDeque<Int>()
var result = 0; var num = 0; var sign = 1
for (ch in s) {
when {
ch.isDigit() -> num = num * 10 + (ch - '0')
ch == '+' || ch == '-' -> {
result += sign * num; num = 0
sign = if (ch == '+') 1 else -1
}
ch == '(' -> {
stack.addLast(result)
stack.addLast(sign)
result = 0; sign = 1
}
ch == ')' -> {
result += sign * num; num = 0
result *= stack.removeLast() // sign
result += stack.removeLast() // prev result
}
}
}
return result + sign * num
}
// ── Calculator II: +, -, *, / (no parens) ──
fun calculateII(s: String): Int {
val stack = ArrayDeque<Int>()
var num = 0; var op = '+'
for ((i, ch) in s.withIndex()) {
if (ch.isDigit()) num = num * 10 + (ch - '0')
if ((!ch.isDigit() && ch != ' ') || i == s.lastIndex) {
when (op) {
'+' -> stack.addLast(num)
'-' -> stack.addLast(-num)
'*' -> stack.addLast(stack.removeLast() * num)
'/' -> stack.addLast(stack.removeLast() / num)
}
op = ch; num = 0
}
}
return stack.fold(0) { acc, v -> acc + v }
}
When to Use
calculator i (+ - parens)Why: '(' pushes current result + sign onto stack, ')' pops and combines. Parentheses create scopes that the stack manages naturally.
calculator ii (* / precedence)Why: * and / process immediately (pop, compute, push). + and - defer by pushing. Final sum of stack gives result. Higher precedence = immediate processing.
- Sign tracking:
'(' pushes result + sign, ')' pops and applies
- Calculator II:
* and / process immediately; + and - defer to final sum
- Combine both for a full calculator with parentheses and precedence
▶ Visualize
fun asteroidCollision(asteroids: IntArray): IntArray {
val stack = ArrayDeque<Int>()
for (ast in asteroids) {
var alive = true
while (alive && ast < 0 &&
stack.isNotEmpty() && stack.last() > 0) {
if (stack.last() < -ast) {
stack.removeLast() // top explodes
} else if (stack.last() == -ast) {
stack.removeLast() // both explode
alive = false
} else {
alive = false // current explodes
}
}
if (alive) stack.addLast(ast)
}
return stack.toIntArray()
}
When to Use
asteroid collisionWhy: Stack simulates collisions: positive = right, negative = left. Collision only when top is positive and current is negative. Compare absolute values to determine survivor.
particle collision simulationWhy: Any problem where elements moving in opposite directions cancel each other. Stack tracks surviving elements.
- Collision only when stack top > 0 and current < 0 (moving toward each other)
- Compare absolute values: larger survives, equal both explode
- Non-colliding asteroids (same direction, or left-then-right) just push onto stack
▶ Visualize
fun removeKdigits(num: String, k: Int): String {
var remaining = k
val stack = ArrayDeque<Char>()
for (digit in num) {
while (remaining > 0 && stack.isNotEmpty() &&
stack.last() > digit) {
stack.removeLast()
remaining--
}
stack.addLast(digit)
}
// Remove remaining from end
repeat(remaining) { stack.removeLast() }
// Trim leading zeros
while (stack.isNotEmpty() && stack.first() == '0') {
stack.removeFirst()
}
return if (stack.isEmpty()) "0"
else stack.joinToString("")
}
When to Use
remove k digits (smallest number)Why: Monotonic increasing stack. Greedily remove digits that are greater than the next digit — this produces the smallest possible number.
smallest subsequence of length n-kWhy: Generalization: build the lexicographically smallest/largest sequence by greedily maintaining monotonic order with a removal budget.
- Monotonic increasing stack — greedily remove digits greater than the next one
- If removals remain after scanning, remove from the end (largest digits in increasing sequence)
- Trim leading zeros — essential post-processing step
▶ Visualize
// ── Iterative DFS for graph ──
fun dfsIterative(graph: Map<Int, List<Int>>, start: Int): List<Int> {
val visited = mutableSetOf<Int>()
val stack = ArrayDeque<Int>()
val result = mutableListOf<Int>()
stack.addLast(start)
while (stack.isNotEmpty()) {
val node = stack.removeLast()
if (node in visited) continue
visited.add(node)
result.add(node)
for (neighbor in (graph[node] ?: emptyList()).reversed()) {
if (neighbor !in visited) stack.addLast(neighbor)
}
}
return result
}
// ── Iterative inorder traversal for tree ──
fun inorderIterative(root: TreeNode?): List<Int> {
val result = mutableListOf<Int>()
val stack = ArrayDeque<TreeNode>()
var curr = root
while (curr != null || stack.isNotEmpty()) {
while (curr != null) {
stack.addLast(curr)
curr = curr.left
}
curr = stack.removeLast()
result.add(curr.`val`)
curr = curr.right
}
return result
}
When to Use
iterative preorder dfsWhy: Explicit stack replaces the call stack. Avoids StackOverflowError on deep graphs/trees. Same time complexity as recursive DFS.
iterative inorder dfsWhy: JVM default stack depth ~5000-10000 frames. For trees with depth > 5000, iterative DFS is required.
dfs on deep chain (avoid stack overflow)Why: Heap memory is much larger than stack memory. Explicit stack uses heap, supporting millions of elements.
- Recursion = implicit stack — any recursive DFS can be made iterative with an explicit stack
- For graphs: push neighbors in reversed order so left/first neighbor is processed first
- Inorder tree traversal: go left as far as possible, then process node, then go right
▶ Visualize
// ── Batch version ──
fun stockSpan(prices: IntArray): IntArray {
val result = IntArray(prices.size)
val stack = ArrayDeque<Int>() // indices
for (i in prices.indices) {
while (stack.isNotEmpty() &&
prices[stack.last()] <= prices[i]) {
stack.removeLast()
}
result[i] = if (stack.isEmpty()) i + 1
else i - stack.last()
stack.addLast(i)
}
return result
}
// ── Online version (streaming) ──
class StockSpanner {
private val stack = ArrayDeque<Pair<Int, Int>>() // (price, span)
fun next(price: Int): Int {
var span = 1
while (stack.isNotEmpty() && stack.last().first <= price) {
span += stack.removeLast().second
}
stack.addLast(price to span)
return span
}
}
When to Use
stock spanWhy: Monotonic decreasing stack. Span = number of consecutive days with price ≤ today's price. Pop all smaller/equal, span = distance to remaining top.
consecutive lower/equal daysWhy: Same as stock span — the popped elements represent consecutive days that don't exceed the current price.
previous greater elementWhy: The element remaining on top of the stack after popping is the previous greater element. Span = distance to it.
- Monotonic decreasing stack — pop all elements ≤ current price
- Span =
current index - previous greater element's index
- Online variant stores (price, span) pairs — no need for index tracking
▶ Visualize
Common Pitfalls
// ⚠ Empty stack exception
stack.removeLast() // 💥 NoSuchElementException if empty
stack.last() // 💥 same
// RIGHT:
stack.removeLastOrNull()
stack.lastOrNull()
if (stack.isNotEmpty()) stack.removeLast()
// ⚠ Wrong end of ArrayDeque
stack.addFirst(x) // 💥 This is queue behavior, not stack!
stack.removeFirst() // 💥 This is dequeue, not pop!
// RIGHT: addLast/removeLast for stack
// ⚠ java.util.Stack instead of ArrayDeque
val bad = java.util.Stack<Int>()
bad[2] // 💥 random access — breaks abstraction
// RIGHT: Kotlin ArrayDeque
// ⚠ Stack overflow with deep recursion
fun deep(n: Int) { if (n > 0) deep(n - 1) }
deep(100000) // 💥 StackOverflowError
// RIGHT: convert to iterative with explicit stack
// ⚠ Forgetting sentinel in monotonic problems
// Without sentinel, elements remaining on stack
// are never processed (e.g., largest rectangle)
// RIGHT: append sentinel value (0 or Int.MIN_VALUE)
// ⚠ Confusing stack order with output order
// Stack iteration = bottom → top
// Stack drain (pop all) = top → bottom
// RIGHT: use reversed() if you need top → bottom list
// ⚠ Memory leak in array-based stack
// After pop, old reference still in array
data[size] = null // RIGHT: null out after pop
- Always use null-safe variants (
removeLastOrNull, lastOrNull) or check isEmpty() first
addLast/removeLast for stack behavior in Kotlin ArrayDeque
- Avoid
java.util.Stack — use Kotlin ArrayDeque instead
- Convert deep recursion to iterative with explicit stack to prevent
StackOverflowError
Quick Reference
| Operation |
Array-Based |
Linked-List-Based |
| push | O(1) amortized | O(1) |
| pop | O(1) | O(1) |
| peek | O(1) | O(1) |
| isEmpty | O(1) | O(1) |
| size | O(1) | O(1) |
| search / contains | O(n) | O(n) |
| clear | O(n) | O(1) |
- Kotlin
ArrayDeque: addLast = push, removeLast = pop, last() = peek
- Null-safe:
lastOrNull(), removeLastOrNull()
- Array-based clear is O(n) due to nulling references; linked-list clear can just drop the head
| Algorithm |
Time |
Space |
| Valid Parentheses | O(n) | O(n) |
| Min Stack | O(1) per op | O(n) |
| Evaluate RPN | O(n) | O(n) |
| Monotonic Stack | O(n) | O(n) |
| Largest Rectangle | O(n) | O(n) |
| Trapping Rain Water | O(n) | O(n) |
| Daily Temperatures | O(n) | O(n) |
| Simplify Path | O(n) | O(n) |
| Decode String | O(n×maxK) | O(n) |
| Basic Calculator | O(n) | O(n) |
| Asteroid Collision | O(n) | O(n) |
| Remove K Digits | O(n) | O(n) |
| DFS (Explicit Stack) | O(V+E) | O(V) |
| Stock Span | O(n) | O(n) |
| If You See... | Try This |
| "valid parentheses" / "brackets" | Push/pop matching |
| "next greater/smaller element" | Monotonic stack |
| "daily temperatures" / "stock span" | Monotonic stack + distance |
| "largest rectangle" / "histogram" | Monotonic increasing stack |
| "trapping rain water" | Monotonic decreasing stack |
| "evaluate expression" / "calculator" | Operator stack / RPN |
| "decode string" / "nested encoding" | Two stacks (counts + strings) |
| "simplify path" | Stack as directory history |
| "O(1) min/max with stack" | Parallel min/max stack |
| "DFS iterative" / "avoid recursion" | Explicit stack DFS |
| "asteroid collision" / "cancellation" | Simulation stack |
| "remove digits" / "smallest number" | Monotonic stack greedy |
1. Matching
Parentheses, brackets, tags — push opener, pop on closer
2. Monotonic
Next greater/smaller, histogram, temperatures, span
3. Expression Evaluation
RPN, calculator, decode string — operand/operator stacks
4. Simulation
Asteroid collision, remove digits — stack models state
5. Recursion Elimination
DFS, tree traversal — explicit stack replaces call stack
- Matching: anything with open/close pairs — stack ensures correct nesting
- Monotonic: "next greater/smaller" or "bounded area" — O(n) via push/pop once each
- Expression: nested structures with operators — stacks manage scope and precedence
- Simulation: elements interact in LIFO order — stack tracks surviving state
- Recursion elimination: any DFS or recursive algorithm can use an explicit stack