Filter concepts by levelShowing all levels.

Data Structures & Algorithms · Section 11

Stacks

Level
beginner
Read
75 min
Concepts
8

The LIFO principle and why a Python list already is a stack, the canonical valid-parentheses problem, the monotonic stack pattern for next-greater/next-smaller, postfix expression evaluation, replacing recursion with an explicit stack, the min-stack trick for O(1) running minimum, and why only the end of a list is fast.

What is true here

  1. A stack serves the most recently pushed item first (LIFO) — the only directly reachable item is the top.
  2. Python's list is already a stack: append()/pop() at the end are both O(1); anything touching index 0 is O(n).
  3. A monotonic stack answers "next greater/smaller element" for every position in one O(n) pass, by popping whatever the current value invalidates.
  4. A parallel min-stack, pushed and popped in lockstep with the main stack, answers "what is the minimum right now?" in O(1).

What you will be able to do

  • Implement a stack using a Python list and explain why only the end is fast
  • Check whether brackets are balanced using a stack, including both failure cases
  • Apply the monotonic stack pattern to find the next greater or smaller element in O(n)
  • Build a min-stack that answers its running minimum in O(1), and explain why a single variable cannot

Concepts

From the LIFO principle and a list-backed implementation, through the canonical bracket-matching and monotonic-stack patterns, to replacing recursion with an explicit stack and tracking a running minimum in O(1).

LIFO principle

referencebeginner

A stack is Last-In-First-Out: the most recently added item is always the first one removed. Everything below it stays buried until it is.

lifo.pypython
stack = []
stack.append('a')
stack.append('b')
stack.append('c')

print(stack.pop())   # 'c' — the most recently pushed item, out first
print(stack)          # ['a', 'b'] — 'a' is still buried under 'b'

Remember: LIFO means the last thing pushed is the first thing popped — picture a stack of plates, not a line of people.

Was this helpful?

Implementing a stack with list (append/pop from the end)

standardbeginner

Python's list already is a stack — append() pushes onto the end, and pop() with no argument removes and returns the top, both in O(1).

Think of it as

Treat the end of the list as the top of the stack, never the front. append/pop() at the end touch only the last slot, with nothing to shift. pop(0) or insert(0, x) at the front would have to shift every remaining element over by one, which is why the front is never used for a Python list stack.

What we're doing: Push three values, peek the top, then pop them off in LIFO order.

list_stack.pypython
stack = []
stack.append(10)
stack.append(20)
stack.append(30)

print(stack[-1])
print(stack.pop())
print(stack.pop())
print(stack)
5
stack[-1] reads the top (30) without removing it — the stack is unchanged after this line.
6
pop() removes and returns 30, the most recently pushed value.
7
pop() again removes 20 — the next-most-recent push, now that 30 is gone.
Output
30
30
20
[10]

Why this works: 30 was pushed last, so it is both the peeked top and the first value popped. After two pops, only the first-pushed value, 10, remains.

Remember: A Python list already is a stack — append() to push, pop() to pop, and stack[-1] to peek. Never touch index 0.

Was this helpful?

Valid parentheses / balanced brackets

corebeginner

Scan left to right: push every opening bracket. On a closing bracket, it must match whatever is on top of the stack — if it does not, or the stack is empty, the string is not balanced.

Think of it as

The stack remembers, in order, which brackets are still open and waiting to be closed. A closing bracket can only close the most recently opened one still waiting — exactly what popping the top of a stack gives you. If the string is balanced, the stack is empty the moment the scan ends.

What we're doing: Check whether a string of brackets is balanced, using a stack to track what is still open.

valid_parentheses.pypython
def is_valid(s):
    pairs = {')': '(', ']': '[', '}': '{'}
    stack = []
    for ch in s:
        if ch in pairs:
            if not stack or stack.pop() != pairs[ch]:
                return False
        else:
            stack.append(ch)
    return not stack


print(is_valid("{[()]}"))
print(is_valid("{[(])}"))
5
ch is a closing bracket — it must match whatever the stack currently has on top.
6
not stack catches "nothing open to close"; stack.pop() != pairs[ch] catches "closes the wrong thing" — either one means not balanced.
Output
True
False

Why this works: "{[()]}" closes every bracket in the exact reverse order it opened them, so the stack ends empty. "{[(])}" closes "(" with "]" — a mismatch caught the moment ] is compared against the top of the stack.

Forgetting to check the stack is empty before popping

Wrong

python
def is_valid_wrong(s):
    pairs = {')': '(', ']': '[', '}': '{'}
    stack = []
    for ch in s:
        if ch in pairs:
            if stack.pop() != pairs[ch]:
                return False
        else:
            stack.append(ch)
    return not stack


print(is_valid_wrong(")"))

Better

python
def is_valid(s):
    pairs = {')': '(', ']': '[', '}': '{'}
    stack = []
    for ch in s:
        if ch in pairs:
            if not stack or stack.pop() != pairs[ch]:
                return False
        else:
            stack.append(ch)
    return not stack

What you see: is_valid_wrong(")") raises IndexError: pop from empty list, instead of returning False.

Why: A closing bracket with nothing open to match is exactly the "not balanced" case, not a program crash. `not stack` must be checked before popping, every time — `or` short-circuits, so pop() is only reached once the empty case has already been ruled out.

  • Valid ParentheseseasyLeetCode

    Exactly the example above — push opens, and on a close, the stack must be non-empty with a matching top.

  • Same push/pop-on-match shape, but the "match" is two equal characters in a row instead of a bracket pair.

Remember: Push opens. On a close, the stack must be non-empty and its top must be the matching open — a non-empty stack at the end means something was never closed.

Was this helpful?

Monotonic stack pattern (next greater/smaller element)

coreintermediate

Keep the stack always increasing or always decreasing by popping off elements that break the order before pushing the new one — each pop tells you the next greater (or smaller) element for whatever it pops.

Think of it as

Walk the array left to right, keeping a stack of indices whose "next greater element" is not yet known. Before pushing the current value, pop every index on top whose value is smaller than the current one — the current value IS their next greater element. What remains on the stack after each step is always in decreasing order, bottom to top.

What we're doing: For each number, find the next greater number to its right (or -1 if none exists), using a decreasing stack of indices.

monotonic_stack.pypython
def next_greater_elements(nums):
    result = [-1] * len(nums)
    stack = []
    for i, n in enumerate(nums):
        while stack and nums[stack[-1]] < n:
            result[stack.pop()] = n
        stack.append(i)
    return result


print(next_greater_elements([2, 1, 2, 4, 3]))
4
While the current value n is bigger than the value at the index on top of the stack, that index has found its answer.
5
result[stack.pop()] = n records n as the next greater element for the popped index, then discards that index — it no longer needs tracking.
Output
[4, 2, 4, -1, -1]

Why this works: For 2 (index 0) the next greater value is 4 (index 3). For 1 (index 1) it is 2 (index 2). For 2 (index 2) it is 4 (index 3). 4 (index 3) and 3 (index 4) have nothing greater to their right, so both stay -1.

Storing values instead of indices on the stack

Wrong

python
def next_greater_elements_wrong(nums):
    result = [-1] * len(nums)
    stack = []  # stores VALUES, not indices
    for n in nums:
        while stack and stack[-1] < n:
            popped = stack.pop()
            result[nums.index(popped)] = n  # always finds the FIRST matching value
        stack.append(n)
    return result


print(next_greater_elements_wrong([2, 1, 2, 4, 3]))

Better

python
def next_greater_elements(nums):
    result = [-1] * len(nums)
    stack = []
    for i, n in enumerate(nums):
        while stack and nums[stack[-1]] < n:
            result[stack.pop()] = n
        stack.append(i)
    return result

What you see: next_greater_elements_wrong([2, 1, 2, 4, 3]) returns [4, 2, -1, -1, -1] — index 2's answer (4) never gets filled in.

Why: nums.index(2) always resolves to index 0, the FIRST occurrence of the value 2 — so when the second 2 (at index 2) is popped, its answer overwrites index 0's slot again instead of filling index 2's. Storing indices instead of values sidesteps the ambiguity entirely.

  • Next Greater Element IeasyLeetCode

    Run the decreasing-stack scan on the full array once, then look up each queried value in the resulting map.

  • Daily TemperaturesmediumLeetCode

    Same pattern, but store the DISTANCE to the next greater index (i - stack.pop()) instead of the value itself.

Remember: Push while the stack still satisfies the order; the moment a new value breaks it, popping IS the answer for what got popped — each index is pushed and popped at most once, so the total work is O(n).

Was this helpful?

Evaluating expressions (postfix/infix, conceptually)

standardintermediate

Postfix (Reverse Polish) notation puts operators after their operands, so a stack evaluates it in one left-to-right pass: push numbers, and on an operator, pop the top two operands, combine them, and push the result back.

Think of it as

Infix ("3 + 4") needs precedence rules and parentheses to know what to compute first. Postfix ("3 4 +") needs none of that — by the time you reach an operator, both operands it needs are already the top two items on the stack, in the right order.

What we're doing: Evaluate a postfix expression using a stack.

eval_postfix.pypython
def eval_postfix(tokens):
    stack = []
    ops = {
        '+': lambda a, b: a + b,
        '-': lambda a, b: a - b,
        '*': lambda a, b: a * b,
        '/': lambda a, b: int(a / b),
    }
    for tok in tokens:
        if tok in ops:
            b = stack.pop()
            a = stack.pop()
            stack.append(ops[tok](a, b))
        else:
            stack.append(int(tok))
    return stack.pop()


print(eval_postfix(["3", "4", "+", "2", "*"]))
10
b is popped first — it is the operand closer to the operator, i.e. the SECOND one in the original expression.
11
a is popped second — the operand further from the operator, i.e. the FIRST one.
12
a OP b, in that order, matters for non-commutative operators like - and /.
Output
14

Why this works: Postfix "3 4 + 2 *" means (3 + 4) then multiply by 2: push 3, push 4, + combines them into 7, push 2, * combines 7 and 2 into 14 — the exact order a stack naturally processes them in.

Remember: Postfix needs no precedence rules because a stack already enforces the right order: pop two operands, combine, push the result — repeat until one value is left.

Was this helpful?

Using a stack to simulate recursion iteratively (e.g. DFS)

standardintermediate

Every recursive call already uses a stack — the call stack — to remember where to return to. Swapping in your own explicit stack (a list) turns the same traversal into a loop, with no risk of hitting Python's recursion limit.

Think of it as

A recursive call pushes a new frame onto the call stack and pops it on return. An explicit stack does the same job by hand — push what still needs visiting, pop it to visit it, and push whatever it reveals next (its children, its neighbors). The traversal ORDER is identical to the recursive version; only who manages the stack changes.

What we're doing: Traverse a tree depth-first using an explicit stack instead of recursive calls.

dfs_iterative.pypython
def dfs_iterative(root):
    if root is None:
        return []
    visited = []
    stack = [root]
    while stack:
        node = stack.pop()
        visited.append(node['value'])
        for child in reversed(node['children']):
            stack.append(child)
    return visited


tree = {
    'value': 1,
    'children': [
        {'value': 2, 'children': []},
        {'value': 3, 'children': [{'value': 4, 'children': []}]},
    ],
}
print(dfs_iterative(tree))
7
node is the most recently pushed item — exactly what a recursive call would be visiting right now.
9
reversed(...) pushes the LAST child first, so the FIRST child ends up on top and gets visited next.
Output
[1, 2, 3, 4]

Why this works: Pushing children in reverse order means the first child ends up on top of the stack and gets popped next — reproducing the same left-to-right visiting order recursion would give, just driven by an explicit stack instead of the call stack.

Remember: An explicit stack replaces the call stack, not the algorithm — push what still needs visiting, pop to visit it, and push children in reverse to keep the same left-to-right order recursion would give.

Was this helpful?

Min-stack (stack that tracks its own running minimum in O(1))

coreintermediate

A min-stack answers 'what is the smallest value currently in the stack?' in O(1) by keeping a second stack alongside the first, tracking the running minimum at every point in time.

Think of it as

Every time you push onto the main stack, also push the CURRENT minimum — the smaller of the new value and the previous minimum — onto a parallel min-stack. Popping pops both stacks together, so the top of the min-stack is always correct for whatever is left in the main stack, even after values are removed.

What we're doing: Push a sequence of values onto a min-stack and read the current minimum in O(1) at each step, including after popping the minimum off.

min_stack.pypython
class MinStack:
    def __init__(self):
        self.stack = []
        self.min_stack = []

    def push(self, val):
        self.stack.append(val)
        current_min = val if not self.min_stack else min(val, self.min_stack[-1])
        self.min_stack.append(current_min)

    def pop(self):
        self.stack.pop()
        self.min_stack.pop()

    def get_min(self):
        return self.min_stack[-1]


ms = MinStack()
ms.push(5)
ms.push(2)
ms.push(7)
print(ms.get_min())
ms.pop()
print(ms.get_min())
ms.pop()
print(ms.get_min())
8
current_min is the smaller of the new value and whatever the min-stack currently reports — never a full rescan of self.stack.
9
min_stack grows by exactly one entry on every push, in lockstep with the main stack.
Output
2
2
5

Why this works: After pushing 5, 2, 7 the minimum is 2. Popping 7 leaves [5, 2] — the minimum is still 2, since 2 is still in the stack. Popping again leaves only [5], and get_min() correctly reports 5 — the discarded minimum (2) never lingers, because its min_stack entry was popped at the exact same moment.

Tracking one running minimum instead of a full parallel stack

Wrong

python
class MinStackWrong:
    def __init__(self):
        self.stack = []
        self.min_val = None

    def push(self, val):
        self.stack.append(val)
        if self.min_val is None or val < self.min_val:
            self.min_val = val

    def pop(self):
        self.stack.pop()

    def get_min(self):
        return self.min_val


ms = MinStackWrong()
ms.push(5)
ms.push(2)
ms.pop()
print(ms.get_min())

Better

python
class MinStack:
    def __init__(self):
        self.stack = []
        self.min_stack = []

    def push(self, val):
        self.stack.append(val)
        current_min = val if not self.min_stack else min(val, self.min_stack[-1])
        self.min_stack.append(current_min)

    def pop(self):
        self.stack.pop()
        self.min_stack.pop()

    def get_min(self):
        return self.min_stack[-1]

What you see: After push(5), push(2), pop() — leaving only 5 in the stack — get_min() still returns 2, the value that was just removed, instead of 5.

Why: A single min_val variable only ever moves down, never back up, because pop() has no idea whether the value it just removed WAS the minimum. The parallel min_stack fixes this by remembering the minimum at every point in time, so popping automatically reveals the correct prior minimum.

  • Min StackmediumLeetCode

    Exactly the class above — a parallel stack tracking the running minimum alongside the main one.

  • Baseball GameeasyLeetCode

    Not about minimums, but the same shape: a stack whose top is read and modified as new tokens arrive, one at a time.

Remember: Push the running minimum onto a parallel stack alongside every value — popping both together means the min-stack's top is always correct for whatever remains, in O(1), with no rescan.

Was this helpful?

Why appending/popping from the end of a Python list is O(1) but from the front is O(n)

referencebeginner

A Python list is stored as one contiguous block of memory. Adding or removing at the end just touches the last slot; adding or removing at the front has to shift every remaining element over by one.

front_vs_end.pypython
lst = [10, 20, 30, 40]

lst.append(50)      # O(1) — touches only the last slot
lst.pop()             # O(1) — touches only the last slot

lst.insert(0, 5)      # O(n) — shifts every existing element right by one
lst.pop(0)             # O(n) — shifts every remaining element left by one

Remember: list only gets O(1) at the end — the moment code touches index 0 (insert(0, ...), pop(0)), the cost becomes O(n), which is why deque exists for anything needing fast operations on both ends.

Was this helpful?

Spotted a mistake on this page? Tell us.