Skip to content
DSA stacks queues 8 min read

Stack Applications: Balanced Parentheses, Expressions & Undo

The stack earns its keep because LIFO order matches a huge family of problems: anything involving nesting, matching, or “go back to the previous state”. This page works through three classic applications — balanced parentheses, postfix expression evaluation, and undo/redo — with correct, idiomatic code in all four languages. If you are new to stacks, start with the stacks introduction.

1. Balanced parentheses

Problem: given a string of brackets ()[]{}, decide whether every opener has a matching closer in the right order. Push each opener; on a closer, the top of the stack must be its matching opener. At the end the stack must be empty.

#include <string>
#include <stack>
#include <unordered_map>
bool isBalanced(const std::string& s) {
    std::unordered_map<char, char> match = {{')','('}, {']','['}, {'}','{'}};
    std::stack<char> st;
    for (char c : s) {
        if (c == '(' || c == '[' || c == '{') st.push(c);
        else if (match.count(c)) {
            if (st.empty() || st.top() != match[c]) return false;
            st.pop();
        }
    }
    return st.empty();
}

Complexity: O(n) time, O(n) space (worst case all openers).

Pitfall: Forgetting the final st.empty() check. "(((" never hits a mismatch but is still unbalanced — leftover openers mean failure.

2. Evaluating a postfix expression

In postfix (Reverse Polish) notation, operators come after their operands, e.g. 3 4 + 2 * = (3+4)*2 = 14. No parentheses needed. Push numbers; on an operator, pop the top two, apply, push the result. The last value on the stack is the answer.

#include <vector>
#include <string>
#include <stack>
int evalPostfix(const std::vector<std::string>& tokens) {
    std::stack<int> st;
    for (const std::string& t : tokens) {
        if (t == "+" || t == "-" || t == "*" || t == "/") {
            int b = st.top(); st.pop();
            int 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(std::stoi(t));
    }
    return st.top();
}

Complexity: O(n) time, O(n) space.

Pitfall: Operand order. The second value popped is the left operand. a - b and b - a differ — get the order wrong and subtraction/division silently break.

3. Undo / redo

Editors keep two stacks: an undo stack of past actions and a redo stack. Doing an action pushes it on undo and clears redo. Undo pops from undo and pushes onto redo; redo does the reverse.

#include <stack>
#include <string>
struct History {
    std::stack<std::string> undo, redo;
    void doAction(const std::string& a) { undo.push(a); while(!redo.empty()) redo.pop(); }
    void undoAction() { if (!undo.empty()) { redo.push(undo.top()); undo.pop(); } }
    void redoAction() { if (!redo.empty()) { undo.push(redo.top()); redo.pop(); } }
};

Each operation is O(1).

Why clear redo on a new action? Once you branch off in a new direction, the old “future” no longer makes sense — that’s exactly how every real editor behaves.

Other places stacks shine

  • Infix → postfix conversion (the shunting-yard algorithm).
  • Iterative DFS on trees and graphs.
  • Monotonic stack problems like next-greater-element — see monotonic stack.

Practice these on the stacks & queues exercises, or move on to queues.

Last updated June 25, 2026
Was this helpful?