Skip to the chapter
←DSAGGroundworkΣDSA
DSA/Intermediate/I18
30 / 48 written chapters
/

Levels and plan
  1. B1Complexity analysis
  2. B2JavaScript toolkit
  3. B3Arrays & strings
  4. B4Prefix sums
  5. B5Hashing
  6. B6Two pointers
  7. B7Sliding window
  8. B8Binary search
  9. B9Basic recursion
  10. B10Sorting algorithms
  11. B11Stacks & queues
  12. B12Linked lists
  1. I1Trees
  2. I2BST operations
  3. I3Tree problems in depth
  4. I4Heaps & priority queues
  5. I5Graphs: representation
  6. I6BFS on grids
  7. I7Graph problems
  8. I8Topological patterns
  9. I9Backtracking
  10. I10DP: 1D
  11. I11DP: 2D
  12. I12DP state machines
  13. I13Greedy algorithms
  14. I14Intervals
  15. I15Bit manipulation
  16. I16Matrix problems
  17. I17Tries
  18. I18Monotonic stack & queue
  19. I19Math for interviews
  20. I20Design & simulation
  21. I21Divide and conquer
  22. I22Randomization
  1. A1Advanced DP
  2. A2Union-Find
  3. A3Advanced graph algorithms
  4. A4Minimum Spanning Tree
  5. A5Advanced graphs
  6. A6Segment & Fenwick trees
  7. A7Sparse tables
  8. A8String algorithms
  9. A9Design problems
  10. A10Advanced backtracking
  11. A11Games & probability DP
  12. A12Ordered structures
  13. A13Sweep line & geometry
  14. A14Interview strategy

ΣI18 · Part 2 · Intermediate

Monotonic stack & queue in depth

Throw away every element that can never win again — what survives is already sorted.

26 min read1 diagram15 sections15 exercises
30 of 48

In this chapter

  • The insight: some elements become permanently useless
  • The canonical shape
  • Next greater element
  • Daily Temperatures, Next Greater Element I and Stock Span
  • Largest rectangle in histogram

and 10 more

about 26 min
15On this page
  1. The insight: some elements become permanently useless
  2. The canonical shape
  3. Next greater element
  4. Daily Temperatures, Next Greater Element I and Stock Span
  5. Largest rectangle in histogram
  6. Trapping rain water, the monotonic-stack way
  7. Sum of Subarray Minimums: count the windows each element rules
  8. Lexicographic stacks: Remove K Digits and Remove Duplicate Letters
  9. 132 pattern: a stack that remembers the best "2"
  10. Car Fleet: sort by position, keep a stack of arrival times
  11. Monotonic deque: sliding window maximum
  12. Deque with prefix sums: shortest subarray with sum at least K
  13. Deque with DP: Jump Game VI
  14. See the stack work
  15. Recognizing it in an unseen problem

The insight: some elements become permanently useless

Suppose you're scanning left to right looking for each element's next greater element. You reach value 7 and the pending element behind it is a 3. That 3 is finished — 7 is its answer, and no later element can ever be its answer instead. But more than that: the 3 is now useless to everyone. Any future element looking backward for something bigger will hit the 7 before it reaches the 3. So the 3 can be discarded entirely.

Do this consistently and the pending set is always sorted — a monotonic stack. You never search it, never sort it, never scan it: you only pop from the top while the invariant is violated. Each element is pushed exactly once and popped at most once, so the total work across the whole scan is O(n), even though the inner while loop can pop many elements on a single step.

heights = [2, 1, 5, 6, 2, 3] — stack holds indices, heights strictly increasing bottom to top after i=1 after i=3 at i=4, h=2 after i=5 1 1 5 6 1 5 pop 6 pop 1 2 3 a bar is popped exactly when its right boundary is found — and the new stack top is its left boundary
The stack is never searched or sorted — it stays ordered because anything out of order is popped on arrival.

The canonical shape

// The template. Two decisions define every variant:
//   1. the comparison in the while condition (< vs > vs <= vs >=)
//   2. what you do at pop time vs. at push time
function monotonic(nums) {
  const stack = []; // store INDICES, not values — you almost always need positions

  for (let i = 0; i < nums.length; i++) {
    while (stack.length && nums[stack[stack.length - 1]] < nums[i]) {
      const j = stack.pop();
      // nums[i] is j's NEXT GREATER — resolve j here
    }
    // whatever is on top now is i's PREVIOUS GREATER-OR-EQUAL (or none if empty)
    stack.push(i);
  }
  // anything left on the stack has no next greater element
}
One pass gives you two answers A single decreasing stack resolves next greater at pop time and previous greater at push time, in the same loop. Most people write two passes (one forward, one backward) for problems that need both boundaries. You don't have to — and saying so mid-interview is a strong signal that you understand the structure rather than the recipe.
QuestionStack order (bottom → top)Pop while top ...
Next greater elementdecreasingvalue < nums[i]
Next smaller elementincreasingvalue > nums[i]
Previous greater elementdecreasingsame loop, read the top after popping
Previous smaller elementincreasingsame loop, read the top after popping

Ties: with these strict pop tests, the top you read after popping is the previous greater-or-equal (or smaller-or-equal) element. Pop on equal values too (<= or >=) when the previous element must be strictly greater or strictly smaller; the pop-time answer then becomes next greater-or-equal, so a single pass cannot make both sides strict.

Memorize the derivation, not the table: "I want the next bigger thing, so a pending element stops being pending the moment something bigger arrives, so I pop while the top is smaller, so the stack is decreasing." Regenerate it in ten seconds at the whiteboard instead of recalling four near-identical rules under pressure.

Next greater element

// For each element, the first larger value to its right; -1 if none. O(n).
function nextGreater(nums) {
  const res = new Array(nums.length).fill(-1);
  const stack = []; // indices; nums[stack] non-increasing (equal values stay on the stack)

  for (let i = 0; i < nums.length; i++) {
    while (stack.length && nums[stack[stack.length - 1]] < nums[i]) {
      res[stack.pop()] = nums[i];
    }
    stack.push(i);
  }
  return res; // leftovers keep their -1 — nothing bigger ever came
}

nextGreater([2, 1, 2, 4, 3]); // [4, 2, 4, -1, -1]

The circular variant ("Next Greater Element II") wraps around the end of the array. The fix is not a second algorithm — just walk the index twice and mod:

function nextGreaterCircular(nums) {
  const n = nums.length;
  const res = new Array(n).fill(-1);
  const stack = [];

  for (let step = 0; step < 2 * n; step++) { // two laps: the second one resolves the wrap-around
    const i = step % n;
    while (stack.length && nums[stack[stack.length - 1]] < nums[i]) {
      res[stack.pop()] = nums[i];
    }
    if (step < n) stack.push(i); // only push during the first lap, or indices duplicate
  }
  return res;
}
⚠ Strict vs. non-strict comparison decides how ties behave < vs <= in the while condition changes whether equal elements pop each other. For "next strictly greater" you must use <, otherwise two equal values resolve each other incorrectly. For histogram-style problems with duplicate heights, >= (popping equals) is usually the right choice — it can compute a too-small width for one of the duplicates, but the largest duplicate is always measured with the full width, so the maximum still comes out correct. Reason about ties explicitly; don't guess.

Daily Temperatures, Next Greater Element I and Stock Span

Three of the most common warm-ups are the template above with a different thing written at pop time or push time. In each case the stack holds indices (or, when the values are distinct and positions are not needed, values) whose answer is still pending.

Daily Temperatures asks how many days you wait for a warmer day. The answer for day j is the distance to its next greater element, i - j, written when j is popped.

function dailyTemperatures(temperatures) {
  const wait = new Array(temperatures.length).fill(0); // 0 means "never warmer"
  const stack = [];
  for (let i = 0; i < temperatures.length; i++) {
    while (stack.length && temperatures[stack[stack.length - 1]] < temperatures[i]) {
      const j = stack.pop();
      wait[j] = i - j;
    }
    stack.push(i);
  }
  return wait;
}

Dry run: [73, 74, 75, 71, 69, 72, 76, 73]

itempPops (index → wait)Stack of indices after
073—[0]
1740 → 1[1]
2751 → 1[2]
371—[2, 3]
469—[2, 3, 4]
5724 → 1, 3 → 2[2, 5]
6765 → 1, 2 → 4[6]
773—[6, 7]

Indices 6 and 7 are still on the stack at the end and keep their 0. The result is [1, 1, 4, 2, 1, 1, 0, 0]. O(n) time and O(n) space.

Next Greater Element I gives two arrays: for every value of nums1, a subset of nums2 with distinct values, find the next greater element of that value inside nums2. Compute the answer for every value of nums2 with the stack, store it in a Map, then look up each value of nums1. Because the values are distinct and no positions are needed, the stack can hold values.

function nextGreaterElementI(nums1, nums2) {
  const next = new Map(); // value → its next greater value in nums2
  const stack = [];
  for (const value of nums2) {
    while (stack.length && stack[stack.length - 1] < value) next.set(stack.pop(), value);
    stack.push(value);
  }
  return nums1.map((value) => next.get(value) ?? -1);
}

O(n + m) time. If nums2 could repeat a value, a Map keyed by value would mix up the copies; keep indices on the stack in that case.

Stock Span asks, for each day, how many consecutive days ending today (today included) had a price at most today's. That is the distance back to the previous strictly greater price, so the answer is written at push time, from whatever is left on top after the pops, and the pop test is "less than or equal".

function stockSpans(prices) {
  const spans = [];
  const stack = []; // indices; prices strictly decreasing bottom to top
  for (let i = 0; i < prices.length; i++) {
    while (stack.length && prices[stack[stack.length - 1]] <= prices[i]) stack.pop();
    spans.push(stack.length ? i - stack[stack.length - 1] : i + 1);
    stack.push(i);
  }
  return spans;
}

Dry run: [100, 80, 60, 70, 60, 75, 85]

ipricePopped (prices)Top leftSpan
0100—none1 (i + 1)
180—index 01 - 0 = 1
260—index 11
37060index 13 - 1 = 2
460—index 31
57560, 70index 15 - 1 = 4
68575, 80index 06 - 0 = 6

The spans are [1, 1, 1, 2, 1, 4, 6]. The online version (a class with a next(price) method that is called once per day) keeps the same stack, but stores { price, day } pairs and a day counter, since there is no array to index into. Each call is O(1) amortised. Note the symmetry with Daily Temperatures: one writes a distance forward at pop time, the other a distance backward at push time.

Largest rectangle in histogram

This is the problem that makes the pattern click. A rectangle of height heights[j] extends left until it hits a strictly shorter bar and right until it hits a strictly shorter bar. So each bar needs its previous smaller and next smaller index — exactly what an increasing stack hands you: i is the right boundary at pop time, and the new stack top is the left boundary.

// O(n) time, O(n) space
function largestRectangleArea(heights) {
  const stack = []; // indices; heights strictly increasing bottom to top (>= pops equal heights)
  let best = 0;

  for (let i = 0; i <= heights.length; i++) {
    // SENTINEL: a virtual height-0 bar past the end drains the stack — no cleanup loop
    const h = i === heights.length ? 0 : heights[i];

    while (stack.length && heights[stack[stack.length - 1]] >= h) {
      const height = heights[stack.pop()];
      // left boundary = one past the new top; if the stack emptied, this bar reached index 0
      const left = stack.length ? stack[stack.length - 1] + 1 : 0;
      best = Math.max(best, height * (i - left)); // width = right boundary i, exclusive
    }
    stack.push(i);
  }
  return best;
}

largestRectangleArea([2, 1, 5, 6, 2, 3]); // 10 — the 5 and 6 bars, width 2

Dry run: largestRectangleArea([2, 1, 5, 6, 2, 3]) — the same array as the figure above

ihpopped (height)leftwidthareabest
1120122
4263166
425221010
60 (sentinel)351310
60 (sentinel)224810
60 (sentinel)106610

Row three is the whole problem: popping the 6 first (width 1, area 6) looks smaller, but the very next pop of the 5 reaches back to left = 2 because the 6 is already gone from the stack — the rectangle of height 5 spans both index 2 and index 3. That's the payoff of storing indices, not values: the width of a popped bar's rectangle depends on what's left standing, which changes after every pop.

⚠ Two off-by-one traps live in the width calculation (1) When the stack empties after a pop, the left boundary is 0, not stack.top + 1 — that bar was shorter than everything before it, so it extends all the way to the start. Forgetting this silently under-counts the widest rectangles. (2) The width is i - left, not i - left + 1, because i is the first bar that breaks the rectangle, so it is an exclusive right boundary. Sanity-check both against [2] (answer 2) and [2, 2] (answer 4) before declaring victory.

Directly on top of this: Maximal Rectangle in a binary matrix. Walk the rows, maintain a running "height of consecutive 1s ending at this row" array, and call largestRectangleArea on it once per row — O(rows × cols) total. Recognizing that a hard 2D problem is this 1D problem run row-by-row is exactly the kind of reduction interviews reward.

Say it like this → "Every rectangle is bounded by the first shorter bar on each side, so I need previous-smaller and next-smaller for each bar. An increasing monotonic stack gives me both in one pass: the index that triggers a pop is the right boundary, and whatever is left on the stack is the left boundary. I'll append a virtual zero-height bar so the stack drains without a separate cleanup loop."

Trapping rain water, the monotonic-stack way

You have probably seen the two-pointer solution. The stack version is worth knowing because it computes the water in horizontal layers rather than vertical columns, and it's the same skeleton as the histogram — which means one mental model covers both problems.

// O(n) time, O(n) space — fills water layer by layer
function trap(height) {
  const stack = []; // indices; heights non-increasing bottom to top (equal heights stay)
  let water = 0;

  for (let i = 0; i < height.length; i++) {
    while (stack.length && height[stack[stack.length - 1]] < height[i]) {
      const bottom = stack.pop(); // the floor of the basin we're about to fill
      if (!stack.length) break;   // no left wall → water spills off the edge

      const left = stack[stack.length - 1];
      const width = i - left - 1;                                    // strictly between the two walls
      const bounded = Math.min(height[left], height[i]) - height[bottom]; // shorter wall caps the level
      water += width * bounded;
    }
    stack.push(i);
  }
  return water;
}

trap([0,1,0,2,1,0,1,3,2,1,2,1]); // 6

The break when the stack empties is the whole "you need walls on both sides" rule, expressed structurally. And notice bounded subtracts height[bottom]: you're adding only the slab above the previously-filled level, never double-counting a layer you already paid for. The two-pointer solution is O(1) space and is the better final answer — but explaining the layered view first shows you understand why the two-pointer bound works.

Sum of Subarray Minimums: count the windows each element rules

Add up the minimum of every contiguous subarray. There are O(n²) subarrays, so listing them is too slow for large n. Turn the sum around: sum = Σ arr[i] × (the number of subarrays whose minimum is arr[i]). This is the contribution technique. A subarray with minimum at position i starts somewhere among the positions that are reachable to the left without meeting a smaller value, and ends somewhere reachable to the right, so the count is leftCount × rightCount. Those two extents are the distances to the previous smaller and the next smaller element, which one monotonic stack produces.

Ties need a rule, or equal values would claim the same subarray twice. Decide that a subarray belongs to its leftmost minimum. Then on the left, an equal value blocks the extent (the equal one is more to the left and wins); on the right, an equal value does not block it. Strict on one side and non-strict on the other is the asymmetry that makes every subarray count exactly once.

function sumSubarrayMins(arr) {
  const MOD = 1000000007;
  const stack = []; // indices; values non-decreasing bottom to top, so a pop means a strictly smaller value arrived
  let total = 0;

  for (let i = 0; i <= arr.length; i++) {
    const current = i === arr.length ? -Infinity : arr[i]; // sentinel drains the stack
    while (stack.length && arr[stack[stack.length - 1]] > current) {
      const mid = stack.pop();
      const leftBound = stack.length ? stack[stack.length - 1] : -1; // previous element that is smaller or EQUAL
      const leftCount = mid - leftBound;  // starts: leftBound + 1 .. mid
      const rightCount = i - mid;         // ends: mid .. i - 1 (i is the next STRICTLY smaller)
      total = (total + arr[mid] * leftCount * rightCount) % MOD;
    }
    stack.push(i);
  }
  return total;
}

Dry run: sumSubarrayMins([3, 1, 2, 4])

ElementStarts available (left)Ends available (right)WindowsContribution
31 (only itself)1 (the 1 stops it)13
12 (the 3 and itself)3 (itself, 2, 4)66
21 (the 1 blocks the left)2 (itself and 4)24
41114

The total is 3 + 6 + 4 + 4 = 17, which matches the 10 subarrays' minimums added by hand. Tie check on [2, 2]: the first 2 gets 1 start × 2 ends (windows [2] and [2, 2]) = 2 windows, the second gets 1 × 1, giving 2 × 2 + 2 × 1 = 6, the correct sum of 2 + 2 + 2. If both sides were strict the pair window would be counted twice (total 8); if both sides were non-strict it would be counted zero times (total 4).

O(n) time and O(n) space. The product arr[mid] * leftCount * rightCount stays below 253 for the usual limits (values and n up to about 3 × 104); if both could be around 109 you would need BigInt or a modular multiply. The same recipe with "maximum" instead of "minimum" gives Sum of Subarray Ranges as max-sum minus min-sum.

Lexicographic stacks: Remove K Digits and Remove Duplicate Letters

A monotonic stack also builds the smallest string by deleting characters. The idea: scan left to right; if the new character is smaller than the one on top, then keeping the larger character earlier makes the result larger, so delete it if you are allowed to. The stack ends up non-decreasing, which is the lexicographic minimum.

Remove K Digits: delete exactly k digits from a number string to make the smallest possible number.

function removeKdigits(num, k) {
  const stack = [];
  for (const digit of num) {
    while (k > 0 && stack.length && stack[stack.length - 1] > digit) {
      stack.pop(); // a larger digit earlier in the number loses to a smaller one that follows
      k--;
    }
    stack.push(digit);
  }
  while (k > 0) { stack.pop(); k--; } // still need deletions: the end holds the largest digits

  let start = 0;
  while (start < stack.length - 1 && stack[start] === "0") start++; // strip leading zeros, keep one digit
  const result = stack.slice(start).join("");
  return result === "" ? "0" : result;
}

Dry run: removeKdigits("1432219", 3)

digitPops (k left)Stack after
1—1
4—1 4
34 (k = 2)1 3
23 (k = 1)1 2
2—1 2 2
12 (k = 0)1 2 1
9k is 0, no more pops1 2 1 9

The answer is "1219". Three details lose offers: forgetting the leftover deletions ("12345" with k = 2 never pops in the loop and must lose its last two digits, giving "123"), forgetting to strip leading zeros ("10200" with k = 1 becomes "0200" before stripping, "200" after), and returning an empty string instead of "0" when everything is deleted.

Remove Duplicate Letters: remove repeated letters so that each letter of the string appears exactly once and the result is the smallest in lexicographic order. The same greedy applies with one extra rule: you may pop a larger letter only if it appears again later, because otherwise popping it would lose that letter forever. Track each letter's last index and which letters are already on the stack.

function removeDuplicateLetters(s) {
  const last = new Map();
  for (let i = 0; i < s.length; i++) last.set(s[i], i);

  const inStack = new Set();
  const stack = [];
  for (let i = 0; i < s.length; i++) {
    const ch = s[i];
    if (inStack.has(ch)) continue; // already placed, in the best position found so far
    while (stack.length && stack[stack.length - 1] > ch && last.get(stack[stack.length - 1]) > i) {
      inStack.delete(stack.pop()); // bigger, and it will come again later, so it can wait
    }
    stack.push(ch);
    inStack.add(ch);
  }
  return stack.join("");
}

Dry run: removeDuplicateLetters("cbacdcbc")

ichActionStack after
0cpushc
1bc is bigger and appears again at index 7: pop it, push bb
2ab is bigger and appears again at index 6: pop it, push aa
3cpusha c
4dpusha c d
5calready on the stack: skipa c d
6bd is bigger but its last index is 4, so it cannot wait; push ba c d b
7calready on the stack: skipa c d b

The answer is "acdb". O(n) time, because each character is pushed and popped at most once, with O(alphabet) extra space for the two tables. The pattern "pop while the top is worse and it is safe to lose" covers both problems; what "safe" means is the only thing that changes (a remaining deletion budget, or a later copy of the same letter).

132 pattern: a stack that remembers the best "2"

Is there a triple of positions i < j < k with nums[i] < nums[k] < nums[j]? Call the three values "1", "3" and "2" after their relative sizes. A cubic search tries every triple. Scan from the right instead, and keep two things: a decreasing stack of values that could play "3", and third, the largest value known to be a valid "2" (some value that has a strictly larger value on its left, to the right of the scan point). When the scan reaches a new element that is smaller than third, it is a valid "1" and the answer is yes.

function find132pattern(nums) {
  let third = -Infinity; // the best "2" so far: popped values are smaller than a later "3" to their left
  const stack = [];      // candidate "3"s, decreasing bottom to top (equal values stay)
  for (let i = nums.length - 1; i >= 0; i--) {
    if (nums[i] < third) return true; // nums[i] is the "1": it is below a "2" that sits under a bigger "3"
    while (stack.length && stack[stack.length - 1] < nums[i]) third = stack.pop();
    stack.push(nums[i]);
  }
  return false;
}

Dry run: [3, 1, 4, 2]

inums[i]Check against thirdPopsthird afterStack after
322 < -Infinity? no—-Infinity[2]
244 < -Infinity? no22[4]
111 < 2? yesreturn true: the triple is 1, 4, 2

O(n) time and O(n) space. Why third only ever grows: a value is pushed only if it is not below third (otherwise we would have returned), so every later pop is at least third. The comparisons are strict on both sides because the pattern needs 1 < 2 < 3; with <= the array [1, 2, 2] would wrongly succeed. Scanning left to right with the same ideas needs a prefix-minimum array, and the right-to-left stack version avoids it.

Car Fleet: sort by position, keep a stack of arrival times

Cars drive toward a target on a single lane and cannot pass; a faster car that catches a slower one slows to its speed and from then on they travel as one fleet. Count the fleets that reach the target. The key observation is that only arrival times matter. Sort the cars by position, closest to the target first. A car's own time is (target - position) / speed. If it is no more than the time of the fleet in front, it catches up (or arrives together with it) and merges; if it is strictly more, it is slower than everything ahead and forms a new fleet, which the stack records.

function carFleet(target, position, speed) {
  const order = position.map((_, i) => i).sort((a, b) => position[b] - position[a]); // nearest to the target first
  const fleetTimes = []; // strictly increasing: each fleet is slower than the one in front
  for (const i of order) {
    const time = (target - position[i]) / speed[i];
    if (fleetTimes.length === 0 || time > fleetTimes[fleetTimes.length - 1]) fleetTimes.push(time);
  }
  return fleetTimes.length;
}

Dry run: target = 12, position = [10, 8, 0, 5, 3], speed = [2, 4, 1, 1, 3]

Car (position)Time to targetCompare with fleet in frontFleet times after
10(12 - 10) / 2 = 1none in front[1]
8(12 - 8) / 4 = 11 is not above 1: merges[1]
5(12 - 5) / 1 = 77 > 1: new fleet[1, 7]
3(12 - 3) / 3 = 33 is not above 7: merges[1, 7]
0(12 - 0) / 1 = 1212 > 7: new fleet[1, 7, 12]

The answer is 3. The cost is the sort, O(n log n). Since a car only ever compares with the last fleet time, a single running maximum would do instead of a stack; the stack framing shows the family resemblance (a pending element is absorbed by a stronger one) and is easier to extend. Equal times always compare equal here, because division of the same rational gives the same double. In principle two different times could round to the same double when positions and speeds are huge; if the limits allow that, compare cross-multiplied integers instead.

Monotonic deque: sliding window maximum

Same invariant, one extra requirement: elements also expire off the front when they fall out of the window. A stack can't do that, so you use a deque — pop from the back to maintain monotonicity, shift from the front to evict stale indices. The front is always the window's maximum because everything smaller behind it was discarded on arrival.

// max of every window of size k — O(n) time, O(k) space
function maxSlidingWindow(nums, k) {
  const dq = []; // indices; nums[dq] strictly decreasing front to back (<= pops equal values)
  const out = [];

  for (let i = 0; i < nums.length; i++) {
    if (dq.length && dq[0] <= i - k) dq.shift(); // front fell out of the window — evict

    // anything smaller than or equal to nums[i] can never be a max again: nums[i] is newer AND at least as big
    while (dq.length && nums[dq[dq.length - 1]] <= nums[i]) dq.pop();

    dq.push(i);
    if (i >= k - 1) out.push(nums[dq[0]]); // front = current window max
  }
  return out;
}

maxSlidingWindow([1,3,-1,-3,5,3,6,7], 3); // [3, 3, 5, 5, 6, 7]
⚠ Array.prototype.shift() is not O(1) In the abstract this is a deque; in JavaScript, shift() on a plain array is O(n) in the general case because it re-indexes. V8 optimizes small arrays well enough that this passes in practice, but the honest O(n) implementation uses a head pointer into a fixed array and advances it instead of shifting. If an interviewer asks "is that really O(n) overall?" — that's what they're probing. The one-line fix: let head = 0; then head++ in place of shift(), and read dq[head] for the front.
ApproachTimeSpaceNote
Recompute each windowO(n·k)O(1)The baseline to state and reject
Max-heap with lazy deletionO(n log n)O(n)Works, and generalizes to "kth largest in window"
Monotonic dequeO(n)O(k)Optimal — each index enters and leaves once
Balanced BST / multisetO(n log k)O(k)Needed if the window query is median or kth, not max

That last row is the useful boundary: a monotonic deque works because max lets you discard dominated elements forever. If the query were "median of every window," nothing is discardable — you'd need two heaps or an ordered multiset. Knowing why the deque stops working is more valuable than knowing that it works.

Say it like this → "If a new element is both larger and more recent than something already in the deque, that older element is permanently dominated — it can never be the max of any future window. So I drop it. What's left is decreasing, the front is the current max, and each index is pushed and popped exactly once, giving O(n) total rather than O(n·k)."

Deque with prefix sums: shortest subarray with sum at least K

Find the length of the shortest non-empty subarray with sum at least k, where the array may contain negative numbers. The sliding window from the earlier chapter fails here: with positive numbers, growing the window raises the sum and shrinking lowers it, so a two-pointer rule is valid. With negatives, adding an element can lower the sum and removing one can raise it, so neither pointer has a safe move.

Switch to prefix sums, where prefix[j] - prefix[i] >= k for i < j is the condition, and keep a deque of candidate start indices i. Two observations prune the deque. First, if some start i already satisfies the condition for the current end j, record j - i and discard i for good: every later end gives a longer subarray from i. Second, if the new prefix[j] is at most the prefix at the back of the deque, the back start can never beat j: j is closer to every future end and has a prefix that is no larger, so it gives at least as large a sum. Popping those keeps the deque's prefix values increasing.

function shortestSubarray(nums, k) {
  const n = nums.length;
  const prefix = new Array(n + 1).fill(0);
  for (let i = 0; i < n; i++) prefix[i + 1] = prefix[i] + nums[i];

  const deque = new Array(n + 1); // indices into prefix; head and tail replace shift() and pop()
  let head = 0, tail = 0;
  let best = Infinity;

  for (let j = 0; j <= n; j++) {
    while (head < tail && prefix[j] - prefix[deque[head]] >= k) {
      best = Math.min(best, j - deque[head]); // this start is finished: it cannot do better later
      head++;
    }
    while (head < tail && prefix[deque[tail - 1]] >= prefix[j]) tail--; // dominated starts
    deque[tail++] = j;
  }
  return best === Infinity ? -1 : best;
}

Dry run: shortestSubarray([2, -1, 2], 3), prefix = [0, 2, 1, 3]

jprefix[j]Front check (prefix[j] - front start)Back popsDeque of start indices
00deque is empty—[0]
122 - 0 = 2 < 3—[0, 1]
211 - 0 = 1 < 3index 1 (prefix 2 is at least 1)[0, 2]
333 - 0 = 3 is at least 3: best = 3, drop index 0; 3 - 1 = 2 < 3—[2, 3]

The answer is 3 (the whole array). Each index enters the deque once and leaves once, so the time is O(n) and the space is O(n) for the prefix array. Using shift() instead of a head pointer would hide an O(n) inside the loop, as discussed in the deque section. The structure is the deque of the sliding-window maximum, with two differences: values are prefix sums, and the front is also popped on a success, not only when it falls out of a fixed-size window.

Deque with DP: Jump Game VI

You start at index 0 and may jump forward by 1 to k steps; the score is the sum of the values you land on, including both ends. Find the maximum score at the last index. The DP is dp[i] = nums[i] + max(dp[i - k .. i - 1]), a maximum over a window of the last k DP values. Computing each maximum by scanning costs O(n · k); but "maximum of every window of size k" is the sliding window maximum. Keep a deque of indices whose DP values decrease, take the front as the window maximum, and push each new dp[i] after popping the smaller values behind it.

function maxResult(nums, k) {
  const n = nums.length;
  const dp = new Array(n);
  dp[0] = nums[0];
  const deque = [0]; // indices; dp values strictly decreasing from head to tail
  let head = 0;

  for (let i = 1; i < n; i++) {
    if (deque[head] < i - k) head++; // the front can no longer reach i
    dp[i] = nums[i] + dp[deque[head]];  // the best place to jump from
    while (deque.length > head && dp[deque[deque.length - 1]] <= dp[i]) deque.pop();
    deque.push(i);
  }
  return dp[n - 1];
}

Dry run: maxResult([1, -1, -2, 4, -7, 3], 2)

iWindow of indicesFront (best dp)dp[i]Deque after
0——1[0]
10dp[0] = 1-1 + 1 = 0[0, 1]
20, 1dp[0] = 1-2 + 1 = -1[0, 1, 2]
31, 2 (index 0 left)dp[1] = 04 + 0 = 4[3] (4 beats 0 and -1)
42, 3dp[3] = 4-7 + 4 = -3[3, 4]
53, 4dp[3] = 43 + 4 = 7[5]

The answer is 7, along the path 0, 1, 3, 5 (values 1, -1, 4, 3). O(n) time, O(n) space. The same trick speeds up any DP whose transition is "best of the previous k states": the deque holds dominated-free candidates, and the front is the maximum. When the transition takes a minimum, flip the comparison. The DP chapters mention this as the monotone-deque optimisation.

See the stack work

Watch the stack stay decreasing. Every index is pushed once and popped at most once, which is the whole argument for O(n) despite the inner while loop.

Monotonic stack — next greater element

Stack (indices waiting)
Answers

Recognizing it in an unseen problem

  • The literal words "next greater," "next smaller," "previous warmer day," "first element to the right that…" — that's a monotonic stack, unconditionally
  • Each element needs its span or boundaries — "how far can this bar/temperature/stock price extend before something bigger stops it" (Daily Temperatures, Stock Span, Largest Rectangle, Maximal Rectangle, Sum of Subarray Minimums)
  • The brute force is an O(n²) double loop where the inner loop scans rightward until a condition trips — that inner scan is what the stack amortizes away
  • "Maximum/minimum of every window of size k" with a fixed k → monotonic deque. The extra front-eviction is the only difference from a stack
  • Distinguish from a plain sliding window: sliding window maintains an aggregate (sum, count, set) that updates in O(1); monotonic structures maintain an ordered candidate set because the aggregate (max, min) can't be undone incrementally when an element leaves
  • Distinguish from a heap: use a heap when you need the kth or the median, or when elements arrive without a scan order. Use a monotonic deque when a newer-and-better element makes an older one permanently irrelevant
  • "How many days until," "how many consecutive days up to now" → distances: write i - j at pop time (Daily Temperatures) or i - top at push time (Stock Span)
  • "Sum over all subarrays of their min or max" → the contribution technique: each element times (left extent × right extent), with strict on one side and non-strict on the other to break ties
  • "Smallest result after deleting characters or digits," in order → a stack that pops a worse top while it is allowed to lose it (a budget of K, or a later copy)
  • A pattern like "small, big, medium" over positions → scan from the end with a decreasing stack and remember the last popped value
  • Moving things that merge when a faster one catches a slower one → sort by position and compare arrival times
  • A sliding window on an array with negatives and a "sum at least K" target → prefix sums with a monotonic deque of candidate starts
  • A DP of the form "best of the previous k states plus a cost" → a monotonic deque over the DP values
  • Pitfalls: pushing values instead of indices (you'll need positions for widths), the wrong strictness on ties, forgetting the sentinel so the stack never drains, and assuming shift() is free
Before you move on
  • Derive, don't recall, whether a stack should be increasing or decreasing for "next greater" versus "next smaller" — say the one-sentence reasoning out loud.
  • Write largestRectangleArea from memory, including the sentinel bar, and explain both off-by-one traps in the width calculation.
  • Explain why one decreasing-stack pass resolves both "next greater" (at pop time) and "previous greater" (at push time) without a second pass.
  • Explain why Array.prototype.shift() silently breaks the promised O(n) bound for a monotonic deque, and what the head-pointer fix looks like.
  • Given a new problem, decide between a monotonic stack/deque, a plain sliding window, and a heap — based on whether the aggregate can be "undone" when an element leaves.
  • Write Daily Temperatures and Stock Span, and say which of the two writes its answer at pop time and which at push time.
  • Compute Sum of Subarray Minimums by the contribution technique, and show with [2, 2] why one side of the tie must be strict and the other not.
  • Build the smallest number by removing K digits, and explain the three edge cases: leftover deletions, leading zeros and the empty result.
  • Say why Shortest Subarray with Sum at Least K cannot use a plain sliding window with negative numbers, and what the deque holds instead.
  • Turn "dp[i] = nums[i] + max of the last k dp values" into an O(n) loop with a monotonic deque.

15Practice this chapter

Opens in the editor: write it, run it, and check it against real tests.

  • 8 testsDaily Temperaturesintermediate
  • 8 testsNext Greater Element Ibeginner
  • 8 testsNext Greater Element II (Circular)advanced
  • 8 testsLargest Rectangle in Histogramadvanced
  • 8 testsMaximal Rectangleadvanced
  • 9 testsSliding Window Maximumintermediate
  • 8 testsOnline Stock Spanintermediate
  • 9 testsSum of Subarray Minimumsadvanced
  • 9 testsRemove K Digitsintermediate
  • 8 testsRemove Duplicate Lettersintermediate
  • 9 tests132 Patternadvanced
  • 8 testsCar Fleetintermediate
  • 9 testsShortest Subarray With Sum at Least Kadvanced
  • 9 testsJump Game VIadvanced
  • 8 testsLongest Continuous Subarray With Absolute Diff <= Limitintermediate
All 318 for this level →

✓Check yourself

5 questions, pass at 4. The check runs in your browser and the result stays on this device, so it is a self-check and not a certificate.

I18

Finished reading?

0 of 48 chapters read in this series

Mark as read

→Next stop

  1. I17Previous · I17Tries
  2. I18you are here
  3. Next · I19Math for interviewsSmall, exact facts about numbers: how Euclid, the sieve, modular arithmetic and a few counting identities turn huge inputs into short loops, and where JavaScript's numbers stop being exact.47 min read

01On this page

15 sections · 0%

  1. 01The insight: some elements become permanently useless
  2. 02The canonical shape
  3. 03Next greater element
  4. 04Daily Temperatures, Next Greater Element I and Stock Span
  5. 05Largest rectangle in histogram
  6. 06Trapping rain water, the monotonic-stack way
  7. 07Sum of Subarray Minimums: count the windows each element rules
  8. 08Lexicographic stacks: Remove K Digits and Remove Duplicate Letters
  9. 09132 pattern: a stack that remembers the best "2"
  10. 10Car Fleet: sort by position, keep a stack of arrival times
  11. 11Monotonic deque: sliding window maximum
  12. 12Deque with prefix sums: shortest subarray with sum at least K
  13. 13Deque with DP: Jump Game VI
  14. 14See the stack work
  15. 15Recognizing it in an unseen problem
Mark as read

[] chapters · t top