Hi👋SpeedAlgo • Deliberate Practice & Cognitive Ergonomics for Software Engineers

An interactive algorithm mastery and technical interview preparation platform published by Hi👋WebEnterprise. Built for senior and staff software engineers preparing for rigorous coding screens at top tech companies (FAANG/MAMAA).

13 Core Algorithmic Patterns & 225 Practice Problems

  • 1. Two Pointers (16 Paradigms, 43 Problems): Opposite ends, sorted pair sums, palindrome verification, trapping rain water, 3Sum, container with most water — includes the Sliding Window and Fast & Slow Pointers paradigms (contiguous subarray invariants, longest substrings, Floyd's cycle detection, linked list middle).
  • 2. Binary Search (9 Paradigms, 14 Problems): Monotonic predicate partitioning (feasible(x)), boundary search, rotated sorted arrays, square roots, minimum ship capacity, median of two sorted arrays, matrix median on value range.
  • 3. Bit Manipulation (5 Paradigms, 11 Problems): Counting set bits, XOR cancellation, bit reversal, bitmask pairing, maximum XOR with a bitwise trie.
  • 4. Math & Geometry (8 Paradigms, 14 Problems): Sieve of Eratosthenes, integer/roman conversions, modular arithmetic, geometric simulation.
  • 5. Tree/Graph Depth-First Search (15 Paradigms, 24 Problems): Path sums, lowest common ancestor, tree diameter, validating BSTs, tree DP (House Robber III, Binary Tree Cameras).
  • 6. Tree/Graph Breadth-First Search (5 Paradigms, 12 Problems): Level-order traversals, shortest path in unweighted graphs, zig-zag traversals, rotting oranges, word ladders.
  • 7. Graphs (12 Paradigms, 22 Problems): Topological sort, cycle detection, Dijkstra's algorithm, bipartite graph validation, network delay.
  • 8. Backtracking (6 Paradigms, 14 Problems): Subsets, permutations, combinations, constraint satisfaction, pruning, N-Queens, Sudoku solver.
  • 9. Dynamic Programming (13 Paradigms, 28 Problems): Memoization vs tabulation, 0/1 knapsack, unbounded knapsack, coin change, edit distance, longest common subsequence.
  • 10. Heap / Priority Queue (9 Paradigms, 11 Problems): Running medians, top-k elements, interval scheduling, IPO, k-way merges.
  • 11. Advanced Data Structures (7 Paradigms, 17 Problems): Trie (prefix tree), Union-Find (Disjoint Set Union), LRU Cache, LFU Cache, range-sum queries with updates.
  • 12. Stack (5 Paradigms, 9 Problems): Next greater element, largest rectangle in histogram, remove k digits, decode string, min stack.
  • 13. Intervals & Greedy (2 Paradigms, 6 Problems): Merge intervals, interval scheduling, meeting rooms by sweep line, jump game.

4-Stage Deliberate Practice Framework

  1. Stage 1 (Compare & Learn): Read the pattern's mental model, loop invariant and Big-O proof, compare the pattern's template with the problem's solution side by side, and use the step-by-step debugger. The solution can also be shown in C#, Java, TypeScript, C++, Go or Rust for almost every problem.
  2. Stage 2 (Active Recall): Rebuild the solution from memory in a code editor, with an invariant checklist and optional progressive hints, and check it against a judged test suite.
  3. Stage 3 (AI Interview Coach, coming soon): A mock-interview stage still in development; for now, students go straight from Stage 2 to Stage 4.
  4. Stage 4 (Solve on Your Own): Solve a new problem on your own against a 15-minute timer; your code is graded against a test suite. Python and TypeScript run in your browser and C# runs on our server (design problems grade in Python and TypeScript).

Equipped with a spaced-repetition review hub on a fixed 7-step schedule (1 day up to 90 days), a two-pane Studio workspace, and study notes.

Pricing, Access & Commercial Terms

  • Core Curriculum: 100% Free. No credit card required.
  • Coins: New accounts get 40 free coins once, the daily claim adds +20 coins once a day, and inviting a friend earns you +25 coins when they complete their first practice step. These are the only ways to earn coins.
  • AI Coach: Each AI coach question costs 1-5 coins, priced by the server according to its size.
  • BYOK (Bring Your Own Key): AI coaching is free with your own Google Gemini, OpenAI or OpenRouter API key (BYOK).
  • Extras: Coins also unlock extras for 24 hours: a Recall hint level (1 coin), a Solve solution reveal (2), a LeetCode problem card's solutions (1) and a pattern's extra canonical problems (1).
  • Payments & Refunds: Nothing on the site is sold for money: coins can only be earned, never bought. There are no subscriptions or charges, so there is nothing to refund.
  • Platform Operator: Hi👋WebEnterprise. Support & policies at hispeedalgo.com.
Skip to main content
Hi👋SpeedAlgo

Invariant-First Algorithmic Mastery

Arena
238Items
Theory Context•Stack
P1MediumMental model~4 min speedrun

Augmented Entries (Store a Running Min/Max/Count With Each Item)

The problem

Min Stack (LC 155)(opens leetcode.com)

Build a stack class, MinStack, that behaves like any last-in, first-out stack but can also report its smallest value at any moment. Each of its operations has to run in O(1) time.

  • MinStack() creates an empty stack.
  • void push(int val) puts val on top.
  • void pop() throws away the value on top.
  • int top() returns the value on top and leaves it in place.
  • int getMin() returns the smallest value among everything currently on the stack.
Example 1
Input:["MinStack","push","push","push","getMin","pop","top","getMin"] [[],[-2],[0],[-3],[],[],[],[]]
Output:[null,null,null,null,-3,null,0,-2]
Once -2, 0 and -3 are pushed, the smallest value is -3. Popping removes -3, so 0 is on top again and the smallest value goes back to -2.
(-2, -2)0(0, -2)1(-3, -3)2bottomtop: getMin() = -3

Min-Stack Trick

push / pop stream ➔ each entry stores the running min with it ➔ O(1) query
Spot it when: A stack (or a queue) must answer a question about all of its contents, such as the minimum, the maximum or a running count, in O(1) per call while items are pushed and popped.
Min-Stack Trick
Python
1class MinStack:
2 """Min Stack: each entry carries the minimum of itself and every entry below it.
3 Time: O(1) per call | Space: O(N)
4 """
5 
6 def __init__(self) -> None:
7 # self.stack: one (val, current_min) pair per pushed value; current_min is the minimum of val
8 # and every entry below it, so the top entry always knows the whole stack's minimum.
9 self.stack: list[tuple[int, int]] = []
10 
11 def push(self, val: int) -> None:
12 # 1. Compute This Entry's Running Minimum:
13 # Extend the minimum carried by the entry below with val, or start fresh when the stack is empty.
14 if self.stack:
15 current_min = min(val, self.stack[-1][1])
16 else:
17 current_min = val
18 
19 # 2. Store The Value Together With Its Running Minimum:
20 # Pairing them means nothing below this entry has to change while it stays on the stack.
21 self.stack.append((val, current_min))
22 
23 def pop(self) -> None:
24 self.stack.pop() # The new top already carries the minimum of what is left
25 
26 def top(self) -> int:
27 return self.stack[-1][0]
28 
29 def getMin(self) -> int:
30 return self.stack[-1][1] # Read it off the top entry: no search
Invariant: Every entry (val, current_min) in self.stack holds the minimum of its own val and every value below it, so self.stack[-1][1] is always the minimum of the whole stack.

A stack only changes at its top, so store with each entry the answer for itself and everything below it: push appends (val, current_min) with current_min = min(val, self.stack[-1][1]), and getMin just reads self.stack[-1][1]. While an entry sits on the stack nothing below it can change, so its current_min stays true until the entry itself is popped; pop removes exactly one entry, and the new top already carries the minimum of what is left, so nothing is ever recomputed. The same move carries any value you can build from the entry below: a maximum, a count or a pending increment. The trap is keeping one self.min for the whole stack: it is right after every push, but when pop removes the smallest value it can't know the next smallest without a scan.

See it run

Min Stack, step by step

stack empty · returned none

stack (-2, -2) · returned none

stack (-2, -2) (0, -2) · returned none

stack (-2, -2) (0, -2) (-3, -3) · returned none

stack (-2, -2) (0, -2) (-3, -3) · returned -3

stack (-2, -2) (0, -2) · returned -3

stack (-2, -2) (0, -2) · returned -3, 0

stack (-2, -2) (0, -2) · returned -3, 0, -2

stack (-2, -2) (0, -2) · returned -3, 0, -2 · answer -2

The two top rows are the calls and their arguments. The rows val and min are the stack: each column is one entry, with the bottom entry in column 0. val is the stored value and min is its current_min.

  • call the call that is running now
  • write a value or a current_min that was just stored in a new entry
  • read a stored value or current_min that the call reads
  • popped the entry that pop removes

Start: self.stack is empty. The calls are listed in the two top rows and run from left to right.

push(-2): the stack is empty, so current_min = -2. Append (-2, -2).

push(0): current_min = min(0, -2) = -2, where -2 is the minimum stored in the entry below. Append (0, -2).

push(-3): current_min = min(-3, -2) = -3, where -2 is the minimum stored in the entry below. Append (-3, -3).

getMin(): return self.stack[-1][1] = -3. No search of the stack is needed.

pop(): remove the top entry (-3, -3). The new top is (0, -2), and it already holds the minimum of what is left.

top(): return self.stack[-1][0] = 0. Index 0 is the value, and index 1 is the minimum.

getMin(): return self.stack[-1][1] = -2. A single self.min would still hold -3, which was already popped.

Done: the calls returned -3, 0, -2. The last answer is -2.

call
0push
1push
2push
3getMin
4pop
5top
6getMin
arg
-2
0
-3
val
min
1 / 9
Gotchas1 of 4

Gotcha 1 of 4

  1. 1
    The trap

    Gotcha 1: Keeping one self.min for the whole stack

    Mistake
    def getMin(self) -> int:
        return self.min
    Fix
    def getMin(self) -> int:
        return self.stack[-1][1]

    What happens: One variable self.min is correct after every push. When pop removes the smallest value, self.min still holds it. Finding the next smallest value would need a scan of the whole stack. Each entry's own current_min already holds that value.

    Example: push(-2), push(0), push(-3), pop(), getMin()
    Mistake: returns -3Fix: returns -2
  2. 2

    Gotcha 2: Comparing val with the value below, not its minimum

    Mistake
    current_min = min(val, self.stack[-1][0])
    Fix
    current_min = min(val, self.stack[-1][1])

    What happens: self.stack[-1][0] is only the value just below. A smaller value may sit further down. self.stack[-1][1] is the minimum of every entry below. After pushing 1, 5 and 3, the value below 3 is 5. So current_min becomes 3 instead of 1.

    Example: push(1), push(5), push(3), getMin()
    Mistake: returns 3Fix: returns 1
  3. 3

    Gotcha 3: Reading self.stack[-1] on the first push

    Mistake
    current_min = min(val, self.stack[-1][1])
    Fix
    if self.stack:
        current_min = min(val, self.stack[-1][1])
    else:
        current_min = val

    What happens: On the first push, self.stack is empty. It has no top entry to read. self.stack[-1] then raises an IndexError. The first entry is its own minimum, so current_min = val.

    Example: push(5), getMin()
    Mistake: raises IndexErrorFix: returns 5
  4. 4

    Gotcha 4: Returning the carried minimum from top

    Mistake
    return self.stack[-1][1]
    Fix
    return self.stack[-1][0]

    What happens: Each entry is a pair (val, current_min). top must return the value, which is at index 0. Index 1 holds the minimum. After push(-2) and push(0), top() must return 0, not -2.

    Example: push(-2), push(0), top()
    Mistake: returns -2Fix: returns 0
Not this one if…
  • Each item waits for a later, larger item, as in Daily Temperatures (LC 739): an entry's answer is decided when a newcomer pops it, not by a value stored when it was pushed, so keep a monotonic stack of waiting indices. See Next Greater Element

Deep Dive

Why each entry can carry its own minimum

A stack changes only at its top. While an entry sits on self.stack, the entries below it cannot change. So the minimum of that entry and everything below it stays true until the entry itself is popped.

push stores the pair (val, current_min). It computes current_min = min(val, self.stack[-1][1]), which extends the minimum of the entry below with the new value. getMin reads self.stack[-1][1]. pop removes one entry, and the new top already holds the minimum of what is left. Nothing is ever recomputed.

The board replays the example of Min Stack (LC 155). The calls are push(-2), push(0), push(-3), getMin(), pop(), top() and getMin(). The entries are (-2, -2), (0, -2) and (-3, -3). The first getMin() returns -3. After pop(), the top is (0, -2), so top() returns 0 and the last getMin() returns -2.

The last getMin() is marked as the trap on the board. A single variable self.min would still hold -3 there, because -3 was the smallest value pushed. It would need a scan of the stack to find -2.

The three mistakes inside push, top and getMin

Compare with the minimum of the entry below, self.stack[-1][1], and not with its value, self.stack[-1][0]. After push(1), push(5) and push(3), the value below 3 is 5. Comparing with 5 gives current_min = 3. Comparing with the stored minimum gives 1, which is correct.

The first push has no entry below. self.stack[-1] on an empty list raises an IndexError. The first entry is its own minimum, so current_min = val.

top returns index 0 of the top pair, and getMin returns index 1. After push(-2) and push(0), top() must return 0. Returning index 1 gives -2.

Other values an entry can carry

The same move works for any value that can be built from the entry below. For a maximum, store max(val, below_max). For a count, store the count of the entry below plus one if val matches a condition.

Max Stack (LC 716) uses the pair for peekMax in O(1). Its popMax removes an item from the middle of the stack, and one pair per entry cannot do that, so that call needs a different structure.

Design a Stack With Increment Operation (LC 1381) carries a pending increment. An increment of the bottom k items is recorded on one entry, and pop adds it to the value it returns and passes it to the entry below. Each call stays O(1).

A second way to get the same result is a separate stack of minimums. It pushes to the second stack when val is less than or equal to the current minimum. It saves space when there are many repeated large values. The pair version is easier to get right.

The cost

Every call does a constant amount of work. push makes one min call, and pop, top and getMin read or remove one entry. So each call is O(1).

The stack holds N entries after N pushes, and each entry has two numbers. The space is O(N), and it is about twice the size of a stack of plain values.

Example: after 1000 pushes, self.stack holds 1000 pairs, and getMin() reads one of them. A scan for the minimum would read all 1000.

60-Second Pitch

"A stack changes only at its top. So I store with each entry the answer for itself and everything below it. push appends (val, current_min) with current_min = min(val, self.stack[-1][1]). While an entry is on the stack, nothing below it changes, so its current_min stays true. getMin reads the top entry, and pop removes one entry. The new top already holds the minimum of what is left. Every call is O(1), and the space is O(N), with two numbers per entry. The same pair works for a maximum or a count."

Common Questions

Why not keep one variable self.min?

It is correct after every push. When pop removes the smallest value, self.min still holds that value. Finding the next smallest value needs a scan of the whole stack. The current_min stored with each entry avoids the scan.

Why is the stored current_min always correct?

An entry is added only at the top, and it is removed only from the top. While it is on the stack, the entries below it cannot change. So the minimum of the entry and everything below it stays the same until the entry is popped.

What are the time and space complexity?

Every call is O(1). The space is O(N) for N entries, with two numbers per entry.

Can the same idea track a maximum or a count?

Yes. Store the maximum of val and the maximum below, or a count built from the count below. This works for any value that depends only on the entry and the entries below it. It does not work for removing an item from the middle, as in popMax of Max Stack (LC 716).

How can you save space?

Keep a second stack that holds a value only when it is less than or equal to the current minimum. Pop from it when the same value leaves the main stack. This uses less space when most values are large, but the pair version has less code to get wrong.

SWE Reasoning

Spot it

  • A stack or a queue must answer a question about all of its contents, such as the minimum, the maximum or a count.
  • The answer must be O(1) per call while items are pushed and popped.
  • The answer for an entry depends only on the entry itself and the entries below it.
  • A scan of the whole structure on each query would be too slow.
  • The question would be easy to answer if the structure could not lose items. Only pop makes it hard.

The invariant, step by step

  1. self.stack starts empty. Each entry is a pair (val, current_min).
  2. push computes current_min from the entry below. If the stack is empty, current_min = val. Otherwise current_min = min(val, self.stack[-1][1]).
  3. The pair is appended, so every entry holds the minimum of its own val and every value below it.
  4. Entries below cannot change while an entry sits above them, so the stored current_min stays true.
  5. pop removes the top pair. The new top still holds the minimum of what is left.
  6. getMin returns self.stack[-1][1], and top returns self.stack[-1][0]. Both are O(1).

Traps

  • One self.min for the whole stack. It is right after every push, but after a pop of the smallest value it is out of date. For push(-2), push(0), push(-3), pop(), getMin(), it returns -3 and not -2.
  • Comparing with the value below. Use self.stack[-1][1], the minimum below, and not self.stack[-1][0].
  • The first push. An empty self.stack has no self.stack[-1]. Handle it with current_min = val.
  • Wrong index in top. The value is at index 0. Index 1 is the minimum.
  • Recomputing on pop. If you scan the stack after a pop, the call is O(N). The new top already has the answer.

Practice

Canonical LeetCode:Min Stack (LC 155)Design a Stack With Increment Operation (LC 1381)Remove All Adjacent Duplicates in String II (LC 1209)Online Stock Span (LC 901)Maximum Frequency Stack (LC 895)
Theory Context•Stack
P1MediumMental model~4 min speedrun

Augmented Entries (Store a Running Min/Max/Count With Each Item)

The problem

Min Stack (LC 155)(opens leetcode.com)

Build a stack class, MinStack, that behaves like any last-in, first-out stack but can also report its smallest value at any moment. Each of its operations has to run in O(1) time.

  • MinStack() creates an empty stack.
  • void push(int val) puts val on top.
  • void pop() throws away the value on top.
  • int top() returns the value on top and leaves it in place.
  • int getMin() returns the smallest value among everything currently on the stack.
Example 1
Input:["MinStack","push","push","push","getMin","pop","top","getMin"] [[],[-2],[0],[-3],[],[],[],[]]
Output:[null,null,null,null,-3,null,0,-2]
Once -2, 0 and -3 are pushed, the smallest value is -3. Popping removes -3, so 0 is on top again and the smallest value goes back to -2.
(-2, -2)0(0, -2)1(-3, -3)2bottomtop: getMin() = -3

Min-Stack Trick

push / pop stream ➔ each entry stores the running min with it ➔ O(1) query
Spot it when: A stack (or a queue) must answer a question about all of its contents, such as the minimum, the maximum or a running count, in O(1) per call while items are pushed and popped.
Min-Stack Trick
Python
1class MinStack:
2 """Min Stack: each entry carries the minimum of itself and every entry below it.
3 Time: O(1) per call | Space: O(N)
4 """
5 
6 def __init__(self) -> None:
7 # self.stack: one (val, current_min) pair per pushed value; current_min is the minimum of val
8 # and every entry below it, so the top entry always knows the whole stack's minimum.
9 self.stack: list[tuple[int, int]] = []
10 
11 def push(self, val: int) -> None:
12 # 1. Compute This Entry's Running Minimum:
13 # Extend the minimum carried by the entry below with val, or start fresh when the stack is empty.
14 if self.stack:
15 current_min = min(val, self.stack[-1][1])
16 else:
17 current_min = val
18 
19 # 2. Store The Value Together With Its Running Minimum:
20 # Pairing them means nothing below this entry has to change while it stays on the stack.
21 self.stack.append((val, current_min))
22 
23 def pop(self) -> None:
24 self.stack.pop() # The new top already carries the minimum of what is left
25 
26 def top(self) -> int:
27 return self.stack[-1][0]
28 
29 def getMin(self) -> int:
30 return self.stack[-1][1] # Read it off the top entry: no search
Invariant: Every entry (val, current_min) in self.stack holds the minimum of its own val and every value below it, so self.stack[-1][1] is always the minimum of the whole stack.

A stack only changes at its top, so store with each entry the answer for itself and everything below it: push appends (val, current_min) with current_min = min(val, self.stack[-1][1]), and getMin just reads self.stack[-1][1]. While an entry sits on the stack nothing below it can change, so its current_min stays true until the entry itself is popped; pop removes exactly one entry, and the new top already carries the minimum of what is left, so nothing is ever recomputed. The same move carries any value you can build from the entry below: a maximum, a count or a pending increment. The trap is keeping one self.min for the whole stack: it is right after every push, but when pop removes the smallest value it can't know the next smallest without a scan.

See it run

Min Stack, step by step

stack empty · returned none

stack (-2, -2) · returned none

stack (-2, -2) (0, -2) · returned none

stack (-2, -2) (0, -2) (-3, -3) · returned none

stack (-2, -2) (0, -2) (-3, -3) · returned -3

stack (-2, -2) (0, -2) · returned -3

stack (-2, -2) (0, -2) · returned -3, 0

stack (-2, -2) (0, -2) · returned -3, 0, -2

stack (-2, -2) (0, -2) · returned -3, 0, -2 · answer -2

The two top rows are the calls and their arguments. The rows val and min are the stack: each column is one entry, with the bottom entry in column 0. val is the stored value and min is its current_min.

  • call the call that is running now
  • write a value or a current_min that was just stored in a new entry
  • read a stored value or current_min that the call reads
  • popped the entry that pop removes

Start: self.stack is empty. The calls are listed in the two top rows and run from left to right.

push(-2): the stack is empty, so current_min = -2. Append (-2, -2).

push(0): current_min = min(0, -2) = -2, where -2 is the minimum stored in the entry below. Append (0, -2).

push(-3): current_min = min(-3, -2) = -3, where -2 is the minimum stored in the entry below. Append (-3, -3).

getMin(): return self.stack[-1][1] = -3. No search of the stack is needed.

pop(): remove the top entry (-3, -3). The new top is (0, -2), and it already holds the minimum of what is left.

top(): return self.stack[-1][0] = 0. Index 0 is the value, and index 1 is the minimum.

getMin(): return self.stack[-1][1] = -2. A single self.min would still hold -3, which was already popped.

Done: the calls returned -3, 0, -2. The last answer is -2.

call
0push
1push
2push
3getMin
4pop
5top
6getMin
arg
-2
0
-3
val
min
1 / 9
Gotchas1 of 4

Gotcha 1 of 4

  1. 1
    The trap

    Gotcha 1: Keeping one self.min for the whole stack

    Mistake
    def getMin(self) -> int:
        return self.min
    Fix
    def getMin(self) -> int:
        return self.stack[-1][1]

    What happens: One variable self.min is correct after every push. When pop removes the smallest value, self.min still holds it. Finding the next smallest value would need a scan of the whole stack. Each entry's own current_min already holds that value.

    Example: push(-2), push(0), push(-3), pop(), getMin()
    Mistake: returns -3Fix: returns -2
  2. 2

    Gotcha 2: Comparing val with the value below, not its minimum

    Mistake
    current_min = min(val, self.stack[-1][0])
    Fix
    current_min = min(val, self.stack[-1][1])

    What happens: self.stack[-1][0] is only the value just below. A smaller value may sit further down. self.stack[-1][1] is the minimum of every entry below. After pushing 1, 5 and 3, the value below 3 is 5. So current_min becomes 3 instead of 1.

    Example: push(1), push(5), push(3), getMin()
    Mistake: returns 3Fix: returns 1
  3. 3

    Gotcha 3: Reading self.stack[-1] on the first push

    Mistake
    current_min = min(val, self.stack[-1][1])
    Fix
    if self.stack:
        current_min = min(val, self.stack[-1][1])
    else:
        current_min = val

    What happens: On the first push, self.stack is empty. It has no top entry to read. self.stack[-1] then raises an IndexError. The first entry is its own minimum, so current_min = val.

    Example: push(5), getMin()
    Mistake: raises IndexErrorFix: returns 5
  4. 4

    Gotcha 4: Returning the carried minimum from top

    Mistake
    return self.stack[-1][1]
    Fix
    return self.stack[-1][0]

    What happens: Each entry is a pair (val, current_min). top must return the value, which is at index 0. Index 1 holds the minimum. After push(-2) and push(0), top() must return 0, not -2.

    Example: push(-2), push(0), top()
    Mistake: returns -2Fix: returns 0
Not this one if…
  • Each item waits for a later, larger item, as in Daily Temperatures (LC 739): an entry's answer is decided when a newcomer pops it, not by a value stored when it was pushed, so keep a monotonic stack of waiting indices. See Next Greater Element

Deep Dive

Why each entry can carry its own minimum

A stack changes only at its top. While an entry sits on self.stack, the entries below it cannot change. So the minimum of that entry and everything below it stays true until the entry itself is popped.

push stores the pair (val, current_min). It computes current_min = min(val, self.stack[-1][1]), which extends the minimum of the entry below with the new value. getMin reads self.stack[-1][1]. pop removes one entry, and the new top already holds the minimum of what is left. Nothing is ever recomputed.

The board replays the example of Min Stack (LC 155). The calls are push(-2), push(0), push(-3), getMin(), pop(), top() and getMin(). The entries are (-2, -2), (0, -2) and (-3, -3). The first getMin() returns -3. After pop(), the top is (0, -2), so top() returns 0 and the last getMin() returns -2.

The last getMin() is marked as the trap on the board. A single variable self.min would still hold -3 there, because -3 was the smallest value pushed. It would need a scan of the stack to find -2.

The three mistakes inside push, top and getMin

Compare with the minimum of the entry below, self.stack[-1][1], and not with its value, self.stack[-1][0]. After push(1), push(5) and push(3), the value below 3 is 5. Comparing with 5 gives current_min = 3. Comparing with the stored minimum gives 1, which is correct.

The first push has no entry below. self.stack[-1] on an empty list raises an IndexError. The first entry is its own minimum, so current_min = val.

top returns index 0 of the top pair, and getMin returns index 1. After push(-2) and push(0), top() must return 0. Returning index 1 gives -2.

Other values an entry can carry

The same move works for any value that can be built from the entry below. For a maximum, store max(val, below_max). For a count, store the count of the entry below plus one if val matches a condition.

Max Stack (LC 716) uses the pair for peekMax in O(1). Its popMax removes an item from the middle of the stack, and one pair per entry cannot do that, so that call needs a different structure.

Design a Stack With Increment Operation (LC 1381) carries a pending increment. An increment of the bottom k items is recorded on one entry, and pop adds it to the value it returns and passes it to the entry below. Each call stays O(1).

A second way to get the same result is a separate stack of minimums. It pushes to the second stack when val is less than or equal to the current minimum. It saves space when there are many repeated large values. The pair version is easier to get right.

The cost

Every call does a constant amount of work. push makes one min call, and pop, top and getMin read or remove one entry. So each call is O(1).

The stack holds N entries after N pushes, and each entry has two numbers. The space is O(N), and it is about twice the size of a stack of plain values.

Example: after 1000 pushes, self.stack holds 1000 pairs, and getMin() reads one of them. A scan for the minimum would read all 1000.

60-Second Pitch

"A stack changes only at its top. So I store with each entry the answer for itself and everything below it. push appends (val, current_min) with current_min = min(val, self.stack[-1][1]). While an entry is on the stack, nothing below it changes, so its current_min stays true. getMin reads the top entry, and pop removes one entry. The new top already holds the minimum of what is left. Every call is O(1), and the space is O(N), with two numbers per entry. The same pair works for a maximum or a count."

Common Questions

Why not keep one variable self.min?

It is correct after every push. When pop removes the smallest value, self.min still holds that value. Finding the next smallest value needs a scan of the whole stack. The current_min stored with each entry avoids the scan.

Why is the stored current_min always correct?

An entry is added only at the top, and it is removed only from the top. While it is on the stack, the entries below it cannot change. So the minimum of the entry and everything below it stays the same until the entry is popped.

What are the time and space complexity?

Every call is O(1). The space is O(N) for N entries, with two numbers per entry.

Can the same idea track a maximum or a count?

Yes. Store the maximum of val and the maximum below, or a count built from the count below. This works for any value that depends only on the entry and the entries below it. It does not work for removing an item from the middle, as in popMax of Max Stack (LC 716).

How can you save space?

Keep a second stack that holds a value only when it is less than or equal to the current minimum. Pop from it when the same value leaves the main stack. This uses less space when most values are large, but the pair version has less code to get wrong.

SWE Reasoning

Spot it

  • A stack or a queue must answer a question about all of its contents, such as the minimum, the maximum or a count.
  • The answer must be O(1) per call while items are pushed and popped.
  • The answer for an entry depends only on the entry itself and the entries below it.
  • A scan of the whole structure on each query would be too slow.
  • The question would be easy to answer if the structure could not lose items. Only pop makes it hard.

The invariant, step by step

  1. self.stack starts empty. Each entry is a pair (val, current_min).
  2. push computes current_min from the entry below. If the stack is empty, current_min = val. Otherwise current_min = min(val, self.stack[-1][1]).
  3. The pair is appended, so every entry holds the minimum of its own val and every value below it.
  4. Entries below cannot change while an entry sits above them, so the stored current_min stays true.
  5. pop removes the top pair. The new top still holds the minimum of what is left.
  6. getMin returns self.stack[-1][1], and top returns self.stack[-1][0]. Both are O(1).

Traps

  • One self.min for the whole stack. It is right after every push, but after a pop of the smallest value it is out of date. For push(-2), push(0), push(-3), pop(), getMin(), it returns -3 and not -2.
  • Comparing with the value below. Use self.stack[-1][1], the minimum below, and not self.stack[-1][0].
  • The first push. An empty self.stack has no self.stack[-1]. Handle it with current_min = val.
  • Wrong index in top. The value is at index 0. Index 1 is the minimum.
  • Recomputing on pop. If you scan the stack after a pop, the call is O(N). The new top already has the answer.

Practice

Canonical LeetCode:Min Stack (LC 155)Design a Stack With Increment Operation (LC 1381)Remove All Adjacent Duplicates in String II (LC 1209)Online Stock Span (LC 901)Maximum Frequency Stack (LC 895)
© 2026 Hi👋WebEnterprise. All rights reserved.
Sitemap•llms.txt•