🗂️Data StructureIntermediate

Stack

Key Points

  • A stack is a Last-In, First-Out (LIFO) data structure where push, pop, and top operations run in O(1) time.
  • Stacks are perfect for problems that require reversing order or matching pairs, like balanced parentheses and undo/redo.
  • Expression parsing uses stacks to convert infix to postfix (RPN) and to evaluate the postfix efficiently in O(n) time.
  • Depth-First Search (DFS) can be implemented iteratively with a stack to avoid recursion limits and control traversal order.
  • Monotonic stacks solve Next Greater/Smaller Element problems in O(n) by ensuring each element is pushed and popped at most once.
  • The Largest Rectangle in Histogram and Stock Span problems both have elegant O(n) solutions using a monotonic stack.
  • In C++, std::stack provides push, pop, top, empty, and size, typically backed by deque for O(1) operations.
  • Common pitfalls include popping an empty stack, not handling operator precedence/associativity, and integer overflow in area calculations.

Prerequisites

  • Arrays and VectorsStacks are often implemented over arrays/deques and many algorithms process arrays with a stack.
  • Time Complexity (Big-O)Understanding O(1), O(n), and amortized reasoning clarifies why stack-based solutions are efficient.
  • Operator Precedence and AssociativityRequired for correct infix-to-postfix conversion and expression evaluation.
  • Graph Basics (Vertices, Edges, Adjacency List)Needed to apply stacks to iterative DFS and reason about O(|V| + |E|) complexity.
  • Recursion vs IterationHelps understand how explicit stacks replace the call stack in iterative DFS and backtracking.
  • Integer Arithmetic and OverflowHistogram areas and exponentiation can exceed 32-bit limits; 64-bit types prevent overflow.
  • String Parsing and TokenizationNecessary for robust expression parsing with multi-digit numbers and spaces.
  • STL Containers (stack, vector, deque)C++ implementations rely on std::stack and related containers for efficient operations.

Detailed Explanation

Tap terms for definitions

01Overview

A stack is a simple but powerful data structure that organizes data with the Last-In, First-Out (LIFO) rule. Imagine a pile of books on a table: you add a book on top (push), and when you need one, you take the top book off (pop). Computing systems rely on stacks for many tasks, such as managing function calls (the call stack), parsing expressions, undo/redo operations in editors, and exploring graphs. In competitive programming and everyday development, stacks provide O(1) time for core operations: push (insert at the top), pop (remove the top), and top/peek (inspect the top). Stacks support linear-time solutions to many problems that otherwise appear to require nested loops—for example, the Next Greater Element, Largest Rectangle in Histogram, and Stock Span are each solvable in O(n) using a monotonic stack. In C++, the STL offers std::stack, a container adaptor that exposes only stack-like operations and is typically implemented using std::deque underneath. Stacks are not only conceptual tools but also performance enablers: when used correctly, they simplify logic, avoid recursion limits, and reduce algorithmic complexity.

02Intuition & Analogies

Think of a stack like a stack of trays in a cafeteria. New trays are added to the top, and customers always take the top one. You cannot reach into the middle without removing the top trays first—that’s the essence of LIFO. This natural behavior aligns with how we process nested tasks: if you start washing dishes, then answer a quick call, you first finish the call (the last started) before returning to the dishes (the first started). In computing, function calls behave the same way: when one function calls another, the new function’s context goes on top of the call stack; when it returns, the program resumes the previous function. For parsing expressions, imagine reading “(2 + 3) * 4”. To compute it correctly, you need to pause the multiplication until you handle what’s inside the parentheses—just like temporarily stacking a task until the inner one is finished. For finding the next greater temperature in a list of daily temperatures, a monotonic stack works like a waiting line of days that still need a warmer day; as soon as a hotter day arrives, you resolve as many waiting days as possible in one go. The power of stacks comes from this disciplined, top-focused access pattern which makes both the logic and runtime behavior straightforward and predictable.

03Formal Definition

A stack is an abstract data type defined by a sequence S with a distinguished end called the top. It supports the following operations: push(x): insert element x at the top; pop(): remove and return the element at the top (undefined if empty); top()/peek(): return, but do not remove, the top element (undefined if empty); empty(): return whether S has no elements; size(): return the number of elements. The access pattern enforces LIFO semantics: the most recently pushed element among those not yet popped is always at the top. In typical implementations, these operations run in constant time, O(1), because they modify or read only the top position. A monotonic stack is a stack that maintains a monotone property (e.g., values increasing or decreasing from bottom to top) by popping elements that violate the property when a new element arrives. This invariant enables linear-time solutions to certain array problems: each element is pushed at most once and popped at most once, so the total number of stack operations across the whole algorithm is O(n).

04When to Use

Use a stack when you need LIFO behavior or when a problem naturally involves reversing order, nesting, or deferring work. Classic applications include: 1) Balanced parentheses and syntax checking—push opening symbols, pop on matching closing symbols; 2) Expression parsing—convert infix to postfix using the shunting-yard algorithm, then evaluate postfix; 3) DFS traversal—replace recursion with an explicit stack to avoid stack overflows and to customize visiting order; 4) Backtracking—push choices and pop when you backtrack (e.g., maze solving, generating permutations); 5) Monotonic stack patterns—Next Greater/Smaller Element, computing spans (Stock Span), and finding the Largest Rectangle in a Histogram. Prefer a monotonic stack when you need, for each element, the nearest greater/smaller element to the left or right. Avoid stacks when you require random access to many positions (arrays/vectors are better) or when you need to remove items from the middle (use other containers like deque or list).

⚠️Common Mistakes

  • Popping or peeking from an empty stack (underflow). Always check empty() before pop() or top().
  • Forgetting operator precedence and associativity in infix-to-postfix conversion, especially handling right-associative '^'. Use a clear precedence function and condition.
  • Mishandling multi-digit numbers or spaces while tokenizing expressions. Parse consecutive digits into a full number and separate tokens explicitly.
  • Overflow with areas or products in histogram and expression evaluation. Use 64-bit integers (long long) for heights, areas, and intermediate results.
  • Not clearing the stack between test cases in competitive programming, causing leftover elements to pollute the next case.
  • Incorrect monotonic invariant: pushing elements without popping those that break the monotone order leads to O(n^2) behavior and wrong answers. Ensure you pop while the top violates the invariant.
  • For iterative DFS, pushing neighbors without checking visited can cause many duplicates; either check before pushing or guard on pop to maintain O(n + m).
  • Using recursion for deep DFS without considering recursion limits can cause stack overflows; prefer an explicit stack for very deep graphs.

Key Formulas

Total time for n pushes/pops

Explanation: Performing n push/pop/top operations on a standard stack takes linear time overall. Since each operation is O(1), the total across n operations is proportional to n.

Monotonic stack bound

Explanation: In a monotonic stack algorithm, each element is pushed once and popped at most once. Therefore, the total number of stack operations is at most 2n, giving linear time.

DFS Time Complexity

Explanation: Depth-first search visits every vertex and edge a constant number of times. The runtime is linear in the size of the graph: vertices plus edges.

DFS Space Complexity

Explanation: In the worst case, the explicit stack (or recursion) may hold up to O(|V|) vertices, such as when the graph is a path.

Histogram Area per Bar

Explanation: For each bar i, find the first smaller bar to the left and to the right . The maximal rectangle using bar i as the limiting height has width ( - - 1) and height .

Stock Span from Previous Greater

Explanation: The span at day i equals the distance to the previous day with a strictly greater price. A monotonic stack can compute all previous-greater indices in O(n).

Linear pass

Explanation: Many stack algorithms perform a single left-to-right (and sometimes right-to-left) pass, touching each element a constant number of times, resulting in O(n) time.

Expression Parsing Time

Explanation: Converting infix to postfix via the shunting-yard algorithm, then evaluating postfix, both run in time linear in the length of the token sequence.

Complexity Analysis

Core stack operations—push, pop, and top—are O(1) time and O(1) auxiliary space per operation because they access only the top element and adjust a pointer or index. Across n operations, the total time is O(n). For applications built on stacks, the complexity depends on how many times each input element is pushed and popped. Balanced parentheses checking is O(n) time and O(n) space in the worst case (all opening brackets) since each character is examined once and may be stored until matched. Infix-to-postfix conversion and postfix evaluation each run in O(n), where n is the number of tokens; space is O(n) for the operator/operand stacks. Iterative DFS with a stack visits each vertex and edge at most a constant number of times, yielding O(|V| + |E|) time and O(|V|) space for the visited array and the explicit stack (which may hold up to O(|V|) vertices in degenerate cases). Monotonic stack patterns, such as Next Greater Element and Largest Rectangle in Histogram, achieve O(n) time because each index is pushed once and popped at most once; their space usage is O(n) for the stack and auxiliary arrays (like answers or previous/next smaller indices). While the asymptotic bounds are tight, careful engineering matters: use 64-bit integers for arithmetic that can overflow (e.g., histogram areas or exponentiation), check for empty stacks to avoid underflow, and clear stacks between test cases in competitive programming to maintain correctness and linear-time behavior.

Code Examples

Balanced Parentheses Checker ((), {}, [])
1#include <bits/stdc++.h>
2using namespace std;
3
4// Returns true if the string has balanced brackets: (), {}, []
5bool isBalanced(const string &s) {
6 stack<char> st;
7 auto isOpen = [](char c){ return c=='(' || c=='{' || c=='['; };
8 auto matches = [](char open, char close){
9 return (open=='(' && close==')') ||
10 (open=='{' && close=='}') ||
11 (open=='[' && close==']');
12 };
13
14 for (char c : s) {
15 if (isOpen(c)) {
16 st.push(c);
17 } else if (c==')' || c=='}' || c==']') {
18 if (st.empty()) return false; // underflow: no opener to match
19 char top = st.top(); st.pop();
20 if (!matches(top, c)) return false; // mismatched types
21 }
22 // ignore other non-bracket characters if present
23 }
24 return st.empty(); // all openers must be matched
25}
26
27int main() {
28 ios::sync_with_stdio(false);
29 cin.tie(nullptr);
30
31 // Example usage
32 vector<string> tests = {
33 "(a+b)*[c-{d/(e+f)}]",
34 "([)]", // wrong nesting
35 "((()))", // balanced
36 "([{}])", // balanced
37 "((]" // mismatch
38 };
39
40 for (const auto &t : tests) {
41 cout << t << " -> " << (isBalanced(t) ? "Balanced" : "Not Balanced") << "\n";
42 }
43 return 0;
44}
45

We scan the string once. Opening brackets are pushed. For each closing bracket, we require that the top of the stack is the corresponding opening bracket. If at any point the stack is empty or types mismatch, the string is not balanced. At the end, the stack must be empty.

Time: O(n)Space: O(n)
Infix to Postfix (Shunting-Yard) and Postfix Evaluation
1#include <bits/stdc++.h>
2using namespace std;
3
4int precedence(char op) {
5 if (op == '+' || op == '-') return 1;
6 if (op == '*' || op == '/') return 2;
7 if (op == '^') return 3; // exponent (right-associative)
8 return 0;
9}
10
11bool isRightAssociative(char op) {
12 return op == '^';
13}
14
15// Convert infix expression with non-negative integers and operators +,-,*,/,^ and parentheses to postfix (tokens separated by spaces)
16string infixToPostfix(const string &expr) {
17 string out;
18 stack<char> ops;
19 int n = (int)expr.size();
20 for (int i = 0; i < n; ) {
21 if (isspace((unsigned char)expr[i])) { i++; continue; }
22 if (isdigit((unsigned char)expr[i])) {
23 // parse multi-digit number
24 long long val = 0;
25 while (i < n && isdigit((unsigned char)expr[i])) {
26 val = val * 10 + (expr[i] - '0');
27 i++;
28 }
29 out += to_string(val) + ' ';
30 } else if (expr[i] == '(') {
31 ops.push('(');
32 i++;
33 } else if (expr[i] == ')') {
34 // pop until '('
35 while (!ops.empty() && ops.top() != '(') {
36 out += ops.top(); out += ' ';
37 ops.pop();
38 }
39 if (!ops.empty() && ops.top() == '(') ops.pop();
40 else throw runtime_error("Mismatched parentheses");
41 i++;
42 } else {
43 // operator
44 char op = expr[i++];
45 int p = precedence(op);
46 if (p == 0) throw runtime_error(string("Unknown token: ") + op);
47 // pop while top has higher precedence, or equal and left-associative
48 while (!ops.empty() && ops.top() != '(') {
49 int pt = precedence(ops.top());
50 if (pt > p || (pt == p && !isRightAssociative(op))) {
51 out += ops.top(); out += ' ';
52 ops.pop();
53 } else break;
54 }
55 ops.push(op);
56 }
57 }
58 // flush remaining operators
59 while (!ops.empty()) {
60 if (ops.top() == '(') throw runtime_error("Mismatched parentheses");
61 out += ops.top(); out += ' ';
62 ops.pop();
63 }
64 return out;
65}
66
67long long ipow(long long a, long long e) {
68 long long res = 1;
69 while (e > 0) {
70 if (e & 1) res = res * a;
71 a = a * a;
72 e >>= 1;
73 }
74 return res;
75}
76
77// Evaluate postfix where tokens are space-separated numbers and operators +,-,*,/,^
78long long evalPostfix(const string &postfix) {
79 istringstream iss(postfix);
80 stack<long long> st;
81 string tok;
82 while (iss >> tok) {
83 if (tok.size() == 1 && string("+-*/^").find(tok[0]) != string::npos) {
84 if (st.size() < 2) throw runtime_error("Insufficient operands");
85 long long a = st.top(); st.pop();
86 long long b = st.top(); st.pop();
87 long long r = 0;
88 switch (tok[0]) {
89 case '+': r = b + a; break;
90 case '-': r = b - a; break;
91 case '*': r = b * a; break;
92 case '/': if (a == 0) throw runtime_error("Division by zero"); r = b / a; break;
93 case '^': if (a < 0) throw runtime_error("Negative exponent not supported"); r = ipow(b, a); break;
94 }
95 st.push(r);
96 } else {
97 // number token
98 long long val = stoll(tok);
99 st.push(val);
100 }
101 }
102 if (st.size() != 1) throw runtime_error("Invalid postfix expression");
103 return st.top();
104}
105
106int main() {
107 ios::sync_with_stdio(false);
108 cin.tie(nullptr);
109
110 string expr;
111 // Example input: (2 + 3) * 4 ^ 2 - 5
112 getline(cin, expr);
113 try {
114 string post = infixToPostfix(expr);
115 cout << "Postfix: " << post << "\n";
116 cout << "Value: " << evalPostfix(post) << "\n";
117 } catch (const exception &e) {
118 cerr << "Error: " << e.what() << "\n";
119 }
120 return 0;
121}
122

We implement the shunting-yard algorithm to convert infix to postfix, honoring precedence and associativity (with '^' right-associative). Postfix is then evaluated using a value stack: push numbers, and when an operator appears, pop two operands, compute, and push the result. The example supports multi-digit non-negative integers and basic arithmetic.

Time: O(n)Space: O(n)
Iterative DFS Using an Explicit Stack
1#include <bits/stdc++.h>
2using namespace std;
3
4// Perform iterative DFS starting from 'src'. Returns visitation order.
5vector<int> dfs_iterative(int n, const vector<vector<int>> &adj, int src) {
6 vector<int> order;
7 vector<char> vis(n+1, 0);
8 stack<int> st;
9 st.push(src);
10 while (!st.empty()) {
11 int u = st.top(); st.pop();
12 if (vis[u]) continue; // guard duplicates
13 vis[u] = 1;
14 order.push_back(u);
15 // Push neighbors; to mimic recursive DFS order (u->smallest first), push in reverse
16 for (int i = (int)adj[u].size()-1; i >= 0; --i) {
17 int v = adj[u][i];
18 if (!vis[v]) st.push(v);
19 }
20 }
21 return order;
22}
23
24int main() {
25 ios::sync_with_stdio(false);
26 cin.tie(nullptr);
27
28 int n, m; // number of vertices (1..n) and edges
29 cin >> n >> m;
30 vector<vector<int>> adj(n+1);
31 for (int i = 0; i < m; ++i) {
32 int u, v; cin >> u >> v;
33 adj[u].push_back(v);
34 adj[v].push_back(u); // make directed if needed by removing this line
35 }
36 for (int u = 1; u <= n; ++u) sort(adj[u].begin(), adj[u].end());
37
38 int src = 1;
39 vector<int> order = dfs_iterative(n, adj, src);
40 for (int x : order) cout << x << ' ';
41 cout << '\n';
42 return 0;
43}
44

We simulate recursive DFS with an explicit stack to control the traversal and avoid recursion depth issues. Each vertex is pushed when discovered and processed once when popped (guarded by a visited check), ensuring O(|V| + |E|) time. Sorting adjacency lists provides a deterministic visitation order.

Time: O(|V| + |E|)Space: O(|V| + |E|)
Monotonic Stack: Next Greater Element and Largest Rectangle in Histogram
1#include <bits/stdc++.h>
2using namespace std;
3
4// Next Greater Element (to the right). Returns indices of next greater (or -1).
5vector<int> nextGreaterIndex(const vector<long long> &a) {
6 int n = (int)a.size();
7 vector<int> nge(n, -1);
8 stack<int> st; // indices with decreasing values
9 for (int i = 0; i < n; ++i) {
10 while (!st.empty() && a[i] > a[st.top()]) {
11 nge[st.top()] = i;
12 st.pop();
13 }
14 st.push(i);
15 }
16 return nge;
17}
18
19// Largest Rectangle in Histogram using a monotonic (increasing) stack
20long long largestRectangleArea(const vector<long long> &h) {
21 int n = (int)h.size();
22 vector<long long> heights = h;
23 heights.push_back(0); // sentinel to flush the stack
24 stack<int> st; // indices of increasing heights
25 long long best = 0;
26 for (int i = 0; i <= n; ++i) {
27 long long cur = heights[i];
28 while (!st.empty() && cur < heights[st.top()]) {
29 long long height = heights[st.top()]; st.pop();
30 int left = st.empty() ? -1 : st.top();
31 int right = i; // first smaller to the right is i
32 long long width = right - left - 1;
33 best = max(best, height * width);
34 }
35 st.push(i);
36 }
37 return best;
38}
39
40int main() {
41 ios::sync_with_stdio(false);
42 cin.tie(nullptr);
43
44 int n; cin >> n;
45 vector<long long> a(n);
46 for (int i = 0; i < n; ++i) cin >> a[i];
47
48 vector<int> nge = nextGreaterIndex(a);
49 cout << "Next Greater Element values (or -1):\n";
50 for (int i = 0; i < n; ++i) {
51 long long val = (nge[i] == -1 ? -1 : a[nge[i]]);
52 cout << val << (i+1==n?'\n':' ');
53 }
54
55 int m; cin >> m;
56 vector<long long> h(m);
57 for (int i = 0; i < m; ++i) cin >> h[i];
58 cout << "Largest Rectangle Area: " << largestRectangleArea(h) << "\n";
59 return 0;
60}
61

For Next Greater Element, maintain a decreasing stack of indices; when a larger element arrives, resolve all smaller elements waiting for their next greater. For the histogram, maintain an increasing stack; when a shorter bar arrives, repeatedly compute maximal rectangles for bars that end at the current position. Both algorithms are linear because each index is pushed and popped at most once.

Time: O(n) for NGE, O(m) for histogramSpace: O(n) for NGE, O(m) for histogram