150. Evaluate Reverse Polish Notation
Learn how to solve LeeCode 150 evaluate Reverse Polish Notation (RPN) expressions using Python. This LeetCode solution demonstrates stack-based arithmetic computation, featuring different implementation approaches and best practices for handling postfix mathematical expressions.
LeetCode Link: 150. Evaluate Reverse Polish Notation
Problem Definition
You are given an array of strings tokens
that represents an arithmetic expression in a Reverse Polish Notation.
Evaluate the expression. Return an integer that represents the value of the expression.
Note that:
- The valid operators are
'+'
,'-'
,'*'
, and'/'
. - Each operand may be an integer or another expression.
- The division between two integers always truncates toward zero.
- There will not be any division by zero.
- The input represents a valid arithmetic expression in a reverse polish notation.
- The answer and all the intermediate calculations can be represented in a 32-bit integer.
Example 1:
Input: tokens = ["2","1","+","3","*"]
Output: 9
Explanation: ((2 + 1) * 3) = 9
Example 2:
Input: tokens = ["4","13","5","/","+"]
Output: 6
Explanation: (4 + (13 / 5)) = 6
Example 3:
Input: tokens = ["10","6","9","3","+","-11","*","/","*","17","+","5","+"]
Output: 22
Explanation: ((10 * (6 / ((9 + 3) * -11))) + 17) + 5
= ((10 * (6 / (12 * -11))) + 17) + 5
= ((10 * (6 / -132)) + 17) + 5
= ((10 * 0) + 17) + 5
= (0 + 17) + 5
= 17 + 5
= 22
Constraints:
1 <= tokens.length <= 104
tokens[i]
is either an operator:"+"
,"-"
,"*"
, or"/"
, or an integer in the range[-200, 200]
.
A Note About Expression Notation
The expressions we're used to seeing are infix expressions, as they match our natural way of thinking. However, infix expressions aren't very computer-friendly.
Take the infix expression 4 + 13 / 5
for example. When a computer scans this from left to right, it reaches 13 and needs to:
- Check what operator follows 13
- Compare operator precedence
- Perform the division with 5
- Then backtrack to position 4 to do the addition
Quite cumbersome, isn't it?
However, when we convert this to postfix (RPN) notation: ["4", "13", "5", "/", "+"]
, everything changes. The computer can process this sequentially using a stack, without worrying about operator precedence or backtracking. This makes postfix notation extremely computer-friendly.
This problem not only serves as an excellent coding exercise but also demonstrates how computers "think" differently from humans.
Thought Process
What is Reverse Polish Notation (RPN)?
RPN is a mathematical notation where the operators follow their operands (also called postfix notation). For example:
- Infix:
(1 + 2) * (3 + 4)
- RPN:
1 2 + 3 4 + *
Advantages of RPN:
- Eliminates the need for parentheses, making expressions unambiguous.
- Facilitates computation using a stack: operands are pushed onto the stack, and operators pop the required operands for evaluation.
Approach to Solve the Problem
- Use a stack to evaluate the RPN:
- Push operands onto the stack.
- When encountering an operator, pop the top two operands from the stack, apply the operator, and push the result back onto the stack.
- Division is special:
- Integer division truncates toward zero, which differs slightly from the behavior of Python’s division operator
/
. To ensure correctness, implement a custom division function that handles truncation.
- Integer division truncates toward zero, which differs slightly from the behavior of Python’s division operator
Solutions
Using Explicit Operations
class Solution:
def evalRPN(self, tokens: list[str]) -> int:
stack = []
for token in tokens:
if token not in ['+', '-', '*', '/']: # Push operand onto the stack
stack.append(int(token))
else: # Apply the operator to the top two operands
op2 = stack.pop()
op1 = stack.pop()
if token == '+':
stack.append(op1 + op2)
elif token == '-':
stack.append(op1 - op2)
elif token == '*':
stack.append(op1 * op2)
elif token == '/': # Integer division truncating toward zero
stack.append(int(op1 / op2))
return stack.pop()
Using operator
Module for Clean Code
This method uses Python's built-in operator
module to map operators (+
, -
, *
, /
) to their corresponding functions. A custom div
function handles integer division to ensure truncation toward zero.
from operator import add, sub, mul
def div(x, y):
# Custom integer division with truncation toward zero
return int(x / y) if x * y > 0 else -(abs(x) // abs(y))
class Solution:
op_map = {'+': add, '-': sub, '*': mul, '/': div}
def evalRPN(self, tokens: list[str]) -> int:
stack = []
for token in tokens:
if token not in self.op_map: # If it's an operand, push onto the stack
stack.append(int(token))
else: # It's an operator, apply it to the top two operands
op2 = stack.pop()
op1 = stack.pop()
stack.append(self.op_map[token](op1, op2))
return stack.pop()
Alternative: Using eval()
(Not Recommended)
Using Python’s eval()
function can simplify the implementation but introduces performance and security concerns. Only use this method in controlled environments.
class Solution:
def evalRPN(self, tokens: list[str]) -> int:
stack = []
for token in tokens:
if token.lstrip('-').isdigit(): # Check if token is a number
stack.append(int(token))
else:
op2 = stack.pop()
op1 = stack.pop()
stack.append(eval(f"{op1}{token}{op2}"))
return stack.pop()
Main reasons why eval()
is not recommended:
Security: eval()
can execute arbitrary Python code, which could be dangerous if the input isn't trusted
# Seemingly harmless eval:
eval("4+3") # Works fine
# But eval can execute ANY Python code:
eval("__import__('os').system('rm -rf /')") # Could delete system files
eval("__import__('subprocess').run(['malicious_command'])") # Could run malware
eval("open('passwords.txt', 'r').read()") # Could read sensitive files
In the above solution, while the input tokens are controlled, it's generally considered bad practice to use eval()
because:
- If the input validation (
token.lstrip('-').isdigit()
) fails - If the code is reused in another context where input isn't as controlled
- If future modifications accidentally weaken the input validation
Performance: It's slower than direct arithmetic operations
Clarity: It makes the code less explicit about what operations are being performed
Complexity Analysis
Time Complexity: O(n)
- We iterate through each token exactly once
- Each stack operation (push/pop) takes O(1) time
- Each arithmetic operation takes O(1) time
- Total time is linear with respect to the input length
Space Complexity: O(n)
- In the worst case, we might need to store all operands on the stack
- For example, with input like ["1", "2", "3", "4", "+", "+", "+"]
- Maximum stack size will be proportional to number of operands
- Additional space used by variables is constant O(1)
Key Takeaways
- Stack-Based Computation: RPN expressions are naturally suited for stacks. Each operator processes the last two operands, which matches the stack's LIFO nature.
- Integer Division: Ensure you handle division correctly with truncation toward zero, as Python’s
/
operator may not behave as expected. - RPN in Real Life: Reverse Polish Notation was popular in early calculators (like HP models) and remains an efficient way for computers to evaluate expressions.
Discussion