Week 7: Stacks, Queues & Monotonic Stacks

Last week's linked list gave you O(1) insertion and deletion at a known position — a stack and a queue are just that same idea with the position locked down to "one end" and "each end" respectively, which is exactly why both std::stack and std::queue are implemented as thin adaptors over a deque or list rather than new data structures. The monotonic stack pattern you'll learn this week is one of the highest-leverage tricks in this course: it turns an apparent O(n²) nested loop into O(n) with a small invariant, and it's the same call-stack-as-a-stack intuition Week 8's recursion depends on. The std::deque techniques here also preview the BFS queues you'll use for graph traversal starting Week 12.

Module 5 of 17 Week 7 of 20 ~3–4 Hours Hands-on Exercise Included

By the end of this week, you'll be able to

  • Use std::stack to validate balanced parentheses and similar bracket-matching problems
  • Apply the monotonic stack pattern to solve next-greater-element problems in O(n)
  • Use std::deque as a monotonic window to solve sliding-window-maximum problems in O(n)

1. Stack Fundamentals & Balanced Parentheses

A stack is a Last-In-First-Out (LIFO) structure: you can only push onto the top and pop from the top, in O(1) time each. std::stack in <stack> is an adaptor — by default it wraps a std::deque and exposes only the operations a stack needs: push, pop, top and empty.

balanced_parens.cpp
#include <stack>
#include <string>
#include <unordered_map>
using namespace std;

// Returns true if every bracket in s is closed in the right order.
// O(n) time, O(n) space in the worst case (all opening brackets).
bool isBalanced(const string& s) {
    stack<char> open;
    unordered_map<char, char> match = {{')', '('}, {']', '['}, {'}', '{'}};

    for (char c : s) {
        if (c == '(' || c == '[' || c == '{') {
            open.push(c);
        } else if (match.count(c)) {
            if (open.empty() || open.top() != match[c]) return false;
            open.pop();
        }
    }
    return open.empty();   // nothing left unclosed
}

The LIFO order is exactly what makes a stack the right tool here: the most recently opened bracket must be the next one closed, which is precisely what top() gives you access to. Any problem with a "most recently seen, still unresolved" item is a strong signal that a stack belongs in your solution.

The call stack is a stack too

Every recursive function call in Week 8 pushes a new frame onto the program's call stack and pops it on return — the same LIFO discipline you're implementing explicitly here. Any recursive algorithm can, in principle, be rewritten iteratively with an explicit std::stack standing in for those call frames.

2. The Monotonic Stack Pattern

A monotonic stack keeps its elements in strictly increasing or decreasing order at all times, by popping off elements that would violate that order before pushing a new one. The classic use is the next greater element problem: for each element, find the first element to its right that's strictly larger.

next_greater_element.cpp
#include <vector>
#include <stack>
using namespace std;

// For each index i, result[i] is the next element to the right that's greater
// than nums[i], or -1 if none exists. O(n) time, O(n) space.
vector<int> nextGreaterElement(const vector<int>& nums) {
    int n = nums.size();
    vector<int> result(n, -1);
    stack<int> indices;   // stores INDICES whose next-greater is still unknown,
                           // with nums[indices] kept in decreasing order top-to-bottom

    for (int i = 0; i < n; i++) {
        // nums[i] is bigger than everything smaller still waiting on the stack --
        // pop and resolve each of those
        while (!indices.empty() && nums[indices.top()] < nums[i]) {
            result[indices.top()] = nums[i];
            indices.pop();
        }
        indices.push(i);
    }
    return result;   // whatever's left on the stack has no next greater element
}

The complexity argument is worth stating precisely: the inner while loop looks like it could make this O(n²), but every index is pushed exactly once and popped at most once across the entire run, so total work across all iterations is O(n), not O(n) per iteration — this is called amortized analysis, and it's the reasoning behind why the monotonic stack pattern is O(n) despite the nested loop shape.

Recognize the shape, not just the name

"Next greater/smaller element," "daily temperatures," "largest rectangle in a histogram," and "trapping rain water" all reduce to a monotonic stack once you frame them as "for each element, what's the nearest element to the left/right satisfying some comparison?" Store indices, not values, whenever you need to know how far apart two elements are.

3. Queue Fundamentals

A queue is First-In-First-Out (FIFO): elements are added at the back and removed from the front, both in O(1). std::queue in <queue> is another adaptor over std::deque by default, exposing push, pop, front and back.

queue_basics.cpp
#include <queue>
using namespace std;

int main() {
    queue<int> q;
    q.push(1);           // back: [1]
    q.push(2);           // back: [1, 2]
    q.push(3);           // back: [1, 2, 3]

    int f = q.front();   // f == 1 -- the first element pushed
    q.pop();              // removes 1, queue is now [2, 3]

    while (!q.empty()) {
        // process q.front(), then q.pop()
        q.pop();
    }
}

FIFO order is exactly what you want when processing elements in the order they were discovered rather than the order you most recently touched them — which is precisely why breadth-first search on a graph or tree, covered starting Week 9's level-order traversal and continuing into Week 12's graph BFS, always uses a queue rather than a stack: it guarantees you finish exploring everything at the current "distance" before moving farther out.

Stack vs. queue is a one-question decision

"Do I want to process the most recently added item next, or the earliest added item next?" Most-recent points to a stack (DFS, undo history, bracket matching); earliest points to a queue (BFS, task scheduling, print queues). Getting this choice wrong doesn't crash your code — it just silently explores or processes things in the wrong order.

4. std::deque & Sliding Window Maximum

std::deque ("double-ended queue," <deque>) supports O(1) push and pop at both ends — push_front, push_back, pop_front, pop_back — which is more flexibility than either stack or queue exposes on their own. That flexibility is exactly what the sliding window maximum problem needs: find the maximum of every size-k contiguous window as it slides across an array.

sliding_window_maximum.cpp
#include <vector>
#include <deque>
using namespace std;

// Returns the maximum of every contiguous window of size k.
// O(n) time, O(k) space -- each index is pushed and popped from the
// deque at most once, the same amortized argument as the monotonic stack.
vector<int> maxSlidingWindow(const vector<int>& nums, int k) {
    deque<int> dq;   // stores INDICES, nums[dq] kept in decreasing order front-to-back
    vector<int> result;

    for (int i = 0; i < (int)nums.size(); i++) {
        // drop indices that fell out of the window on the left
        if (!dq.empty() && dq.front() <= i - k) {
            dq.pop_front();
        }
        // maintain decreasing order -- anything smaller than nums[i] can
        // never be the max again while nums[i] is still in the window
        while (!dq.empty() && nums[dq.back()] < nums[i]) {
            dq.pop_back();
        }
        dq.push_back(i);

        if (i >= k - 1) {
            result.push_back(nums[dq.front()]);   // front is always the current max
        }
    }
    return result;
}

This is the monotonic stack idea applied at both ends at once: the front of the deque always holds the index of the current window's maximum, and it gets evicted from the front only once it slides outside the window, and from the back whenever a larger value arrives to make it irrelevant. Compare this to the brute-force approach of scanning all k elements for every window position, which costs O(n × k) — the deque version does the same job in O(n) total.

Don't confuse this deque with a priority_queue

A priority_queue (covered in Week 11) always gives you the true maximum of everything you've inserted, with O(log n) insertion. The monotonic deque here is cheaper — O(1) amortized per element — precisely because it only needs to answer "what's the max of the current window," and it exploits the fact that old, smaller elements become provably useless and can be discarded rather than kept around.

5. Hands-on Exercise

Hands-on

Solve "Next Greater Element II" and "Sliding Window Maximum" together

Combine this week's monotonic stack and monotonic deque patterns to handle a circular array and a windowed maximum in the same program.

Requirements:

  1. Implement vector<int> nextGreaterCircular(const vector<int>& nums) where the array is treated as circular — an element may find its next-greater by wrapping around to the start. Achieve this by conceptually iterating the array twice (index i % n) while only recording results on the first pass.
  2. State in a comment why the circular version is still O(n) time and O(n) space despite the "two passes."
  3. Implement maxSlidingWindow exactly as shown in Section 4, then also implement a brute-force maxSlidingWindowBruteForce that recomputes the max of each window from scratch.
  4. Write a randomized test that generates an array of 2,000 random integers and a random k between 1 and the array's length, and asserts both sliding-window-maximum implementations agree on every window.
  5. Time both sliding-window-maximum implementations on an array of 200,000 elements with <chrono> and print the results.
Hint

For the circular version, push and pop using i % n as the actual index but loop i from 0 to 2 * n - 1; only write into result[i % n] when i < n. Each index still enters and leaves the stack at most twice total, so the amortized O(n) bound still holds.

6. Knowledge Check

Four quick questions. Expand each to check your answer.

Q1

Why is a stack the right structure for validating balanced parentheses, rather than, say, just counting opens and closes?

Counting only checks that the total number of opens equals the total number of closes, but it can't detect wrong ordering or mismatched bracket types, like "([)]", which has equal counts but isn't balanced. A stack tracks exactly which bracket is "currently open and most recently unmatched," so each closing bracket can be checked against the correct corresponding opener via top().

Q2

The next-greater-element algorithm has a while loop nested inside a for loop. Why is its overall time complexity still O(n) and not O(n²)?

Each index is pushed onto the stack exactly once (in the for loop) and can be popped at most once (in the while loop) over the entire execution, so the total number of push and pop operations across all iterations is bounded by 2n. This amortized-analysis argument shows the real cost is O(n) total, even though any single iteration's while loop could, in isolation, run many times.

Q3

Why does breadth-first search use a queue instead of a stack?

BFS needs to fully process every node at the current distance from the start before moving on to nodes farther away, which requires processing nodes in the exact order they were discovered — FIFO order, which is what a queue guarantees. A stack would instead dive depth-first into whichever node was most recently discovered, which is DFS's behavior, not BFS's.

Q4

In the sliding window maximum algorithm, why is it safe to permanently discard an element from the back of the deque as soon as a larger element arrives?

If a new element is larger than an existing element near the back of the deque, and the new element entered the window later, then the new element will still be inside the window for at least as long as the older, smaller one — meaning the older element can never again be the maximum of any window it still belongs to while the larger one is also present. Since it can never win, keeping it around only wastes space and comparisons, so discarding it is safe.