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."""
answer = [-1] * len(nums)
stack: list[int] = [] # indices; nums[stack] is non-increasing
for index, value in enumerate(nums):
# Everything smaller than value has just found its answer.
while stack and nums[stack[-1]] < value:
answer[stack.pop()] = index
stack.append(index)
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] = []
for index, height in enumerate([*heights, 0]): # 0 is lower than any real bar
while stack and heights[stack[-1]] >= height:
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.
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]
"""
answer = [0] * len(temperatures)
stack: list[int] = [] # indices of days still waiting; temps decreasing
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.
while stack and temperatures[stack[-1]] < temperature:
earlier = stack.pop()
answer[earlier] = index - earlier
stack.append(index)
# 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.
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
stack: list[int] = [] # indices; nums[stack] is non-increasing
# Two laps. The first seeds the stack, the second resolves the wrap-around.
for i in range(2 * n):
value = nums[i % n]
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.
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] = {}
stack: list[int] = [] # values this time, since nums2 has no duplicates
for value in nums2:
while stack and stack[-1] < value:
greater[stack.pop()] = value
stack.append(value)
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
stack: list[int] = [] # indices; heights non-increasing
for right, bar in enumerate(height):
# A taller bar closes off one or more basins to its left.
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]
width = right - left - 1
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
left, right = 0, len(height) - 1
left_max, right_max = height[left], height[right]
total = 0
while left < right:
if left_max <= right_max:
left += 1
left_max = max(left_max, height[left])
total += left_max - height[left]
else:
right -= 1
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.
< 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
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.
for index, bar in enumerate([*heights, 0]):
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.
left = stack[-1] + 1 if stack else 0
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.
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.
"""
if not matrix or not matrix[0]:
return 0
heights = [0] * len(matrix[0])
best = 0
for row in matrix:
for c, cell in enumerate(row):
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.