Loading learning content...
Consider the expression 3 + 4 * 2. What's the answer? If you follow standard mathematical conventions, you'd compute 4 * 2 = 8 first (multiplication before addition), then 3 + 8 = 11. But the order of evaluation isn't explicit in the notation—you need to know the rules of operator precedence.
Now consider: 3 4 2 * +. This is the same expression in postfix notation (also called Reverse Polish Notation or RPN). Here, the order of operations is explicit in the structure: when you see an operator, you know exactly which operands it applies to. No parentheses needed. No precedence rules needed.
This seemingly obscure notation is the foundation of:
In this page, we'll master the elegant algorithm that evaluates postfix expressions using a stack—and understand why this notation is so powerful.
By the end of this page, you will understand what postfix notation is and why it exists, master the stack-based algorithm for evaluating postfix expressions, trace through complex examples step-by-step, handle edge cases correctly, and see how this relates to compiler design and virtual machines.
There are three main ways to write mathematical expressions, differing in where the operator appears relative to its operands:
1. Infix Notation (Standard mathematical notation)
3 + 43 + 4 * 22. Prefix Notation (Polish Notation)
+ 3 4+ 3 * 4 23. Postfix Notation (Reverse Polish Notation - RPN)
3 4 +3 4 2 * +| Infix (Standard) | Prefix | Postfix | Result |
|---|---|---|---|
| 3 + 4 |
| 3 4 + | 7 |
| 3 + 4 * 2 |
| 3 4 2 * + | 11 |
| (3 + 4) * 2 |
| 3 4 + 2 * | 14 |
| 3 * (4 + 2) |
| 3 4 2 + * | 18 |
| (1 + 2) * (3 + 4) |
| 1 2 + 3 4 + * | 21 |
In postfix notation, parentheses disappear! The expression (3 + 4) * 2 becomes 3 4 + 2 *. Reading left to right, we first compute 3 4 + (= 7), then 7 2 * (= 14). The structure encodes the order of operations implicitly.
Why Postfix for Computers?
Computers love postfix for several reasons:
While humans find infix natural (we're taught 3 + 4 from childhood), computers find postfix easier to process. Most compilers convert infix to postfix (or a tree representation) before evaluation.
The algorithm for evaluating postfix expressions is beautifully simple:
Algorithm: Evaluate Postfix Expression
Key Insight: When we encounter an operator, its operands are the most recently pushed values. This is exactly what a stack provides! The LIFO property ensures we get the right operands in the right order.
Detailed Walkthrough: 3 4 2 * +
Let's trace through the expression that represents 3 + (4 * 2):
| Token | Action | Stack After | Explanation |
|---|---|---|---|
| 3 | Push operand | [3] | First operand |
| 4 | Push operand | [3, 4] | Second operand |
| 2 | Push operand | [3, 4, 2] | Third operand |
| Pop 2, 4; push 4*2=8 | [3, 8] | Multiply top two | |
| Pop 8, 3; push 3+8=11 | [11] | Add remaining two | |
| END | Return top of stack | 11 | Final result |
For non-commutative operators (subtraction, division), the order matters. If the stack is [a, b] with b on top, and we see -, we compute a - b, NOT b - a. The first popped value is the second operand, the second popped value is the first operand.
Let's implement the postfix evaluation algorithm in multiple languages. We'll handle the basic arithmetic operators: +, -, *, /.
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788
from typing import List, Union def evaluate_postfix(expression: str) -> float: """ Evaluate a postfix (RPN) expression. Tokens are space-separated. Supports: +, -, *, / Time Complexity: O(n) where n is the number of tokens Space Complexity: O(n) for the stack in worst case Args: expression: Space-separated postfix expression Returns: The result of evaluating the expression Raises: ValueError: If the expression is malformed ZeroDivisionError: If division by zero occurs """ # Handle empty input if not expression or not expression.strip(): raise ValueError("Empty expression") tokens = expression.split() stack: List[float] = [] # Define supported operators operators = { '+': lambda a, b: a + b, '-': lambda a, b: a - b, '*': lambda a, b: a * b, '/': lambda a, b: a / b, # Raises ZeroDivisionError if b=0 } for token in tokens: if token in operators: # Operator: pop two operands, compute, push result if len(stack) < 2: raise ValueError( f"Insufficient operands for operator '{token}'. " f"Stack has {len(stack)} values, need 2." ) # IMPORTANT: Pop order matters for non-commutative operators! # Second popped value is the first operand b = stack.pop() # Second operand (right of operator in infix) a = stack.pop() # First operand (left of operator in infix) result = operators[token](a, b) stack.append(result) else: # Operand: try to parse as number and push try: value = float(token) stack.append(value) except ValueError: raise ValueError(f"Invalid token: '{token}'") # Final validation if len(stack) != 1: raise ValueError( f"Malformed expression: expected 1 result, got {len(stack)}. " f"Remaining stack: {stack}" ) return stack[0] # Test casestest_cases = [ ("3 4 +", 7), # Simple addition ("3 4 *", 12), # Simple multiplication ("3 4 2 * +", 11), # 3 + (4 * 2) = 11 ("3 4 + 2 *", 14), # (3 + 4) * 2 = 14 ("5 1 2 + 4 * + 3 -", 14), # 5 + ((1 + 2) * 4) - 3 = 14 ("10 2 /", 5), # Division ("10 3 -", 7), # Subtraction (order matters!) ("2 3 4 * +", 14), # 2 + (3 * 4) = 14] print("Postfix Expression Evaluation Tests:")print("-" * 50)for expr, expected in test_cases: result = evaluate_postfix(expr) status = "✓" if result == expected else "✗" print(f"{status} '{expr}' = {result} (expected {expected})")Let's trace through some more complex expressions to build deeper intuition.
Example 1: 5 1 2 + 4 * + 3 -
This represents the infix expression: 5 + ((1 + 2) * 4) - 3 = 5 + 12 - 3 = 14
| Token | Stack Before | Action | Stack After |
|---|---|---|---|
| 5 | [] | Push 5 | [5] |
| 1 | [5] | Push 1 | [5, 1] |
| 2 | [5, 1] | Push 2 | [5, 1, 2] |
| [5, 1, 2] | Pop 2,1; push 1+2=3 | [5, 3] | |
| 4 | [5, 3] | Push 4 | [5, 3, 4] |
| [5, 3, 4] | Pop 4,3; push 3*4=12 | [5, 12] | |
| [5, 12] | Pop 12,5; push 5+12=17 | [17] | |
| 3 | [17] | Push 3 | [17, 3] |
| [17, 3] | Pop 3,17; push 17-3=14 | [14] |
Example 2: 2 3 + 4 5 * *
This represents: (2 + 3) * (4 * 5) = 5 * 20 = 100
| Token | Stack Before | Action | Stack After |
|---|---|---|---|
| 2 | [] | Push 2 | [2] |
| 3 | [2] | Push 3 | [2, 3] |
| [2, 3] | Pop 3,2; push 2+3=5 | [5] | |
| 4 | [5] | Push 4 | [5, 4] |
| 5 | [5, 4] | Push 5 | [5, 4, 5] |
| [5, 4, 5] | Pop 5,4; push 4*5=20 | [5, 20] | |
| [5, 20] | Pop 20,5; push 5*20=100 | [100] |
Notice that the stack depth corresponds to the 'pending' operands waiting for their operator. After an operator consumes two operands and produces one result, the stack depth decreases by 1. The expression is well-formed if we end with exactly 1 value and never go below 1 when trying to pop for an operator.
Real implementations must handle various edge cases. Here's how to validate and gracefully handle them:
3 + → Error when popping for +3 4 5 + → Stack has 2 values at end5 0 / → Runtime error3 abc + → Can't parse 'abc'42 → Valid! Result is 42-3 4 + → Must handle negative sign correctly3.5 2.1 + → Result is 5.61234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586
from typing import List, Optional, Tuplefrom enum import Enum class EvalError(Enum): EMPTY_EXPRESSION = "Empty expression" INSUFFICIENT_OPERANDS = "Insufficient operands for operator" TOO_MANY_OPERANDS = "Expression has unused operands" INVALID_TOKEN = "Invalid token" DIVISION_BY_ZERO = "Division by zero" def evaluate_postfix_safe( expression: str) -> Tuple[Optional[float], Optional[str]]: """ Evaluate postfix expression with comprehensive error handling. Returns: (result, None) on success (None, error_message) on failure """ if not expression or not expression.strip(): return None, EvalError.EMPTY_EXPRESSION.value tokens = expression.split() stack: List[float] = [] operators = {'+', '-', '*', '/'} for i, token in enumerate(tokens): if token in operators: if len(stack) < 2: return None, ( f"{EvalError.INSUFFICIENT_OPERANDS.value} " f"'{token}' at position {i}" ) b = stack.pop() a = stack.pop() if token == '/' and b == 0: return None, ( f"{EvalError.DIVISION_BY_ZERO.value} at position {i}" ) if token == '+': stack.append(a + b) elif token == '-': stack.append(a - b) elif token == '*': stack.append(a * b) elif token == '/': stack.append(a / b) else: try: # Handle negative numbers like "-3" stack.append(float(token)) except ValueError: return None, ( f"{EvalError.INVALID_TOKEN.value}: '{token}' " f"at position {i}" ) if len(stack) != 1: return None, ( f"{EvalError.TOO_MANY_OPERANDS.value}: " f"{len(stack)} values remain in stack" ) return stack[0], None # Test error handlingerror_cases = [ ("", "Empty"), ("3 +", "Insufficient operands"), ("3 4 5 +", "Too many operands"), ("3 abc +", "Invalid token"), ("5 0 /", "Division by zero"),] print("Error Handling Tests:")print("-" * 50)for expr, description in error_cases: result, error = evaluate_postfix_safe(expr) print(f"'{expr}': {description}") print(f" Result: {result}, Error: {error}") print()The basic algorithm easily extends to support more operators, including:
3 ~ for negation (pops 1, pushes 1)3 4 > → 0 or 1 (boolean)3 sqrt, 2 3 powcond then else ? (ternary)The key modification: operators can have different arities (number of operands). Binary operators pop 2, unary operators pop 1, ternary operators pop 3.
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788
import mathfrom typing import Callable, List def evaluate_extended_postfix(expression: str) -> float: """ Evaluate postfix expression with extended operator set. Supports: - Binary: +, -, *, /, ^, %, max, min - Unary: neg (negation), sqrt, sin, cos, log - Constants: pi, e """ tokens = expression.split() stack: List[float] = [] # Binary operators (pop 2, push 1) binary_ops: dict[str, Callable[[float, float], float]] = { '+': lambda a, b: a + b, '-': lambda a, b: a - b, '*': lambda a, b: a * b, '/': lambda a, b: a / b, '^': lambda a, b: a ** b, '%': lambda a, b: a % b, 'max': lambda a, b: max(a, b), 'min': lambda a, b: min(a, b), } # Unary operators (pop 1, push 1) unary_ops: dict[str, Callable[[float], float]] = { 'neg': lambda x: -x, 'sqrt': lambda x: math.sqrt(x), 'sin': lambda x: math.sin(x), 'cos': lambda x: math.cos(x), 'log': lambda x: math.log(x), 'abs': lambda x: abs(x), 'floor': lambda x: math.floor(x), 'ceil': lambda x: math.ceil(x), } # Constants constants: dict[str, float] = { 'pi': math.pi, 'e': math.e, } for token in tokens: if token in binary_ops: if len(stack) < 2: raise ValueError(f"Need 2 operands for {token}") b, a = stack.pop(), stack.pop() stack.append(binary_ops[token](a, b)) elif token in unary_ops: if len(stack) < 1: raise ValueError(f"Need 1 operand for {token}") x = stack.pop() stack.append(unary_ops[token](x)) elif token in constants: stack.append(constants[token]) else: try: stack.append(float(token)) except ValueError: raise ValueError(f"Unknown token: {token}") if len(stack) != 1: raise ValueError(f"Malformed: {len(stack)} values remain") return stack[0] # Examples with extended operatorsexamples = [ ("2 3 ^", 8), # 2^3 = 8 ("16 sqrt", 4), # √16 = 4 ("3 4 max", 4), # max(3,4) = 4 ("5 neg", -5), # -5 ("pi 2 /", math.pi/2), # π/2 ("e 1 log", 1), # ln(e) = 1 ("2 3 4 + *", 14), # 2 * (3+4) = 14] print("Extended Postfix Evaluation:")for expr, expected in examples: result = evaluate_extended_postfix(expr) print(f"'{expr}' = {result:.4f} (expected {expected:.4f})")The postfix evaluation algorithm isn't just a theoretical exercise—it's the foundation of stack-based virtual machines that power modern software:
Java Virtual Machine (JVM)
Java bytecode uses a stack-based evaluation model. When you write:
int x = 3 + 4 * 2;
The compiler generates bytecode roughly equivalent to:
iconst_3 // Push 3
iconst_4 // Push 4
iconst_2 // Push 2
imul // Pop 4,2; push 4*2=8
iadd // Pop 3,8; push 3+8=11
istore_1 // Pop 11, store in local variable 1
This is postfix evaluation in action!
Why Stack Machines?
Stack machines have elegant properties:
The trade-off is that real hardware uses registers, so stack machine bytecode must be translated to register-based native code (by the JIT compiler) for peak performance.
When you learn postfix evaluation, you're learning the computational model behind billions of devices. Every Android phone evaluates Dalvik/ART bytecode. Every web browser runs WASM. Every JVM application uses this principle. This 'simple' algorithm scales from classroom exercise to planet-scale infrastructure.
Time Complexity: O(n)
Where n is the number of tokens in the expression:
Total: O(n) time.
Space Complexity: O(n)
In the worst case, the expression could be all operands followed by operators:
1 2 3 4 5 + + + + (= 1 + 2 + 3 + 4 + 5 = 15)
Here, the stack grows to hold all operands before any operator is applied.
Practical Considerations:
| Expression Type | Max Stack Size | Example |
|---|---|---|
| All operands then operators | n/2 + 1 | 1 2 3 4 + + + |
| Interleaved evenly | 2 | 1 2 + 3 + 4 + |
| Deeply nested | (n/2) | (((a op b) op c) op d) |
| Single operand | 1 | 42 |
Is O(n) optimal?
Yes! Any algorithm must read all tokens at least once, so Ω(n) is a lower bound. Our algorithm matches this lower bound.
Note on tokenization:
Our analysis assumes tokens are pre-split. If parsing a raw string with multi-digit numbers, tokenization adds O(n) work (scanning the input), which doesn't change the overall O(n) complexity.
What's Next:
We've learned to evaluate postfix expressions. But how do we create them? In the next page, we'll master the Shunting-Yard Algorithm—the elegant method for converting familiar infix expressions into postfix form, bridging human-readable notation and computer-friendly representation.
You now understand postfix expression evaluation—a fundamental algorithm that powers virtual machines, calculators, and compilers worldwide. The simplicity of the stack-based approach masks its profound importance in computing infrastructure.