DSA sheet · Stack · Concept video
Stack Patterns Overview
This is the opening video of the stack topic. It answers two questions, what is a stack and why do we need one when we already have arrays, and then walks through the six patterns that the teacher says cover about 90–95% of the stack questions you'll meet in interviews and online tests. No single problem is solved fully here. Instead, every pattern gets its idea, a tiny worked example, and the reason a stack beats the plain-array approach. This page adds a ready-to-use Python template for each pattern, so you can come back here before solving any problem in the sheet.
This concept page follows its own order:
① what a stack is → ② why we need it / how to spot a stack problem → ③ each pattern: idea → tiny example → why not an array → Python template → ④ which sheet problems belong to which pattern → ⑤ remember
- Part 0 · What a stack is
- Part A · Why we need a stack, and how to spot a stack problem
- Part B · Pattern 1: Monotonic stack
- Part C · Pattern 2: Expression evaluation
- Part D · Pattern 3: Undo (cancel the previous item)
- Part E · Pattern 4: Parentheses
- Part F · Pattern 5: Stack-based design
- Part G · Pattern 6: Recursive stack
- Part H · Which sheet problem uses which pattern
- Part I · Revision page
Part 0 · What a stack is
1A linear structure without indexing
Like an array, a stack is linear: items sit one after another in a line. The big difference: an array lets you jump to any position with an index (arr[2]). A stack has no indexing. You may only work at one end, called the top.
Three operations are allowed, all at the top:
| operation | what it does | Python (list as a stack) | cost |
|---|---|---|---|
| push | add an item on top | st.append(x) | O(1) |
| pop | remove the top item | st.pop() | O(1) |
| peek | look at the top item, don't remove it | st[-1] | O(1) |
| empty check | is anything inside? | not st | O(1) |
2Watch it work: push 1, 2, 4, 5
Take the items 1, 2, 4, 5 and push them one by one. Each new item lands on top of the previous one:
Now suppose you want to remove 2. You can't reach in and grab it. Only the top can be popped: first 5, then 4, and only then is 2 on top and poppable. The item that went in last comes out first. This rule is called LIFO (Last In, First Out).
3Python habits used in every page of this notebook
st = [] # empty stack
st.append(1) # push -> [1]
st.append(2) # push -> [1, 2] (left = bottom, right = top)
top = st[-1] # peek -> 2
x = st.pop() # pop -> x = 2, st = [1]
if st: # ALWAYS check before pop / peek
st.pop()
print(st) # []IndexError. So write while st and … or if st: before every st.pop() and st[-1].Part A · Why we need a stack, and how to spot a stack problem
1The problem with arrays: looking back costs a second loop
Say you're walking through an array [1, 2, 3, …] and you've reached 3. Now the question wants something about the items before 3 (like 2 or 1). With a plain array you must start a second loop that walks backwards from 3. Doing that from every position is a loop inside a loop → O(n²).
2What the stack does instead
As we walk forward, we keep the earlier items that still matter (for example, the biggest or smallest ones so far, whatever the question needs) in a stack. When we need to "look back", the right item is waiting on top, with no backward loop. One pass → O(n).
The price is memory: the stack can hold up to n items, so O(n) extra space. A stack is the right tool when that extra space is allowed.
3How to spot a stack problem
• From each position you'd have to walk right to left (or left to right again) to find something.
• Your brute force is O(n²) because of that second loop, and you want O(n).
• Extra space O(n) is acceptable.
Part B · Pattern 1: Monotonic stack
1The idea
Monotonic means "always moving in one direction". A monotonic stack is an ordinary stack where we make sure the items inside are always sorted, either increasing or decreasing from bottom to top.
A normal stack would simply hold everything we push. A monotonic stack, before pushing a new item, pops every item on top that would break the order. Those popped items are ones we have proved useless for the rest of the problem.
2Tiny example: push 2, 4, 3, 8, 1
A normal stack ends as [2, 4, 3, 8, 1], with no order at all. Here is what the two monotonic kinds keep:
| item | increasing stack: pop while top > item | stack after | decreasing stack: pop while top < item | stack after |
|---|---|---|---|---|
| 2 | nothing | [2] | nothing | [2] |
| 4 | nothing (2 < 4) | [2, 4] | pop 2 | [4] |
| 3 | pop 4 (4 > 3) | [2, 3] | nothing (4 > 3) | [4, 3] |
| 8 | nothing | [2, 3, 8] | pop 3, pop 4 | [8] |
| 1 | pop 8, 3, 2 | [1] | nothing (8 > 1) | [8, 1] |
At every row, each stack is sorted. That's the whole definition.
3Why it's useful (what the stack remembers)
Take "next greater element": for every item, find the first bigger item to its right. Walk from the right and keep a decreasing stack (bottom → top). When a new item x arrives, every smaller-or-equal item on top gets popped. Why is that safe? Any item further left looks right and sees x first, and x is at least as big, so a popped item can never be the first bigger one for anybody again. What survives on top is exactly the answer for x.
The teacher says about 25–30% of stack questions belong to this pattern (next greater element, next smaller element, daily temperatures, …). She leaves the "how to maintain it in code" for the problem videos. Problems 2–4 of this notebook cover it in full.
4Why not an array?
Without the stack, for every item you'd scan right until you find a bigger one: O(n²). With the stack, every item is pushed once and popped at most once: O(n).
5Python template
def next_greater(arr):
ans = [-1] * len(arr)
st = [] # bottom -> top: strictly decreasing values
for i in range(len(arr) - 1, -1, -1): # answer is on the right -> walk from the right
while st and st[-1] <= arr[i]: # not greater -> useless forever
st.pop()
if st:
ans[i] = st[-1] # survivor on top = first greater on the right
st.append(arr[i])
return ans| knob | choices |
|---|---|
| direction | answer on the right → loop right to left · answer on the left → loop left to right |
| pop condition | "greater" questions → pop while top <= x · "smaller" questions → pop while top >= x |
| what to store | values, or indices when the answer is a distance or a position |
Part C · Pattern 2: Expression evaluation
1The idea
When the input contains numbers and operators (+, −, ×, ÷) and you must calculate the result, think stack. The reason is precedence: by the BODMAS rule, × and ÷ must happen before + and −. So you often can't apply an operator the moment you see it. You have to keep earlier numbers waiting somewhere and come back to them. Keeping things waiting is what a stack is for.
2Tiny example: 2 + 3 * 5 - 4
- Correct (BODMAS): 3 × 5 = 15 first, then 2 + 15 − 4 = 13.
- Wrong (just left to right): 2 + 3 = 5, × 5 = 25, − 4 = 21.
To get 13, we evaluate 3 × 5 and only then go back left to add the 2. "Going back left" with plain arrays means nested loops (O(n²)). The stack avoids that.
3The stack method (three things to track)
The teacher keeps track of three things: the pointer i that walks the string, the last operator seen (op), and the stack of numbers. The rule when a number is complete:
| last operator was… | do this | why |
|---|---|---|
+ | push +num | Addition is lowest priority. Don't add yet: a × might follow and grab this number. |
− | push −num | Same priority as +. Store the number with its minus sign, then everything becomes a sum at the end. |
× | pop the top, push top × num | × binds to the nearest number on its left, and that number is on top of the stack. |
÷ | pop the top, push top ÷ num (cut toward zero) | Same reason as ×. |
At the end, the answer = sum of everything in the stack.
Hand table for 2+3*5-4
| i | char | what happens | stack AFTER | op after |
|---|---|---|---|---|
| 0 | 2 | build number: num = 2 (can't do anything with it yet) | [] | + (start) |
| 1 | + | number done. Last op is + → push 2 | [2] | + |
| 2 | 3 | num = 3 | [2] | + |
| 3 | * | last op + → push 3 (don't add 2 + 3, a × may follow!) | [2, 3] | * |
| 4 | 5 | num = 5 | [2, 3] | * |
| 5 | - | last op × → pop 3, push 3 × 5 = 15 | [2, 15] | − |
| 6 | 4 | num = 4, end of string → last op − → push −4 | [2, 15, −4] | - |
Sum = 2 + 15 − 4 = 13 ✓
4Python template
def calculate(s):
st = []
num = 0
op = '+' # pretend there is a '+' before the first number
for i, ch in enumerate(s):
if ch.isdigit():
num = num * 10 + int(ch) # numbers can have many digits
if (not ch.isdigit() and ch != ' ') or i == len(s) - 1:
if op == '+':
st.append(num)
elif op == '-':
st.append(-num)
elif op == '*':
st.append(st.pop() * num)
else: # '/': cut toward zero
a = st.pop()
q = abs(a) // num
st.append(q if a >= 0 else -q)
op = ch # remember this operator for the NEXT number
num = 0
return sum(st)| line | what it means |
|---|---|
| op = '+' | The first number has no operator before it. Treating it as "+" makes it go straight into the stack. |
| num = num * 10 + int(ch) | Builds multi-digit numbers: "12" → 1, then 12. |
| if (not ch.isdigit() and ch != ' ') or i == len(s) - 1: | A number has just ended: either we hit an operator, or we're at the last character. |
| st.append(st.pop() * num) | × uses the nearest number on its left, which is the top of the stack. |
| q = abs(a) // num ... | Python's // rounds down (−7 // 2 = −4). Calculator problems want "toward zero" (−3). So divide the absolute values and put the sign back. |
| return sum(st) | Only + and − remain, and they're baked into the signs. Just add up. |
+?→ Because we don't know yet whether a higher-priority × or ÷ comes next and needs that number. Pushing keeps it available. All the additions happen safely at the very end.
Part D · Pattern 3: Undo (cancel the previous item)
1The idea
Some questions say: "when you meet a certain character, delete (undo) the one just before it". The "one just before" is exactly the top of a stack. So deleting = popping. The teacher finds this the easiest pattern.
2Tiny example: "babbbacc", where two equal neighbours cancel
Rule: if the current character equals the one right before it, both disappear.
Brute force first
For each i, start j = i − 1 and compare. Only the adjacent character matters (a b farther back can't cancel this one). When they match, delete both from the string. Deleting from the middle of a string shifts all later characters left, which is O(n) each time. Over the whole string → O(n²).
With a stack
The stack holds the characters that survive so far. For each new character: if it matches the top, pop (that deletes the earlier one) and don't push the current one (it's deleted too). Otherwise push it. Popping from the top never shifts anything, so each step is O(1).
| i | char | what we pop (and why) | what we push | stack AFTER |
|---|---|---|---|---|
| 0 | b | nothing (empty) | b | [b] |
| 1 | a | nothing (top b ≠ a) | a | [b, a] |
| 2 | b | nothing (top a ≠ b) | b | [b, a, b] |
| 3 | b | pop b: equals current → both cancel | nothing | [b, a] |
| 4 | b | nothing (top a ≠ b) | b | [b, a, b] |
| 5 | a | nothing | a | [b, a, b, a] |
| 6 | c | nothing | c | [b, a, b, a, c] |
| 7 | c | pop c: equals current | nothing | [b, a, b, a] |
Read the stack bottom to top: "baba". Check by eye: babbbacc. The circled pairs cancel, and b a b a is left.
while?→ For "pairs cancel" rules, one check is enough. The stack never has two equal neighbours (we'd have cancelled them), so after one pop the new top can't match the current character again. Any later chain reaction happens naturally: the next character is compared with the new top. For rules where one character can cancel many earlier ones, you would need a
while.3Python template
def remove_pairs(s):
st = []
for ch in s:
if st and st[-1] == ch: # current cancels the previous survivor
st.pop() # delete the earlier one ...
else: # ... and do NOT push the current one
st.append(ch)
return ''.join(st) # survivors, bottom to topPart E · Pattern 4: Parentheses
1The idea
Whenever a question asks you to match brackets, check whether brackets are valid, or count what's needed to make them valid, think stack. An opening bracket alone tells you nothing. The checking only starts when a closing bracket appears, and then you must look back for its partner. Looking back = stack.
The partner of a closing bracket must be the most recent unmatched opening bracket. That's precisely the top of the stack.
2Tiny example: [(()){}]
| i | char | what we pop (and why) | what we push | stack AFTER |
|---|---|---|---|---|
| 0 | [ | opener: nothing to check | [ | [ '[' ] |
| 1 | ( | opener | ( | [ '[', '(' ] |
| 2 | ( | opener | ( | [ '[', '(', '(' ] |
| 3 | ) | pop (: top matches | nothing | [ '[', '(' ] |
| 4 | ) | pop (: matches | nothing | [ '[' ] |
| 5 | { | opener | { | [ '[', '{' ] |
| 6 | } | pop {: matches | nothing | [ '[' ] |
| 7 | ] | pop [: matches | nothing | [ ] |
Stack empty at the end → valid. Without a stack, each closer would start a j loop back towards index 0 to find its partner → O(n²).
3Python template
def is_valid(s):
partner = {')': '(', ']': '[', '}': '{'}
st = []
for ch in s:
if ch in '([{':
st.append(ch) # opener: just remember it
else:
if not st or st[-1] != partner[ch]:
return False # closer with no / wrong opener
st.pop() # matched pair disappears
return not st # leftover openers = invalidnot st at the very end?→ A string like
"((" never hits a bad closer, but its openers are never closed. A non-empty stack at the end means unmatched openers → invalid.4Space saver: a counter for one bracket type
When there is only one kind of bracket, ( and ), the stack would only ever hold (. So we don't need the items, only how many there are. A plain integer counter does the job in O(1) space. (Not shown in the video. It's the usual follow-up in the parentheses problems of the sheet.)
def min_add_to_make_valid(s):
open_count = 0 # '(' still waiting for a ')'
need_open = 0 # ')' that had no '(' before them
for ch in s:
if ch == '(':
open_count += 1 # like a push
elif open_count > 0:
open_count -= 1 # like a pop: matched
else:
need_open += 1 # a ')' with nothing to match
return need_open + open_count # brackets we must addPart F · Pattern 5: Stack-based design
1The idea
Here you're asked to build a data structure using stacks, for example a queue, a min stack or a max stack. The teacher says beginners usually find this pattern tricky, but it isn't really hard, and 3–4 practice problems are enough.
The contrast that matters: a stack is LIFO. Push 1, 2, 3, 4 and pop gives 4 first. A queue is FIFO (First In, First Out). Push 1, 2, 3, 4 and remove gives 1 first, like a line at a ticket counter.
2Tiny example: a queue from two stacks
Use one stack for input (where pushes go) and one for output (where pops come from). When output is empty and someone wants the front, pour all of input into output. Pouring reverses the order, so the oldest item ends up on top of output.
| operation | in-stack after | out-stack after | value returned |
|---|---|---|---|
| push(1) | [1] | [] | - |
| push(2) | [1, 2] | [] | - |
| push(3) | [1, 2, 3] | [] | - |
| pop() | [] | [3, 2] | 1 (out was empty → pour 3, 2, 1 → pop the top 1) |
| push(4) | [4] | [3, 2] | - |
| pop() | [4] | [3] | 2 (out not empty, no pouring) |
| peek() | [4] | [3] | 3 |
| pop() | [4] | [] | 3 |
| pop() | [] | [] | 4 (pour 4, then pop) |
| empty() | [] | [] | True |
Out came 1, 2, 3, 4, the same order they went in. A queue ✓.
3Why it's still fast: amortised O(1)
A single pop may pour many items, which looks like O(n). But each item is poured at most once in its life: pushed into "in" once, moved to "out" once, popped from "out" once. Over any sequence of k operations the total work is O(k), so the average per operation is O(1). This kind of average-over-a-sequence cost is called amortised.
4Python templates
class MyQueue:
def __init__(self):
self.inp = [] # new items land here
self.out = [] # oldest item is on top here
def push(self, x):
self.inp.append(x)
def _move(self):
if not self.out: # pour only when out is empty
while self.inp:
self.out.append(self.inp.pop())
def pop(self):
self._move()
return self.out.pop()
def peek(self):
self._move()
return self.out[-1]
def empty(self):
return not self.inp and not self.outclass MinStack:
def __init__(self):
self.st = [] # each entry: (value, min of everything up to here)
def push(self, val):
cur_min = val if not self.st else min(val, self.st[-1][1])
self.st.append((val, cur_min))
def pop(self):
self.st.pop()
def top(self):
return self.st[-1][0]
def getMin(self):
return self.st[-1][1] # O(1): stored alongside the topout is empty?→ Items already in
out are older than everything in inp. If we poured on top of them, newer items would land above older ones and come out first, which breaks FIFO.Part G · Pattern 6: Recursive stack
1The idea
You get a stack and must change it, for example reverse it, delete the bottom (or the middle) item, or insert an item at the bottom. The catch: no extra data structure is allowed. You may only use the stack itself and recursion.
2Tiny example: delete the bottom of [1, 2, 3, 4]
The obvious way: pop 4, 3, 2 into another list, pop 1 and throw it away, then push 2, 3, 4 back. That uses an extra list, which is not allowed here.
The recursive way: pop the top and hold it in a local variable of the current function call, then call the function again on the smaller stack. When only one item is left, that's the bottom, so remove it. While the calls return, each one pushes its held item back. The "extra storage" is Python's own call stack, not a data structure we built.
3Python templates
def insert_at_bottom(st, x):
if not st: # empty -> x is now the bottom
st.append(x)
return
top = st.pop() # hold the top in this call
insert_at_bottom(st, x)
st.append(top) # put it back on the way out
def reverse_stack(st):
if not st:
return
top = st.pop()
reverse_stack(st) # reverse the rest
insert_at_bottom(st, top) # old top goes to the bottom
def delete_bottom(st):
if not st:
return
if len(st) == 1: # only the bottom is left
st.pop()
return
top = st.pop()
delete_bottom(st)
st.append(top)| function | example | time |
|---|---|---|
insert_at_bottom([1, 2, 3], 9) | → [9, 1, 2, 3] | O(n) |
reverse_stack([1, 2, 3, 4]) | → [4, 3, 2, 1] | O(n²): n inserts, each O(n) |
delete_bottom([1, 2, 3, 4]) | → [2, 3, 4] | O(n) |
→ Yes, O(n) of it. The rule in these problems is about not creating an extra data structure yourself. Recursion is the allowed tool.
Part H · Which sheet problem uses which pattern
Grouped by the technique each problem needs (the sheet's own order may group a few slightly differently):
| pattern | problems in this notebook | what the stack holds |
|---|---|---|
| Monotonic | 2 Next Greater Element · 3 Next Greater Element II · 4 Daily Temperatures · 5 Asteroid Collision · 6 Largest Rectangle in Histogram · 9 Maximal Rectangle | sorted candidates (values or indices) |
| Monotonic + greedy | 23 Remove K Digits · 24 Remove Duplicate Letters | the best answer built so far. Pop a bigger previous digit/letter to make the result smaller. |
| Expression evaluation | 7 Basic Calculator II · 8 Evaluate Reverse Polish Notation · 22 Decode String (nested, uses a stack per bracket level) | numbers (with signs) waiting to be combined |
| Undo | 10 Backspace String Compare · 11 Remove All Adjacent Duplicates · 12 Make The String Great · 13 Min Length After Removing Substrings | characters that survive so far |
| Parentheses | 14 Valid Parentheses · 15 Minimum Add · 16 Score of Parentheses · 17 Longest Valid Parentheses · 25 Minimum Remove to Make Valid | unmatched openers (or their indices) |
| Design | 18 Queue using Stacks · 19 Stack using Queues · 20 Min Stack · 21 Stack With Increment | the data, plus extra info (min, pending increments…) |
| Recursive stack | covered in the Recursion notebook (reverse a stack, sort a stack) | the call stack holds popped items |
The teacher's advice: solve at least 3–4 problems from each pattern and tick them off in the sheet as you go.
Part I · Revision page
| pattern | signal in the question | core move | brute force → stack |
|---|---|---|---|
| Monotonic | next / previous greater or smaller | pop what can never be an answer, then push | O(n²) → O(n) |
| Expression | numbers + operators, "evaluate" | last operator decides: push ±num, or pop-combine-push | O(n²) → O(n) |
| Undo | a character deletes the previous one | match → pop (and don't push) | O(n²) → O(n) |
| Parentheses | match / validate / count brackets | opener push, closer must match top | O(n²) → O(n) |
| Design | "implement X using stacks", min/max stack | two stacks, or store extra info per item | amortised O(1) per op |
| Recursive | change a stack with no extra structure | pop, recurse, push back | uses the call stack |
2. Need to look back at earlier items? A stack turns the O(n²) back-scan into O(n), for O(n) space.
3. Monotonic: keep it sorted by popping useless items. Each item is pushed once and popped once.
4. Expression / Undo / Parentheses: the top is "the most recent thing still waiting".
5. Design uses two stacks (amortised O(1)). Recursive changes the stack using only the call stack.
✗ applying + immediately in an expression (breaks BODMAS: 21 instead of 13)
✗ using
// for "truncate toward zero" with negative numbers✗ pushing the current character after it cancelled the top (undo pattern)
✗ forgetting the "stack must be empty at the end" check for brackets
✗ pouring into the out-stack while it still has items (queue design)
print(next_greater([2, 4, 3, 8, 1])) # [4, 8, 8, -1, -1]
print(calculate("2+3*5-4")) # 13
print(remove_pairs("babbbacc")) # baba
print(is_valid("[(()){}]")) # True
print(min_add_to_make_valid("())(")) # 2
q = MyQueue(); q.push(1); q.push(2); q.push(3)
print(q.pop(), q.peek()) # 1 2
st = [1, 2, 3, 4]; reverse_stack(st)
print(st) # [4, 3, 2, 1]Based on this video: Stack Data Structure | 6 Stack Patterns