Learn DSA Part 6: Stack and Queue - Monotonic Stack, Deque and Interview Problems

Learn DSA Part 6: Stack and Queue - CodingHumans

Stacks and queues are simple containers, but they power some of the most elegant interview solutions: matching brackets, evaluating expressions, "next greater element" problems, sliding window maximums and every breadth-first search. The real skill is recognising when a problem secretly needs one.

This is Part 6 of the Learn DSA series. We cover how stacks and queues work, which classes to use in C++ and Java, the monotonic stack and deque patterns, and the classic design questions. Code is in C++ and Java.

1. Stack: last in, first out

A stack works like a pile of plates: you add (push) and remove (pop) only at the top, so the last item in is the first out (LIFO). Push, pop and peek are all O(1).

Stacks appear whenever you need to remember something and come back to it in reverse order: the function call stack, undo history, browser back button, matching opening and closing brackets, and converting recursion into iteration (iterative DFS).

LanguageUseAvoid
C++stack<int>, or vector with push_back/pop_back-
JavaArrayDeque<Integer> with push/pop/peekThe legacy Stack class (synchronized and slower)

2. Queue and deque: first in, first out

A queue is a line at a ticket counter: add at the back (enqueue) and remove from the front (dequeue), so the first in is the first out (FIFO). Both operations are O(1). A deque (double-ended queue) allows O(1) insertion and removal at both ends.

Queues are the backbone of breadth-first search (Parts 9 and 11), task scheduling and buffering. Internally they are usually a circular array or a linked list.

NeedC++Java
Queuequeue<int>ArrayDeque with offer/poll/peek
Dequedeque<int>ArrayDeque with offerFirst/offerLast/pollFirst/pollLast
Priority queue (heap)priority_queuePriorityQueue (see Part 10)

3. Pattern 1: matching and parsing with a stack

When something must be "closed" in the reverse order it was "opened", use a stack. Push every opening bracket; on a closing bracket, the top of the stack must be its matching opener.

// Valid Parentheses: (), [], {}. O(n) time, O(n) space
bool isValid(const string& s) {
    stack<char> st;
    for (char c : s) {
        if (c == '(' || c == '[' || c == '{') { st.push(c); continue; }
        if (st.empty()) return false;
        char t = st.top(); st.pop();
        if ((c == ')' && t != '(') || (c == ']' && t != '[') ||
            (c == '}' && t != '{')) return false;
    }
    return st.empty();
}
// Valid Parentheses: (), [], {}. O(n) time, O(n) space
static boolean isValid(String s) {
    Deque<Character> st = new ArrayDeque<>();
    for (char c : s.toCharArray()) {
        if (c == '(' || c == '[' || c == '{') { st.push(c); continue; }
        if (st.isEmpty()) return false;
        char t = st.pop();
        if ((c == ')' && t != '(') || (c == ']' && t != '[') ||
            (c == '}' && t != '{')) return false;
    }
    return st.isEmpty();
}

The same idea evaluates Reverse Polish Notation (push numbers, pop two on an operator), decodes strings like 3[a2[c]], simplifies Unix paths and handles asteroid collisions.

4. Pattern 2: monotonic stack for "next greater" problems

A monotonic stack keeps its elements in increasing or decreasing order. It answers questions like "for each element, what is the next greater element to its right?" in O(n) instead of O(n^2). Keep indices of elements still waiting for an answer; when a bigger value arrives, it is the answer for every smaller value it pops.

// Next greater element to the right (-1 if none). O(n)
vector<int> nextGreater(const vector<int>& a) {
    int n = a.size();
    vector<int> ans(n, -1);
    stack<int> st;                          // indices waiting for an answer
    for (int i = 0; i < n; i++) {
        while (!st.empty() && a[st.top()] < a[i]) {
            ans[st.top()] = a[i];
            st.pop();
        }
        st.push(i);
    }
    return ans;
}
// Next greater element to the right (-1 if none). O(n)
static int[] nextGreater(int[] a) {
    int n = a.length;
    int[] ans = new int[n];
    Arrays.fill(ans, -1);
    Deque<Integer> st = new ArrayDeque<>();  // indices waiting for an answer
    for (int i = 0; i < n; i++) {
        while (!st.isEmpty() && a[st.peek()] < a[i]) ans[st.pop()] = a[i];
        st.push(i);
    }
    return ans;
}

Each index is pushed once and popped at most once, so the whole loop is O(n). Variants solve Daily Temperatures, Online Stock Span, Next Smaller Element and the hard Largest Rectangle in Histogram (where each bar's width is found from its previous and next smaller bars).

5. Pattern 3: monotonic deque for sliding window maximum

To get the maximum of every window of size k, keep a deque of indices whose values are in decreasing order. Remove from the back any index whose value is smaller than the incoming one (it can never be a maximum again), and remove from the front any index that has left the window. The front is always the current window's maximum.

// Maximum of every window of size k. O(n)
vector<int> maxSlidingWindow(const vector<int>& a, int k) {
    deque<int> dq;                          // indices, values decreasing
    vector<int> res;
    for (int i = 0; i < (int)a.size(); i++) {
        while (!dq.empty() && dq.front() <= i - k) dq.pop_front(); // left window
        while (!dq.empty() && a[dq.back()] <= a[i]) dq.pop_back(); // never max
        dq.push_back(i);
        if (i >= k - 1) res.push_back(a[dq.front()]);
    }
    return res;
}
// Maximum of every window of size k. O(n)
static int[] maxSlidingWindow(int[] a, int k) {
    Deque<Integer> dq = new ArrayDeque<>();  // indices, values decreasing
    int[] res = new int[a.length - k + 1];
    for (int i = 0; i < a.length; i++) {
        while (!dq.isEmpty() && dq.peekFirst() <= i - k) dq.pollFirst();
        while (!dq.isEmpty() && a[dq.peekLast()] <= a[i]) dq.pollLast();
        dq.offerLast(i);
        if (i >= k - 1) res[i - k + 1] = a[dq.peekFirst()];
    }
    return res;
}

This turns an O(n x k) brute force into O(n), and is a common follow-up to the sliding window techniques in Part 4.

6. Classic design questions

  • Min Stack (getMin in O(1)): push pairs of (value, minimum so far), or keep a second stack of minimums.
  • Queue using two stacks: push into an "in" stack; to dequeue, if the "out" stack is empty, move everything from "in" to "out", then pop from "out". Each element moves at most twice, so operations are amortized O(1).
  • Stack using queues: after each push, rotate the queue so the newest element is at the front. Push becomes O(n) and pop O(1).
  • Circular queue: a fixed array with head and tail indices that wrap around using modulo.
Interview tip: when a brute-force solution keeps looking back for "the previous bigger" or "the next smaller" element, a monotonic stack is almost always the intended O(n) solution.
Key takeaways
  • A stack is LIFO and a queue is FIFO; push, pop, enqueue and dequeue are all O(1).
  • In Java use ArrayDeque for both stacks and queues instead of the legacy Stack class.
  • Use a stack whenever things must be closed in reverse order: brackets, expressions, nested decoding.
  • Monotonic stacks answer next-greater and previous-smaller questions in O(n).
  • A monotonic deque gives sliding window maximums in O(n); two stacks make a queue with amortized O(1) operations.

Practice problems

These are classic LeetCode-style problems, listed from easy to hard. For each one, decide first whether you need a plain stack, a monotonic stack or a deque.

  1. Valid Parentheses (easy)
  2. Implement Queue using Stacks (easy)
  3. Next Greater Element I (easy)
  4. Min Stack (medium)
  5. Evaluate Reverse Polish Notation (medium)
  6. Daily Temperatures (medium)
  7. Online Stock Span (medium)
  8. Decode String (medium)
  9. Asteroid Collision (medium)
  10. Design Circular Queue (medium)
  11. Sliding Window Maximum (hard)
  12. Largest Rectangle in Histogram (hard)
  13. Basic Calculator (hard)

Interview questions and FAQ

What is the difference between a stack and a queue?

A stack removes the most recently added item first (LIFO); a queue removes the oldest item first (FIFO). Stacks suit backtracking and nested structures; queues suit level-by-level processing such as BFS.

Why should I use ArrayDeque instead of Stack in Java?

Stack extends the old Vector class, so every method is synchronized and slower, and it allows index-based access that breaks the stack abstraction. ArrayDeque is faster and is the class the Java documentation recommends.

Why is the monotonic stack solution O(n) when it has a nested while loop?

Every index is pushed exactly once and popped at most once. Across the whole run the inner loop pops at most n times in total, so the overall work is O(n).

How do you implement a queue using two stacks with O(1) operations?

Push to an input stack. To pop, move all elements to an output stack only when it is empty, then pop from it. Each element is moved at most once, so the cost averages to O(1) per operation (amortized).

What causes a stack overflow?

The call stack has a limited size. Very deep recursion, such as recursing over a 10^6-node linked list, can exceed it. Converting the recursion to an explicit stack or a loop avoids the problem.

Comments

Answered 0/0Correct 0