Part V · Aggregates, Stacks and Graphs Pattern 14 4 problems

Monotonic Stack

A stack that refuses to hold anything out of order. Whatever it evicts, it evicts at the exact moment that item’s answer becomes known.

Every problem here has the same brute force. For each element, scan outward until you find the next bigger or smaller one. That is O(n²). A stack kept in sorted order makes it O(n). Each element is pushed once and popped once. The hard part is not the code. It is seeing that the question is a “next greater” question in disguise.

In C# the tool is Stack<int> with Push, Pop, Peek and Count. All four are O(1). The Python version of this page is here.

Contents

  1. When to use
  2. Core idea
  3. The four variants
  4. The templates
  5. Common mistakes
  6. Daily Temperatures
  7. Next Greater Element II
  8. Trapping Rain Water
  9. Largest Rectangle in Histogram
  10. Recap

When to use

The trigger. For each element you need the nearest element on one side that beats it. That could be the nearest larger or smaller value. It could be how far away it is. It could be how far a value spreads before something taller stops it.
The recognition test. Write the brute force in your head: for each i, walk right until I find something bigger. If that is the shape, it is a monotonic stack. That one sentence is the whole diagnosis.

Core idea

Walk the array once. Keep a stack of indices whose answers are still unknown. Keep the values at those indices in order, say strictly decreasing. A new value may break that order. Then everything it beats is popped, and the new value is the answer for each thing it pops. Push the new index and carry on.
stack holds indices with decreasing values 75 71 69 all still waiting for a bigger value 76 76 arrives it pops 69, then 71, then 75 and it is the answer for all three each index is pushed once and popped at most once so the total work is O(n) Invariant: everything on the stack is still unanswered, and it is sorted. Both facts are what make the pops correct.
Figure 14.1 — The nested while loop looks quadratic and is not. Every index enters the stack once and leaves once.

Reading the figure. Blue bars sit on the stack, tallest at the bottom. The amber 76 arrives. The red arrows show it popping 69, 71 and 75 in that order. Each pop writes 76 as that bar’s answer.

Why the nested loop is still linear

Amortised argument. Each index is pushed exactly once. Each pop removes an index for good, so there are at most n pops in the whole run. The inner while may run many times on one pass and zero times on the next. But the total inner steps over the whole loop is at most n. Total: O(n). This is the same accounting as the sliding window.

The four variants

All four are the same loop. Only the comparison and the reading point change. Write this list out before coding and you will not get the sign backwards.

One loop gives you both sides. When an index is popped, the arriving element is its next boundary. Whatever is left under it on the stack is its previous boundary. Largest Rectangle in Histogram uses exactly that. It is why that problem needs one pass, not two.

The templates

Template A — next greater element, by index
public static class NextGreater
{
    /// <summary>For each index, the index of the next strictly greater value.</summary>
    /// <param name="nums">The values to scan.</param>
    /// <returns>answer[i] is the index of the next greater value, or -1 if none.</returns>
    /// <example><c>NextGreater.Indices([2, 1, 3])</c> returns <c>[2, 2, -1]</c>.</example>
    public static int[] Indices(int[] nums)
    {
        // -1 means "no greater value to the right". Every slot starts there.
        var answer = new int[nums.Length];
        Array.Fill(answer, -1);
        // Indices, not values. nums at those indices is non-increasing.
        var stack = new Stack<int>();

        // index is the current position. 0: start at the first element.
        // Invariant: the stack holds every earlier index still waiting for an answer.
        for (int index = 0; index < nums.Length; index++)
        {
            // Everything smaller than nums[index] has just found its answer.
            // Count > 0 first: Peek on an empty stack throws.
            while (stack.Count > 0 && nums[stack.Peek()] < nums[index])
                answer[stack.Pop()] = index;

            stack.Push(index);   // index now waits for its own greater value
        }

        return answer;   // anything left on the stack keeps -1
    }
}

Store indices, not values. You can always read the value from the index. You cannot get a distance back from a value.

Template B — the sentinel flush
public static class SentinelFlush
{
    /// <summary>Runs an increasing stack with a fake 0 bar at the end.</summary>
    /// <param name="heights">Non-negative bar heights.</param>
    /// <param name="resolve">Per-problem work for a popped bar, called as
    /// resolve(popped, index). index is the first bar to its right that is not taller.
    /// Example: Largest Rectangle computes the popped bar's area here.</param>
    /// <example><c>SentinelFlush.Run([2, 4, 3], log)</c> calls log(1, 2), log(2, 3),
    /// then log(0, 3).</example>
    public static void Run(int[] heights, Action<int, int> resolve)
    {
        var stack = new Stack<int>();
        int n = heights.Length;

        // index runs 0..n. The extra index n is a fake bar of height 0.
        // 0 is lower than any real bar, so it pops everything still stacked.
        for (int index = 0; index <= n; index++)
        {
            int height = index == n ? 0 : heights[index];

            // >= pops equal bars too. Count > 0 guards Peek.
            while (stack.Count > 0 && heights[stack.Peek()] >= height)
                resolve(stack.Pop(), index);

            stack.Push(index);
        }
    }
}

A sentinel removes the “now drain whatever is left” block after the loop. You get one code path instead of two. The leftover case is where bugs live. C# arrays cannot grow, so the sentinel is the index == n check, not an appended element.

without a sentinel 2 i=0 4 i=1 3 i=2 loop ends, stack still [0, 2] a second drain loop is needed with a sentinel 0 at the end 2 i=0 4 i=1 3 i=2 0 the 0 pops 3, then 2, inside the loop one code path, nothing left over
Figure 14.2 — A final value lower than every bar empties the stack before the loop ends.

Reading the figure. Blue bars are still on the stack. Green bars have been popped and resolved. On the left the 3 pops the 4. But bars 2 and 3 are stranded when the input runs out. On the right the amber sentinel 0 arrives last. The red arrows show it popping both stranded bars. No cleanup code is needed.

Common mistakes

A faster stack. In hot code you can use a plain int[] of size n plus a top counter. Push is stack[top++] = i. Pop is stack[--top]. It avoids the bounds and version checks in Stack<int>. In an interview, Stack<int> reads better. Mention the array form only if asked about speed.

The problems

1. Daily Temperatures Medium

Problem

Given daily temperatures, return an array where answer[i] is the number of days you must wait after day i for a warmer temperature. If no warmer day comes, put 0.

Approach

Solution

public static class DailyTemperatures
{
    /// <summary>Days to wait for a warmer temperature, per day.</summary>
    /// <param name="temperatures">Daily temperature readings.</param>
    /// <returns>answer[i] is the number of days after i until a warmer day, or 0.</returns>
    /// <example><c>DailyTemperatures.Wait([73, 74, 75, 71, 69, 72, 76, 73])</c>
    /// returns <c>[1, 1, 4, 2, 1, 1, 0, 0]</c>.</example>
    public static int[] Wait(int[] temperatures)
    {
        // C# fills new arrays with 0, which means "no warmer day ahead".
        var answer = new int[temperatures.Length];
        var stack = new Stack<int>();   // indices of days still waiting. Temps decrease

        // index is today. 0: start at the first day.
        // Invariant: the stack holds every earlier day with no warmer day yet,
        // coldest on top.
        for (int index = 0; index < temperatures.Length; index++)
        {
            // Today is warmer than everything waiting on top of the stack,
            // so it is the answer for all of them. Count > 0 guards Peek.
            while (stack.Count > 0 && temperatures[stack.Peek()] < temperatures[index])
            {
                int earlier = stack.Pop();
                answer[earlier] = index - earlier;   // days waited: the gap in index
            }

            stack.Push(index);   // today now waits for its own warmer day
        }

        // Whatever is still stacked never warmed up, and 0 is already there.
        return answer;
    }
}

Walkthrough

[73, 74, 75, 71, 69, 72, 76, 73], one line per day:

Indices 6 and 7 stay stacked and keep their 0. The stack is shown bottom to top.

73 day 0 74 day 1 75 day 2 71 day 3 69 day 4 72 day 5 76 day 6 73 day 7 wait 1 1 4 2 1 1 0 0
Figure 14.3 — Each arrow is one pop. The warmer day that pops an index is that index's answer.

Reading the figure. Each bar is one day. A red arrow runs from a day to the warmer day that popped it off the stack. The cell under each bar is the wait, which is the gap between the two day numbers. Green days found an answer. Blue days 6 and 7 never get popped, so they keep 0.

TimeO(n)SpaceO(n)

The worst case for space is a strictly decreasing input. Nothing is ever popped, so the stack holds all n indices. Name it when asked.

Edge cases to raise

Say this out loud: “The stack holds days that have not yet seen anything warmer, in decreasing order. A warm day resolves all of them at once. Each index is pushed and popped once, so it is linear.”

2. Next Greater Element II Medium

Problem

Given a circular array, return the next greater element for each position. The search wraps past the end and back to the start. Use -1 where none exists.

The circular trick

Do not build a doubled array. Do not write modular index math in three places. Instead loop 2n times and index with i % n. Two laps are always enough. After one full lap, every element has seen every other one. Then add one rule: only push indices during the first lap. The second lap exists only to resolve leftovers, so it must not create new ones.

Approach

Solution

public static class NextGreaterCircular
{
    /// <summary>Next greater element for each position in a circular array.</summary>
    /// <param name="nums">The circular array. The search wraps past the end.</param>
    /// <returns>answer[i] is the first value greater than nums[i], scanning forward
    /// and wrapping around, or -1 if there is none.</returns>
    /// <example><c>NextGreaterCircular.Solve([1, 2, 1])</c> returns <c>[2, -1, 2]</c>.
    /// </example>
    public static int[] Solve(int[] nums)
    {
        int n = nums.Length;
        var answer = new int[n];
        Array.Fill(answer, -1);          // -1: "no greater value", the default
        var stack = new Stack<int>();   // indices. nums at those is non-increasing

        // Two laps. The first seeds the stack, the second resolves the wrap-around.
        // i runs 0..2n-1: 2 * n covers both laps. 0: start of lap one.
        for (int i = 0; i < 2 * n; i++)
        {
            int value = nums[i % n];     // % n: wrap i back into 0..n-1

            // Count > 0 guards Peek. The top is the latest index still waiting.
            while (stack.Count > 0 && nums[stack.Peek()] < value)
                answer[stack.Pop()] = value;

            if (i < n)                  // i < n: first lap only. Never push on lap two
                stack.Push(i);
        }

        return answer;
    }
}

Walkthrough

nums = [1, 2, 1], so the loop runs 6 times:

Index 1 holds the array maximum, so it keeps -1. That is correct.

lap 1: resolve and push lap 2: resolve only, no push 1 i=0, index 0 2 i=1, index 1 1 i=2, index 2 1 i=3, index 0 2 i=4, index 1 1 i=5, index 2 answer[0] = 2 answer[2] = 2, found in lap 2 Index 1 holds the maximum, so nothing ever pops it. answer = [2, -1, 2]
Figure 14.4 — The second lap lets the last element see the elements before it, without pushing anything new.

Reading the figure. The six cells are the loop running 2n times over [1, 2, 1]. Blue cells are lap 1, where indices are pushed. Dashed cells are lap 2, where nothing is pushed. Each green arc is a pop that writes an answer. Index 2 only finds its answer because lap 2 wraps back to the 2 at the front.

TimeO(n)SpaceO(n)

2n passes is still O(n). Do not let the doubling talk you into calling it quadratic.

The non-circular sibling

public static class NextGreaterElementI
{
    /// <summary>Next Greater Element I. nums1 is a subset of nums2. Solve on nums2,
    /// then look each nums1 value up.</summary>
    /// <param name="nums1">The values to answer for. All appear in nums2.</param>
    /// <param name="nums2">Distinct values to scan.</param>
    /// <returns>For each nums1 value, its next greater value in nums2, or -1.</returns>
    /// <example><c>NextGreaterElementI.Solve([4, 1, 2], [1, 3, 4, 2])</c> returns
    /// <c>[-1, 3, -1]</c>.</example>
    public static int[] Solve(int[] nums1, int[] nums2)
    {
        var greater = new Dictionary<int, int>();   // value -> next greater value in nums2
        var stack = new Stack<int>();   // values this time, since nums2 has no duplicates

        // Invariant: the stack holds earlier values with no greater value yet.
        foreach (int value in nums2)
        {
            // value is the answer for every smaller value on top of the stack.
            while (stack.Count > 0 && stack.Peek() < value)
                greater[stack.Pop()] = value;
            stack.Push(value);
        }

        // -1: values never popped have no greater value to the right.
        return [.. nums1.Select(value => greater.GetValueOrDefault(value, -1))];
    }
}

Here the values are distinct, so a value-keyed Dictionary is safe. The stack can hold values directly. When duplicates are possible, go back to indices. GetValueOrDefault replaces Python’s dict.get(key, default).

Edge cases to raise

Say this out loud: “Two laps with a modulo index handle the wrap-around. The second lap must not push anything. Otherwise an element with no greater value would get resolved by its own second copy.”

3. Trapping Rain Water Hard

Problem

Given an elevation map where height[i] is the height of a bar of width 1, compute how much rain water is trapped after it rains.

The governing fact

The water above position i is Math.Min(tallest bar to the left, tallest bar to the right) - height[i], or zero if that is negative. The shorter of the two walls holds the water in. That is why the min is there. Every solution below is a different way to get those two maxima cheaply.
This is not Container With Most Water. That problem picks two bars and ignores everything between them. This one uses every bar. The terrain in between holds or spills the water. Interviewers ask them back to back to see whether you notice.

Solution: monotonic stack, filling layer by layer

public static class RainWaterStack
{
    /// <summary>Total rain water trapped above an elevation map. Fills the terrain
    /// in horizontal layers. Each pop finds a basin floor. The bar under it on the
    /// stack is the left wall, and the arriving bar is the right wall.</summary>
    /// <param name="height">Non-negative bar heights.</param>
    /// <returns>Total trapped water.</returns>
    /// <example><c>RainWaterStack.Trap([0, 1, 0, 2, 1, 0, 1, 3, 2, 1, 2, 1])</c>
    /// returns <c>6</c>.</example>
    public static int Trap(int[] height)
    {
        int total = 0;                   // 0: no water counted yet
        var stack = new Stack<int>();   // indices. Heights non-increasing

        // right is the arriving bar. 0: start at the first bar.
        // Invariant: the stack holds bars that may still be a left wall,
        // tallest at the bottom.
        for (int right = 0; right < height.Length; right++)
        {
            int bar = height[right];

            // A taller bar closes off one or more basins to its left.
            // The top of the stack is the lowest waiting bar.
            while (stack.Count > 0 && height[stack.Peek()] < bar)
            {
                int floor = stack.Pop();

                if (stack.Count == 0)
                    break;               // 0 left: no left wall, so the water spills out

                int left = stack.Peek(); // the new top is the left wall
                // - 1: count only the cells strictly between the two walls.
                int width = right - left - 1;
                // Water rises to the lower wall, above the floor already filled.
                int depth = Math.Min(height[left], bar) - height[floor];
                total += width * depth;
            }

            stack.Push(right);
        }

        return total;
    }
}

Solution: two pointers, O(1) space

public static class RainWaterTwoPointers
{
    /// <summary>Same answer in O(1) space, closing in from both ends. If the tallest
    /// bar seen from the left is no taller than the tallest seen from the right, the
    /// left max is the binding wall for the left position. So that side is safe to
    /// settle now.</summary>
    /// <param name="height">Non-negative bar heights.</param>
    /// <returns>Total trapped water.</returns>
    /// <example><c>RainWaterTwoPointers.Trap([4, 2, 0, 3, 2, 5])</c> returns <c>9</c>.
    /// </example>
    public static int Trap(int[] height)
    {
        if (height.Length == 0)
            return 0;                    // 0 bars, so 0 water

        // 0 is the first index. Length - 1 is the last index.
        int left = 0, right = height.Length - 1;
        int leftMax = height[left], rightMax = height[right];
        int total = 0;                   // 0: no water counted yet

        // Invariant: every bar outside left..right is settled and counted.
        // leftMax and rightMax are the tallest bars seen from each side.
        while (left < right)
        {
            if (leftMax <= rightMax)
            {
                left++;                  // +1: step the left pointer inward
                leftMax = Math.Max(leftMax, height[left]);
                // Water here is the wall height minus the bar. 0 if it is the wall.
                total += leftMax - height[left];
            }
            else
            {
                right--;                 // -1: step the right pointer inward
                rightMax = Math.Max(rightMax, height[right]);
                total += rightMax - height[right];
            }
        }

        return total;
    }
}

The three solutions, ranked

A good order under pressure. Describe the Math.Min(leftMax, rightMax) rule. Write the two-array version. Then say “I can drop the arrays with two pointers” and write that. The stack version is worth knowing too. It is the same machinery as the next problem, and it counts the water in a different shape.

Walkthrough of the stack version

height = [0, 1, 0, 2, 1, 0, 1, 3, 2, 1, 2, 1]. At index 3 the bar of height 2 arrives. It pops the 0 at index 2. It finds index 1 underneath as the left wall. It adds width 1 × depth (Math.Min(1, 2) - 0) = 1. Later the bar of height 3 at index 7 pops a run of small bars. It adds the large middle basin. The layers add up to 6.

0 1 0 2 1 0 1 3 2 1 2 1 1 1 3 1 Gray bars are the terrain. Each blue block is the water added by one pop. blocks are added when the bar at index 3, 6, 7, then 10 arrives total water = 1 + 1 + 3 + 1 = 6
Figure 14.5 — The stack fills water in horizontal layers, one layer per pop, instead of column by column.

Reading the figure. The numbers under the bars are the heights. Each blue block is one layer of water. Its number is width times depth. A block appears when a taller bar arrives and pops a lower one. The wide block of 3 sits on top of a smaller one. A later pop can fill above an earlier layer.

StackO(n) time, O(n) spaceTwo pointersO(n) time, O(1) space

Edge cases to raise

Say this out loud: “Water above a position is the smaller of the two maxima around it, minus the bar. With two pointers I always move the side whose max is smaller. That side’s answer is already decided.”

4. Largest Rectangle in Histogram Hard

Problem

Given bar heights of width 1, find the area of the largest rectangle that fits inside the histogram.

The reframing

Every candidate rectangle is limited in height by its shortest bar. So go over the bars and ask, for each one: if this bar is the shortest in the rectangle, how wide can it be? The nearest strictly shorter bar on each side bounds the answer. That is the previous-smaller and next-smaller pair. One monotonic stack gives both in a single pass.

Once you say “each bar is the height, and I need its left and right smaller walls”, the problem is done. Getting there is the interview.

Approach

Solution

public static class LargestRectangle
{
    /// <summary>Area of the largest rectangle that fits under a histogram. Each bar
    /// is tried as the limiting height. Its width runs between the nearest shorter
    /// bar on each side, and one increasing stack finds both.</summary>
    /// <param name="heights">Non-negative bar heights, each of width 1.</param>
    /// <returns>The maximum rectangle area.</returns>
    /// <example><c>LargestRectangle.Area([2, 1, 5, 6, 2, 3])</c> returns <c>10</c>.
    /// </example>
    public static int Area(int[] heights)
    {
        int n = heights.Length;
        int best = 0;                    // 0: the empty rectangle, area 0
        var stack = new Stack<int>();   // indices. Heights strictly increasing

        // index runs 0..n. Index n is a sentinel bar of height 0. It is shorter
        // than any bar, so it flushes the stack inside the loop.
        // Invariant: each stacked bar is taller than the one below it.
        for (int index = 0; index <= n; index++)
        {
            int bar = index == n ? 0 : heights[index];

            // index is the nearest shorter-or-equal bar to the right of the top.
            while (stack.Count > 0 && heights[stack.Peek()] >= bar)
            {
                int height = heights[stack.Pop()];

                // Left wall: the new top is the nearest shorter bar to the left.
                // + 1: the rectangle starts just past that shorter bar.
                // 0: with an empty stack the rectangle reaches index 0.
                int left = stack.Count > 0 ? stack.Peek() + 1 : 0;
                // Width runs from left up to index - 1, so it is index - left.
                best = Math.Max(best, height * (index - left));
            }

            stack.Push(index);
        }

        return best;
    }
}

Walkthrough

heights = [2, 1, 5, 6, 2, 3]:

The winner is the 5 × 2 rectangle over the bars of height 5 and 6.

2 i=0 1 i=1 5 i=2 6 i=3 2 i=4 3 i=5 5 × 2 = 10 bar 5 at i=2 sets the height left wall: i=1, height 1 (left under it on the stack) right wall: i=4, height 2 (the bar that pops it) width 4 − 1 − 1 = 2
Figure 14.6 — One pop gives both walls. The arriving bar is the right wall, and the new stack top is the left.

Reading the figure. The dashed green box is the winning rectangle. Its height comes from the bar of 5 (blue). The red bar is the nearest shorter bar on the left. It sits under the 5 on the stack. The amber bar is the nearest shorter bar on the right. Its arrival pops the 5. The width is the gap between the two walls.

TimeO(n)SpaceO(n)

The follow-up: Maximal Rectangle in a binary matrix

public static class MaximalRectangle
{
    /// <summary>Largest all-ones rectangle in a binary matrix. Treat each row as the
    /// base of a histogram. Bar heights are the runs of ones stacked above that row.
    /// Then it is Largest Rectangle, once per row.</summary>
    /// <param name="matrix">Rows of '0' and '1' chars, all the same length.</param>
    /// <returns>The area of the largest rectangle of ones.</returns>
    /// <example><c>MaximalRectangle.Area([['1', '1'], ['1', '1']])</c> returns
    /// <c>4</c>.</example>
    public static int Area(char[][] matrix)
    {
        // matrix[0] is the first row. Length 0 there means no columns.
        if (matrix.Length == 0 || matrix[0].Length == 0)
            return 0;                    // 0: no cells, so no rectangle

        // One bar per column. new int[] starts at 0: no ones yet.
        var heights = new int[matrix[0].Length];
        int best = 0;                    // 0: no rectangle found yet

        // Invariant: heights[c] = run of '1' cells ending at this row in column c.
        foreach (char[] row in matrix)
        {
            // c is the column. 0: start at the left edge.
            for (int c = 0; c < row.Length; c++)
                // + 1 extends the run by this cell. 0 resets it on a '0' cell.
                heights[c] = row[c] == '1' ? heights[c] + 1 : 0;

            best = Math.Max(best, LargestRectangle.Area(heights));
        }

        return best;
    }
}

A Hard problem that shrinks to a few lines once you own the histogram routine. This is why the histogram problem gets asked at all. LargestRectangle.Area only reads heights, so reusing one array across rows is safe.

Edge cases to raise

Say this out loud: “Fix each bar as the limiting height and ask how far it can spread. Its walls are the nearest shorter bar on each side. One increasing stack gives me both, because the index under a popped one is its left wall.”

Recap

The six things to carry forward

Where this goes next

Pattern 15, Topological Sort, moves back to graphs. It is the ordering algorithm that BFS and DFS both hint at. It is the standard answer to any question about dependencies.


← 13 — Prefix Sum and Hash Map 15 — Topological Sort →