Monotonic Stack
Keep a stack that is always sorted. The moment a new element breaks the order, everyone it "beats" gets popped, and each popped element learns its answer right then.
1. What is this pattern for?
A monotonic stack is a plain Python list used as a stack, with one rule: its values stay in order (always decreasing, or always increasing, from bottom to top). Before you push a new element, you pop everything that would break the order.
That popping is the useful part. When element x pops element y, you've learned something about y: x is the first element after y that is bigger (or smaller) than it. So the pattern answers "next greater / next smaller / previous greater / previous smaller" for every index in one pass, O(n), instead of the O(n²) "for each element, scan right" brute force.
It's the engine behind a family of famous problems: daily temperatures, stock span, largest rectangle in a histogram, trapping rain water, sum of subarray minimums and "remove digits to make the smallest number".
2. How it works (bisected)
- Decide what you're looking for. "Next greater" means you keep a decreasing stack (a bigger newcomer pops smaller ones). "Next smaller" means you keep an increasing stack.
- Store indices, not values. With an index you can read the value (
nums[j]), compute distances (i - j) and write into an answer array (ans[j]). - Walk left to right. For each new element
xat indexi: while the stack's top is "beaten" byx, pop it and recordx(ori) as its answer. - Push
i. It now waits on the stack until something beats it. - Leftovers have no answer. Anything still on the stack at the end never found a greater (smaller) element, so it keeps the default (
-1,0, orn). - Why it's
O(n): each index is pushed once and popped at most once. The innerwhileloop looks scary but does at mostnpops in total.
The same machine run the other way around: when you want the previous smaller element, keep an increasing stack. After popping, whatever is left on top is the answer for the new element.
3. Seeing a problem: how to spot Monotonic Stack
Trigger words
- "next greater / next smaller element", "previous greater / smaller"
- "how many days until a warmer temperature", "stock span", "how far can each person see"
- "largest rectangle", "maximal rectangle", "trapping rain water"
- "sum of minimums (or maximums) of all subarrays"
- "remove k digits / letters to get the smallest (lexicographically) result"
Shape of the input
- A 1-D array (or string) where order matters and you can't sort it.
- Each element's answer depends on the nearest element to its left/right that is bigger or smaller.
- The brute force is "for each
i, scan outward until you hit something bigger". That'sO(n²).
4. When to use it, and when not to
| Use Monotonic Stack when… | Prefer something else when… |
|---|---|
| You need the nearest greater/smaller element for every index | You need the k-th largest overall, ignoring position: use Top K Elements (heap) |
| You need the max/min of a sliding window of size k | That's a monotonic deque: the same idea, popping from both ends. See Sliding Window |
| Each bar/element "extends" until something smaller blocks it (histograms) | The question is about pairs that sum to a target in a sorted array: use Two Pointers |
| You're building the smallest/largest sequence greedily by deleting characters | You need range sums, not nearest-bigger: use Prefix Sum |
| Order of the input must be kept (no sorting allowed) | The data is sorted and you're searching for a value: use Modified Binary Search |
Cost: O(n) time (each index is pushed and popped at most once) and O(n) extra space for the stack in the worst case (an already-sorted input never pops).
5. The Python template (memorize this)
# A) Next GREATER element to the right: keep a DECREASING stack of indices
def next_greater(nums):
# answer[i] starts as -1, meaning "nobody bigger shows up to the right"
answer = [-1] * len(nums)
# stack holds INDICES whose answer we do not know yet.
# Their values are decreasing from bottom to top.
stack = []
for i in range(len(nums)):
current = nums[i]
# While the value on top of the stack is smaller than current,
# current is the FIRST bigger value that index has seen.
while len(stack) > 0 and nums[stack[-1]] < current:
top_index = stack.pop() # remove the waiting index...
answer[top_index] = current # ...and give it its answer
# i now waits on the stack for its own next greater element
stack.append(i)
# Indices still on the stack never found anything bigger: they keep -1
return answer
# B) Previous SMALLER element to the left: keep an INCREASING stack
def previous_smaller(nums):
# answer[i] = index of the nearest smaller value to the LEFT of i (-1 if none)
answer = [-1] * len(nums)
# stack holds indices whose values are increasing from bottom to top
stack = []
for i in range(len(nums)):
current = nums[i]
# Values that are >= current can never be the "previous smaller"
# for current or for anyone after it, so throw them away.
while len(stack) > 0 and nums[stack[-1]] >= current:
stack.pop()
# Whatever survives on top is smaller than current: that is the answer
if len(stack) > 0:
answer[i] = stack[-1]
else:
answer[i] = -1
# current may be the "previous smaller" for someone later
stack.append(i)
return answer
What the code is doing, line by line:
- Template A, setup:
answer = [-1] * len(nums)makes a list with one -1 per element (the default "no bigger value" answer).stack = []is a plain Python list used as a stack:appendpushes on top,pop()removes the top,stack[-1]reads the top without removing it. - Template A, loop:
for i in range(len(nums))visits every index from left to right, andcurrent = nums[i]is the newcomer. - Template A, the
while:len(stack) > 0makes sure the stack isn't empty before we peek atstack[-1]. If the top's value is smaller thancurrent, thencurrentis that index's next greater element, so we pop the index and write the answer. We repeat until the top is not smaller. - Template A, push:
stack.append(i)puts the newcomer on the stack, where it waits for its own answer. Because every smaller top was removed first, the stack stays decreasing. - Template B, setup: same shape, but here the answer is an index on the left, and the stack is kept increasing.
- Template B, the
while: pop every index whose value is>= current. Such a value is no use as a "previous smaller" forcurrent, andcurrentblocks it for everyone further right too. - Template B, record: after popping, the top (if the stack isn't empty) is the nearest strictly smaller value to the left, so
answer[i] = stack[-1]; otherwise the answer is -1. Then pushi. - Both functions touch each index at most twice (one push, at most one pop), so the total work is
O(n)even though there is a loop inside a loop.
The four variants differ only in the comparison and in when you record the answer:
| Want | Stack order | Pop while top is… | Answer recorded… |
|---|---|---|---|
| Next greater | decreasing | < x | for the popped index |
| Next smaller | increasing | > x | for the popped index |
| Previous greater | decreasing | <= x | for i, from the top after popping |
| Previous smaller | increasing | >= x | for i, from the top after popping |
6. How to remember it forever
How this hook works: what “The tall newcomer clears the line” means
What the slogan means, phrase by phrase
- “The line”
- The stack: indices that are still waiting for their answer (the first taller one to their right).
- “The tall newcomer”
- The current element,
nums[i], arriving from the right. - “clears the line”
- While the person at the back of the line is shorter than the newcomer, they leave (
pop): the newcomer is their answer. - “Whoever is left is always in sorted order”
- Anyone shorter than someone behind them would already have been cleared, so the stack is always decreasing from bottom to top. That's the "monotonic" in the name.
Why this picture fits the pattern: Monotonic-stack problems ask, for every element, "who is the nearest bigger (or smaller) one to my right?" Elements wait in line until their answer walks in, and each one is answered the moment it arrives.
The story → the code
| In the story | In the code |
|---|---|
| People waiting in line | stack of indices |
| The newcomer | current = nums[i] |
| Shorter person at the back | nums[stack[-1]] < current |
| Leaves with their answer | answer[stack.pop()] = current |
| Newcomer joins the line | stack.append(i) |
| Still waiting at closing time | answer stays -1 |
Watch it on numbers
Next greater element for nums = [2, 1, 5, 3, 6] (values shown in the line for readability; the code stores their indices).
- 2 arrives: the line is empty, so 2 joins → [2].
- 1 arrives: not taller than 2, so 1 joins → [2, 1].
- 5 arrives: clears 1 (answer 5) and 2 (answer 5), then joins → [5].
- 3 arrives and joins → [5, 3]. 6 arrives: clears 3 (answer 6) and 5 (answer 6) → [6].
- Closing time: 6 never saw anyone taller → −1. Answer
[5, 5, 6, 6, -1]. Everyone joined once and left once:O(n).
The trap it saves you from
Storing values instead of indices (Daily Temperatures needs i - top_index for the distance), or flipping the comparison and keeping the wrong order.
It still works when…
For the next smaller element, a short newcomer clears the taller people instead, so the stack is increasing. For a circular array (503), walk the line twice. Largest Rectangle in Histogram: when a bar is cleared, the newcomer is its right boundary and whoever is now at the back of the line is its left boundary.
Picture
A row of buildings at sunset. Walking right, each new tall building "casts its shadow" back over the shorter ones in front of it. Those shorter ones are popped: their next taller building has been found.
Chant
"While top loses, pop and record; then push me."
Direction rule
"Hunt greater, stack falls. Hunt smaller, stack climbs." Store indices, never just values.
7. LeetCode problems, solved step by step
Medium 739. Daily Temperatures Start here Google Meta Amazon Microsoft
Problem: Given daily temperatures, for each day return how many days you must wait until a warmer day. If there's none, put 0.
Solve it on LeetCode ↗ · Video walkthrough ↗
temperatures = [73,74,75,71,69,72,76,73][1,1,4,2,1,1,0,0]temperatures = [30,40,50,60][1,1,1,0]Constraints: 1 ≤ temperatures.length ≤ 10⁵ · 30 ≤ temperatures[i] ≤ 100
Hint 1
For each day you need the next warmer day: a "next greater element" question.
Hint 2
Keep a stack of days still waiting for a warmer one (temperatures decreasing upward). Each new day pops and answers every colder day on top.
From brute force to the pattern: Scanning forward from every day for a warmer one is O(n²). A monotonic stack answers each day the moment its warmer day arrives, and each day is pushed and popped once: O(n).
Spot it: "wait until a warmer day" = next greater element, but we want the distance, so we store indices.
Key insight: Keep a decreasing stack of day-indices that are still waiting. When a warmer day i arrives, it answers every colder day waiting_day it pops: answer[waiting_day] = i - waiting_day.
Approach:
- Create
answer = [0] * n. 0 means "no warmer day", the default for days that never get answered. - Walk the days left to right with an empty stack of indices.
- While today is warmer than the day on top of the stack, pop that day
waiting_dayand writeanswer[waiting_day] = i - waiting_day. - Push today's index. It now waits for its own warmer day.
- Days still on the stack at the end keep 0.
from typing import List
class Solution:
def dailyTemperatures(self, temperatures: List[int]) -> List[int]:
n = len(temperatures)
# answer[day] = how many days until a warmer day (0 = never gets warmer)
answer = [0] * n
# stack holds indices of days still waiting for a warmer day
stack = []
for i in range(n):
today = temperatures[i]
# Today is warmer than the day on top? Then that day's wait is over.
while len(stack) > 0 and temperatures[stack[-1]] < today:
waiting_day = stack.pop() # day that just found a warmer day
answer[waiting_day] = i - waiting_day # distance in days
# Today starts waiting for its own warmer day
stack.append(i)
# Days left on the stack never got warmer, so they keep 0
return answer
What the code is doing, line by line:
answer = [0] * nmakes a list ofnzeros. 0 is the right answer for any day that never sees a warmer day, so we only overwrite days that do.stackholds day indices (not temperatures) because we need them to compute distances. Their temperatures go down from bottom to top.- For each day
i,todayis its temperature.stack[-1]is the index on top, sotemperatures[stack[-1]]is that day's temperature. - The
whileloop keeps going as long as the stack is not empty and the top day is colder than today. Each such day has just found its first warmer day: pop it and store the gapi - waiting_day. - When the loop stops, the top (if any) is at least as warm as today, so today has to wait too:
stack.append(i). - Return
answer; leftover days on the stack keep their 0.
Dry run on the example: temperatures = [73, 74, 75, 71, 69, 72, 76, 73]
| i | today | Popped (answer = i − day) | stack after | answer |
|---|---|---|---|---|
| 0 | 73 | nothing | [0] | [0, 0, 0, 0, 0, 0, 0, 0] |
| 1 | 74 | day 0 (73): answer = 1-0 = 1 | [1] | [1, 0, 0, 0, 0, 0, 0, 0] |
| 2 | 75 | day 1 (74): answer = 2-1 = 1 | [2] | [1, 1, 0, 0, 0, 0, 0, 0] |
| 3 | 71 | nothing | [2, 3] | [1, 1, 0, 0, 0, 0, 0, 0] |
| 4 | 69 | nothing | [2, 3, 4] | [1, 1, 0, 0, 0, 0, 0, 0] |
| 5 | 72 | day 4 (69): answer = 5-4 = 1; day 3 (71): answer = 5-3 = 2 | [2, 5] | [1, 1, 0, 2, 1, 0, 0, 0] |
| 6 | 76 | day 5 (72): answer = 6-5 = 1; day 2 (75): answer = 6-2 = 4 | [6] | [1, 1, 4, 2, 1, 1, 0, 0] |
| 7 | 73 | nothing | [6, 7] | [1, 1, 4, 2, 1, 1, 0, 0] |
Final answer: [1, 1, 4, 2, 1, 1, 0, 0].
Edge cases to test:
- Strictly decreasing temperatures → all 0.
- Strictly increasing → all 1 except the last day.
- Equal temperatures are not warmer.
Complexity: O(n) time, O(n) space.
Medium 503. Next Greater Element II Start here Google Meta Amazon Microsoft
Problem: The array is circular: after the last element comes the first. For each element, return the next greater value when walking forward (wrapping around), or -1.
nums = [1,2,1][2,-1,2]nums = [1,2,3,4,3][2,3,4,-1,4]Constraints: 1 ≤ nums.length ≤ 10⁴ · -10⁹ ≤ nums[i] ≤ 10⁹
Hint 1
The array is circular: after the last element, the search continues from the start.
Hint 2
Run the next-greater stack over the array twice (indices 0..2n − 1, using i % n), only pushing in the first round.
From brute force to the pattern: For each element, scanning up to n − 1 steps around the circle is O(n²). Two laps of a monotonic stack handle the wrap-around in O(n).
Spot it: "next greater element" → decreasing stack. "Circular" → the answer for an element near the end may be near the start.
Key insight: Walk the array twice (step from 0 to 2n-1, using step % n). The second lap lets leftover elements find answers at the beginning. Only push indices during the first lap so nothing is answered twice.
Approach:
- Fill
answerwith -1 and start an empty decreasing stack of indices. - Loop
stepfrom 0 to2n - 1and readcurrent = nums[step % n]. The second lap plays the role of the wrap-around. - While the top's value is smaller than
current, pop it and set its answer tocurrent. - Push the index only during the first lap (
step < n), so every index waits at most once. - Whatever remains (the maximum, and equal copies of it) keeps -1.
from typing import List
class Solution:
def nextGreaterElements(self, nums: List[int]) -> List[int]:
n = len(nums)
# -1 means "no greater value anywhere around the circle"
answer = [-1] * n
# decreasing stack of indices still waiting for an answer
stack = []
# Walk the array twice (2 * n steps) to simulate wrapping around
for step in range(2 * n):
index = step % n # % n turns step n, n+1, ... back into 0, 1, ...
current = nums[index]
# current answers every smaller value waiting on the stack
while len(stack) > 0 and nums[stack[-1]] < current:
top_index = stack.pop()
answer[top_index] = current
# Only push during the first lap so each index waits at most once
if step < n:
stack.append(index)
return answer
What the code is doing, line by line:
answerstarts as all -1 (the answer for anything that never finds a bigger value).stackholds indices waiting for their answer; their values decrease from bottom to top.stepruns from 0 to2n - 1.index = step % nis the real position: the remainder after dividing byn, so stepsn, n+1, ...map back to0, 1, .... This second lap is how the array "wraps around".- The
whileloop pops every waiting index whose value is smaller thancurrentand recordscurrentas its answer, just like the next-greater template. if step < n: indices are pushed only during the first lap. In the second lap we only answer leftover indices; pushing again would make them wait twice.- Indices still on the stack at the end (the maximum and its equal copies) keep -1.
Dry run on the example: nums = [1, 2, 1] (n = 3)
| step | index | current | Popped | Pushed? | stack after | answer |
|---|---|---|---|---|---|---|
| 0 | 0 | 1 | nothing | yes | [0] | [-1, -1, -1] |
| 1 | 1 | 2 | index 0: answer = 2 | yes | [1] | [2, -1, -1] |
| 2 | 2 | 1 | nothing | yes | [1, 2] | [2, -1, -1] |
| 3 | 0 | 1 | nothing | no (2nd lap) | [1, 2] | [2, -1, -1] |
| 4 | 1 | 2 | index 2: answer = 2 | no (2nd lap) | [1] | [2, -1, 2] |
| 5 | 2 | 1 | nothing | no (2nd lap) | [1] | [2, -1, 2] |
Final answer: [2, -1, 2].
Edge cases to test:
- All equal values → all −1.
- The maximum element → −1.
- The answer wraps around, e.g.
[1, 2, 1]→[2, -1, 2].
Complexity: O(n) time (2n steps, each index pushed/popped once), O(n) space.
Medium 402. Remove K Digits Google Meta Amazon Microsoft
Problem: Given a number as a string and an integer k, delete exactly k digits so the remaining number is as small as possible. Return it without leading zeros ("0" if empty).
num = "1432219", k = 3"1219"num = "10200", k = 1"200"Constraints: 1 ≤ k ≤ num.length ≤ 10⁵ · num consists of only digits. · num does not have any leading zeros except for the zero itself.
Hint 1
To make the number small, the leftmost digits matter most. When is it worth deleting a digit?
Hint 2
Keep an increasing stack of digits: while the top is bigger than the incoming digit and you can still delete, pop it.
From brute force to the pattern: Trying every combination of k deletions is exponential; deleting the best single digit k times is O(n · k). A greedy monotonic stack removes every "peak" digit in one pass: O(n).
Spot it: "remove k characters to get the smallest result" while keeping order. A digit should be deleted when a smaller digit comes right after it. That's "pop the top while it's bigger than the newcomer": an increasing stack.
Key insight: Leftmost digits matter most. Greedily, whenever the top of the stack is larger than the incoming digit and you still have deletions left, pop it. If deletions remain at the end, the stack is non-decreasing, so drop from the tail.
Approach:
- Scan the digits left to right, keeping a stack of digits you've decided to keep.
- While you still have deletions (
deletions_left > 0) and the top digit is bigger than the current digit, pop it and dodeletions_left = deletions_left - 1. A smaller digit moving left always shrinks the number. - Push the current digit.
- If deletions remain, the stack is non-decreasing, so pop that many digits off the end (the largest ones).
- Strip leading zeros. Return
"0"if nothing is left.
class Solution:
def removeKdigits(self, num: str, k: int) -> str:
# how many digits we are still allowed to delete
deletions_left = k
# digits we keep, in non-decreasing order bottom -> top
stack = []
for i in range(len(num)):
digit = num[i]
# A bigger digit sitting right before a smaller one makes the number
# bigger, so delete it (as long as we still have deletions left).
while deletions_left > 0 and len(stack) > 0 and stack[-1] > digit:
stack.pop()
deletions_left = deletions_left - 1
stack.append(digit)
# Deletions left over: the stack is non-decreasing, so the biggest
# digits are at the end. Remove them one at a time from the end.
while deletions_left > 0:
stack.pop()
deletions_left = deletions_left - 1
# Skip leading zeros: move 'start' past every "0" at the front
start = 0
while start < len(stack) and stack[start] == "0":
start = start + 1
# Build the answer string one character at a time
result = ""
for i in range(start, len(stack)):
result = result + stack[i]
# Nothing left means the number is zero
if result == "":
return "0"
return result
What the code is doing, line by line:
deletions_leftis a copy ofkthat we count down.stackholds the digits (as characters) we have decided to keep.- For each
digit, thewhileloop pops the top digit if we still have deletions, the stack is not empty, and the top is bigger thandigit. Comparing single-character strings like"4" > "3"works just like comparing the digits. - Push
digit. The stack stays non-decreasing from bottom to top. - After the scan, any remaining deletions are used on the end of the stack, where the largest digits sit (e.g.
"112"withk = 1drops the final 2). startwalks forward past any"0"characters at the front, because a number never shows leading zeros.- The
forloop fromstartto the end glues the remaining digits intoresult. - If every digit was removed (or all were zeros),
resultis empty, so return"0".
Dry run on the example: num = "1432219", k = 3
| i | digit | Popped | deletions_left | stack after |
|---|---|---|---|---|
| 0 | 1 | nothing | 3 | 1 |
| 1 | 4 | nothing | 3 | 14 |
| 2 | 3 | 4 | 2 | 13 |
| 3 | 2 | 3 | 1 | 12 |
| 4 | 2 | nothing | 1 | 122 |
| 5 | 1 | 2 | 0 | 121 |
| 6 | 9 | nothing | 0 | 1219 |
No deletions are left over and there are no leading zeros, so the answer is "1219".
Edge cases to test:
kequals the length →"0".- Leading zeros after deletion must be stripped, e.g.
"10200",k = 1→"200". - Increasing digits: remove from the end.
Complexity: O(n) time, O(n) space.
Medium 907. Sum of Subarray Minimums Google Amazon Microsoft
Problem: Add up min(sub) over every contiguous subarray of arr. Return the sum modulo 10⁹+7.
arr = [3,1,2,4]17arr = [11,81,94,43,3]444Constraints: 1 ≤ arr.length ≤ 3 * 10⁴ · 1 ≤ arr[i] ≤ 3 * 10⁴
Hint 1
Instead of finding every subarray's minimum, ask: for how many subarrays is arr[i] the minimum?
Hint 2
Count = (distance to the previous smaller) × (distance to the next smaller). Find both with monotonic stacks, and break ties on one side only.
From brute force to the pattern: Enumerating all O(n²) subarrays and tracking their minima is O(n²). Counting each element's contribution with previous/next-smaller stacks is O(n).
Spot it: There are O(n²) subarrays, so enumerating them is too slow. Flip the question: for each element, in how many subarrays is it the minimum? That depends on the nearest smaller element on each side, which is a Monotonic Stack job.
Key insight: If arr[i]'s previous smaller is at L and its next smaller is at R, then any subarray that starts in (L, i] and ends in [i, R) has arr[i] as its minimum. That's (i − L) × (R − i) subarrays. Contribution = arr[i] × (i − L) × (R − i). To avoid double-counting equal values, use "strictly smaller" on one side and "smaller or equal" on the other.
Approach:
- For each index
i, findL= index of the previous strictly smaller element (−1 if none) with an increasing stack scanned left to right. - Find
R= index of the next smaller-or-equal element (n if none) with a second stack scanned right to left. Mixing strict and non-strict counts tied subarrays exactly once. arr[i]is the minimum of(i − L) × (R − i)subarrays.- Add
arr[i] × (i − L) × (R − i)for everyiand take the result mod 10⁹+7.
from typing import List
class Solution:
def sumSubarrayMins(self, arr: List[int]) -> int:
MOD = 10**9 + 7
n = len(arr)
# left_count[i] = how many choices for the LEFT end where arr[i] is the minimum
# right_count[i] = how many choices for the RIGHT end where arr[i] is the minimum
left_count = [0] * n
right_count = [0] * n
# Pass 1, left to right: previous STRICTLY smaller element
stack = []
for i in range(n):
# Pop everything >= arr[i]; they are not "strictly smaller"
while len(stack) > 0 and arr[stack[-1]] >= arr[i]:
stack.pop()
if len(stack) > 0:
previous_smaller = stack[-1]
else:
previous_smaller = -1 # nothing smaller on the left
left_count[i] = i - previous_smaller
stack.append(i)
# Pass 2, right to left: next smaller-OR-EQUAL element (tie-break)
stack = []
i = n - 1
while i >= 0:
# Pop everything > arr[i]; equal values stay and act as a wall
while len(stack) > 0 and arr[stack[-1]] > arr[i]:
stack.pop()
if len(stack) > 0:
next_smaller_or_equal = stack[-1]
else:
next_smaller_or_equal = n # nothing smaller on the right
right_count[i] = next_smaller_or_equal - i
stack.append(i)
i = i - 1 # move one step to the left
# Add up each element's contribution: value * (#left ends) * (#right ends)
total = 0
for i in range(n):
contribution = arr[i] * left_count[i] * right_count[i]
total = total + contribution
return total % MOD
What the code is doing, line by line:
MODis 10⁹+7, required by the problem because the sum can be huge.left_countandright_countstart as lists of zeros, one slot per element.- Pass 1 (left to right) is the "previous smaller" template. Pop every index whose value is
>= arr[i]; what survives on top is the nearest strictly smaller element on the left (or -1 if the stack is empty).left_count[i] = i - previous_smalleris how many start positions(previous_smaller, i]keeparr[i]as the minimum. - Pass 2 (right to left) uses
i = n - 1/while i >= 0/i = i - 1to walk backwards. It pops only values that are strictly greater, so an equal value stays as a wall. The survivor is the nearest smaller-or-equal element on the right (ornif none).right_count[i]is how many end positions[i, next_smaller_or_equal)are allowed. - Using "strictly smaller" on one side and "smaller or equal" on the other means a subarray with several equal minimums is counted for exactly one of them.
- The final loop multiplies
arr[i] × left_count[i] × right_count[i](value × number of subarrays where it is the minimum), adds everything up, and returns the total mod 10⁹+7.
Dry run on the example: arr = [3, 1, 2, 4] (n = 4)
| i | arr[i] | previous_smaller | left_count | next_smaller_or_equal | right_count | contribution | total |
|---|---|---|---|---|---|---|---|
| 0 | 3 | -1 | 1 | 1 | 1 | 3*1*1 = 3 | 3 |
| 1 | 1 | -1 | 2 | 4 | 3 | 1*2*3 = 6 | 9 |
| 2 | 2 | 1 | 1 | 4 | 2 | 2*1*2 = 4 | 13 |
| 3 | 4 | 2 | 1 | 4 | 1 | 4*1*1 = 4 | 17 |
Final answer: 17 % (10⁹+7) = 17.
Edge cases to test:
- Equal values: use
<on one side and≤on the other so no subarray is counted twice. - A single element.
- Take the answer modulo
10⁹ + 7.
Complexity: O(n) time, O(n) space.
Hard 84. Largest Rectangle in Histogram Google Meta Amazon Cisco Microsoft
Problem: Bars of width 1 have heights heights[i]. Find the area of the largest rectangle that fits entirely inside the histogram.
heights = [2,1,5,6,2,3]10heights = [2,4]4Constraints: 1 ≤ heights.length ≤ 10⁵ · 0 ≤ heights[i] ≤ 10⁴
Hint 1
For each bar, the widest rectangle of its height extends until a shorter bar on each side.
Hint 2
Keep an increasing stack of bar indices; when a shorter bar arrives, pop: the popped bar's rectangle ends here and starts after the new top.
From brute force to the pattern: Expanding left and right from every bar is O(n²). An increasing stack finds each bar's left and right boundaries the moment they become known: O(n).
Spot it: A rectangle that uses bar i as its shortest bar can stretch left and right until it hits a shorter bar. "Nearest shorter on both sides" → an increasing stack.
Key insight: Keep indices with increasing heights. When bar i is shorter than the top, the top bar can't extend past i, so its rectangle is finished: its right wall is i and its left wall is the new top of the stack after popping. Width = i − left − 1. Append a sentinel height 0 at the end to flush everything.
Approach:
- Append a sentinel height 0 so every bar gets popped at the end.
- Keep a stack of indices whose heights are increasing.
- When the current height
his ≤ the top's height, pop the top. Its rectangle can't extend pasti. - Its left wall is the new top of the stack (or −1 if empty), so width =
i − left − 1. Updatebestwithheight × width. - Push
iand continue.bestholds the answer at the end.
from typing import List
class Solution:
def largestRectangleArea(self, heights: List[int]) -> int:
# Copy the heights and add a sentinel bar of height 0 at the end.
# The 0 is shorter than everything, so it pops (finishes) every bar.
bars = []
for i in range(len(heights)):
bars.append(heights[i])
bars.append(0)
# stack holds indices whose heights are increasing bottom -> top
stack = []
best = 0
for i in range(len(bars)):
current_height = bars[i]
# The top bar cannot stretch past i, so its rectangle is finished
while len(stack) > 0 and bars[stack[-1]] >= current_height:
top_index = stack.pop()
height = bars[top_index]
# Left wall = first shorter bar to the left (new top), or -1
if len(stack) > 0:
left_wall = stack[-1]
else:
left_wall = -1
# The rectangle spans the bars strictly between the two walls
width = i - left_wall - 1
area = height * width
if area > best:
best = area
stack.append(i)
return best
What the code is doing, line by line:
- The first loop copies
heightsintobars(so we don't change the caller's list), then appends a sentinel 0. Because 0 is shorter than every real bar, reaching it pops everything left on the stack, so no bar is forgotten. stackholds indices of bars with increasing heights.bestis the biggest area found so far.- For each bar
i, thewhileloop runs while the bar on top is at least as tall as the current one. That top bar can't extend to the right pasti, so its rectangle is complete. top_index = stack.pop()removes it;heightis its height, which is the height of its rectangle.- After popping, the new top (if any) is the nearest shorter bar on the left: the left wall. If the stack is empty, the rectangle reaches the very start, so we use -1 as the wall.
width = i - left_wall - 1counts the bars strictly between the two walls.area = height * width; keep it if it beatsbest.- Push
i. The stack stays increasing.
Dry run on the example: heights = [2, 1, 5, 6, 2, 3] (so bars = [2, 1, 5, 6, 2, 3, 0])
| i | bars[i] | Pops (width = i − left_wall − 1) | stack after | best |
|---|---|---|---|---|
| 0 | 2 | nothing | [0] | 0 |
| 1 | 1 | pop 0 (h=2): width 1-(-1)-1=1, area 2 | [1] | 2 |
| 2 | 5 | nothing | [1, 2] | 2 |
| 3 | 6 | nothing | [1, 2, 3] | 2 |
| 4 | 2 | pop 3 (h=6): width 4-(2)-1=1, area 6; pop 2 (h=5): width 4-(1)-1=2, area 10 | [1, 4] | 10 |
| 5 | 3 | nothing | [1, 4, 5] | 10 |
| 6 | 0 | pop 5 (h=3): width 6-(4)-1=1, area 3; pop 4 (h=2): width 6-(1)-1=4, area 8; pop 1 (h=1): width 6-(-1)-1=6, area 6 | [6] | 10 |
Final answer: 10 (bars of height 5 and 6, using height 5 × width 2).
Edge cases to test:
- All bars equal height → height × n.
- Strictly increasing heights (the end sentinel matters).
- A single bar.
Complexity: O(n) time, O(n) space.
8. Common mistakes
ans[j] from a value alone. Push indices and read nums[stack[-1]].< vs <= with duplicatesDecide whether equal values count as "greater". In counting problems (907), use strict on one side and non-strict on the other, or you'll double-count.while len(stack) > 0 and ... (or the short form while stack and ...). Reading stack[-1] on an empty list raises IndexError.9. Self-check quiz
To find the next greater element for every index, the stack's values (bottom → top) should be…
The code has a while loop nested inside a for loop. What's the total time complexity?
"For each building, how many buildings to its right can it see before a taller one blocks the view?"
"Return the maximum of every window of size k." Which variant fits best?
10. Practice list (in order)
| # | Problem | Level | Data structure | Twist |
|---|---|---|---|---|
| 496 | Next Greater Element I | Easy | Warm-up: template + hash map lookup | |
| 739 | Daily Temperatures | Medium | Store indices, answer = distance | |
| 503 | Next Greater Element II | Medium | Circular: loop twice | |
| 901 | Online Stock Span | Medium | Streaming "previous greater", store (price, span) | |
| 402 | Remove K Digits | Medium | Greedy pop with a deletion budget | |
| 1081 | Smallest Subsequence of Distinct Characters | Medium | Only pop if the char appears again later | |
| 907 | Sum of Subarray Minimums | Medium | Contribution counting, tie-break | |
| 456 | 132 Pattern | Medium | Scan from the right, track the "2" | |
| 853 | Car Fleet | Medium | Sort by position; a stack of arrival times that must strictly increase | |
| 316 | Remove Duplicate Letters | Medium | Increasing stack; pop a bigger letter only if it appears again later | |
| 581 | Shortest Unsorted Continuous Subarray | Medium | Right end = last element below the running max; left end = last element (from the right) above the running min | |
| 962 | Maximum Width Ramp | Medium | Decreasing stack of candidate starts; then scan ends from the right, popping matched starts | |
| 1008 | Construct Binary Search Tree from Preorder Traversal | Medium | A stack of the current right spine: pop smaller nodes to find the parent of a right child | |
| 2104 | Sum of Subarray Ranges | Medium | Σ(max) − Σ(min) over all subarrays, each via contribution counting with monotonic stacks | |
| 1762 | Buildings With an Ocean View | Medium | Scan from the right keeping the tallest seen; a building sees the ocean if it's taller than everything to its right | |
| 1856 | Maximum Subarray Min-Product | Medium | For each element as the minimum, extend to the nearest smaller on both sides (monotonic stack) and use a prefix sum | |
| 2487 | Remove Nodes From Linked List | Medium | Keep a non-increasing stack of nodes: a bigger node pops every smaller node before it | |
| 1696 | Jump Game VI | Medium | dp[i] = nums[i] + max(dp[i−k..i−1]); keep that window max in a monotonic deque | |
| 84 | Largest Rectangle in Histogram | Hard | Increasing stack + sentinel | |
| 42 | Trapping Rain Water | Hard | Water fills "valleys" when a taller bar pops | |
| 85 | Maximal Rectangle | Hard | Each row becomes a histogram → 84 | |
| 239 | Sliding Window Maximum | Hard | Monotonic deque | |
| 975 | Odd Even Jump | Hard | Next jump targets via sorted order + monotonic stack; then DP from the right | |
| 1944 | Number of Visible People in a Queue | Hard | Scan from the right with a decreasing stack; each pop is a person you can see | |
| 2454 | Next Greater Element IV | Hard | Two monotonic stacks: waiting-for-first-greater and waiting-for-second-greater; move popped groups across in order | |
| 1526 | Minimum Number of Increments on Subarrays to Form a Target Array | Hard | Every rise target[i] − target[i−1] > 0 needs that many new operations; drops reuse existing ones | |
| 2281 | Sum of Total Strength of Wizards | Hard | Monotonic stack gives each element's range as the minimum; a prefix sum of prefix sums adds all subarray sums in O(1) |
Worked solutions for the practice list
Every problem in the table that isn't solved in section 7 is worked out below. Click a problem name in the table to jump to it.
Easy 496. Next Greater Element I Start here Google Meta Amazon Microsoft
Problem: nums1 is a subset of nums2 (all values distinct). For each value in nums1, find the first greater value to its right in nums2, or -1.
nums1 = [4,1,2], nums2 = [1,3,4,2][-1,3,-1]nums1 = [2,4], nums2 = [1,2,3,4][3,-1]Constraints: 1 ≤ nums1.length ≤ nums2.length ≤ 1000 · 0 ≤ nums1[i], nums2[i] ≤ 10⁴ · All integers in nums1 and nums2 are unique. · All the integers of nums1 also appear in nums2.
Hint 1
Compute the next greater element for every value of nums2 once.
Hint 2
Use a monotonic stack over nums2 and store the answers in a dictionary; then look up each nums1 value.
From brute force to the pattern: For each value of nums1, finding it in nums2 and scanning right is O(m · n). One monotonic-stack pass over nums2 plus a hash map gives O(m + n).
Spot it: "Next greater element" is the template word for word. The only twist is that answers are looked up by value, so store them in a dict.
Approach:
- Scan
nums2with a decreasing stack of values. - When
currentis bigger than the top, pop the top and recordnext_bigger[top] = current. - Push
current. - Answer each query in
nums1: look it up innext_bigger, or use -1 if it was never popped.
from typing import List
class Solution:
def nextGreaterElement(self, nums1: List[int], nums2: List[int]) -> List[int]:
# next_bigger[value] = first bigger value to its right in nums2
next_bigger = {}
# decreasing stack of VALUES (allowed because the values are distinct)
stack = []
for i in range(len(nums2)):
current = nums2[i]
# current is the first bigger value for every smaller value on top
while len(stack) > 0 and stack[-1] < current:
smaller_value = stack.pop()
next_bigger[smaller_value] = current
stack.append(current)
# Answer each query; values never popped have no bigger value (-1)
answer = []
for i in range(len(nums1)):
value = nums1[i]
if value in next_bigger:
answer.append(next_bigger[value])
else:
answer.append(-1)
return answer
What the code is doing, line by line:
next_biggeris a dictionary that maps a value to the first bigger value after it innums2. We key by value (not index) because the queries innums1are values.stackholds values still waiting for a bigger value; they are decreasing from bottom to top. Storing values instead of indices is safe here only because all values are distinct.- For each
currentinnums2, thewhileloop pops every smaller value on top and recordscurrentas its answer. It is the first bigger value each of them has met. - Push
current; it now waits for its own bigger value. - The second loop builds the answer for
nums1. If a value is innext_bigger, use that; otherwise it was never popped, so nothing bigger came after it: use -1.
Dry run on the example: nums1 = [4, 1, 2], nums2 = [1, 3, 4, 2]
| i | current | Popped (value → answer) | stack after | next_bigger |
|---|---|---|---|---|
| 0 | 1 | nothing | [1] | {} |
| 1 | 3 | 1 → 3 | [3] | {1: 3} |
| 2 | 4 | 3 → 4 | [4] | {1: 3, 3: 4} |
| 3 | 2 | nothing | [4, 2] | {1: 3, 3: 4} |
Queries: 4 is not in the dictionary → -1; 1 → 3; 2 is not in the dictionary → -1. Final answer: [-1, 3, -1].
Edge cases to test:
- The value is the last element of
nums2→ −1. - No greater value exists → −1.
nums1equal tonums2.
Complexity: O(n + m) time, O(m) space.
Medium 901. Online Stock Span Google Meta Amazon
Problem: Prices arrive one per day. For each new price, return its span: how many consecutive days, ending today and going backwards, had a price ≤ today's.
["StockSpanner", "next", "next", "next", "next", "next", "next", "next"]
[[], [100], [80], [60], [70], [60], [75], [85]][null, 1, 1, 1, 2, 1, 4, 6]Constraints: 1 ≤ price ≤ 10⁵ · At most 10⁴ calls will be made to next.
Hint 1
The span of today = 1 + the spans of the consecutive previous days with price ≤ today's.
Hint 2
Keep a stack of (price, span); pop every smaller-or-equal price and absorb its span.
From brute force to the pattern: Walking back day by day for each new price is O(n) per call. A monotonic stack merges smaller days into the next bigger one, so each price is popped once: amortised O(1) per call.
Spot it: "Going backwards until a bigger price" means you need the previous greater element, in a stream. A decreasing stack that remembers how many days each entry already covers answers it.
Approach:
- Keep a stack of
(price, span)pairs with strictly decreasing prices. - On a new price, start with
span = 1(today). - While the top's price is ≤ today's, pop it and add its span. Those days are all covered by today now.
- Push
(price, span)and returnspan.
class StockSpanner:
def __init__(self):
# Each entry is a pair (price, span). Prices are strictly decreasing
# from bottom to top.
self.stack = []
def next(self, price: int) -> int:
span = 1 # today always counts
# Every earlier price <= today is covered by today's span.
# Its span already counts the days IT covered, so add them all at once.
while len(self.stack) > 0 and self.stack[-1][0] <= price:
top_pair = self.stack.pop()
top_span = top_pair[1] # index 1 = the span part
span = span + top_span
self.stack.append((price, span))
return span
What the code is doing, line by line:
__init__creates an empty stack. Each entry will be a pair(price, span);pair[0]is the price andpair[1]is the span.next(price)starts withspan = 1because today itself always counts.self.stack[-1][0]is the price of the top entry. While it is<= price, that day (and every day it already covered) is part of today's span, so we pop it and add its span to ours. We never need those days again: today is at least as high and blocks them.- Push
(price, span)so later days can absorb today's whole run in one step, then returnspan.
Dry run on the example: calls next(100), next(80), next(60), next(70), next(60), next(75), next(85)
| price | Popped pairs | span returned | stack after (bottom → top) |
|---|---|---|---|
| 100 | nothing | 1 | (100, 1) |
| 80 | nothing | 1 | (100, 1) (80, 1) |
| 60 | nothing | 1 | (100, 1) (80, 1) (60, 1) |
| 70 | (60, 1) | 2 | (100, 1) (80, 1) (70, 2) |
| 60 | nothing | 1 | (100, 1) (80, 1) (70, 2) (60, 1) |
| 75 | (60, 1), (70, 2) | 4 | (100, 1) (80, 1) (75, 4) |
| 85 | (75, 4), (80, 1) | 6 | (100, 1) (85, 6) |
Outputs: [1, 1, 1, 2, 1, 4, 6].
Edge cases to test:
- Rising prices → spans grow 1, 2, 3, …
- Falling prices → every span is 1.
- Equal prices count as ≤ and are absorbed.
Complexity: Amortized O(1) per call (each day is pushed and popped once), O(n) space.
Medium 1081. Smallest Subsequence of Distinct Characters Google Amazon
Problem: Return the lexicographically smallest subsequence of s that contains every distinct letter exactly once. (Same as LeetCode 316.)
s = "bcabc""abc"s = "cbacdcbc""acdb"Constraints: 1 ≤ s.length ≤ 1000 · s consists of lowercase English letters.
Hint 1
You want the smallest string that uses each distinct letter exactly once. When can you remove a letter you've already placed?
Hint 2
Pop the top if it's bigger than the new letter and it appears again later. Skip letters already in the stack.
From brute force to the pattern: Generating subsequences of distinct letters is exponential. A greedy monotonic stack, with each letter's last index and an in-stack set, builds the answer in O(n).
Spot it: "Smallest result" built by deleting characters while keeping order is the same greedy as Remove K Digits: an increasing stack. The extra rule is that you can only drop a letter if it appears again later.
Approach:
- Record
last_index[letter], the last index of each letter. - Scan
s. Skipletterif it's already on the stack. - While the top is bigger than
letterand appears again later (last_index[top] > i), pop it. You'll pick it up again later, in a better spot. - Push
letterand mark it as used. Glue the stack into a string at the end.
class Solution:
def smallestSubsequence(self, s: str) -> str:
# last_index[letter] = last position where that letter appears.
# Later positions overwrite earlier ones, so we end with the last one.
last_index = {}
for i in range(len(s)):
letter = s[i]
last_index[letter] = i
stack = [] # letters of the answer, built left to right
used = set() # letters currently on the stack
for i in range(len(s)):
letter = s[i]
if letter in used:
continue # each letter appears only once in the answer
# Drop a bigger letter on top if it shows up again later
# (we can re-add it later in a better position).
while len(stack) > 0 and stack[-1] > letter and last_index[stack[-1]] > i:
removed_letter = stack.pop()
used.remove(removed_letter)
stack.append(letter)
used.add(letter)
# Glue the letters together into one string
result = ""
for i in range(len(stack)):
result = result + stack[i]
return result
What the code is doing, line by line:
- The first loop fills
last_index. Each time a letter is seen, its entry is overwritten with the current position, so at the end it holds the last position of every letter. stackis the answer being built;usedis a set that answers "is this letter already on the stack?" instantly.- In the main loop, a letter that is already used is skipped with
continue: the answer must contain each letter exactly once. - The
whileloop pops the top letter if three things are true: the stack is not empty, the top letter is bigger than the current one (so putting the current letter earlier makes the string smaller), and the top letter appears again later (last_index[...] > i), so it is safe to drop it now and add it back later. A popped letter is also removed fromused. - Push the current letter and mark it as used.
- The last loop glues the stacked letters into one string, left to right.
Dry run on the example: s = "bcabc", so last_index = {'b': 3, 'c': 4, 'a': 2}
| i | letter | Popped | stack after |
|---|---|---|---|
| 0 | b | nothing | b |
| 1 | c | nothing | bc |
| 2 | a | c (appears again at 4), b (appears again at 3) | a |
| 3 | b | nothing | ab |
| 4 | c | nothing | abc |
Final answer: "abc".
Edge cases to test:
- All letters distinct → the letters in their original order.
- Letters already in increasing order.
- A letter whose last occurrence is early, so it can't be popped.
Complexity: O(n) time, O(1) extra space (at most 26 letters).
Medium 456. 132 Pattern Amazon
Problem: Return True if there are indices i < j < k with nums[i] < nums[k] < nums[j] (small, big, middle).
nums = [1,2,3,4]falsenums = [3,1,4,2]trueConstraints: n == nums.length · 1 ≤ n ≤ 2 * 10⁵ · -10⁹ ≤ nums[i] ≤ 10⁹
Hint 1
You need i < j < k with nums[i] < nums[k] < nums[j]. Try scanning from the right.
Hint 2
Keep a decreasing stack and a value third (the best nums[k] so far); any number below third completes the pattern.
From brute force to the pattern: Checking all triples is O(n³); fixing j and tracking the minimum to its left is O(n²). Scanning from the right with a monotonic stack keeps the largest valid "2" for every "3": O(n).
Spot it: For each candidate "3" (nums[j]), we want the largest "2" to its right that is smaller than it. That's a previous/next-greater relationship, so use a monotonic stack scanned from the right.
Approach:
- Scan from right to left, keeping a decreasing stack and
third= the best "2" seen so far (starts at −∞). - If the current value is less than
third, it's a valid "1". Return True. - While the top is smaller than the current value, pop it into
third. The current value is a "3" that sits to the left of the popped "2". - Push the current value. If the scan ends, return False.
from typing import List
class Solution:
def find132pattern(self, nums: List[int]) -> bool:
# third = best "2" found so far (it has a bigger "3" to its left)
third = float("-inf")
# decreasing stack of values, filled while scanning right -> left
stack = []
i = len(nums) - 1
while i >= 0:
current = nums[i]
# current < third means: current ("1") < third ("2") < some "3"
if current < third:
return True
# current is a "3" for every smaller value on top; they become "2" candidates
while len(stack) > 0 and stack[-1] < current:
third = stack.pop()
stack.append(current)
i = i - 1 # move one step to the left
return False
What the code is doing, line by line:
thirdstarts at minus infinity (float("-inf"), smaller than every number), meaning "no valid 2 found yet".i = len(nums) - 1andwhile i >= 0walk the array from the last index down to 0.i = i - 1at the bottom of the loop moves one step left.- If
current < third, thencurrentis the "1": it sits to the left of a "3" and a "2" with1 < 2 < 3. ReturnTrue. - Otherwise, the
whileloop pops every value smaller thancurrent. Each popped value has a bigger value (current) to its left, so it is a valid "2". Because the stack is decreasing, the last value popped is the biggest such "2", which gives the best chance of finding a "1" later. - Push
current; it may serve as a "3" or a "2" for values further left. - If the scan finishes without finding a "1", return
False.
Dry run on the example: nums = [3, 1, 4, 2] (scanning right to left)
| i | current | What happens | third after | stack after |
|---|---|---|---|---|
| 3 | 2 | 2 < -inf? no. Nothing to pop. | -inf | [2] |
| 2 | 4 | 4 < -inf? no. Pop 2 (smaller than 4), so third = 2. | 2 | [4] |
| 1 | 1 | 1 < third=2: return True | 2 | [4] |
Final answer: True (the pattern is 1, 4, 2).
Edge cases to test:
- Fewer than 3 elements → False.
- Strictly increasing → False.
- Duplicates, e.g.
[1, 3, 3]→ False.
Complexity: O(n) time, O(n) space.
Hard 42. Trapping Rain Water Google Meta Nvidia Amazon Microsoft
Problem: Bars of width 1 have heights height[i]. How much rain water is trapped between them after it rains?
Solve it on LeetCode ↗ · Video walkthrough ↗
height = [0,1,0,2,1,0,1,3,2,1,2,1]6height = [4,2,0,3,2,5]9Constraints: n == height.length · 1 ≤ n ≤ 2 * 10⁴ · 0 ≤ height[i] ≤ 10⁵
Hint 1
Water above bar i = min(highest bar to its left, highest bar to its right) − height[i].
Hint 2
With two pointers, the side whose running maximum is smaller already knows its water level. Settle that side and move it inward.
From brute force to the pattern: For every bar, scan left and right for the tallest walls: O(n²). Precomputing left-max and right-max arrays makes it O(n) time but O(n) space. Two pointers drop the arrays: the lower running maximum decides the water, so one side can always be settled: O(1) space.
Spot it: Water collects in a "valley": a lower bar with a taller bar on each side. When a taller bar arrives, it closes the valleys of the shorter bars before it. That's "pop while the top is smaller".
Approach:
- Keep a stack of indices with decreasing heights.
- When
height[i]is taller than the top, pop the top. That bar is the valley's bottom. - If the stack is now empty, there's no left wall, so stop. Otherwise the new top is the left wall.
- Water layer =
(min(left wall, current) − bottom) × (i − left − 1). Add it. - Push
i. (A two-pointer version also works inO(1)space.)
from typing import List
class Solution:
def trap(self, height: List[int]) -> int:
# stack holds indices whose heights are decreasing bottom -> top
stack = []
water = 0
for i in range(len(height)):
current_height = height[i]
# A taller bar closes the valley of the shorter bar on top
while len(stack) > 0 and height[stack[-1]] < current_height:
bottom = stack.pop() # the valley floor
if len(stack) == 0:
break # no left wall: water spills out
left_wall = stack[-1]
# Water rises to the LOWER of the two walls
if height[left_wall] < current_height:
water_level = height[left_wall]
else:
water_level = current_height
depth = water_level - height[bottom]
width = i - left_wall - 1
water = water + depth * width # one horizontal layer of water
stack.append(i)
return water
What the code is doing, line by line:
stackholds indices of bars with decreasing heights (each one could be the left wall of a future pool).wateris the running total.- For each bar
i, thewhileloop runs as long as the bar on top of the stack is shorter than the current bar. That shorter bar is the floor of a valley whose right wall is bari. bottom = stack.pop()takes the floor off the stack. If the stack is now empty there is no left wall, so water would spill out:break.- Otherwise the new top is the left wall. Water can only rise to the lower of the two walls, so
water_levelis the smaller ofheight[left_wall]andcurrent_height. depthis how far that level is above the floor, andwidthis the number of columns strictly between the walls.depth * widthis one horizontal slab of water, added to the total.- After the pops, push
i. It may be the left wall of a later pool.
Dry run on the example: height = [0, 1, 0, 2, 1, 0, 1, 3, 2, 1, 2, 1]
| i | height[i] | Pops and water added | stack after | water |
|---|---|---|---|---|
| 0 | 0 | nothing | [0] | 0 |
| 1 | 1 | pop 0: no left wall, stop | [1] | 0 |
| 2 | 0 | nothing | [1, 2] | 0 |
| 3 | 2 | pop 2: left wall 1, depth 1-0=1, width 1, +1; pop 1: no left wall, stop | [3] | 1 |
| 4 | 1 | nothing | [3, 4] | 1 |
| 5 | 0 | nothing | [3, 4, 5] | 1 |
| 6 | 1 | pop 5: left wall 4, depth 1-0=1, width 1, +1 | [3, 4, 6] | 2 |
| 7 | 3 | pop 6: left wall 4, depth 1-1=0, width 2, +0; pop 4: left wall 3, depth 2-1=1, width 3, +3; pop 3: no left wall, stop | [7] | 5 |
| 8 | 2 | nothing | [7, 8] | 5 |
| 9 | 1 | nothing | [7, 8, 9] | 5 |
| 10 | 2 | pop 9: left wall 8, depth 2-1=1, width 1, +1 | [7, 8, 10] | 6 |
| 11 | 1 | nothing | [7, 8, 10, 11] | 6 |
Final answer: 6.
Edge cases to test:
- Fewer than 3 bars → 0.
- Monotonic heights (only rising or only falling) → 0.
- A deep valley between two tall walls, e.g.
[5, 0, 0, 5]→ 10.
Complexity: O(n) time, O(n) space.
Hard 85. Maximal Rectangle Google Amazon Microsoft
Problem: In a binary matrix of "0"/"1", find the area of the largest rectangle containing only 1s.
matrix = [["1","0","1","0","0"],["1","0","1","1","1"],["1","1","1","1","1"],["1","0","0","1","0"]]6matrix = [["0"]]0Constraints: rows == matrix.length · cols == matrix[i].length · 1 ≤ rows, cols ≤ 200 · matrix[i][j] is '0' or '1'.
Hint 1
Treat each row as the ground of a histogram: the height of a column is the number of consecutive 1s ending at this row.
Hint 2
For each row, update the heights and run Largest Rectangle in Histogram.
From brute force to the pattern: Checking every possible rectangle is O(r² · c²). Building histograms row by row and solving each in O(c) with a stack gives O(r · c).
Spot it: Treat each row as the floor of a histogram: bar c = how many consecutive 1s stand above (and including) this row in column c. Each row is then exactly problem 84.
Approach:
- Keep
heights[c]. For each row, set it toheights[c] + 1if the cell is "1", otherwise 0. - Run the Largest Rectangle in Histogram stack algorithm on
heights. - Track the best area over all rows.
from typing import List
class Solution:
def maximalRectangle(self, matrix: List[List[str]]) -> int:
if len(matrix) == 0:
return 0
column_count = len(matrix[0])
# heights[j] = number of consecutive "1"s ending at the current row in column j
heights = [0] * column_count
best = 0
for i in range(len(matrix)):
row = matrix[i]
# Grow or reset each column's bar for this row
for j in range(column_count):
if row[j] == "1":
heights[j] = heights[j] + 1
else:
heights[j] = 0
# Problem 84 on this row's histogram
area = self.largest(heights)
if area > best:
best = area
return best
def largest(self, heights: List[int]) -> int:
# Same as problem 84: copy the bars and add a sentinel 0
bars = []
for i in range(len(heights)):
bars.append(heights[i])
bars.append(0)
stack = [] # indices, heights increasing
best = 0
for i in range(len(bars)):
current_height = bars[i]
while len(stack) > 0 and bars[stack[-1]] >= current_height:
top_index = stack.pop()
height = bars[top_index]
if len(stack) > 0:
left_wall = stack[-1]
else:
left_wall = -1
width = i - left_wall - 1
area = height * width
if area > best:
best = area
stack.append(i)
return best
What the code is doing, line by line:
- An empty matrix has no rectangle, so return 0 right away.
heightshas one bar per column.heights[j]counts how many"1"s stand in an unbroken column ending at the current row.- For each row
i, the inner loop updates every columnj: a"1"makes the bar one taller, a"0"knocks it down to 0 (a rectangle of 1s can't pass through a 0). - After updating,
heightsis a histogram whose floor is this row.self.largest(heights)finds the biggest rectangle in it, and we keep the best area over all rows. largestis exactly problem 84: copy the bars, add a sentinel 0, keep an increasing stack of indices, and every time a bar is popped computeheight × (i − left_wall − 1).
Dry run on the example matrix [["1","0","1","0","0"],["1","0","1","1","1"],["1","1","1","1","1"],["1","0","0","1","0"]]:
| Row i | Row cells | heights after update | largest(heights) | best |
|---|---|---|---|---|
| 0 | 1 0 1 0 0 | [1, 0, 1, 0, 0] | 1 | 1 |
| 1 | 1 0 1 1 1 | [2, 0, 2, 1, 1] | 3 | 3 |
| 2 | 1 1 1 1 1 | [3, 1, 3, 2, 2] | 6 | 6 |
| 3 | 1 0 0 1 0 | [4, 0, 0, 3, 0] | 4 | 6 |
Final answer: 6 (the 2 × 3 block of 1s in rows 1–2, columns 2–4).
Edge cases to test:
- All zeros → 0.
- A single row or column.
- Values are the strings
"0"/"1", not integers.
Complexity: O(R·C) time, O(C) space.
Hard 239. Sliding Window Maximum Google Meta Amazon Cisco Microsoft
Problem: Return the maximum of every contiguous window of size k as it slides from left to right.
nums = [1,3,-1,-3,5,3,6,7], k = 3[3,3,5,5,6,7]nums = [1], k = 1[1]Constraints: 1 ≤ nums.length ≤ 10⁵ · -10⁴ ≤ nums[i] ≤ 10⁴ · 1 ≤ k ≤ nums.length
Hint 1
Recomputing max for each window is O(k). Which elements can never be a future maximum?
Hint 2
Keep a deque of indices with decreasing values: drop smaller ones from the back when a bigger value arrives, and drop the front when it leaves the window.
From brute force to the pattern: Taking max of every window is O(n · k). A monotonic deque keeps only candidates that could still become a maximum; each index is added and removed once, so O(n).
Spot it: Each window's max is "the biggest element that hasn't been beaten by a later, bigger one". That's a monotonic structure, but old indices also leave from the front, so use a deque.
Approach:
- Keep a deque of indices whose values are decreasing from front to back.
- For each new
x, pop from the back while the back's value is ≤x. Those values can never be a window max again. - Append
i. If the front index has slid out of the window (≤ i − k), pop it from the front. - Once
i ≥ k − 1, the front's value is the current window's maximum.
from collections import deque
from typing import List
class Solution:
def maxSlidingWindow(self, nums: List[int], k: int) -> List[int]:
# A deque is a list that is fast to change at BOTH ends:
# append(x) adds at the back, pop() removes from the back,
# popleft() removes from the front, window[0] reads the front.
# It holds indices whose values are decreasing front -> back.
window = deque()
result = []
for i in range(len(nums)):
current = nums[i]
# Values <= current can never be a window maximum again
while len(window) > 0 and nums[window[-1]] <= current:
window.pop()
window.append(i)
# The front index slid out of the window [i - k + 1, i]
if window[0] <= i - k:
window.popleft()
# Once the first full window is reached, the front is the maximum
if i >= k - 1:
result.append(nums[window[0]])
return result
What the code is doing, line by line:
window = deque()creates a double-ended queue. Unlike a plain list, removing from the front (popleft()) is instant. It stores indices, and the values at those indices go down from front to back.result = []collects one maximum per window.- The
forloop visits every indexi;current = nums[i]is the new value entering the window. - The
whileloop removes indices from the back whose values are<= current. Whilecurrentis in the window, those smaller, older values can never be the maximum, and they will leave the window beforecurrentdoes. window.append(i)puts the new index at the back. The deque is still decreasing.- The window covers indices
i - k + 1toi. If the front index is<= i - k, it has slid out, sopopleft()removes it. At most one index leaves per step. - Once
i >= k - 1we have a full window, and the front of the deque holds the index of its maximum, so we appendnums[window[0]].
Dry run on the example: nums = [1, 3, -1, -3, 5, 3, 6, 7], k = 3
| i | current | Removed | window after (indices, values) | result |
|---|---|---|---|---|
| 0 | 1 | nothing | [0] (values [1]) | [] |
| 1 | 3 | pop back 0 | [1] (values [3]) | [] |
| 2 | -1 | nothing | [1, 2] (values [3, -1]) | [3] |
| 3 | -3 | nothing | [1, 2, 3] (values [3, -1, -3]) | [3, 3] |
| 4 | 5 | pop back 3, pop back 2, pop back 1 | [4] (values [5]) | [3, 3, 5] |
| 5 | 3 | nothing | [4, 5] (values [5, 3]) | [3, 3, 5, 5] |
| 6 | 6 | pop back 5, pop back 4 | [6] (values [6]) | [3, 3, 5, 5, 6] |
| 7 | 7 | pop back 6 | [7] (values [7]) | [3, 3, 5, 5, 6, 7] |
Final answer: [3, 3, 5, 5, 6, 7].
Edge cases to test:
k = 1→ the array itself.k = n→ one value, the overall maximum.- Decreasing input, so the deque grows to size
k.
Complexity: O(n) time (each index enters and leaves the deque once), O(k) space.
Medium 853. Car Fleet Google Amazon
Problem: n cars drive toward target on a one-lane road. Car i starts at position[i] with speed[i]. A faster car that catches a slower one ahead must slow down and drive with it as one fleet. How many fleets arrive at the target?
Solve it on LeetCode ↗ · Video walkthrough ↗
target = 12, position = [10,8,0,5,3], speed = [2,4,1,1,3]3target = 10, position = [3], speed = [3]1Constraints: n == position.length == speed.length · 1 ≤ n ≤ 10⁵ · 0 < target ≤ 10⁶ · 0 ≤ position[i] < target · All the values of position are unique. · 0 < speed[i] ≤ 10⁶
Hint 1
Sort the cars by position, closest to the target first. When does a car catch the fleet ahead?
Hint 2
Compute each car's arrival time; a car that would arrive no later than the fleet ahead joins it.
From brute force to the pattern: Simulating the cars minute by minute is slow and fiddly. Sorting by position and comparing arrival times with a stack (or a single "slowest time ahead" value) is O(n log n).
Spot it: Process cars from closest-to-target to farthest. Each car's solo arrival time is (target − pos) / speed. A car behind can only stay separate if it arrives later than the fleet ahead. That's a stack whose times strictly increase.
Approach:
- Pair positions with speeds and sort by position, descending (nearest the target first).
- For each car compute
time = (target − pos) / speed. - If the stack is empty or
timeis greater than the top's time, this car can't catch the fleet ahead: push it as a new fleet. - Otherwise it catches up and merges into that fleet (don't push; the fleet keeps the slower, later time).
- The stack's size is the number of fleets.
class Solution:
def carFleet(self, target: int, position: List[int], speed: List[int]) -> int:
# pair each car's position with its speed, nearest-to-target first
cars = sorted(zip(position, speed), reverse=True)
# arrival times of the fleets ahead, slowest (latest) on top
stack = []
for pos, spd in cars:
time = (target - pos) / spd
if not stack or time > stack[-1]:
# can't catch the fleet ahead before the target: a new fleet
stack.append(time)
# else: it catches up and joins the fleet ahead (nothing to push)
return len(stack)
What the code is doing, line by line:
sorted(zip(position, speed), reverse=True):zippairs each position with its speed, and sorting the pairs by position, descending, puts the car nearest the target first.time = (target - pos) / spd: how long this car would take on its own.if not stack or time > stack[-1]:the fleet directly ahead arrives atstack[-1]. If this car needs more time, it never catches up: it leads a new fleet.- If
time <= stack[-1], the car catches the fleet ahead at or before the target and is slowed to its pace, so it adds no fleet. return len(stack): one entry per fleet.
Dry run on the example:
target = 12, position = [10, 8, 0, 5, 3], speed = [2, 4, 1, 1, 3] (LeetCode Example 1). Returns 3.
| car (pos, speed) | time to target | top of stack | action | fleets |
|---|---|---|---|---|
| (10, 2) | 1.0 | — | new fleet | 1 |
| (8, 4) | 1.0 | 1.0 | 1.0 ≤ 1.0 → joins | 1 |
| (5, 1) | 7.0 | 1.0 | new fleet | 2 |
| (3, 3) | 3.0 | 7.0 | 3.0 ≤ 7.0 → joins | 2 |
| (0, 1) | 12.0 | 7.0 | new fleet | 3 |
Edge cases to test:
- A single car → 1.
- Cars that reach the target at exactly the same time merge.
- A car already at the target.
Complexity: O(n log n) time for the sort, O(n) space.
Medium 316. Remove Duplicate Letters Google Amazon
Problem: Remove duplicate letters so each letter appears once, and return the lexicographically smallest such result (a subsequence of s).
s = "bcabc""abc"s = "cbacdcbc""acdb"Constraints: 1 ≤ s.length ≤ 10⁴ · s consists of lowercase English letters.
Hint 1
Build the answer left to right. When would you want to remove a letter you already placed?
Hint 2
Keep an increasing stack; pop a bigger letter if it appears again later. Skip letters already in the stack.
From brute force to the pattern: Generating every subsequence with each letter once and picking the smallest is exponential. The greedy stack makes each decision locally and safely, in one pass.
Spot it: "Lexicographically smallest subsequence" + greedy removal of bigger letters that come before smaller ones = a monotonic (increasing) stack, with a guard: never pop a letter that won't appear again.
Why not another pattern? Trying all subsequences with each letter once is exponential. Plain sorting ignores that the result must keep the original order.
Approach:
- Record each letter's last index.
- Scan
s. Skip letters already in the stack. - While the top is bigger than the current letter and appears again later, pop it (it can be placed later, in a better position).
- Push the current letter. The stack is the answer.
class Solution:
def removeDuplicateLetters(self, s: str) -> str:
last = {c: i for i, c in enumerate(s)} # last index of every letter
stack = []
in_stack = set()
for i, c in enumerate(s):
if c in in_stack:
continue # already placed in the best spot so far
while stack and stack[-1] > c and last[stack[-1]] > i:
in_stack.remove(stack.pop()) # a bigger letter that can come later: drop it now
stack.append(c)
in_stack.add(c)
return "".join(stack)
What the code is doing, line by line:
last[c]tells us whether a letter can still be placed later.- Letters already in the stack are skipped: their current position is already optimal.
- Popping a bigger top letter makes the result smaller, but only if that letter appears again (
last[...] > i). - The stack stays as small as possible at every step and contains each letter once.
Dry run: use the Step through the solution player under the code. It runs this exact code on Example 1 and shows every line with the variables at that moment.
Edge cases to test:
- All letters distinct → unchanged.
"bcabc"→"abc".- A big letter that appears only once must stay, e.g.
"cbacdcbc"→"acdb".
Complexity: O(n) time (each letter is pushed and popped at most once), O(1) space for 26 letters.
Medium 581. Shortest Unsorted Continuous Subarray Google
Problem: Find the shortest subarray that, if sorted, makes the whole array sorted. Return its length.
nums = [2,6,4,8,10,9,15]5nums = [1,2,3,4]0Constraints: 1 ≤ nums.length ≤ 10⁴ · -10⁵ ≤ nums[i] ≤ 10⁵
Hint 1
Which elements are definitely out of place?
Hint 2
An element smaller than the maximum to its left (or bigger than the minimum to its right) must be inside the subarray.
From brute force to the pattern: Sorting a copy and comparing with the original is O(n log n). Two linear scans with a running max and running min find the same boundaries in O(n).
Spot it: An element is out of place if something before it is bigger (or something after it is smaller). Tracking the running maximum from the left and the running minimum from the right finds both boundaries, the same "compare with what came before" idea as a monotonic stack, but with one value.
Why not another pattern? Sorting a copy and comparing is O(n log n). A monotonic stack finds the boundaries in O(n) with O(n) space; the running max/min version needs only O(1).
Approach:
- Left to right with
running_max: anynums[i] < running_maxis out of place; the last suchiis the right boundary. - Right to left with
running_min: anynums[i] > running_minis out of place; the last suchi(leftmost) is the left boundary. - If no boundary was found, the array is sorted: 0. Otherwise
right − left + 1.
class Solution:
def findUnsortedSubarray(self, nums: List[int]) -> int:
n = len(nums)
right = -1
running_max = float("-inf")
for i in range(n):
if nums[i] < running_max:
right = i # something before it is bigger: i must be inside the subarray
else:
running_max = nums[i]
left = 0
running_min = float("inf")
for i in range(n - 1, -1, -1):
if nums[i] > running_min:
left = i # something after it is smaller: i must be inside
else:
running_min = nums[i]
return right - left + 1 if right != -1 else 0
What the code is doing, line by line:
- From the left, an element smaller than the maximum before it can't stay where it is, so the subarray must reach at least that far right.
- From the right, an element bigger than the minimum after it must be inside the subarray too.
- The last such positions in each scan are the tightest boundaries.
right == -1means no element was out of place.
Dry run: use the Step through the solution player under the code. It runs this exact code on Example 1 and shows every line with the variables at that moment.
Edge cases to test:
- Already sorted → 0.
- Reverse sorted →
n. - Duplicates at the boundary, e.g.
[1, 3, 2, 2, 2]→ 4.
Complexity: O(n) time, O(1) space.
Medium 962. Maximum Width Ramp Google Amazon
Problem: A ramp is a pair i < j with nums[i] ≤ nums[j]; its width is j − i. Return the maximum width (0 if none).
nums = [6,0,8,2,1,5]4nums = [9,8,1,0,1,9,4,0,4,1]7Constraints: 2 ≤ nums.length ≤ 5 * 10⁴ · 0 ≤ nums[i] ≤ 5 * 10⁴
Hint 1
Which indices can possibly be the left end of the widest ramp?
Hint 2
Keep only new minimums from the left (a decreasing stack), then scan right ends from the end of the array.
From brute force to the pattern: Checking every pair is O(n²). Only prefix minima can be optimal starts, and scanning ends from the right lets each start be matched and discarded once: O(n).
Spot it: The best start for any ramp is a "new minimum" from the left (a smaller value earlier always beats it). Those candidates form a decreasing stack. Then scan j from the right and pop every start that nums[j] can reach.
Why not another pattern? Checking all pairs is O(n²). Sorting indices by value and tracking the minimum index is O(n log n); the stack is O(n).
Approach:
- Push index
iwhennums[i]is smaller than the value at the stack's top (a new minimum). - Scan
jfrom the end: while the stack's top value ≤nums[j], pop it and update the widthj − top. - Popping is safe: any later (smaller)
jwould give a narrower ramp for that start.
class Solution:
def maxWidthRamp(self, nums: List[int]) -> int:
stack = [] # indices of decreasing values: the only useful starts
for i in range(len(nums)):
if not stack or nums[i] < nums[stack[-1]]:
stack.append(i) # a new minimum: a better start than anything after it
best = 0
for j in range(len(nums) - 1, -1, -1):
while stack and nums[stack[-1]] <= nums[j]:
best = max(best, j - stack.pop()) # widest ramp for this start
return best
What the code is doing, line by line:
- A start
iis only useful if no earlier index has a smaller or equal value; such indices form a decreasing sequence. - Scanning
jfrom the right means the firstjthat works for a start is the widest one for that start. - After popping a start, no other
jcan beat that width, so it never needs to be checked again.
Dry run: use the Step through the solution player under the code. It runs this exact code on Example 1 and shows every line with the variables at that moment.
Edge cases to test:
- Strictly decreasing → 0.
- All equal →
n − 1. - The best ramp uses the first element.
Complexity: O(n) time, O(n) space.
Medium 1008. Construct Binary Search Tree from Preorder Traversal Google Amazon
Problem: Rebuild the BST whose preorder traversal is preorder (all values distinct).
preorder = [8,5,1,7,10,12][8,5,10,1,7,null,12]preorder = [1,3][1,null,3]Constraints: 1 ≤ preorder.length ≤ 100 · 1 ≤ preorder[i] ≤ 1000 · All the values of preorder are unique.
Hint 1
In preorder, where does the next value go relative to the previous nodes?
Hint 2
Keep a stack of the current path; pop nodes smaller than the new value to find its parent.
From brute force to the pattern: Inserting each value into the BST from the root is O(n log n) on average and O(n²) when skewed. The stack attaches each node in amortised O(1).
Spot it: In preorder, each new value is either the left child of the previous node (if smaller) or the right child of the last ancestor that is smaller than it. A stack of nodes along the current path, popped while smaller than the new value, finds that ancestor: a monotonic (decreasing) stack.
Why not another pattern? Inserting each value from the root is O(n²) on skewed input. Recursion with value bounds is also O(n); the stack makes the same logic iterative.
Approach:
- Make the first value the root and push it.
- For each next value: create a node. Pop while the top is smaller than it; the last popped node is its parent (right child).
- If nothing was popped, it's the left child of the top.
- Push the new node.
class Solution:
def bstFromPreorder(self, preorder: List[int]) -> Optional[TreeNode]:
root = TreeNode(preorder[0])
stack = [root] # current path; values decrease from bottom to top
for value in preorder[1:]:
node = TreeNode(value)
parent = None
while stack and stack[-1].val < value:
parent = stack.pop() # the last smaller ancestor becomes the parent
if parent is not None:
parent.right = node # bigger than parent: right child
else:
stack[-1].left = node # smaller than the top: left child
stack.append(node)
return root
What the code is doing, line by line:
- The stack holds the path of nodes that can still receive children, with values decreasing upward.
- Popping all smaller nodes leaves
parentas the deepest node smaller thanvalue, sovalueis its right child. - If no node is smaller,
valueis smaller than the top and becomes its left child.
Dry run: use the Step through the solution player under the code. It runs this exact code on Example 1 and shows every line with the variables at that moment.
Edge cases to test:
- A single value.
- Strictly increasing (right chain) or decreasing (left chain).
- Values that go deep left, then jump right.
Complexity: O(n) time, O(n) space.
Medium 2104. Sum of Subarray Ranges Google Meta Amazon
Problem: The range of a subarray is max − min. Return the sum of the ranges of all subarrays.
nums = [1,2,3]4nums = [1,3,3]4Constraints: 1 ≤ nums.length ≤ 1000 · -10⁹ ≤ nums[i] ≤ 10⁹
Hint 1
Split the sum: Σ(max − min) = Σmax − Σmin.
Hint 2
Count, for every element, how many subarrays it is the maximum (or minimum) of, using previous/next greater (or smaller) elements.
From brute force to the pattern: Extending every start to the right while tracking min and max is O(n²). Counting each element's contribution with monotonic stacks makes it O(n).
Spot it: Sum of ranges = (sum of all subarray maximums) − (sum of all subarray minimums). Each is the "Sum of Subarray Minimums" contribution trick: element i is the min (or max) of left × right subarrays, found with previous/next smaller (greater) elements on a monotonic stack.
Why not another pattern? Enumerating every subarray while tracking its min and max is O(n²), which passes for n ≤ 1000 but doesn't scale.
Approach:
- Write a helper
total(cmp)that returns the sum over subarrays of the min (or max) using contributions. - For each index, the stack gives how far it extends left and right as the min (strict on one side, non-strict on the other to avoid double counting).
- Contribution of
nums[i]=nums[i] × left × right. - Answer = total of maxima − total of minima.
class Solution:
def subArrayRanges(self, nums: List[int]) -> int:
n = len(nums)
def total(better):
# sum over all subarrays of the element that is "best" (min or max)
result = 0
stack = [] # indices; a sentinel pass at i == n flushes the stack
for i in range(n + 1):
while stack and (i == n or better(nums[i], nums[stack[-1]])):
mid = stack.pop()
left = mid - (stack[-1] if stack else -1) # choices for the left end
right = i - mid # choices for the right end
result = result + nums[mid] * left * right
stack.append(i)
return result
sum_max = total(lambda a, b: a > b) # popped when a bigger value arrives
sum_min = total(lambda a, b: a < b) # popped when a smaller value arrives
return sum_max - sum_min
What the code is doing, line by line:
total(better)computes Σ over subarrays of their best element, wherebetteris>for maxima and<for minima.- When index
midis popped byi,iis the first element to its right that beats it; the new stack top is the previous element that beats-or-equals it. - So
nums[mid]is the best element of every subarray starting in(stack top, mid]and ending in[mid, i):left × rightsubarrays. - The final iteration with
i == npops whatever is left. - Σ(max − min) splits into Σmax − Σmin.
Dry run: use the Step through the solution player under the code. It runs this exact code on Example 1 and shows every line with the variables at that moment.
Edge cases to test:
- A single element → 0.
- Equal values (strict vs non-strict comparisons prevent double counting).
- Negative numbers.
Complexity: O(n) time, O(n) space.
Hard 975. Odd Even Jump Google Amazon
Problem: From index i, odd-numbered jumps go to the smallest value ≥ arr[i] to the right (smallest index on ties); even-numbered jumps go to the largest value ≤ arr[i] to the right. Count the starting indices that can reach the end.
arr = [10,13,12,14,15]2arr = [2,3,1,1,4]3Constraints: 1 ≤ arr.length ≤ 2 * 10⁴ · 0 ≤ arr[i] < 10⁵
Hint 1
Separate the problem: first find where each jump goes, then decide which starts succeed.
Hint 2
Sort indices by value; a monotonic stack over that order gives each index's jump target. Then a DP from the right with two states.
From brute force to the pattern: Scanning right from every index to find the jump target is O(n²). Sorting plus a monotonic stack finds all targets in O(n log n), and the success DP is O(n).
Spot it: Two steps: (1) for every index find its odd-jump and even-jump target, a "next greater element" in sorted order, done with a monotonic stack over indices sorted by value; (2) DP from the right: odd[i] = even[next_odd[i]], even[i] = odd[next_even[i]].
Why not another pattern? Finding targets by scanning right is O(n²). A balanced BST / sorted container scanned from the right is the other O(n log n) way.
Approach:
- Sort indices by
(value, index). Walking them in this order with a stack of indices, every index popped by a larger indexigetsias its odd target. - Sort indices by
(−value, index)and do the same for even targets. odd[n−1] = even[n−1] = True. From right to left:odd[i] = even[odd_next[i]],even[i] = odd[even_next[i]].- Count the indices with
odd[i]true (the first jump is odd).
class Solution:
def oddEvenJumps(self, arr: List[int]) -> int:
n = len(arr)
def next_targets(order):
# for indices visited in `order`, the next index (to the right) in that order
target = [None] * n
stack = []
for i in order:
while stack and stack[-1] < i:
target[stack.pop()] = i # i is the first later index in this sorted order
stack.append(i)
return target
odd_next = next_targets(sorted(range(n), key=lambda i: (arr[i], i))) # smallest value >= arr[i]
even_next = next_targets(sorted(range(n), key=lambda i: (-arr[i], i))) # largest value <= arr[i]
odd = [False] * n # can reach the end starting with an odd jump
even = [False] * n # ... starting with an even jump
odd[n - 1] = True
even[n - 1] = True
for i in range(n - 2, -1, -1):
if odd_next[i] is not None:
odd[i] = even[odd_next[i]] # after an odd jump comes an even jump
if even_next[i] is not None:
even[i] = odd[even_next[i]]
return sum(odd) # every start begins with jump number 1 (odd)
What the code is doing, line by line:
- Sorting by
(value, index)lines indices up so that the odd target ofiis the first index afteriin that order that is also to the right ofiin the array: a next-greater-index problem solved by the stack. - For even jumps, sort by
(−value, index): the largest value ≤arr[i], smallest index on ties. odd[i] = even[odd_next[i]]: after an odd jump the next one is even, so the answer chains through the target.- The last index is already the end, for either parity.
Dry run: use the Step through the solution player under the code. It runs this exact code on Example 1 and shows every line with the variables at that moment.
Edge cases to test:
- A single element → 1.
- Equal values (ties go to the smallest index).
- No jump possible from some index (target is
None).
Complexity: O(n log n) time (the two sorts), O(n) space.
Hard 1944. Number of Visible People in a Queue Google Amazon
Problem: People stand in a line with distinct heights. Person i can see person j > i if everyone between them is shorter than both. Return how many people each person can see to their right.
heights = [10,6,8,5,11,9][3,1,2,1,1,0]heights = [5,1,2,3,10][4,1,1,1,0]Constraints: n == heights.length · 1 ≤ n ≤ 10⁵ · 1 ≤ heights[i] ≤ 10⁵ · All the values of heights are unique.
Hint 1
Who blocks your view to the right?
Hint 2
Process from the right with a decreasing stack; everyone you pop is visible, plus the first taller person.
From brute force to the pattern: For every person, scanning right and stopping at the first taller person is O(n²). The stack keeps only people who can still be seen, and each person is popped once.
Spot it: Looking right until something taller blocks the view: next greater element territory. Scanning from the right with a decreasing stack, the people person i pops (shorter than them) are visible, and the first taller one left on the stack is visible too.
Why not another pattern? For each person, scanning right and tracking the tallest seen so far is O(n²).
Approach:
- Scan from right to left, keeping a stack of heights that are visible from the left (decreasing from bottom to top).
- For person
i: pop everyone shorter; each popped person is visible fromi. - If the stack is non-empty afterwards, the person on top (taller) is visible too: one more.
- Push
heights[i].
class Solution:
def canSeePersonsCount(self, heights: List[int]) -> List[int]:
n = len(heights)
answer = [0] * n
stack = [] # heights to the right that are still visible from the left (decreasing)
for i in range(n - 1, -1, -1):
seen = 0
while stack and stack[-1] < heights[i]:
stack.pop() # shorter than i: visible, and hidden for anyone further left
seen = seen + 1
if stack:
seen = seen + 1 # the first taller person is visible too
answer[i] = seen
stack.append(heights[i])
return answer
What the code is doing, line by line:
- Scanning from the right, the stack holds the people who could still be seen by someone further left.
- Everyone popped is shorter than person
i, so nothing between them blocks the view: all are visible. - Once popped, they're hidden behind person
ifor everyone to the left, so they never need to be considered again. - The remaining top is taller than
i: visible, but it blocks everything behind it.
Dry run: use the Step through the solution player under the code. It runs this exact code on Example 1 and shows every line with the variables at that moment.
Edge cases to test:
- The last person → 0.
- Increasing heights: each person sees exactly one.
- Decreasing heights: each person sees just the next one.
Complexity: O(n) time, O(n) space.
Medium 1762. Buildings With an Ocean View Meta
Problem: The ocean is to the right. Return, in increasing order, the indices of buildings strictly taller than every building to their right.
Solve it on LeetCode ↗ (Premium)
heights = [4,2,3,1][0,2,3]heights = [4,3,2,1][0,1,2,3]heights = [1,3,2,4][3]Constraints: 1 ≤ heights.length ≤ 10⁵ · 1 ≤ heights[i] ≤ 10⁹
Hint 1
Which buildings can block a building's view?
Hint 2
Walk from the ocean side keeping the maximum height.
From brute force to the pattern: Comparing each building with everything to its right is O(n²); the running maximum is O(n).
Spot it: "Taller than everything to the right" → scan right to left with the running maximum. (Left to right, it's a monotonic decreasing stack: a new building pops every shorter-or-equal one, since they've lost their view.)
Why not another pattern? Checking each building against all buildings to its right is O(n²).
Approach:
- Scan from the right with
tallest = 0. - If
heights[i] > tallest, recordi; updatetallest. - Reverse the indices.
class Solution:
def findBuildings(self, heights: List[int]) -> List[int]:
view = []
tallest = 0 # highest building to the right so far
for i in range(len(heights) - 1, -1, -1):
if heights[i] > tallest:
view.append(i) # nothing to its right blocks the view
tallest = max(tallest, heights[i])
return view[::-1] # increasing order
What the code is doing, line by line:
- A building is blocked only by an equal or taller building on its right, which is exactly the running maximum.
- Indices are collected right to left, so they're reversed at the end.
Dry run: use the Step through the solution player under the code. It runs this exact code on Example 1 and shows every line with the variables at that moment.
Edge cases to test:
- Strictly decreasing → all indices.
- Equal heights (only the last one sees).
- One building.
Complexity: O(n) time, O(1) extra space (besides the answer).
Hard 2454. Next Greater Element IV Meta
Problem: For each element, return the second element to its right that is greater than it (or −1).
nums = [2,4,0,9,6][9,6,6,-1,-1]nums = [3,3][-1,-1]Constraints: 1 ≤ nums.length ≤ 10⁵ · 0 ≤ nums[i] ≤ 10⁹
Hint 1
Solve the first-greater version with a monotonic stack first.
Hint 2
Once an element sees its first greater, put it in a second structure waiting for another one.
From brute force to the pattern: Scanning right for each element is O(n²); two stacks are O(n).
Spot it: Next Greater Element with a twist: after an index finds its first greater element it waits for a second. Keep two monotonic decreasing stacks: s1 (no greater seen yet) and s2 (one seen). A new value first resolves s2, then moves resolved items from s1 into s2.
Why not another pattern? Scanning right for each element is O(n²); a heap-based version is O(n log n).
Approach:
- For each
x: pop froms2whilenums[top] < x→ their answer isx. - Pop from
s1whilenums[top] < xinto a temporary list (they just met their first greater). - Push that list onto
s2in its original order (keepss2decreasing); pushiontos1.
class Solution:
def secondGreaterElement(self, nums: List[int]) -> List[int]:
answer = [-1] * len(nums)
s1 = [] # indices still waiting for their first greater element
s2 = [] # indices that saw one greater element, waiting for the second
for i, x in enumerate(nums):
while s2 and nums[s2[-1]] < x:
answer[s2.pop()] = x # second greater found
moved = []
while s1 and nums[s1[-1]] < x:
moved.append(s1.pop()) # x is their first greater
s2.extend(reversed(moved)) # keep s2 in decreasing order of value
s1.append(i)
return answer
What the code is doing, line by line:
- Both stacks hold indices with decreasing values from bottom to top, so pops always take the smallest waiting values first.
xis checked againsts2before the moved items join it, so it can't count as both their first and second greater.- The popped items came off
s1from smallest value upward; reversing restores bottom-to-top decreasing order.
Dry run: use the Step through the solution player under the code. It runs this exact code on Example 1 and shows every line with the variables at that moment.
Edge cases to test:
- Equal values (not greater).
- Decreasing array → all −1.
- Second greater far away.
Complexity: O(n) time (each index moves through each stack once), O(n) space.
Medium 1856. Maximum Subarray Min-Product Amazon
Problem: The min-product of a subarray is min · sum. Return the maximum over all subarrays (mod 10⁹ + 7, after maximising).
nums = [1,2,3,2]14nums = [2,3,3,1,2]18Constraints: 1 ≤ nums.length ≤ 10⁵ · 1 ≤ nums[i] ≤ 10⁷
Hint 1
Fix which element is the minimum.
Hint 2
How far can the subarray extend while that element stays the minimum?
From brute force to the pattern: All subarrays: O(n²); stack boundaries + prefix sums: O(n).
Spot it: Fix the minimum: with all values positive, the best subarray where a[i] is the minimum is the widest one, bounded by the nearest smaller elements (a monotonic stack). Its sum comes from a prefix sum.
Why not another pattern? Trying every subarray is O(n²).
Approach:
- Find
left[i]andright[i](nearest strictly smaller) with a stack. - Candidate:
a[i] · (P[right[i]] − P[left[i] + 1]). - Take the max, then mod.
class Solution:
def maxSumMinProduct(self, nums: List[int]) -> int:
n = len(nums)
P = [0] * (n + 1)
for i, x in enumerate(nums):
P[i + 1] = P[i] + x
left, right = [-1] * n, [n] * n
stack = []
for i, x in enumerate(nums):
while stack and nums[stack[-1]] > x:
right[stack.pop()] = i # first smaller to the right
left[i] = stack[-1] if stack else -1 # nearest not-greater to the left
stack.append(i)
best = max(nums[i] * (P[right[i]] - P[left[i] + 1]) for i in range(n)) # widest range where nums[i] is the min
return best % (10**9 + 7)
What the code is doing, line by line:
- Positive values mean a wider subarray with the same minimum always has a bigger sum.
- Using
≤on the left can shorten some ranges for ties, but the tied element with the widest range still gets the full one, so the maximum is found.
Dry run: use the Step through the solution player under the code. It runs this exact code on Example 1 and shows every line with the variables at that moment.
Edge cases to test:
- All equal (whole array).
- Single element.
- Max before the mod (compare first, mod after).
Complexity: O(n) time and space.
Medium 2487. Remove Nodes From Linked List Amazon
Problem: Remove every node that has a strictly greater value somewhere to its right.
head = [5,2,13,3,8][13,8]head = [1,1,1,1][1,1,1,1]Constraints: The number of the nodes in the given list is in the range [1, 10⁵]. · 1 ≤ Node.val ≤ 10⁵
Hint 1
Each node needs to know if a bigger value comes later.
Hint 2
Use a stack that stays non-increasing.
From brute force to the pattern: Pairwise checks are O(n²); the stack is O(n).
Spot it: "Is there a greater element to the right?" is the monotonic stack question. Keep surviving nodes non-increasing; a larger value removes the smaller ones before it.
Why not another pattern? Checking each node against everything after it is O(n²).
Approach:
- Walk the list; pop nodes with a smaller value than the current node.
- Push the current node.
- Relink the stack's nodes in order.
class Solution:
def removeNodes(self, head: Optional[ListNode]) -> Optional[ListNode]:
stack = []
node = head
while node:
while stack and stack[-1].val < node.val:
stack.pop() # a greater value appears to its right: remove it
stack.append(node)
node = node.next
for a, b in zip(stack, stack[1:]):
a.next = b # relink the survivors
stack[-1].next = None
return stack[0]
What the code is doing, line by line:
- After the walk, the stack holds exactly the nodes with nothing greater to their right, in order.
- Equal values stay (only strictly greater removes).
Dry run: use the Step through the solution player under the code. It runs this exact code on Example 1 and shows every line with the variables at that moment.
Edge cases to test:
- Already non-increasing (unchanged).
- Increasing list (only the last stays).
- Equal values.
Complexity: O(n) time, O(n) space (reversing the list twice gives O(1)).
Hard 1526. Minimum Number of Increments on Subarrays to Form a Target Array Amazon
Problem: Starting from zeros, each operation adds 1 to a subarray. Return the fewest operations to reach target.
target = [1,2,3,2,1]3target = [3,1,1,2]4Constraints: 1 ≤ target.length ≤ 10⁵ · 1 ≤ target[i] ≤ 10⁵ · The input is generated such that the answer fits inside a 32 bit integer.
Hint 1
Look at neighbouring positions. When does a new operation have to start?
Hint 2
Sum the positive differences.
From brute force to the pattern: Repeated range increments are O(n · max); summing rises is O(n).
Spot it: Think of the operations as layers of a skyline (the monotonic stack view): when the height rises, new layers must start; when it falls, some layers end. So the answer is target[0] plus every positive rise.
Why not another pattern? Simulating with repeated range increments is O(n · max).
Approach:
ops = target[0].- For each
i ≥ 1: addmax(0, target[i] − target[i − 1]).
class Solution:
def minNumberOperations(self, target: List[int]) -> int:
ops = target[0] # layers needed for the first column
for prev, cur in zip(target, target[1:]):
ops += max(0, cur - prev) # only a rise needs new layers
return ops
What the code is doing, line by line:
- Operations covering position
i − 1can extend toifor free while the height doesn't rise. - Each unit of rise must begin a new operation, and that's enough.
Dry run: use the Step through the solution player under the code. It runs this exact code on Example 1 and shows every line with the variables at that moment.
Edge cases to test:
- All equal → that value.
- Strictly decreasing → target[0].
- Single element.
Complexity: O(n) time, O(1) space.
Hard 2281. Sum of Total Strength of Wizards Amazon
Problem: Sum min(sub) · sum(sub) over all subarrays (mod 10⁹ + 7).
strength = [1,3,1,2]44strength = [5,4,6]213Constraints: 1 ≤ strength.length ≤ 10⁵ · 1 ≤ strength[i] ≤ 10⁹
Hint 1
For which subarrays is a[i] the minimum? (monotonic stack)
Hint 2
You need the sum of all those subarrays' sums. Can prefix sums of prefix sums give it in O(1)?
From brute force to the pattern: All subarrays: O(n²). Stack + double prefix sums: O(n).
Spot it: Like Sum of Subarray Minimums: each element is the minimum of the subarrays whose ends lie in (left[i], right[i]) from a monotonic stack. Here each such subarray is weighted by its sum, and all those sums together come from a prefix sum of prefix sums.
Why not another pattern? Enumerating subarrays is O(n²) with n = 10⁵.
Approach:
left[i]: previous index with a smaller value (strict);right[i]: next index with a smaller or equal value (ties counted once).P= prefix sums,PP= prefix sums ofP.- Contribution of
i:a[i] · ((PP[r+1] − PP[i+1]) · (i − l) − (PP[i+1] − PP[l+1]) · (r − i)).
class Solution:
def totalStrength(self, strength: List[int]) -> int:
MOD = 10**9 + 7
a, n = strength, len(strength)
left, right = [-1] * n, [n] * n
stack = []
for i in range(n):
while stack and a[stack[-1]] >= a[i]:
right[stack.pop()] = i # next smaller-or-equal
left[i] = stack[-1] if stack else -1 # previous strictly smaller
stack.append(i)
P = [0] * (n + 1) # P[k] = a[0] + ... + a[k-1]
for i, x in enumerate(a):
P[i + 1] = P[i] + x
PP = [0] * (n + 2) # PP[k] = P[0] + ... + P[k-1]
for k in range(n + 1):
PP[k + 1] = PP[k] + P[k]
total = 0
for i in range(n):
l, r = left[i], right[i]
ends = (PP[r + 1] - PP[i + 1]) * (i - l) # sum of P[y+1] over right ends, once per left end
starts = (PP[i + 1] - PP[l + 1]) * (r - i) # sum of P[x] over left ends, once per right end
total += a[i] * (ends - starts)
return total % MOD
What the code is doing, line by line:
- Subarray
[x, y]has sumP[y + 1] − P[x]. Summing over allxin(l, i]andyin[i, r)splits into a sum ofP[y + 1](countedi − ltimes) minus a sum ofP[x](countedr − itimes). - Both sums are ranges of
P, so the prefix sums ofPanswer them inO(1). - Strict on one side and non-strict on the other assigns each subarray to exactly one minimum.
Dry run: use the Step through the solution player under the code. It runs this exact code on Example 1 and shows every line with the variables at that moment.
Edge cases to test:
- Equal values (ties).
- Single element → a².
- Large values (mod).
Complexity: O(n) time and space.
Medium 1696. Jump Game VI Microsoft
Problem: Jump forward 1 to k steps from index 0 to the last index; maximise the sum of visited values.
nums = [1,-1,-2,4,-7,3], k = 27nums = [10,-5,-2,4,0,3], k = 317Constraints: 1 ≤ nums.length, k ≤ 10⁵ · -10⁴ ≤ nums[i] ≤ 10⁴
Hint 1
Write the DP: the best score to reach i.
Hint 2
You need the max of the last k dp values: a monotonic deque.
From brute force to the pattern: Checking k predecessors is O(nk); the deque is O(n).
Spot it: dp[i] needs the maximum of the previous k dp values → a sliding window maximum with a monotonic deque.
Why not another pattern? Scanning k previous values per index is O(n · k).
Approach:
dp[0] = nums[0]; deque holds indices with decreasing dp.- Drop indices older than
i − k;dp[i] = nums[i] + dp[front]. - Pop smaller dp values from the back; push
i.
from collections import deque
class Solution:
def maxResult(self, nums: List[int], k: int) -> int:
dp = [0] * len(nums)
dp[0] = nums[0]
window = deque([0]) # indices with decreasing dp values
for i in range(1, len(nums)):
if window[0] < i - k:
window.popleft() # too far back to jump from
dp[i] = nums[i] + dp[window[0]] # best place to jump from
while window and dp[window[-1]] <= dp[i]:
window.pop() # never better than i again
window.append(i)
return dp[-1]
What the code is doing, line by line:
- The deque front is always the best
dpwithin the lastkindices. - Each index enters and leaves the deque once.
Dry run: use the Step through the solution player under the code. It runs this exact code on Example 1 and shows every line with the variables at that moment.
Edge cases to test:
- k ≥ n (jump straight).
- Negative values.
- k = 1 (visit everything).
Complexity: O(n) time, O(k) space.