Monotonic Stack: Practice Problems with Solutions
Welcome to the practice problems for the monotonic stack. If you need a refresher on the code, the code templates have the pattern in all 6 languages.
Note on the classic set: Largest Rectangle in Histogram and Trapping Rain Water already have full multi-language walkthroughs on the stack pattern problems page . This page skips duplicating those two and instead covers problems that no other pattern page owns.
Recommended Study Order
The problems are ordered by difficulty, but the progression matters as much as the individual solutions.
- Next Greater Element I teaches the push and pop mechanism with a map layered on top.
- Next Greater Element II adds the circular wrap simulated by a double pass over the array.
- Online Stock Span swaps values for (price, span) pairs, which is how the same idea becomes a design problem.
- Sum of Subarray Minimums needs previous and next boundaries at the same time, a real step up in detail.
- Remove K Digits converts the pattern into a greedy sequence builder and forces you to think about ties.
- Car Fleet sorts first, then collapses cars into fleets with a stack of arrival times.
- 132 Pattern reverses the scan direction, which makes it the trickiest of the medium set.
- Maximal Rectangle extends the histogram helper to two dimensions, the classic hard capstone.
Easy Problems
1. Next Greater Element I
LeetCode 496 | Difficulty: Easy
Brief: For every value in nums1, find its next greater element as that value appears inside nums2.
Why this pattern: The question is the exact definition of the pattern. The only extra is that the answer belongs to a subset of the input.
Key Insight: Run the monotonic stack once over nums2 and store every result in a map. Looking up an element of nums1 then costs constant time.
Visual:
graph TD
A["nums2: 1, 3, 4, 2"] --> B["3 pops 1, map[1] = 3"]
B --> C["4 pops 3, map[3] = 4"]
C --> D["4 and 2 get no greater value, map = -1"]
D --> E["nums1 = 4, 1, 2 -> answer -1, 3, -1"]
Code:
var nextGreaterElement = function(nums1, nums2) {
const map = new Map(); // value -> next greater element
const stack = [];
for (const num of nums2) {
// Any waiting value smaller than num has found its answer.
while (stack.length > 0 && stack[stack.length - 1] < num) {
map.set(stack.pop(), num);
}
stack.push(num);
}
return nums1.map(n => map.has(n) ? map.get(n) : -1);
};Here the stack stores values instead of indices. That works because the requested answer is a value, and the map turns one pass into constant-time lookups for nums1.
Medium Problems
2. Next Greater Element II
LeetCode 503 | Difficulty: Medium
Brief: Find the next greater element for every index in a circular array.
Why this pattern: This is the base problem with one extra condition. The circle means a second lap over the same indices is required.
Key Insight: Iterate 2 * n positions using modulo, and push an index onto the stack only during the first lap so it resolves at most once.
Visual:
graph TD
A["circle: 1, 2, 1"] --> B["index 0: next greater 2"]
B --> C["index 1: no greater anywhere, -1"]
C --> D["index 2: wraps to index 1, next greater 2"]
D --> E["answer: 2, -1, 2"]
Code:
var nextGreaterElements = function(nums) {
const n = nums.length;
const res = new Array(n).fill(-1);
const stack = [];
// Two laps simulate the circle so elements at the end
// can still find an answer among elements at the start.
for (let i = 0; i < 2 * n; i++) {
const num = nums[i % n];
while (stack.length > 0 && nums[stack[stack.length - 1]] < num) {
res[stack.pop()] = num;
}
if (i < n) {
stack.push(i);
}
}
return res;
};The double lap is the crux. Everything unresolved after the first pass gets a second chance once the modulo wraps the scan back to the beginning. The maximum value of the array stays -1 because nothing can ever beat it.
3. Online Stock Span
LeetCode 901 | Difficulty: Medium
Brief: Design a class where next(price) returns the number of consecutive past days whose price was lower than or equal to today’s price.
Why this pattern: The span counts back until the previous larger price, which is a previous greater element measured in distance. A stack of (price, span) pairs keeps the whole history compressed.
Key Insight: When a new price pops a pair, fold that pair’s span into the new span. Popped pairs represent entire runs of days, so the history is never rescanned.
Visual:
graph TD
A["prices: 100, 80, 60, 70, 60, 75, 85"] --> B["70 pops 60, span of 70 = 2"]
B --> C["75 pops 70 (span 2) and 60 (span 1), span = 4"]
C --> D["85 pops 75 (span 4) and 60 (span 1), span = 6"]
D --> E["spans: 1, 1, 1, 2, 1, 4, 6"]
Code:
var StockSpanner = function() {
this.stack = []; // pairs [price, span]
};
StockSpanner.prototype.next = function(price) {
let span = 1;
// Each popped pair stands for its whole run of days,
// so the current span grows by more than one at a time.
while (this.stack.length > 0 && this.stack[this.stack.length - 1][0] <= price) {
span += this.stack.pop()[1];
}
this.stack.push([price, span]);
return span;
};The combinatorial trick is folding spans. A long run of smaller days collapses into one pair, so the whole history is never visited again. The <= comparison matters: the definition of span includes equal prices.
4. Sum of Subarray Minimums
LeetCode 907 | Difficulty: Medium
Brief: Sum the minimum of every contiguous subarray.
Why this pattern: For each element, the stretch of subarrays where it is the minimum is bounded by the nearest strictly smaller element on the left and the nearest smaller-or-equal on the right. Both bounds are monotonic stack passes.
Key Insight: The number of subarrays where arr[i] is the minimum equals (i - prev_smaller) * (next_smaller_or_equal - i). Using a smaller-or-equal on only one side prevents double counting across duplicates.
Visual:
graph TD
A["arr: 3, 1, 2, 4"] --> B["3: prev -1, next 1 -> 1 x 1 = 1"]
B --> C["1: prev -1, next 4 -> 2 x 3 = 6"]
C --> D["2: prev 1, next 4 -> 1 x 2 = 2"]
D --> E["4: prev 1, next 4 -> 1 x 1 = 1"]
E --> F["total 3 + 6 + 4 + 4 = 17"]
Code:
var sumSubarrayMins = function(arr) {
const MOD = 1e9 + 7;
const n = arr.length;
// prev[i]: nearest strictly smaller index on the left.
const prev = new Array(n).fill(-1);
let stack = [];
for (let i = 0; i < n; i++) {
while (stack.length > 0 && arr[stack[stack.length - 1]] >= arr[i]) {
stack.pop();
}
prev[i] = stack.length > 0 ? stack[stack.length - 1] : -1;
stack.push(i);
}
// next[i]: nearest smaller-or-equal index on the right.
// The equal side decides that each subarray is credited once.
const next = new Array(n).fill(n);
stack = [];
for (let i = n - 1; i >= 0; i--) {
while (stack.length > 0 && arr[stack[stack.length - 1]] > arr[i]) {
stack.pop();
}
next[i] = stack.length > 0 ? stack[stack.length - 1] : n;
stack.push(i);
}
let total = 0;
for (let i = 0; i < n; i++) {
total = (total + arr[i] * (i - prev[i]) * (next[i] - i)) % MOD;
}
return total;
};This solution runs two passes of the same stack, flipped only by direction. The contribution math depends on the equal policy on the right. Java and C++ need long for the products, because the multiplication can exceed a 32-bit int before the modulo happens.
5. Remove K Digits
LeetCode 402 | Difficulty: Medium
Brief: Remove k digits from the string num so the remaining digits form the smallest possible number.
Why this pattern: The smallest number keeps digits as small as possible from left to right, and a smaller digit can immediately replace a larger one sitting right before it. That replacement is a monotonic stack pop under a removal budget.
Key Insight: While a smaller digit arrives and budget remains, pop the larger digit on top. After the scan, any leftover budget is spent on the tail.
Visual:
graph TD
A["1432219, k = 3"] --> B["4 is removed when 3 arrives"]
B --> C["3 is removed when 2 arrives"]
C --> D["2 is removed when 1 arrives"]
D --> E["result: 1219"]
Code:
var removeKdigits = function(num, k) {
const stack = [];
for (const digit of num) {
// A smaller digit makes the larger one in front
// useless for a minimal number, so remove it.
while (k > 0 && stack.length > 0 && stack[stack.length - 1] > digit) {
stack.pop();
k--;
}
stack.push(digit);
}
// An increasing tail needs no more removals, but the
// budget still gets spent, and removing from the end
// keeps the number smallest.
while (k > 0) {
stack.pop();
k--;
}
return stack.join('').replace(/^0+/, '') || '0';
};The greedy step is the pop. When a smaller digit arrives and budget remains, the larger digit on top must go, because keeping it makes the number bigger. After the scan the tail is increasing, so removals there do the least damage; leading zeros are then stripped from the result.
6. Car Fleet
LeetCode 853 | Difficulty: Medium
Brief: Count the fleets that arrive at the target, given each car’s position and speed.
Why this pattern: A car behind only becomes a new fleet if it arrives later than the fleet in front. Sorting by position descending, then collapsing cars that catch up, is the monotonic stack idea applied to arrival times.
Key Insight: Compute the time each car needs to reach the target. While scanning from the closest car to the farthest, a time not larger than the recorded leader merges into that fleet.
Visual:
graph TD
A["target 12: (10,2) (8,1) (5,1) (3,3) (0,1)"] --> B["reach times: 1, 4, 7, 3, 12"]
B --> C["sorted by position: 1, 4, 7, 3, 12"]
C --> D["stack keeps only larger times: 1, 4, 7, 12"]
D --> E["3 fleets"]
Code:
var carFleet = function(target, position, speed) {
const cars = position
.map((pos, i) => [pos, speed[i]]) // [position, speed]
.sort((a, b) => b[0] - a[0]); // farthest first
const stack = []; // arrival times of leading fleets
for (const [pos, sp] of cars) {
const time = (target - pos) / sp;
// If this car needs no more time than the leader,
// it is absorbed by the fleet running ahead.
if (stack.length === 0 || time > stack[stack.length - 1]) {
stack.push(time);
}
}
return stack.length;
};The ordering step matters. By handling the farthest car first, each farther car only ever compares against the fleet ahead of it. A car with equal arrival time merges with that fleet, so the total count is the number of strictly increasing arrival records.
7. 132 Pattern
LeetCode 456 | Difficulty: Medium
Brief: Return true if there exist three indices i < j < k such that nums[i] < nums[k] < nums[j].
Why this pattern: The pattern needs a candidate “3” with the largest possible “2” behind it. A stack of candidates keeps those values while scanning right to left.
Key Insight: Track third, the largest value that has been popped so far. When scanning from right to left, any nums[i] < third completes the triple, because the popped value sits to the right of nums[j] and is smaller than nums[j].
Visual:
graph TD
A["nums: 3, 1, 4, 2"] --> B["scan right: 2, then 4"]
B --> C["4 pops 2, third = 2"]
C --> D["1 < third(2)? yes"]
D --> E["return true"]
Code:
var find132pattern = function(nums) {
const stack = []; // candidates for the "3"
let third = -Infinity; // best "2" seen so far
for (let i = nums.length - 1; i >= 0; i--) {
// Anything smaller than the best "2" found can be the "1".
if (nums[i] < third) return true;
// Smaller values popped now can serve as "2" for
// an even earlier "1", keep the largest of them.
while (stack.length > 0 && stack[stack.length - 1] < nums[i]) {
third = stack.pop();
}
stack.push(nums[i]);
}
return false;
};The right-to-left pass is the point of the problem. Values that get popped while a taller “3” arrives are exactly the best “2” candidates, because they are both smaller than the “3” and located to its right. Keeping the largest popped value maximizes the chance that an earlier element qualifies as the “1”.
Hard Problems
8. Maximal Rectangle
LeetCode 85 | Difficulty: Hard
Brief: Find the largest rectangle of 1s inside a binary matrix.
Why this pattern: Each row can be turned into a histogram of consecutive ones. The largest-rectangle solver from problem 4 then runs against every row, and the best result across all rows is the answer.
Key Insight: For each cell, add 1 to the column height when the cell is '1', else reset it to 0. Then the histogram helper computes the widest rectangle for that row. The classic duplication of Largest Rectangle in Histogram is avoided by reusing this helper.
Visual:
graph TD
A["row 1: 1 0 1 0 0 -> heights 1 0 1 0 0"] --> B["row 2: 1 0 1 1 1 -> heights 2 0 2 1 1"]
B --> C["row 3: 1 1 1 1 1 -> heights 3 1 3 2 2"]
C --> D["histogram area for row 3: 6"]
D --> E["answer: 6"]
Code:
var maximalRectangle = function(matrix) {
if (matrix.length === 0) return 0;
const cols = matrix[0].length;
const heights = new Array(cols).fill(0);
let maxArea = 0;
for (const row of matrix) {
// Build the histogram for this row from the
// vertical run of consecutive ones in each column.
for (let j = 0; j < cols; j++) {
heights[j] = row[j] === '1' ? heights[j] + 1 : 0;
}
maxArea = Math.max(maxArea, largestRectangleArea(heights.slice()));
}
return maxArea;
};
function largestRectangleArea(heights) {
heights.push(0); // sentinel flushes the remaining bars
const stack = [];
let maxArea = 0;
for (let i = 0; i < heights.length; i++) {
// The top bar's right boundary is the current index.
while (stack.length > 0 && heights[i] < heights[stack[stack.length - 1]]) {
const h = heights[stack.pop()];
const w = stack.length === 0 ? i : i - stack[stack.length - 1] - 1;
maxArea = Math.max(maxArea, h * w);
}
stack.push(i);
}
return maxArea;
}The helper is the whole strongest point of this problem. Turn each row into a histogram, then run the helper over the heights. The sentinel zero at the end forces the remaining bars on the stack to flush without a separate cleanup loop.
These eight problems cover the pattern end to end. Start from the one-pass next greater mechanic, add circular wrapping, distance and pair tricks, then the two-boundary reasoning, and finish with the histogram capstone. If you want more, remember that Largest Rectangle in Histogram and Trapping Rain Water have full solutions on the stack problems page , and they are the standard next stop after this set.