Evaluate Reverse Polish Notation
STACK · MEDIUM
The problem
Section titled “The problem”You’re given an array of string tokens representing an arithmetic expression written in Reverse Polish (postfix) Notation — every operator comes after its two operands instead of between them, so no parentheses are ever needed. Valid tokens are integers (as strings, possibly negative) or one of the four operators +, -, *, /. Division always truncates toward zero, not toward negative infinity. Evaluate the expression and return the resulting integer. For example ["2","1","+","3","*"] means (2 + 1) * 3 = 9, and ["4","13","5","/","+"] means 4 + (13 / 5) = 4 + 2 = 6 (13/5 truncates to 2, not 2.6).
Solution 1: Brute force — repeated rescan and splice, no explicit stack
Section titled “Solution 1: Brute force — repeated rescan and splice, no explicit stack”You don’t strictly need a stack to evaluate RPN — you can treat the token array itself as the workspace. Repeatedly scan from the very start of the array looking for the first operator token. Once found, it’s guaranteed (by the definition of valid RPN) that the two tokens immediately before it are its operands. Apply the operator to them, then splice that 3-token window down to a single result token, and start scanning again from index 0. Keep going until only one token is left — that’s the answer. It’s honest and correct, but every single operator application pays for a full re-scan of whatever’s left, plus the cost of splice shifting every token after it.
Interactive visualization
Section titled “Interactive visualization”tokens = [10, 6, 9, 3, +, -11, *, /, *, 17, +, 5, +] — brute force approach
Press Play or Step to begin.
Step 0 / 20
This animation traces tokens=[“10”,“6”,“9”,“3”,“+”,“-11”,“”,“/”,“”,“17”,“+”,“5”,“+”] (expected: 22), verified above before any HTML was written.
The code
Section titled “The code”function evalRPNBruteForce(tokens) { const OPERATORS = new Set(['+', '-', '*', '/']); const working = tokens.slice();
while (working.length > 1) { let opIdx = -1; for (let i = 0; i < working.length; i++) { if (OPERATORS.has(working[i])) { opIdx = i; break; } }
const a = Number(working[opIdx - 2]); const b = Number(working[opIdx - 1]); const op = working[opIdx]; let result; if (op === '+') result = a + b; else if (op === '-') result = a - b; else if (op === '*') result = a * b; else result = Math.trunc(a / b);
working.splice(opIdx - 2, 3, String(result)); } return Number(working[0]);}Line by line:
OPERATORS.has(working[i])tests for an exact match against+/-/*//— a negative-number token like"-11"never matches, since it’s a two-character string, not the single character"-".- The inner
forloop re-scansworkingfrom index 0 every single time this outerwhilebody runs — that repeated full rescan, not a different algorithmic idea, is what makes this O(n²) instead of O(n). - Because the tokens form valid RPN,
working[opIdx - 2]andworking[opIdx - 1]are guaranteed to be exactly this operator’s two operands — no searching or validation needed to find them. Math.trunc(a / b), notMath.floor(a / b), is required for the division case — truncation drops the fractional part toward zero, whilefloorrounds toward negative infinity and gives the wrong answer whenever the true quotient is negative.working.splice(opIdx - 2, 3, String(result))removes the 3-token window and replaces it with one token in a single call — but every token after that window still has to shift down by two positions, an O(n) cost on top of the rescan.
Complexity
Section titled “Complexity”| Time | Space | Approach |
|---|---|---|
| O(n²) | O(n) | Repeated full rescan + splice per operator, no explicit stack |
Solution 2: Optimal — explicit stack, single pass
Section titled “Solution 2: Optimal — explicit stack, single pass”Walk the tokens exactly once, left to right, using an explicit stack. Every number token gets parsed and pushed. Every operator token pops the top two values — b first, then a, since the stack is LIFO and b was pushed more recently — applies the operator as a op b (never b op a; getting the operand order backward silently breaks non-commutative operators like - and /), and pushes the result back on. After the last token, exactly one value remains on the stack: the answer. No rescanning, no shifting — every push and pop is O(1).
Interactive visualization
Section titled “Interactive visualization”tokens = [10, 6, 9, 3, +, -11, *, /, *, 17, +, 5, +] — optimal stack-based approach
TOKENS (fixed list)
STACK (grows/shrinks)
Press Play or Step to begin.
Step 0 / 21
Same tokens=[“10”,“6”,“9”,“3”,“+”,“-11”,“”,“/”,“”,“17”,“+”,“5”,“+”] as Solution 1, so you can compare step counts directly — the stack view shows values left-to-right, bottom of stack first.
The code
Section titled “The code”function evalRPNOptimal(tokens) { const OPERATORS = new Set(['+', '-', '*', '/']); const stack = [];
for (const token of tokens) { if (OPERATORS.has(token)) { const b = stack.pop(); const a = stack.pop(); let result; if (token === '+') result = a + b; else if (token === '-') result = a - b; else if (token === '*') result = a * b; else result = Math.trunc(a / b); stack.push(result); } else { stack.push(Number(token)); } } return stack[0];}Line by line:
OPERATORS.has(token)is the same exact-match test as the brute-force version —"-11"falls through to theelsebranch and gets parsed as a number, never mistaken for the"-"operator.const b = stack.pop(); const a = stack.pop();— order matters.bis popped first because it was pushed second (LIFO), and the operator is then applied asa op b, matching the original left-to-right operand order in the expression.- Same
Math.trunc(a / b)truncating-toward-zero division as Solution 1 — this isn’t a place the two solutions differ, only how they find their operands differs. stack.push(Number(token))parses every non-operator token as a number exactly once — no re-parsing, no re-scanning of earlier tokens.- After the loop,
stack[0]is the sole remaining value for any valid RPN input — returning it doesn’t need to check the stack’s length, because valid input guarantees exactly one value is left.
Complexity
Section titled “Complexity”| Time | Space | Approach |
|---|---|---|
| O(n) | O(n) | Single linear pass with an explicit stack, O(1) push/pop |
Complexity comparison
Section titled “Complexity comparison”| Solution | Time | Space |
|---|---|---|
| Brute force (rescan + splice) | O(n²) | O(n) |
| Optimal (explicit stack) | O(n) | O(n) |
This is a real, clean asymptotic improvement, not just a different constant factor. Both solutions apply operators in the exact same order and get the exact same operands for each one — the difference is purely how those operands are located. The brute-force version pays an O(n) rescan-plus-shift for every single operator application, while the stack version finds both operands in O(1) via pop(), because the stack’s own LIFO order already keeps “the two most recent unconsumed values” sitting right on top, with zero searching required.
Interview corner
Section titled “Interview corner”Why must the optimal solution pop b before a, and then apply the operator as a op b — not b op a?
- The stack is LIFO: whichever operand was pushed later in the expression comes off the stack first. In postfix, the operand written second in the source is always
b, so it’s on top and pops first. - Getting the assignment backward (
const a = stack.pop(); const b = stack.pop();) would silently swap which value isaand which isb. - For
+and*that swap wouldn’t even be visible — the wrong code would happen to produce right answers on those tokens. For-and/, which aren’t commutative, it produces a confidently wrong answer with no error thrown (e.g.a - bvsb - adiffer by sign whenever they’re unequal).
Both solutions apply operators in the exact same order to the exact same operands. What actually makes the brute-force version O(n²) instead of O(n)?
- The brute-force version re-scans the entire remaining token array from index 0 to find the next operator, every single time — not a smarter search, a fresh linear pass.
splice()also shifts every token after the removed window down by two positions, another O(n) cost layered on top of the scan.- The stack version never re-scans anything: each token is visited exactly once, and finding “the two most recent operands” is an O(1)
pop()because the stack’s own structure already keeps them on top — that’s the entire source of the asymptotic gap, not a different evaluation strategy.
Why is it always safe to assume, without checking, that the two tokens immediately before an operator (brute force) or the top two stack values (optimal) are exactly that operator’s operands?
- This relies on the input being valid RPN, which both solutions are entitled to assume (per the problem’s constraints) — it’s not something either one re-verifies.
- In valid postfix notation, at any point during a left-to-right scan, everything seen so far reduces to a stack of complete, already-evaluated sub-expression results — there’s no way for an operator to appear before both of its operands have been fully resolved.
- That invariant is exactly why “top two of the stack” (or “two tokens immediately before” in the array version) always correctly identifies the operands — no searching, matching, or bracket-tracking is ever needed, unlike evaluating infix expressions.
Trick question — what does this log?
console.log(parseInt('3*'));console.log(Number('3*'));- Answer:
3, thenNaN— two very different results for the same malformed-looking string. parseIntreads digits from the left and silently stops at the first character that isn’t part of a valid number, discarding the rest — it would happily treat a corrupted token like"3*"as the number3, no error, no warning.Number(...)requires the entire string to be a valid numeric literal — anything left over makes the whole conversion fail toNaN.
⚠️ Trap: This is exactly why both solutions above use Number(token) to parse operands, never parseInt(token). Since every token is checked against OPERATORS first, only genuine number tokens ever reach the parse step — but Number() is still the safer default, because a stray malformed token becomes a loud NaN that propagates and breaks the final result, instead of parseInt silently truncating it into a plausible-looking but wrong number.