{}DSA Atlas

Stacks, Queues & Monotonic Structures

LIFO and FIFO containers, bracket matching, expression evaluation, and the monotonic stack/deque patterns for next-greater elements and sliding-window maxima.

Beginner → Intermediate16 practice problems2 easy10 medium4 hard

Stack: last in, first out

A stack supports push, pop and peek at one end, all O(1). Think of a pile of plates.

stack = []
stack.append(1)        # push
top = stack[-1]        # peek
stack.pop()            # pop
if not stack: ...      # empty check
stack<int> st;         // or just use vector<int> with push_back/pop_back/back
st.push(1);
int top = st.top();
st.pop();
if (st.empty()) { }

Queue: first in, first out

from collections import deque
q = deque()
q.append(1)            # enqueue at the back
front = q[0]
q.popleft()            # dequeue from the front, O(1)
q.appendleft(0)        # deque = double-ended: works at both ends
queue<int> q;  q.push(1);  q.front();  q.pop();
deque<int> dq; dq.push_back(1); dq.push_front(0); dq.pop_back(); dq.pop_front();

Queues power BFS (see Graphs) and any simulation where things are processed in arrival order.

Classic stack problems

Bracket matching

def is_valid(s):
    pairs = {")": "(", "]": "[", "}": "{"}
    stack = []
    for ch in s:
        if ch in pairs:
            if not stack or stack.pop() != pairs[ch]:
                return False
        else:
            stack.append(ch)
    return not stack
bool isValid(string s) {
    unordered_map<char, char> pairs = {{')', '('}, {']', '['}, {'}', '{'}};
    vector<char> st;
    for (char c : s) {
        if (pairs.count(c)) {
            if (st.empty() || st.back() != pairs[c]) return false;
            st.pop_back();
        } else st.push_back(c);
    }
    return st.empty();
}

Variants: Minimum Remove to Make Valid Parentheses (push indices of unmatched (), Longest Valid Parentheses (stack of indices seeded with −1).

Min stack

Store the running minimum alongside each value so getMin is O(1).

Python
class MinStack:
    def __init__(self):
        self.st = []                       # (value, min_so_far)
    def push(self, x):
        m = min(x, self.st[-1][1]) if self.st else x
        self.st.append((x, m))
    def pop(self):
        self.st.pop()
    def top(self):
        return self.st[-1][0]
    def getMin(self):
        return self.st[-1][1]

Nested structures: save context on the stack

When you enter a nested level, push the current context; when you leave, pop and merge.

Python
def decode_string(s):
    stack, cur, num = [], "", 0
    for ch in s:
        if ch.isdigit():
            num = num * 10 + int(ch)
        elif ch == "[":
            stack.append((cur, num))
            cur, num = "", 0
        elif ch == "]":
            prev, k = stack.pop()
            cur = prev + cur * k
        else:
            cur += ch
    return cur

Expression evaluation

  • Reverse Polish notation: push numbers, operators pop two operands.
  • Infix with + − and parentheses: keep result and sign; on ( push them and reset.
  • Infix with + − × ÷: push signed terms; apply × and ÷ immediately to the top; sum the stack at the end.
Python
def calculate(s):             # handles + - * / (no parentheses)
    stack, num, op = [], 0, "+"
    for i, ch in enumerate(s):
        if ch.isdigit():
            num = num * 10 + int(ch)
        if ch in "+-*/" or i == len(s) - 1:
            if op == "+": stack.append(num)
            elif op == "-": stack.append(-num)
            elif op == "*": stack.append(stack.pop() * num)
            else: stack.append(int(stack.pop() / num))   # truncate toward 0
            op, num = ch, 0
    return sum(stack)

Monotonic stack

A stack whose contents are kept sorted (increasing or decreasing). Before pushing a new element, pop everything that violates the order. The popped elements have just found their answer: the new element is their next greater (or smaller) element.

Template: next greater element to the right

def next_greater(nums):
    n = len(nums)
    ans = [-1] * n
    stack = []                                 # indices, values decreasing
    for i, x in enumerate(nums):
        while stack and nums[stack[-1]] < x:   # x is the answer for these
            ans[stack.pop()] = x
        stack.append(i)
    return ans
vector<int> nextGreater(const vector<int>& nums) {
    int n = nums.size();
    vector<int> ans(n, -1), st;              // st holds indices
    for (int i = 0; i < n; i++) {
        while (!st.empty() && nums[st.back()] < nums[i]) {
            ans[st.back()] = nums[i];
            st.pop_back();
        }
        st.push_back(i);
    }
    return ans;
}

Which variant do I need?

WantStack order (bottom → top)Pop while
next greaterdecreasingnums[top] < x
next smallerincreasingnums[top] > x
previous greaterdecreasingnums[top] <= x, then top (if any) is the answer for x
previous smallerincreasingnums[top] >= x, then top is the answer for x

Use < vs <= deliberately to decide how ties are handled — it matters when counting subarrays to avoid double counting.

Largest rectangle in a histogram

For each bar, the widest rectangle using its full height extends left and right until a shorter bar. An increasing stack finds both boundaries: when bar j is popped by i, its right boundary is i and its left boundary is the new top.

def largest_rectangle(heights):
    stack, best = [], 0               # increasing heights (indices)
    for i, h in enumerate(heights + [0]):      # sentinel 0 flushes the stack
        while stack and heights[stack[-1]] > h:
            height = heights[stack.pop()]
            left = stack[-1] if stack else -1
            best = max(best, height * (i - left - 1))
        stack.append(i)
    return best
int largestRectangleArea(vector<int> h) {
    h.push_back(0);
    vector<int> st;
    int best = 0;
    for (int i = 0; i < (int)h.size(); i++) {
        while (!st.empty() && h[st.back()] > h[i]) {
            int height = h[st.back()]; st.pop_back();
            int left = st.empty() ? -1 : st.back();
            best = max(best, height * (i - left - 1));
        }
        st.push_back(i);
    }
    return best;
}

Contribution technique: sum of subarray minimums

Instead of enumerating subarrays, ask for how many subarrays is a[i] the minimum? If L = distance to previous strictly smaller and R = distance to next smaller-or-equal, then a[i] is the min of L × R subarrays.

Python
def sum_subarray_mins(arr):
    MOD = 10**9 + 7
    n = len(arr)
    left, right = [0] * n, [0] * n
    st = []
    for i in range(n):                         # previous strictly smaller
        while st and arr[st[-1]] >= arr[i]:
            st.pop()
        left[i] = i - (st[-1] if st else -1)
        st.append(i)
    st = []
    for i in range(n - 1, -1, -1):             # next smaller or equal
        while st and arr[st[-1]] > arr[i]:
            st.pop()
        right[i] = (st[-1] if st else n) - i
        st.append(i)
    return sum(a * l * r for a, l, r in zip(arr, left, right)) % MOD

The asymmetric >= / > ensures equal values are counted exactly once.

Monotonic deque: sliding window max/min

Keep indices in a deque with decreasing values. The front is the current window's maximum. Pop from the back anything smaller than the new element (it can never be a max again), and pop from the front when it slides out of the window.

from collections import deque

def max_sliding_window(nums, k):
    dq, out = deque(), []
    for i, x in enumerate(nums):
        while dq and nums[dq[-1]] <= x:
            dq.pop()
        dq.append(i)
        if dq[0] <= i - k:           # front left the window
            dq.popleft()
        if i >= k - 1:
            out.append(nums[dq[0]])
    return out
vector<int> maxSlidingWindow(vector<int>& nums, int k) {
    deque<int> dq; vector<int> out;
    for (int i = 0; i < (int)nums.size(); i++) {
        while (!dq.empty() && nums[dq.back()] <= nums[i]) dq.pop_back();
        dq.push_back(i);
        if (dq.front() <= i - k) dq.pop_front();
        if (i >= k - 1) out.push_back(nums[dq.front()]);
    }
    return out;
}

O(n) total. The monotonic deque also optimizes DP transitions of the form dp[i] = max(dp[j]) + cost over a sliding range of j (e.g. Jump Game VI, Constrained Subsequence Sum).

Building one structure from another

  • Queue from two stacks: push to inbox; to pop, if outbox is empty pour all of inbox into it, then pop outbox. Each element moves at most twice → amortized O(1).
  • Stack from queues: after pushing x, rotate the queue n−1 times so x is at the front.

Common mistakes

  • Popping from an empty stack — always check stack / !st.empty() first.
  • Storing values when you need indices (for widths or distances). Store indices; you can always look up the value.
  • Forgetting to flush the stack at the end (use a sentinel like the 0 bar).
  • Tie handling (< vs <=) causing double counting in contribution problems.
  • Python integer division: -7 // 2 == -4. For truncation toward zero use int(a / b).

Practice problems

Ordered roughly from warm-up to hard. Try each one for 25–40 minutes before peeking at the key idea; if you needed the hint, mark it “review” and redo it in a few days.

0 / 16 solved
Push openers; on a closer, the top must be its matching opener. Stack must be empty at the end.
Easy
Inbox and outbox stacks; pour inbox into outbox only when outbox is empty. Amortized O(1).
Easy
Store (value, min so far) pairs on the stack.
Medium
Push numbers; on an operator pop b then a, push a op b. Truncate division toward zero.
Medium
Monotonic decreasing stack of indices; a warmer day pops and answers all colder days.
Medium
Circular array: iterate i from 0 to 2n−1 using i % n, only push during the first pass.
Medium
On '[' push (current string, repeat count); on ']' pop and set cur = prev + cur * count.
Medium
Stack of survivors; a left-moving asteroid fights right-moving tops until it dies or wins.
Medium
Stack of (price, span); pop all prices <= today and add their spans.
Medium
Sort by position descending; compute arrival times; a new fleet forms when time exceeds the stack top.
Medium
Each a[i] is the min of (i − prevLess) × (nextLessOrEqual − i) subarrays; find both with monotonic stacks.
Medium
Greedy increasing stack: pop a larger previous digit while k > 0; strip leading zeros.
Medium
Increasing stack; when a bar pops, its width spans from the new top+1 to i−1.
Hard
Deque of indices with decreasing values; drop the front when it leaves the window.
Hard
Running result and sign; on '(' push (result, sign) and reset; on ')' combine with the popped pair.
Hard
Build a histogram of consecutive 1s per row and run Largest Rectangle in Histogram on each row.
Hard

Click the box to cycle: solved ✓ → needs review ↺ → not started. Links open LeetCode.