Skip to the notes
JSGroundwork
JSGroundwork handwritten · web dev
✎Playground→⌘Problems↻Review🔥Progress

Chapters

34 chapters
⌕
Beginner10›
B1Complexity analysisB2Arrays & stringsB3HashingB4Two pointersB5Sliding windowB6Binary searchB7Sorting algorithmsB8Stacks & queuesB9Linked listsB10Basic recursion
Intermediate12›
I1TreesI2Tree problems in depthI3Heaps & priority queuesI4Graphs: representationI5Graph problemsI6BacktrackingI7DP: 1DI8DP: 2DI9Greedy algorithmsI10IntervalsI11Bit manipulationI12Matrix problems
Advanced12›
A1Advanced DPA2Union-FindA3Advanced graph algorithmsA4Minimum Spanning TreeA5TriesA6Segment & Fenwick treesA7String algorithmsA8Monotonic stack & queueA9Design problemsA10Advanced backtrackingA11Topological patternsA12Interview strategy
/ search[ ] chaptert top

DSA in JS levels

1Beginner2Intermediate3Advanced

Ready to read

JSJavaScript⑂Git◎Interview prepΣDSA in JSSDSystem Design
More topics15›
</>HTML{ }CSS⚛ReactNNext.jsNeNest.jsTSTypeScriptNoNode.js🐳DockerDBSQL & Databases✓Testing🔒Web Security☁Cloud & DevOps◈GraphQL◆Redis☸Kubernetes
100%
A8

Monotonic stack & queue in depth

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

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 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

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] strictly decreasing

  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.

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 increasing bottom to top
  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
ihpopped (height)leftwidthareabest
1120122
4263166
425221010
60 (sentinel)351310
60 (sentinel)224810
60 (sentinel)106610
⚠ 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 decreasing bottom to top
  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.

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] decreasing front to back
  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 nums[i] can never be a max again: nums[i] is newer AND bigger
    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)."

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
  • 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
Practice this layer

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

Daily Temperatures5 tests · intermediateNext Greater Element I5 tests · intermediateNext Greater Element II (Circular)5 tests · advancedLargest Rectangle in Histogram5 tests · advancedMaximal Rectangle5 tests · advanced
←previousString algorithms↑ CovernextDesign problems→