core idea
A stack remembers the single most recent thing you haven’t dealt with yet — last in, first out. That’s exactly the shape of “innermost closes first” (brackets, tags, nested calls) and of “who is still waiting to be resolved by something later” (next-greater-element questions). Queues get one problem here too, but only as something you build, not something you traverse with — the FIFO traversal itself belongs to BFS & DFS. This page is deliberately stack-heavy.
| shape | mechanism | typical trigger |
|---|---|---|
| LIFO matching | push openers, pop-and-check on closers | brackets/tags/nesting, “is this valid?” |
| monotonic stack | keep push order increasing or decreasing (strict or with ties, depending on the tie rule); a break resolves whoever’s waiting | “next greater/smaller element”, span or wait-time questions |
| stack-backed design | wrap Stack<T> (or two of them) behind a class whose invariant survives every push and pop |
“design a stack that also…”, “implement X using stacks” |
when to reach for it
- Nesting or matching: brackets, tags, any rule where the innermost thing must resolve before its container can.
- The answer for index
iis “the nearest earlier or later element that beats mine” — the next-greater/warmer-day family. - The problem says “design” or “implement … using a stack/queue” and wants specific operations
in
O(1). - Undo/backtrack semantics: the most recent action is the first one you reverse.
- A brute force would rescan everything to the left (or right) of
ifor everyi— anO(n^2)smell that a stack usually collapses toO(n)because each element is pushed and popped once.
universal templates
LIFO matching — push what you’re waiting to close, check it when a closer arrives:
public bool IsValidSequence(string s)
{
var stack = new Stack<char>();
foreach (char c in s)
{
if (IsOpener(c))
{
stack.Push(c); // opener: wait for its closer
}
else if (IsCloser(c))
{
if (!stack.TryPop(out char top) || !Matches(top, c))
return false; // nothing waiting, or the wrong opener is on top
}
}
return stack.Count == 0; // every opener found its closer
}
Monotonic stack — indices wait in the stack until something breaks the invariant and resolves them:
public int[] ResolveWithMonotonicStack(int[] values)
{
int[] answer = new int[values.Length];
var pending = new Stack<int>(); // indices whose answer isn't known yet
for (int i = 0; i < values.Length; i++)
{
while (pending.Count > 0 && Breaks(values[pending.Peek()], values[i]))
{
int j = pending.Pop(); // i resolves whatever j was waiting for
answer[j] = Resolve(j, i);
}
pending.Push(i); // i now waits for its own resolution
}
return answer; // indices still pending never got resolved
}
Stack-backed design — the stack is the data structure; your job is choosing the invariant that
every Push/Pop must preserve:
public class StackBackedDesign<T>
{
private readonly Stack<T> _primary = new();
// add a second Stack<T> (or any side channel) here when the invariant needs one --
// e.g. a running minimum, or a second stack to reverse order for FIFO.
public void Push(T val)
{
_primary.Push(val);
// update the side channel so the invariant still holds after this push
}
public T Pop()
{
T val = _primary.Pop();
// update the side channel so the invariant still holds after this pop
return val;
}
}
Every problem below is one of these three skeletons with a different invariant plugged in.
the one question to ask
Before you push, ask: “what will I need to know if I pop this later?” If the honest answer is “nothing beyond the raw value,” you want plain LIFO matching. If it’s “whether this breaks a rule I’ve kept true so far,” you want a monotonic stack. If it’s “some derived fact that must stay in lockstep with the stack itself,” you’re designing a stack-backed structure.
problems
Push openers, match closers — the canonical stack problem.
Two stacks, amortized O(1) — flip the in-stack only when out-stack empties.
A second stack that remembers the minimum at every depth.
Operands wait on the stack; operators consume the top two.
Indices wait on a decreasing stack until a warmer day resolves them.
The modern follow-up to Valid Parentheses — mark the offenders, rebuild.
cheat sheet — stack queue
recognize it
- nesting/matching rule ("innermost closes first") → LIFO stack
- "next greater/smaller element", "days until warmer" → monotonic stack
- "design a stack/queue that also supports X in O(1)" → stack-backed design
- undo/backtrack semantics: the most recent action reverses first → stack
key tricks
while (stack.Count > 0 && Breaks(...))notif— one arrival can resolve several waiting entries at once- push the **index**, not the value, whenever you need
i - jor need to know which slot to write back into - two stacks in opposite roles = a queue:
_inabsorbs pushes,_outserves pops, refill only when_outis empty (amortizedO(1)) - a second stack in lockstep with the first turns any "running X" query (min, max) into
O(1)— pushMath.Min(val, prevMin)alongside every value stack.TryPop(out var top)/TryPeekskip the explicitCount == 0check before every pop
common bugs
- checking
stack.Count == 0only at the end and skipping it inside the loop — popping an empty stack throws instead of failing gracefully - using
ifinstead ofwhileagainst the stack top — misses multi-resolve cases (one warm day beating several waiting days at once) - mutating a string/array while iterating over it instead of marking offenders and rebuilding afterward — shifts every index past the edit point
- transferring between two stacks on every call instead of only when the target is empty — still correct, but throws away the amortized
O(1)argument