Stacks and heaps

Stacks, monotonic stacks & heaps

Parentheses and path stacks, next-greater monotonic stacks, and heap patterns (kth, merge-k, running median) — with plain-language explanation before templates.

Why stack vs heap

A stack remembers nested unfinished work (parentheses, path, DFS). A monotonic stack keeps indices in increasing/decreasing order so you can answer ‘next greater’ when you pop. A heap repeatedly gives you the current best item — top-K, merge-K, median with two heaps.

Analogy: ER triage (heap) + plate stack

Heap triage
You only need who’s next — not a full sort.

A stack is a cafeteria plate pile: last on, first off — perfect for nested unfinished work (parentheses, DFS path). A heap is an ER triage board: you always pull the most urgent patient next without sorting everyone by severity every time.

Stack & monotonic stack

Stack vs heap
LIFO vs priority.
Monotonic stack
Pop until order restored → next greater.
def daily_temperatures(t):
    ans = [0]*len(t); st = []
    for i, x in enumerate(t):
        while st and t[st[-1]] < x:
            j = st.pop(); ans[j] = i-j
        st.append(i)
    return ans
int[] dailyTemperatures(int[] t) {
    int[] ans = new int[t.length];
    Deque<Integer> st = new ArrayDeque<>();
    for (int i = 0; i < t.length; i++) {
        while (!st.isEmpty() && t[st.peek()] < t[i]) {
            int j = st.pop();
            ans[j] = i - j;
        }
        st.push(i);
    }
    return ans;
}

Heaps

  • Top K → size-k heap
  • Merge K lists → heap of heads
  • Median stream → two heaps
import heapq
def kth_largest(nums, k):
    h = []
    for x in nums:
        heapq.heappush(h, x)
        if len(h) > k: heapq.heappop(h)
    return h[0]
int kthLargest(int[] nums, int k) {
    PriorityQueue<Integer> h = new PriorityQueue<>();
    for (int x : nums) {
        h.offer(x);
        if (h.size() > k) h.poll();
    }
    return h.peek();
}

Which tool?

Stack — nested structure, undo, parse. Monotonic stack — next greater/smaller. Heap — repeated ‘best of remaining.’

Worked trace: next warmer day

Temps [73,74,75,71,69,72,76]. Stack holds indices of unresolved days (decreasing temps). At 74, pop 73 and set wait=1. At 76, pop everyone remaining and fill waits. You’re explaining the pops — that’s the insight.

When to reach for this pattern

Stack = LIFO discipline on a line of decisions. Heap = ‘give me the current extreme fast’ under a changing set. Monotonic stack is the interview crossover: you maintain candidates in increasing/decreasing order so the top answers ‘next greater’ in amortized O(1).

Core template + worked trace

Monotonic decreasing stack for next warmer / next greater:

def daily_temperatures(t: list[int]) -> list[int]:
    # stack holds indices with decreasing temperatures
    ans = [0] * len(t)
    stack: list[int] = []
    for i, temp in enumerate(t):
        while stack and t[stack[-1]] < temp:
            j = stack.pop()
            ans[j] = i - j          # days until warmer
        stack.append(i)
    return ans

# t=[73,74,75,71,69,72,76,73]
# i=1 (74): pops 0 → ans[0]=1
# i=2 (75): pops 1 → ans[1]=1
# i=5 (72): pops 4,3 → ans[4]=1, ans[3]=2
# i=6 (76): pops 5,2 → ... ans[2]=4
int[] dailyTemperatures(int[] t) {
    // stack holds indices with decreasing temperatures
    int[] ans = new int[t.length];
    Deque<Integer> stack = new ArrayDeque<>();
    for (int i = 0; i < t.length; i++) {
        while (!stack.isEmpty() && t[stack.peek()] < t[i]) {
            int j = stack.pop();
            ans[j] = i - j;          // days until warmer
        }
        stack.push(i);
    }
    return ans;
}

// t=[73,74,75,71,69,72,76,73]
// i=1 (74): pops 0 → ans[0]=1
// i=2 (75): pops 1 → ans[1]=1
// i=5 (72): pops 4,3 → ans[4]=1, ans[3]=2
// i=6 (76): pops 5,2 → ... ans[2]=4

Top-K with a min-heap of size k:

import heapq

def top_k(nums: list[int], k: int) -> list[int]:
    heap: list[int] = []
    for x in nums:
        heapq.heappush(heap, x)
        if len(heap) > k:
            heapq.heappop(heap)   # drop smallest
    return heap  # k largest, unordered

# nums=[3,1,5,12,2,11], k=3 → heap ends as [5,12,11]
List<Integer> topK(int[] nums, int k) {
    PriorityQueue<Integer> heap = new PriorityQueue<>();
    for (int x : nums) {
        heap.offer(x);
        if (heap.size() > k) heap.poll();   // drop smallest
    }
    return new ArrayList<>(heap);  // k largest, unordered
}

// nums=[3,1,5,12,2,11], k=3 → heap ends as [5,12,11]

Edge cases & common bugs

Complexity — say it aloud

Interview talk track

You: ‘I’ll keep a decreasing stack of indices. When today’s temperature is warmer than the top, that earlier day just found its answer — pop and write the distance. Each index enters and leaves once, so O(n).’

Practice set

  • Valid Parentheses
  • Daily Temperatures
  • Next Greater Element II
  • Largest Rectangle in Histogram
  • Kth Largest Element in an Array
  • Top K Frequent Elements
  • Merge K Sorted Lists
  • Find Median from Data Stream

Harder follow-up

Harder variant: Largest Rectangle in Histogram — monotonic increasing stack of indices; when you pop, width spans from the new top+1 to i−1. Same amortized O(n) story.

Pattern bank: more questions + efficient solutions

Five stack/heap staples. Monotonic stacks answer ‘next greater / previous smaller’; heaps answer ‘top-K / merge under order’.

Q: Valid Parentheses — given a string of brackets, return whether every open has a matching close in the correct order.

def is_valid(s):
    pair = {')': '(', ']': '[', '}': '{'}
    st = []
    for ch in s:
        if ch in pair:
            if not st or st[-1] != pair[ch]:
                return False
            st.pop()
        else:
            st.append(ch)
    return not st
# O(n) time, O(n) space
boolean isValid(String s) {
    Deque<Character> st = new ArrayDeque<>();
    for (char ch : s.toCharArray()) {
        if (ch == ')' || ch == ']' || ch == '}') {
            if (st.isEmpty()) return false;
            char open = st.pop();
            if (ch == ')' && open != '(') return false;
            if (ch == ']' && open != '[') return false;
            if (ch == '}' && open != '{') return false;
        } else st.push(ch);
    }
    return st.isEmpty();
}
// O(n) time, O(n) space

Q: Daily Temperatures — for each day, how many days until a warmer temperature? 0 if none (monotonic decreasing stack of indices).

def daily_temperatures(temps):
    n = len(temps)
    ans = [0] * n
    st = []  # indices, temps strictly decreasing
    for i, t in enumerate(temps):
        while st and temps[st[-1]] < t:
            j = st.pop()
            ans[j] = i - j
        st.append(i)
    return ans
# O(n) time, O(n) space
int[] dailyTemperatures(int[] temps) {
    int n = temps.length;
    int[] ans = new int[n];
    Deque<Integer> st = new ArrayDeque<>();
    for (int i = 0; i < n; i++) {
        while (!st.isEmpty() && temps[st.peek()] < temps[i]) {
            int j = st.pop();
            ans[j] = i - j;
        }
        st.push(i);
    }
    return ans;
}
// O(n) time, O(n) space

Q: Largest Rectangle in Histogram — given bar heights of width 1, find the largest rectangle area in the histogram (monotonic stack).

def largest_rectangle_area(heights):
    st = []  # increasing heights indices
    best = 0
    for i, h in enumerate(heights + [0]):
        while st and heights[st[-1]] > h:
            height = heights[st.pop()]
            left = st[-1] if st else -1
            best = max(best, height * (i - left - 1))
        st.append(i)
    return best
# O(n) time, O(n) space
int largestRectangleArea(int[] heights) {
    Deque<Integer> st = new ArrayDeque<>();
    int best = 0, n = heights.length;
    for (int i = 0; i <= n; i++) {
        int h = i == n ? 0 : heights[i];
        while (!st.isEmpty() && heights[st.peek()] > h) {
            int height = heights[st.pop()];
            int left = st.isEmpty() ? -1 : st.peek();
            best = Math.max(best, height * (i - left - 1));
        }
        st.push(i);
    }
    return best;
}
// O(n) time, O(n) space

Q: Top K Frequent Elements — return the k most frequent numbers in nums (order among ties flexible unless specified).

from collections import Counter
import heapq

def top_k_frequent(nums, k):
    cnt = Counter(nums)
    return [x for x, _ in heapq.nlargest(k, cnt.items(), key=lambda kv: kv[1])]
# O(n log k) time, O(n) space
int[] topKFrequent(int[] nums, int k) {
    Map<Integer, Integer> cnt = new HashMap<>();
    for (int x : nums) cnt.merge(x, 1, Integer::sum);
    PriorityQueue<int[]> pq = new PriorityQueue<>(Comparator.comparingInt(a -> a[1]));
    for (var e : cnt.entrySet()) {
        pq.offer(new int[]{e.getKey(), e.getValue()});
        if (pq.size() > k) pq.poll();
    }
    int[] out = new int[k];
    for (int i = k - 1; i >= 0; i--) out[i] = pq.poll()[0];
    return out;
}
// O(n log k) time, O(n) space

Q: Merge K Sorted Lists — merge k sorted linked lists into one sorted list (min-heap on current heads).

import heapq

class ListNode:
    def __init__(self, val=0, next=None):
        self.val, self.next = val, next

def merge_k_lists(lists):
    heap = []
    for i, node in enumerate(lists):
        if node:
            heapq.heappush(heap, (node.val, i, node))
    dummy = cur = ListNode()
    while heap:
        _, i, node = heapq.heappop(heap)
        cur.next = node
        cur = cur.next
        if node.next:
            heapq.heappush(heap, (node.next.val, i, node.next))
    return dummy.next
# O(n log k) time, O(k) space
ListNode mergeKLists(ListNode[] lists) {
    PriorityQueue<ListNode> pq = new PriorityQueue<>(Comparator.comparingInt(a -> a.val));
    for (ListNode node : lists) if (node != null) pq.offer(node);
    ListNode dummy = new ListNode(0), cur = dummy;
    while (!pq.isEmpty()) {
        ListNode node = pq.poll();
        cur.next = node;
        cur = cur.next;
        if (node.next != null) pq.offer(node.next);
    }
    return dummy.next;
}
// O(n log k) time, O(k) space

Pattern drill: 45 minutes

← Lattice