DSA DAY ONE
DSA DAY ONE
PART I — FOUNDATIONS
Chapter 1: C++ Essentials for DSA
Chapter 2: C++ STL Deep Dive
Chapter 3: Complexity Analysis
PART II — LINEAR DATA STRUCTURES
Chapter 4: Arrays & Strings
Chapter 5: Two Pointers & Sliding Window
Chapter 6: Hashing
Chapter 7: Linked Lists
Chapter 8: Stacks
Stack Fundamentals & STL `stack`Implementing a Stack from ScratchMonotonic Stack — The PatternNext Greater, Smaller & Previous Element PatternsExpression Evaluation & ParenthesesStack-Based Iterative Tree & Graph TraversalSample ProblemsLeetCode Problem Set
Chapter 9: Queues & Deques
PART III — TREES
Chapter 10: Binary Trees
Chapter 11: Binary Search Trees (BST)
Chapter 12: Heaps & Priority Queues
Chapter 13: Tries (Prefix Trees)
Chapter 14: Segment Tree & Fenwick Tree
PART IV — ALGORITHMS
Chapter 15: Sorting Algorithms
Chapter 16: Binary Search
Chapter 17: Recursion & Backtracking
Chapter 18: Graphs — Fundamentals & Traversals
Chapter 19: Graph Algorithms — Shortest Paths, MST & Union-Find
Chapter 20: Dynamic Programming
Chapter 21: Greedy Algorithms
Chapter 22: Divide & Conquer
PART V — COMPETITIVE & MATH
Chapter 23: Bit Manipulation
Chapter 24: Math & Number Theory
PART VI — PATTERNS
Chapter 25: Pattern Recognition Mastery
PART VII — SYSTEM DESIGN
Chapter 26: System Design for FAANG Interviews
PART VIII — INTERVIEW STRATEGY
Chapter 27: Cracking FAANG — Strategy & Communication
08

Stacks


Chapter 8 ·  Part 2: LINEAR DATA STRUCTURES ·  Est. 27 min read

Chapter Goal: Achieve mastery over the stack data structure and every pattern that uses it in interviews. The monotonic stack alone unlocks solutions to an entire category of problems that have no intuitive brute-force alternative. Expression evaluation, iterative tree traversal, and simulation problems round out the picture.


8.1 Stack Fundamentals & STL stack

The LIFO Principle

A stack is a Last-In, First-Out (LIFO) container. The element most recently pushed is the first to be removed. Think of a stack of plates — you always add and remove from the top.

Push 1, Push 2, Push 3:
        [3]  ← top
        [2]
        [1]
        ---
Pop  → 3, then 2, then 1

STL stack — Interface

std::stack is an adapter — it wraps a sequence container (default: deque) and restricts access to LIFO operations only.

#include <stack>
 
stack<int> st;
 
// --- Modify ---
st.push(10);        // Push element onto top
st.push(20);
st.push(30);
st.emplace(40);     // Construct in-place (prefer over push for complex types)
st.pop();           // Remove top element (returns void — save top() first!)
 
// --- Access ---
cout << st.top();   // 30 — peek at top without removing (O(1))
cout << st.size();  // 3
cout << st.empty(); // false
 
// Warning: Always check empty() before calling top() or pop()!
while (!st.empty()) {
    cout << st.top() << " ";
    st.pop();
}
// Output: 30 20 10
 
// --- Underlying container options ---
stack<int>                  default_st;     // Uses deque (default)
stack<int, vector<int>>     vector_st;      // Uses vector (faster in practice)
stack<int, list<int>>       list_st;        // Uses list (rarely useful)

Complexity of All Stack Operations

Operation Time Notes
push / emplace O(1) amortized Depends on underlying container
pop O(1) Does NOT return the element
top O(1) Peek without removing
size O(1)
empty O(1)

When to Reach for a Stack

Does the problem involve...
  Matching open/close brackets?              → Stack
  Undoing/backtracking recent decisions?     → Stack
  Processing nested structures?             → Stack
  Converting recursion to iteration?        → Stack (simulate call stack)
  Finding next/previous greater/smaller?    → Monotonic Stack
  Evaluating expressions?                   → Stack(s)

8.2 Implementing a Stack from Scratch

Interviewers sometimes ask you to build a stack without STL.

Array-Based Stack (Fixed Capacity)

class StackArray {
    vector<int> data;
    int topIdx;
    int capacity;
 
public:
    StackArray(int cap) : topIdx(-1), capacity(cap), data(cap) {}
 
    void push(int val) {
        if (topIdx == capacity - 1) throw overflow_error("Stack overflow");
        data[++topIdx] = val;
    }
    void pop() {
        if (empty()) throw underflow_error("Stack underflow");
        topIdx--;
    }
    int top() const {
        if (empty()) throw underflow_error("Empty stack");
        return data[topIdx];
    }
    bool empty() const { return topIdx == -1; }
    int size()   const { return topIdx + 1; }
};
// All operations: O(1). No memory allocation after construction.

Dynamic Stack Using vector

class StackDynamic {
    vector<int> data;
public:
    void push(int val)  { data.push_back(val); }
    void pop()          { if (!empty()) data.pop_back(); }
    int  top()    const { return data.back(); }
    bool empty()  const { return data.empty(); }
    int  size()   const { return data.size(); }
};
// All operations: O(1) amortized (vector doubling for push_back).

Linked List-Based Stack (O(1) Guaranteed)

class StackList {
    struct Node { int val; Node* next; Node(int v, Node* n) : val(v), next(n) {} };
    Node* head = nullptr;
    int sz = 0;
public:
    void push(int val)  { head = new Node(val, head); sz++; }
    void pop()          { if (!head) return; Node* tmp = head; head = head->next; delete tmp; sz--; }
    int  top()    const { return head->val; }
    bool empty()  const { return head == nullptr; }
    int  size()   const { return sz; }
};
// All operations: O(1) guaranteed (no amortized). Slightly more memory (pointer overhead).

8.3 Monotonic Stack — The Pattern

What Is a Monotonic Stack?

A monotonic stack is a stack where elements are always kept in either monotonically increasing or monotonically decreasing order from bottom to top. The key operation: before pushing an element, pop all elements that violate the monotonicity condition. Each popped element has "found its answer" from the element that caused the pop.

Monotonically Decreasing Stack (bottom to top: large to small):
  Violating condition: current >= top  → pop
  Use for: finding NEXT GREATER element

Monotonically Increasing Stack (bottom to top: small to large):
  Violating condition: current <= top  → pop
  Use for: finding NEXT SMALLER element

Why This Works — The Amortized Argument

Each element is pushed exactly once and popped at most once. Over n elements, the total number of push + pop operations is at most 2n. So the entire algorithm is O(n) — not O(n²) as the nested loops might suggest.

// Naive: O(n²) — for each element, scan right for next greater
for (int i = 0; i < n; i++)
    for (int j = i+1; j < n; j++)
        if (arr[j] > arr[i]) { nge[i] = arr[j]; break; }
 
// Monotonic stack: O(n)
stack<int> st;  // stores indices
for (int i = 0; i < n; i++) {
    while (!st.empty() && arr[st.top()] < arr[i]) {
        nge[st.top()] = arr[i];   // arr[i] is the NGE for the popped element
        st.pop();
    }
    st.push(i);
}
// Remaining elements in stack have no NGE → default value (e.g., -1)

The Universal Monotonic Stack Template

// ─── Template: Find answer for each element when condition triggers ───────────
vector<int> result(n, DEFAULT_VALUE);
stack<int> st;  // Always store INDICES, not values (indices give more info)
 
for (int i = 0; i < n; i++) {
 
    // Pop phase: while stack is not empty AND monotonicity is violated
    while (!st.empty() && CONDITION(arr[st.top()], arr[i])) {
        int idx = st.top(); st.pop();
        result[idx] = ANSWER(arr[i], arr[idx]);   // Current element answers the popped one
    }
 
    // Push phase: current element enters the stack
    st.push(i);
}
// Note: any indices remaining in st when the loop ends have no answer found

8.4 Next Greater, Smaller & Previous Element Patterns

All Four Variants — Complete Reference

Problem                  | Stack Type   | Pop When         | Answer for popped
─────────────────────────┼──────────────┼──────────────────┼───────────────────
Next Greater Element     | Decreasing   | arr[curr] > top  | arr[curr]
Next Smaller Element     | Increasing   | arr[curr] < top  | arr[curr]
Previous Greater Element | Decreasing   | arr[curr] < top  | top (before pop*)
Previous Smaller Element | Increasing   | arr[curr] > top  | top (before pop*)

* For Previous patterns: the answer is whatever is left in the stack AFTER popping
  — i.e., the new top after the while loop (before pushing curr).

Next Greater Element (NGE)

vector<int> nextGreater(vector<int>& arr) {
    int n = arr.size();
    vector<int> nge(n, -1);   // Default: no NGE
    stack<int> st;             // Monotonically decreasing (indices)
 
    for (int i = 0; i < n; i++) {
        // Pop all elements smaller than current — their NGE is arr[i]
        while (!st.empty() && arr[st.top()] < arr[i]) {
            nge[st.top()] = arr[i];
            st.pop();
        }
        st.push(i);
    }
    return nge;
}
// Time: O(n), Space: O(n)
 
/*
  arr = [2, 1, 2, 4, 3]
  i=0: push 0.  st=[0]
  i=1: arr[1]=1 < arr[0]=2, no pop. push 1.  st=[0,1]
  i=2: arr[2]=2 > arr[1]=1 → nge[1]=2, pop. arr[0]=2 not < 2, stop. push 2. st=[0,2]
  i=3: arr[3]=4 > arr[2]=2 → nge[2]=4, pop. arr[0]=2 < 4 → nge[0]=4, pop. push 3. st=[3]
  i=4: arr[4]=3 < arr[3]=4, push. st=[3,4]
  Remaining in stack: nge[3]=nge[4]=-1
  Result: [4, 2, 4, -1, -1]
*/

Next Smaller Element (NSE)

vector<int> nextSmaller(vector<int>& arr) {
    int n = arr.size();
    vector<int> nse(n, -1);
    stack<int> st;   // Monotonically increasing
 
    for (int i = 0; i < n; i++) {
        // Pop all elements GREATER than current — their NSE is arr[i]
        while (!st.empty() && arr[st.top()] > arr[i]) {
            nse[st.top()] = arr[i];
            st.pop();
        }
        st.push(i);
    }
    return nse;
}
// Time: O(n), Space: O(n)

Previous Greater Element (PGE)

vector<int> prevGreater(vector<int>& arr) {
    int n = arr.size();
    vector<int> pge(n, -1);
    stack<int> st;   // Monotonically decreasing
 
    for (int i = 0; i < n; i++) {
        // Pop all elements SMALLER OR EQUAL to current
        while (!st.empty() && arr[st.top()] <= arr[i]) st.pop();
 
        // After popping, top of stack is PGE for current element
        if (!st.empty()) pge[i] = arr[st.top()];
 
        st.push(i);
    }
    return pge;
}

Previous Smaller Element (PSE)

vector<int> prevSmaller(vector<int>& arr) {
    int n = arr.size();
    vector<int> pse(n, -1);
    stack<int> st;   // Monotonically increasing
 
    for (int i = 0; i < n; i++) {
        // Pop all elements GREATER OR EQUAL to current
        while (!st.empty() && arr[st.top()] >= arr[i]) st.pop();
 
        // Remaining top is PSE for current element
        if (!st.empty()) pse[i] = arr[st.top()];
 
        st.push(i);
    }
    return pse;
}

Circular Array Variant

For circular arrays (where we wrap around), process the array twice and use modular indexing.

vector<int> nextGreaterCircular(vector<int>& nums) {
    int n = nums.size();
    vector<int> result(n, -1);
    stack<int> st;
 
    for (int i = 0; i < 2 * n; i++) {
        int idx = i % n;
        while (!st.empty() && nums[st.top()] < nums[idx]) {
            result[st.top()] = nums[idx];
            st.pop();
        }
        if (i < n) st.push(idx);  // Only push indices in first pass
    }
    return result;
}
// Time: O(n), Space: O(n)

Application: Largest Rectangle in Histogram — Building on NSE & PSE

For each bar i, the largest rectangle using bar i as the shortest bar has:

  • Width = (next_smaller_right[i] - prev_smaller_left[i] - 1)
  • Height = heights[i]
int largestRectangleArea(vector<int>& heights) {
    int n = heights.size();
 
    // Compute left boundary: index of Previous Smaller Element (or -1)
    vector<int> left(n), right(n);
    stack<int> st;
 
    for (int i = 0; i < n; i++) {
        while (!st.empty() && heights[st.top()] >= heights[i]) st.pop();
        left[i] = st.empty() ? -1 : st.top();
        st.push(i);
    }
 
    while (!st.empty()) st.pop();
 
    // Compute right boundary: index of Next Smaller Element (or n)
    for (int i = n-1; i >= 0; i--) {
        while (!st.empty() && heights[st.top()] >= heights[i]) st.pop();
        right[i] = st.empty() ? n : st.top();
        st.push(i);
    }
 
    // For each bar: area = height * width
    int maxArea = 0;
    for (int i = 0; i < n; i++) {
        int width = right[i] - left[i] - 1;
        maxArea = max(maxArea, heights[i] * width);
    }
    return maxArea;
}
// Time: O(n), Space: O(n)
 
/*
  heights = [2, 1, 5, 6, 2, 3]
  left  (PSE index): [-1, -1, 1, 2,  1,  4]
  right (NSE index): [ 1,  6, 4, 4,  6,  6]
  widths:            [  1,  5, 2, 1,  4,  1]
  areas:             [  2,  5,10, 6,  8,  3]
  maxArea = 10
*/
 
// One-pass solution using a single monotonic stack
int largestRectangleArea_onePass(vector<int>& heights) {
    int n = heights.size();
    stack<int> st;
    int maxArea = 0;
 
    for (int i = 0; i <= n; i++) {
        int h = (i == n) ? 0 : heights[i];  // Sentinel 0 flushes the stack
        while (!st.empty() && heights[st.top()] > h) {
            int height = heights[st.top()]; st.pop();
            int width  = st.empty() ? i : i - st.top() - 1;
            maxArea = max(maxArea, height * width);
        }
        st.push(i);
    }
    return maxArea;
}
// Time: O(n), Space: O(n)

8.5 Expression Evaluation & Parentheses

Pattern 1: Valid Parentheses (Balanced Brackets)

bool isValid(string s) {
    stack<char> st;
    for (char c : s) {
        if (c == '(' || c == '[' || c == '{') {
            st.push(c);
        } else {
            if (st.empty()) return false;
            char top = st.top(); st.pop();
            if (c == ')' && top != '(') return false;
            if (c == ']' && top != '[') return false;
            if (c == '}' && top != '{') return false;
        }
    }
    return st.empty();  // All openers were matched
}
// Time: O(n), Space: O(n)

Pattern 2: Evaluate Reverse Polish Notation (Postfix)

In RPN (postfix notation), operators come after their operands. No need for parentheses — precedence is explicit in ordering.

"2 1 + 3 *"  →  (2+1)*3 = 9
"4 13 5 / +" →  4 + (13/5) = 4+2 = 6
int evalRPN(vector<string>& tokens) {
    stack<long long> st;
 
    for (const string& tok : tokens) {
        if (tok == "+" || tok == "-" || tok == "*" || tok == "/") {
            long long b = st.top(); st.pop();  // Right operand (popped second)
            long long a = st.top(); st.pop();  // Left operand (popped first)
            if      (tok == "+") st.push(a + b);
            else if (tok == "-") st.push(a - b);
            else if (tok == "*") st.push(a * b);
            else                 st.push(a / b);  // Truncates toward zero in C++
        } else {
            st.push(stoll(tok));   // Operand: push as number
        }
    }
    return st.top();
}
// Time: O(n), Space: O(n)

Pattern 3: Basic Calculator II (Infix with +, -, *, /)

Handle operator precedence (* and / before + and -) without parentheses.

Key insight: Process * and / immediately (compute running term). Defer

  • and - by pushing signed terms onto a stack, then sum at end.
int calculate(string s) {
    stack<int> st;
    int num = 0;
    char op = '+';   // Treat the first number as preceded by '+'
 
    for (int i = 0; i < (int)s.size(); i++) {
        char c = s[i];
 
        if (isdigit(c)) {
            num = num * 10 + (c - '0');   // Build multi-digit number
        }
 
        // Process when we hit an operator OR reach end of string
        if ((!isdigit(c) && c != ' ') || i == (int)s.size() - 1) {
            if      (op == '+') st.push(num);
            else if (op == '-') st.push(-num);
            else if (op == '*') { int top = st.top(); st.pop(); st.push(top * num); }
            else if (op == '/') { int top = st.top(); st.pop(); st.push(top / num); }
            op = c;    // Update operator for next number
            num = 0;   // Reset number
        }
    }
 
    int result = 0;
    while (!st.empty()) { result += st.top(); st.pop(); }
    return result;
}
// Time: O(n), Space: O(n)
 
/*
  s = "3+2*2"
  Process 3 with op='+': push 3.  st=[3]
  Process 2 with op='*': push 3,2 but * so pop 3... wait let's trace op changes:
 
  i=0: c='3', num=3
  i=1: c='+', op was '+', push(3), op='+', num=0. stack=[3]
  i=2: c='2', num=2
  i=3: c='*', op='+', push(2),    op='*', num=0. stack=[3,2]  <- wrong!
 
  Actually re-trace with correct op tracking:
  Initially op='+', num=0.
  c='3': num=3
  c='+': op was '+', push(3). op='+'. num=0. st=[3]
  c='2': num=2
  c='*': op was '+', push(2). op='*'. num=0. st=[3,2]
  c='2': num=2  (end of string too)
  i=last: op='*', pop 2, push 2*2=4. st=[3,4]
  sum = 7. ✅
*/

Pattern 4: Decode String (LC 394) — Nested Structures

Use two stacks: one for repeat counts, one for partial strings.

string decodeString(string s) {
    stack<int>    countSt;    // Stack of repeat counts
    stack<string> strSt;      // Stack of partial strings
    string curr = "";
    int num = 0;
 
    for (char c : s) {
        if (isdigit(c)) {
            num = num * 10 + (c - '0');   // Handle multi-digit counts
        } else if (c == '[') {
            countSt.push(num);    // Save current count
            strSt.push(curr);     // Save current string
            num  = 0;             // Reset for next number
            curr = "";            // Reset for inner content
        } else if (c == ']') {
            int k    = countSt.top(); countSt.pop();
            string inner = curr;
            curr = strSt.top();   strSt.pop();   // Restore outer string
            for (int i = 0; i < k; i++) curr += inner;  // Repeat inner k times
        } else {
            curr += c;
        }
    }
    return curr;
}
// Time: O(n * max_k) where max_k is maximum repeat count
// Space: O(n)
 
/*
  s = "3[a2[c]]"
  c='3': num=3
  c='[': push count=3, push curr="", reset. countSt=[3], strSt=[""]
  c='a': curr="a"
  c='2': num=2
  c='[': push count=2, push curr="a", reset. countSt=[3,2], strSt=["","a"]
  c='c': curr="c"
  c=']': k=2, inner="c", curr="a"+"cc"="acc". countSt=[3], strSt=[""]
  c=']': k=3, inner="acc", curr=""+"accaccacc"="accaccacc". Done.
  Result: "accaccacc" ✅
*/

Pattern 5: Longest Valid Parentheses (LC 32)

int longestValidParentheses(string s) {
    stack<int> st;
    st.push(-1);    // Sentinel: base for length calculation
 
    int maxLen = 0;
    for (int i = 0; i < (int)s.size(); i++) {
        if (s[i] == '(') {
            st.push(i);   // Push index of '('
        } else {
            st.pop();     // Match with top (or remove the sentinel)
            if (st.empty()) {
                st.push(i);   // New base: unmatched ')' at i
            } else {
                maxLen = max(maxLen, i - st.top());
            }
        }
    }
    return maxLen;
}
// Time: O(n), Space: O(n)
 
/*
  s = "()(()"
  stack initially: [-1]
  i=0 '(': push 0.  st=[-1,0]
  i=1 ')': pop 0.   st=[-1]. len = 1-(-1) = 2
  i=2 '(': push 2.  st=[-1,2]
  i=3 '(': push 3.  st=[-1,2,3]
  i=4 ')': pop 3.   st=[-1,2]. len = 4-2 = 2
  maxLen = 2
*/

8.6 Stack-Based Iterative Tree & Graph Traversal

Recursive tree/graph traversal uses the call stack implicitly. Converting to iterative requires managing that stack explicitly.

Iterative Inorder Traversal (Left → Root → Right)

vector<int> inorderTraversal(TreeNode* root) {
    vector<int> result;
    stack<TreeNode*> st;
    TreeNode* curr = root;
 
    while (curr || !st.empty()) {
        // Go as far left as possible
        while (curr) {
            st.push(curr);
            curr = curr->left;
        }
        // Process: pop, visit, go right
        curr = st.top(); st.pop();
        result.push_back(curr->val);
        curr = curr->right;
    }
    return result;
}
// Time: O(n), Space: O(h) where h = height

Iterative Preorder Traversal (Root → Left → Right)

vector<int> preorderTraversal(TreeNode* root) {
    if (!root) return {};
    vector<int> result;
    stack<TreeNode*> st;
    st.push(root);
 
    while (!st.empty()) {
        TreeNode* node = st.top(); st.pop();
        result.push_back(node->val);   // Visit root first
        if (node->right) st.push(node->right);  // Push right before left
        if (node->left)  st.push(node->left);   // Left is processed first (LIFO)
    }
    return result;
}
// Time: O(n), Space: O(h)

Iterative Postorder Traversal (Left → Right → Root)

// Method: Reverse of (Root → Right → Left) preorder
// Compute Root→Right→Left using a stack, then reverse the result.
vector<int> postorderTraversal(TreeNode* root) {
    if (!root) return {};
    vector<int> result;
    stack<TreeNode*> st;
    st.push(root);
 
    while (!st.empty()) {
        TreeNode* node = st.top(); st.pop();
        result.push_back(node->val);
        if (node->left)  st.push(node->left);   // Push left first (processed second)
        if (node->right) st.push(node->right);  // Push right second (processed first)
    }
    reverse(result.begin(), result.end());  // Reverse: Root→Right→Left → Left→Right→Root
    return result;
}
// Time: O(n), Space: O(h)

Iterative DFS on a Graph

void dfsIterative(int start, int n, vector<vector<int>>& adj) {
    vector<bool> visited(n, false);
    stack<int> st;
 
    st.push(start);
    while (!st.empty()) {
        int node = st.top(); st.pop();
        if (visited[node]) continue;   // Skip if already visited
        visited[node] = true;
        cout << node << " ";
 
        // Push neighbors (note: order differs from recursive DFS)
        for (int neighbor : adj[node]) {
            if (!visited[neighbor]) st.push(neighbor);
        }
    }
}
// Time: O(V + E), Space: O(V)
// Note: iterative DFS may visit nodes in different order than recursive DFS
//       due to the difference between stack push order and recursion order.

8.7 Sample Problems


Problem 1 — Valid Parentheses (Easy) — LC 20

bool isValid(string s) {
    stack<char> st;
    for (char c : s) {
        if (c=='(' || c=='[' || c=='{') { st.push(c); continue; }
        if (st.empty()) return false;
        char top = st.top(); st.pop();
        if (c==')' && top!='(') return false;
        if (c==']' && top!='[') return false;
        if (c=='}' && top!='{') return false;
    }
    return st.empty();
}
// Time: O(n), Space: O(n)

Problem 2 — Min Stack (Medium) — LC 155

Design a stack that supports push, pop, top, and retrieving the minimum element in constant time.

Key insight: Maintain a second "min stack" that tracks the minimum at each level of the main stack.

class MinStack {
    stack<int> st;
    stack<int> minSt;   // Parallel stack: minSt.top() = current minimum
 
public:
    void push(int val) {
        st.push(val);
        // Push to minSt: smaller of new val and current min
        minSt.push(minSt.empty() ? val : min(val, minSt.top()));
    }
    void pop() {
        st.pop();
        minSt.pop();
    }
    int top()    { return st.top(); }
    int getMin() { return minSt.top(); }   // Always O(1)
};
 
/*
  push(5):  st=[5],    minSt=[5]
  push(3):  st=[5,3],  minSt=[5,3]
  push(7):  st=[5,3,7],minSt=[5,3,3]   <- min is still 3
  getMin()  = 3
  pop():    st=[5,3],  minSt=[5,3]
  getMin()  = 3
  pop():    st=[5],    minSt=[5]
  getMin()  = 5
*/
// All operations: O(1). Space: O(n) for both stacks.

Problem 3 — Daily Temperatures (Medium) — LC 739

For each day, find the number of days until a warmer temperature.

vector<int> dailyTemperatures(vector<int>& T) {
    int n = T.size();
    vector<int> answer(n, 0);
    stack<int> st;   // Monotonically decreasing stack of indices
 
    for (int i = 0; i < n; i++) {
        while (!st.empty() && T[st.top()] < T[i]) {
            answer[st.top()] = i - st.top();   // Days to wait = i - popped index
            st.pop();
        }
        st.push(i);
    }
    return answer;   // Unprocessed indices keep their default 0
}
// Time: O(n), Space: O(n)
 
/*
  T = [73,74,75,71,69,72,76,73]
  i=0: push 0. st=[0]
  i=1: T[1]=74>T[0]=73 → ans[0]=1-0=1, pop. push 1. st=[1]
  i=2: T[2]=75>T[1]=74 → ans[1]=2-1=1, pop. push 2. st=[2]
  i=3: T[3]=71<T[2]=75, push 3. st=[2,3]
  i=4: T[4]=69<T[3]=71, push 4. st=[2,3,4]
  i=5: T[5]=72>T[4]=69 → ans[4]=5-4=1, pop.
       T[5]=72>T[3]=71 → ans[3]=5-3=2, pop. push 5. st=[2,5]
  i=6: T[6]=76>T[5]=72 → ans[5]=6-5=1, pop.
       T[6]=76>T[2]=75 → ans[2]=6-2=4, pop. push 6. st=[6]
  i=7: T[7]=73<T[6]=76, push 7. st=[6,7]
  Remaining: ans[6]=0, ans[7]=0
  Result: [1,1,4,2,1,1,0,0]
*/

Problem 4 — Next Greater Element I (Easy) — LC 496

Given two arrays where nums1 is a subset of nums2, for each element in nums1 find its next greater element in nums2.

vector<int> nextGreaterElement(vector<int>& nums1, vector<int>& nums2) {
    // Build NGE map for nums2 using monotonic stack
    unordered_map<int,int> nge;
    stack<int> st;   // Decreasing stack of values
 
    for (int x : nums2) {
        while (!st.empty() && st.top() < x) {
            nge[st.top()] = x;
            st.pop();
        }
        st.push(x);
    }
    // Remaining in stack have no NGE — map will return 0 (use count check)
 
    vector<int> result;
    for (int x : nums1) {
        result.push_back(nge.count(x) ? nge[x] : -1);
    }
    return result;
}
// Time: O(n + m), Space: O(n)

Problem 5 — Remove K Digits (Medium) — LC 402

Remove k digits from a number string to make the smallest possible number.

Key insight: Use a monotonically increasing stack (keep digits small from left to right). Whenever a new digit is smaller than the stack top, pop (remove the larger digit from the result).

string removeKdigits(string num, int k) {
    string result;   // Acts as our stack (string for easy substring)
 
    for (char c : num) {
        // While we can still remove (k > 0) and top is larger, remove it
        while (k > 0 && !result.empty() && result.back() > c) {
            result.pop_back();
            k--;
        }
        result.push_back(c);
    }
 
    // If k > 0 remaining, remove from the right (largest digits are there now)
    while (k-- > 0) result.pop_back();
 
    // Remove leading zeros
    int start = result.find_first_not_of('0');
    result = (start == string::npos) ? "0" : result.substr(start);
 
    return result;
}
// Time: O(n), Space: O(n)
 
/*
  num = "1432219", k = 3
  c='1': result="1"
  c='4': 4>1, push.  result="14"
  c='3': 3<4, pop(4) k=2, 3>1, push. result="13"
  c='2': 2<3, pop(3) k=1, 2>1, push. result="12"
  c='2': 2>=2, push. result="122"
  c='1': 1<2, pop(2) k=0. push. result="121" (k=0, stop popping)
  c='9': k=0, just push. result="1219"
  Trim leading zeros: "1219"
  Result: "1219"
*/

Problem 6 — Evaluate Reverse Polish Notation (Medium) — LC 150

int evalRPN(vector<string>& tokens) {
    stack<long long> st;
    for (const string& t : tokens) {
        if (t=="+"||t=="-"||t=="*"||t=="/") {
            long long b=st.top(); st.pop();
            long long a=st.top(); st.pop();
            if (t=="+") st.push(a+b);
            else if (t=="-") st.push(a-b);
            else if (t=="*") st.push(a*b);
            else             st.push(a/b);
        } else {
            st.push(stoll(t));
        }
    }
    return (int)st.top();
}
// Time: O(n), Space: O(n)

Problem 7 — Decode String (Medium) — LC 394

string decodeString(string s) {
    stack<int>    countSt;
    stack<string> strSt;
    string curr = "";
    int num = 0;
 
    for (char c : s) {
        if (isdigit(c))  { num = num*10 + (c-'0'); }
        else if (c=='[') { countSt.push(num); strSt.push(curr); num=0; curr=""; }
        else if (c==']') {
            int k = countSt.top(); countSt.pop();
            string inner = curr;
            curr = strSt.top(); strSt.pop();
            for (int i=0; i<k; i++) curr += inner;
        } else { curr += c; }
    }
    return curr;
}
// Time: O(n * max_k), Space: O(n)

Problem 8 — Asteroid Collision (Medium) — LC 735

Asteroids moving right are positive, left are negative. On collision, the smaller one explodes. Same size: both explode. Find the state after all collisions.

vector<int> asteroidCollision(vector<int>& asteroids) {
    stack<int> st;
 
    for (int a : asteroids) {
        bool destroyed = false;
 
        // Collision: a moves left (a<0) and top moves right (st.top()>0)
        while (!st.empty() && a < 0 && st.top() > 0) {
            if (st.top() < -a) {
                st.pop();        // Stack top destroyed, continue checking
                continue;
            } else if (st.top() == -a) {
                st.pop();        // Both destroyed
            }
            destroyed = true;    // Current asteroid destroyed
            break;
        }
        if (!destroyed) st.push(a);
    }
 
    vector<int> result;
    while (!st.empty()) { result.push_back(st.top()); st.pop(); }
    reverse(result.begin(), result.end());
    return result;
}
// Time: O(n), Space: O(n)
 
/*
  asteroids = [5, 10, -5]
  a=5:   st=[5]   (moving right, no collision)
  a=10:  st=[5,10] (moving right)
  a=-5:  -5 collides with 10. |10| > |-5|, -5 destroyed. st=[5,10]
  Result: [5, 10]
*/

Problem 9 — Car Fleet (Medium) — LC 853

n cars drive toward a target. Each car has a position and speed. Cars at the same position can merge into a fleet. Count the number of fleets.

Key insight: Sort by position (descending). A car forms a new fleet if its arrival time is strictly greater than the car ahead (it won't catch up).

int carFleet(int target, vector<int>& position, vector<int>& speed) {
    int n = position.size();
    vector<pair<int,double>> cars(n);
 
    for (int i = 0; i < n; i++)
        cars[i] = {position[i], (double)(target - position[i]) / speed[i]};
 
    sort(cars.rbegin(), cars.rend());  // Sort by position descending (closest to target first)
 
    stack<double> st;   // Stack of arrival times
    for (auto& [pos, time] : cars) {
        // If this car arrives LATER than the current fleet leader, it's a new fleet
        if (st.empty() || time > st.top()) {
            st.push(time);
        }
        // Otherwise, it catches up and merges (don't push — it joins the fleet ahead)
    }
    return st.size();
}
// Time: O(n log n), Space: O(n)

Problem 10 — Largest Rectangle in Histogram (Hard) — LC 84

int largestRectangleArea(vector<int>& heights) {
    int n = heights.size();
    stack<int> st;
    int maxArea = 0;
 
    for (int i = 0; i <= n; i++) {
        int h = (i == n) ? 0 : heights[i];   // Sentinel 0 flushes remaining
        while (!st.empty() && heights[st.top()] > h) {
            int height = heights[st.top()]; st.pop();
            int width  = st.empty() ? i : i - st.top() - 1;
            maxArea = max(maxArea, height * width);
        }
        st.push(i);
    }
    return maxArea;
}
// Time: O(n), Space: O(n)

Problem 11 — Longest Valid Parentheses (Hard) — LC 32

int longestValidParentheses(string s) {
    stack<int> st;
    st.push(-1);
    int maxLen = 0;
 
    for (int i = 0; i < (int)s.size(); i++) {
        if (s[i] == '(') {
            st.push(i);
        } else {
            st.pop();
            if (st.empty()) st.push(i);   // Unmatched ')' becomes new base
            else maxLen = max(maxLen, i - st.top());
        }
    }
    return maxLen;
}
// Time: O(n), Space: O(n)

Problem 12 — Maximum Width Ramp (Medium) — LC 962

Find the maximum width j - i such that i < j and nums[i] <= nums[j].

Key insight: Build a decreasing stack of candidate left endpoints. Then scan from right: for each j, pop stack while the condition is met, recording the max width.

int maxWidthRamp(vector<int>& nums) {
    int n = nums.size();
    stack<int> st;
 
    // Build decreasing stack: potential left endpoints
    for (int i = 0; i < n; i++) {
        if (st.empty() || nums[i] < nums[st.top()]) {
            st.push(i);
        }
    }
 
    int maxWidth = 0;
    // Scan from right: greedily match with furthest left endpoint
    for (int j = n - 1; j >= 0; j--) {
        while (!st.empty() && nums[st.top()] <= nums[j]) {
            maxWidth = max(maxWidth, j - st.top());
            st.pop();
        }
    }
    return maxWidth;
}
// Time: O(n), Space: O(n)

8.8 LeetCode Problem Set

# Problem Difficulty Core Technique Link
1 Valid Parentheses Easy Stack match LC 20
2 Next Greater Element I Easy Monotonic stack + hash map LC 496
3 Daily Temperatures Medium Monotonic stack (days gap) LC 739
4 Evaluate Reverse Polish Notation Medium Operand/operator stack LC 150
5 Remove K Digits Medium Monotonic increasing stack LC 402
6 Decode String Medium Two-stack nested structure LC 394
7 Largest Rectangle in Histogram Hard Monotonic stack (NSE + PSE) LC 84
8 Longest Valid Parentheses Hard Stack of indices + sentinel LC 32

Suggested order: #1 and #2 are essential warm-ups. #3 (Daily Temperatures) is the canonical monotonic stack problem — solve it until it feels obvious. Then #4 and #6 (expression/nested). #5 (Remove K Digits) teaches the "greedy pop" idea. #7 (Histogram) is one of the hardest stack problems — essential for FAANG and builds on everything in Section 8.4.


← PreviousChapter 7: Linked ListsNext →Chapter 9: Queues & Deques
Chapter 8 — Progress
0 / 29 COMPLETE
Fundamentals
  • Know all five STL stack operations (push, pop, top, size, empty) with complexity
  • Always check `empty()` before calling `top()` or `pop()`
  • Implement a stack using a `vector` from scratch (3 lines of code)
  • Know the default underlying container is `deque`; prefer `stack<T, vector<T>>`
Monotonic Stack
  • State the amortized O(n) argument: each element pushed and popped at most once
  • Write the monotonic stack template from memory (indices, not values)
  • Implement NGE (Next Greater Element) from memory
  • Implement NSE (Next Smaller Element) from memory
  • Know the direction: decreasing stack for NGE, increasing for NSE
  • Implement the circular array variant (process 2n indices, push only first n)
NGE/NSE/PGE/PSE Decision Table
  • State which stack direction and pop condition each of the four variants uses
  • Know: for Previous patterns, the answer is the stack top AFTER popping (before pushing)
  • Implement Largest Rectangle in Histogram using PSE + NSE arrays
  • Implement Largest Rectangle in Histogram using one-pass with sentinel 0
Expression Evaluation
  • Implement Valid Parentheses with three bracket types
  • Implement Evaluate RPN: pop two operands, apply operator, push result
  • Know: b = st.top() is popped FIRST (right operand), then a (left operand)
  • Implement Basic Calculator II: push signed terms for +/-, compute immediately for */
  • Implement Decode String using two stacks (counts and partial strings)
  • Implement Longest Valid Parentheses using stack of indices with sentinel -1
Iterative Tree/Graph Traversal
  • Implement iterative inorder: go-left-then-pop-visit-go-right loop
  • Implement iterative preorder: push root, pop-visit-push-right-push-left
  • Implement iterative postorder: preorder variant reversed
  • Implement iterative DFS: push start, pop-check-visit-push-neighbors
Problem Patterns to Recognize
  • "Find next greater/smaller in array" → monotonic stack, O(n)
  • "Remove digits/elements to minimize/maximize" → greedy monotonic stack
  • "Evaluate expression string" → operator/operand stack(s)
  • "Nested structure with brackets and counts" → two-stack decode pattern
  • "Simulate recursion iteratively" → explicit stack
Notes▸