A stack that refuses to hold anything out of order. Whatever it evicts, it evicts at exactly the moment the answer for that item becomes known.
Every problem here has the same brute force: for each element, scan outward until you find the next bigger or smaller one. That is O(n²). A stack kept in sorted order turns it into O(n), because each element is pushed once and popped once. The hard part is not the code, it is seeing that the question is a “next greater” question in disguise.
| Signal | What it looks like |
|---|---|
| Next or previous | “next greater element”, “previous smaller”, “how many days until a warmer day”. |
| Span or reach | “stock span”, “how far can this bar extend”, “width of the region it dominates”. |
| Skyline or histogram | Bars, heights, buildings, water. Almost always this pattern. |
| Remove to optimise | “remove k digits to make the smallest number”, “remove duplicate letters, smallest result”. |
| Sliding maximum | “maximum of every window of size k” uses the deque form of the same idea. |
while loop looks quadratic and is not. Every index enters the stack once and leaves once.n pops across the whole run. The inner while may run many times on one iteration and zero times on the next, but the total number of inner steps over the entire loop is bounded by n. Total: O(n). This is the same accounting as the sliding window.All four are the same loop. Only the comparison and the direction change. Write this table out before coding and you will not get the sign backwards.
| You want | Stack holds | Pop while | Scan |
|---|---|---|---|
| Next greater to the right | decreasing values | stack top < current | left to right |
| Next smaller to the right | increasing values | stack top > current | left to right |
| Previous greater to the left | decreasing values | stack top < current | left to right, read the top before pushing |
| Previous smaller to the left | increasing values | stack top > current | left to right, read the top before pushing |
def next_greater(nums: list[int]) -> list[int]:
"""For each index, the index of the next strictly greater value, or -1."""
# -1 means "no greater value to the right". Every slot starts there.
answer = [-1] * len(nums)
stack: list[int] = [] # indices. nums[stack] is non-increasing
# index is the current position.
# Invariant: the stack holds every earlier index still waiting for an answer.
for index, value in enumerate(nums):
# Everything smaller than value has just found its answer.
# stack[-1] is the top: the most recent index still waiting.
while stack and nums[stack[-1]] < value:
answer[stack.pop()] = index
stack.append(index) # index now waits for its own greater value
return answer # anything left on the stack keeps -1
Store indices, not values. You can always read the value from the index, but you cannot recover a distance from a value.
def with_sentinel(heights: list[int]) -> None:
"""Append an impossible final value so the stack empties inside the loop."""
stack: list[int] = []
# [*heights, 0]: one extra fake bar at index len(heights).
# It pops everything left on the stack before the loop ends.
for index, height in enumerate([*heights, 0]): # 0 is lower than any real bar
# stack[-1] is the top. >= pops equal bars too.
while stack and heights[stack[-1]] >= height:
# resolve(popped, index): the per-problem work for a popped bar.
# index is the first bar to its right that is not taller.
# Largest Rectangle: area = height of popped bar * its width.
resolve(stack.pop(), index)
stack.append(index)
A sentinel removes the “now drain whatever is left” block after the loop. One code path instead of two, and the leftover case is where bugs live.
Reading the figure. Blue bars are still on the stack. Green bars have been popped and resolved. On the left the 3 pops the 4, but bars 2 and 3 are stranded when the input runs out. On the right the amber sentinel 0 arrives last. The red arrows show it popping both stranded bars, so no cleanup code is needed.
while.< versus <= with equal values. Strict keeps equal elements stacked; non-strict pops them. For Largest Rectangle either works, for “next strictly greater” only strict is right. Ask what equality should mean.stack[-1] on an empty stack. Always test stack and … first, and rely on Python’s short-circuit.list.pop(0). That is a queue operation and it is O(n). A stack pops from the end with pop().Given daily temperatures, return an array where answer[i] is the number of days you must wait after day i for a warmer temperature. If no warmer day comes, put 0.
0, so no cleanup is needed.def daily_temperatures(temperatures: list[int]) -> list[int]:
"""Days to wait for a warmer temperature, per day.
Args:
temperatures: Daily temperature readings.
Returns:
answer[i] is the number of days after i until a warmer day, or 0.
Example:
>>> daily_temperatures([73, 74, 75, 71, 69, 72, 76, 73])
[1, 1, 4, 2, 1, 1, 0, 0]
"""
# 0 means "no warmer day ahead". Days that never warm up keep it.
answer = [0] * len(temperatures)
stack: list[int] = [] # indices of days still waiting. Temps decrease
# index is today. Invariant: the stack holds every earlier day
# with no warmer day yet, coldest on top.
for index, temperature in enumerate(temperatures):
# This day is warmer than everything waiting on top of the stack,
# so it is the answer for all of them. stack[-1] is the top.
while stack and temperatures[stack[-1]] < temperature:
earlier = stack.pop()
answer[earlier] = index - earlier # days waited: the gap in index
stack.append(index) # today now waits for its own warmer day
# Whatever is still stacked never warmed up, and 0 is already there.
return answer
[73, 74, 75, 71, 69, 72, 76, 73]:
| index | temp | pops | stack after |
|---|---|---|---|
| 0 | 73 | none | [0] |
| 1 | 74 | 0, wait 1 | [1] |
| 2 | 75 | 1, wait 1 | [2] |
| 3 | 71 | none | [2, 3] |
| 4 | 69 | none | [2, 3, 4] |
| 5 | 72 | 4 wait 1, then 3 wait 2 | [2, 5] |
| 6 | 76 | 5 wait 1, then 2 wait 4 | [6] |
| 7 | 73 | none | [6, 7] |
Indices 6 and 7 stay stacked and keep their 0.
Reading the figure. Each bar is one day. A red arrow runs from a day to the warmer day that popped it off the stack. The cell under each bar is the wait, which is the gap between the two day numbers. Green days found an answer. Blue days 6 and 7 never get popped, so they keep 0.
Worst case for space is a strictly decreasing input, where nothing is ever popped and the stack holds all n indices. Worth naming when asked.
1 except the last.< is strict, so nothing pops and all answers are 0. Correct, since equal is not warmer. Confirm that reading.Given a circular array, return the next greater element for each position. Searching wraps around past the end and back to the start. Use -1 where none exists.
2n times and index with i % n. Two laps are always enough, because after one full lap every element has seen every other element. Then add one rule: only push indices during the first lap. The second lap exists purely to resolve leftovers, so it must not create new ones.if i < n guard on the push is the entire difference from the non-circular version.def next_greater_elements(nums: list[int]) -> list[int]:
"""Next greater element for each position in a circular array.
Args:
nums: The circular array. Searching wraps past the end.
Returns:
answer[i] is the first value greater than nums[i], scanning forward
and wrapping around, or -1 if there is none.
Example:
>>> next_greater_elements([1, 2, 1])
[2, -1, 2]
"""
n = len(nums)
answer = [-1] * n # -1: "no greater value", the default
stack: list[int] = [] # indices. nums[stack] is non-increasing
# Two laps. The first seeds the stack, the second resolves the wrap-around.
# 2 * n: i runs over both laps, 0..2n-1.
for i in range(2 * n):
value = nums[i % n] # % n: wrap i back into 0..n-1
# stack[-1] is the top: the most recent index still waiting.
while stack and nums[stack[-1]] < value:
answer[stack.pop()] = value
if i < n: # never push on the second lap
stack.append(i)
return answer
nums = [1, 2, 1], so the loop runs 6 times:
| i | value | pops | push? | answer |
|---|---|---|---|---|
| 0 | 1 | none | yes | [-1, -1, -1] |
| 1 | 2 | index 0 gets 2 | yes | [2, -1, -1] |
| 2 | 1 | none | yes | [2, -1, -1] |
| 3 | 1 | none | no | [2, -1, -1] |
| 4 | 2 | index 2 gets 2 | no | [2, -1, 2] |
| 5 | 1 | none | no | [2, -1, 2] |
Index 1 holds the array maximum, so it correctly keeps -1.
Reading the figure. The six cells are the loop running 2n times over [1, 2, 1]. Blue cells are lap 1, where indices are pushed. Dashed cells are lap 2, where nothing is pushed. Each green arc is a pop that writes an answer. Index 2 only finds its answer because lap 2 wraps back to the 2 at the front.
2n iterations is still O(n). Do not let the doubling talk you into calling it quadratic.
def next_greater_element_i(nums1: list[int], nums2: list[int]) -> list[int]:
"""Next Greater Element I: nums1 is a subset of nums2. Solve on nums2, then look up."""
greater: dict[int, int] = {} # value -> its next greater value in nums2
stack: list[int] = [] # values this time, since nums2 has no duplicates
# Invariant: the stack holds earlier values with no greater value yet.
for value in nums2:
# stack[-1] is the top. value is the answer for every smaller one there.
while stack and stack[-1] < value:
greater[stack.pop()] = value
stack.append(value)
# -1: values never popped have no greater value to the right.
return [greater.get(value, -1) for value in nums1]
Here the values are guaranteed distinct, so a value-keyed map is safe and the stack can hold values directly. When duplicates are possible, go back to indices.
[5, 5, 5]: all -1, because < is strict.-1, and there may be several copies of it.[-1].Given an elevation map where height[i] is the height of a bar of width 1, compute how much rain water is trapped after it rains.
i is min(tallest bar to the left, tallest bar to the right) - height[i], or zero if that is negative. Water is held in by the shorter of the two walls, which is why the min is there. Every solution below is a different way to get those two maxima cheaply.def trap(height: list[int]) -> int:
"""Total rain water trapped above an elevation map.
Fills the terrain in horizontal layers. Each pop identifies a basin
floor, with the popped bar's left neighbour on the stack as one wall
and the arriving bar as the other.
Args:
height: Non-negative bar heights.
Returns:
Total trapped water.
Example:
>>> trap([0, 1, 0, 2, 1, 0, 1, 3, 2, 1, 2, 1])
6
"""
total = 0 # 0: no water counted yet
stack: list[int] = [] # indices. Heights non-increasing
# right is the arriving bar. Invariant: the stack holds bars that may
# still be a left wall, tallest at the bottom.
for right, bar in enumerate(height):
# A taller bar closes off one or more basins to its left.
# stack[-1] is the top: the lowest waiting bar.
while stack and height[stack[-1]] < bar:
floor = stack.pop()
if not stack:
break # no left wall, so the water spills out
left = stack[-1] # the new top is the left wall
# - 1: count only the cells strictly between the two walls.
width = right - left - 1
# Water rises to the lower wall, above the floor already filled.
depth = min(height[left], bar) - height[floor]
total += width * depth
stack.append(right)
return total
def trap_two_pointers(height: list[int]) -> int:
"""Same answer in O(1) space, converging from both ends.
The insight: if the tallest bar seen from the left is no taller than the
tallest seen from the right, then the left maximum is the binding wall
for the left position, whatever lies in between. So it is safe to settle
that side immediately.
"""
if not height:
return 0 # 0: no bars, so no water
# 0 is the first index. len - 1 is the last index.
left, right = 0, len(height) - 1
left_max, right_max = height[left], height[right]
total = 0 # 0: no water counted yet
# Invariant: every bar outside left..right is settled and counted.
# left_max and right_max are the tallest bars seen from each side.
while left < right:
if left_max <= right_max:
left += 1 # +1: step the left pointer inward
left_max = max(left_max, height[left])
# Water here is the wall height minus the bar. 0 if it is the wall.
total += left_max - height[left]
else:
right -= 1 # -1: step the right pointer inward
right_max = max(right_max, height[right])
total += right_max - height[right]
return total
| Approach | Time | Space | Verdict |
|---|---|---|---|
| Precompute left-max and right-max arrays | O(n) | O(n) | Easiest to derive and explain. Start here. |
| Monotonic stack | O(n) | O(n) | The pattern answer. Fills horizontally, one basin per pop. |
| Two pointers | O(n) | O(1) | The best answer. Offer it last, with the reason it is safe. |
min(left_max, right_max) rule, write the two-array version, then say “I can drop the arrays with two pointers” and write that. The stack version is worth knowing because it is the same machinery as the next problem, and because it computes the water in a completely different geometry.height = [0, 1, 0, 2, 1, 0, 1, 3, 2, 1, 2, 1]. At index 3 the bar of height 2 arrives. It pops the 0 at index 2, finds index 1 underneath as the left wall, and adds width 1 × depth (min(1, 2) - 0) = 1. Later the bar of height 3 at index 7 pops a run of small bars and adds the large middle basin. The layers accumulate to 6.
Reading the figure. The numbers under the bars are the heights. Each blue block is one layer of water. Its number is width times depth. A block appears when a taller bar arrives and pops a lower one. The wide block of 3 sits on top of a smaller one. A later pop can fill above an earlier layer.
< in the stack version keeps equal bars stacked so no zero-depth basins are counted.Given bar heights of width 1, find the area of the largest rectangle that fits inside the histogram.
Once you say “each bar is the height, and I need its left and right smaller boundaries”, the problem is finished. Getting there is the interview.
0. It is shorter than every real bar, so it drains the stack inside the loop and removes the cleanup block entirely.def largest_rectangle_area(heights: list[int]) -> int:
"""Area of the largest rectangle that fits under a histogram.
Each bar is considered as the limiting height of a rectangle. Its width
runs between the nearest shorter bar on each side, both of which fall out
of a single increasing stack.
Args:
heights: Non-negative bar heights, each of width 1.
Returns:
The maximum rectangle area.
Example:
>>> largest_rectangle_area([2, 1, 5, 6, 2, 3])
10
"""
best = 0 # 0: the empty rectangle, area 0
stack: list[int] = [] # indices. Heights strictly increasing
# The trailing 0 is shorter than any bar, so it flushes the stack and
# every bar gets resolved inside the loop.
# Invariant: each stacked bar is taller than the one below it.
for index, bar in enumerate([*heights, 0]):
# stack[-1] is the top. index is the nearest shorter bar to its right.
while stack and heights[stack[-1]] >= bar:
height = heights[stack.pop()]
# Left boundary: whatever is now on top is the nearest shorter
# bar to the left. If the stack is empty, the rectangle reaches
# all the way to index 0.
# + 1: the rectangle starts just past that shorter bar.
left = stack[-1] + 1 if stack else 0
# Width runs from left up to index - 1, so it is index - left.
best = max(best, height * (index - left))
stack.append(index)
return best
heights = [2, 1, 5, 6, 2, 3]:
| index | bar | pops and areas | best |
|---|---|---|---|
| 0 | 2 | none | 0 |
| 1 | 1 | pop 2, width 1, area 2 | 2 |
| 2 | 5 | none | 2 |
| 3 | 6 | none | 2 |
| 4 | 2 | pop 6 width 1 area 6, pop 5 width 2 area 10 | 10 |
| 5 | 3 | none | 10 |
| 6 | 0 | pop 3 area 3, pop 2 area 8, pop 1 area 6 | 10 |
The winner is the 5 × 2 rectangle spanning the bars of height 5 and 6.
Reading the figure. The dashed green box is the winning rectangle. Its height comes from the bar of 5 (blue). The red bar is the nearest shorter bar on the left, and it sits under the 5 on the stack. The amber bar is the nearest shorter bar on the right, and its arrival pops the 5. The width is the gap between the two walls.
def maximal_rectangle(matrix: list[list[str]]) -> int:
"""Largest all-ones rectangle in a binary matrix.
Treat each row as the base of a histogram whose bar heights are the
runs of ones stacked above that row. Then it is this problem, once per row.
"""
# matrix[0] is the first row. An empty first row means no columns.
if not matrix or not matrix[0]:
return 0 # 0: no cells, so no rectangle
# One bar per column. matrix[0] gives the column count. 0: no ones yet.
heights = [0] * len(matrix[0])
best = 0 # 0: no rectangle found yet
# Invariant: heights[c] = run of "1" cells ending at this row in column c.
for row in matrix:
for c, cell in enumerate(row):
# + 1 extends the run by this cell. 0 resets it on a "0" cell.
heights[c] = heights[c] + 1 if cell == "1" else 0
best = max(best, largest_rectangle_area(heights))
return best
A Hard problem that becomes eight lines once you own the histogram routine. Worth knowing that this is why the histogram problem is asked at all.
h × n.0.>= rather than > means equal bars are popped early with a short width. That is safe, because the leftmost of the equal run is popped last and gets the full width.while.Pattern 15, Topological Sort, moves back to graphs. It is the ordering algorithm that BFS and DFS both hint at without covering, and it is the standard answer to any question about dependencies.