A clear explanation of evaluating arithmetic expressions with parentheses, precedence, and integer division.
Problem Restatement
We are given a valid arithmetic expression string s.
The expression may contain:
non-negative integers
+
-
*
/
(
)
spacesWe need to evaluate the expression and return the integer result.
The expression follows normal arithmetic rules:
- Parentheses are evaluated first.
- Multiplication and division happen before addition and subtraction.
- Operators with the same precedence are evaluated from left to right.
- Integer division truncates toward zero.
- We cannot use built-in expression evaluators like
eval.
Input and Output
| Item | Meaning |
|---|---|
| Input | A valid expression string s |
| Output | The integer value of the expression |
| Operators | +, -, *, / |
| Parentheses | Supported |
| Division | Truncates toward zero |
Example function shape:
def calculate(s: str) -> int:
...Examples
Example 1:
s = "1+1"Output:
2Example 2:
s = "6-4/2"Output:
4Division happens before subtraction:
4 / 2 = 2
6 - 2 = 4Example 3:
s = "2*(5+5*2)/3+(6/2+8)"Output:
21Inside the first parentheses:
5 + 5 * 2 = 15So:
2 * 15 / 3 = 10Inside the second parentheses:
6 / 2 + 8 = 11Total:
10 + 11 = 21First Thought: Use a Stack for Precedence
If there were no parentheses, this would be like Basic Calculator II.
We can scan the expression from left to right.
Addition and subtraction can be delayed by pushing signed numbers into a stack.
Multiplication and division must be applied immediately to the previous number.
For example:
s = "6-4/2"We push 6.
Then we see -4, so we push -4.
Then /2 should apply to the previous number, so -4 / 2 = -2.
The stack becomes:
[6, -2]The final sum is:
6 + (-2) = 4Parentheses add one extra requirement: when we see (, we recursively evaluate everything until the matching ).
Key Insight
Use recursive parsing.
Define a helper function that evaluates the current expression until either:
- the end of the string
- a closing parenthesis
)
Inside each recursive call, use the same stack idea:
| Operator | Action |
|---|---|
+ | Push num |
- | Push -num |
* | Pop previous value, multiply by num, push result |
/ | Pop previous value, divide by num, truncate toward zero, push result |
When the helper sees (, it recursively evaluates the inner expression and treats the returned value as the current number.
Handling Division
Python integer division with // floors toward negative infinity.
But the problem requires truncation toward zero.
For example:
-3 / 2 = -1Python gives:
-3 // 2 == -2So we should use:
int(a / b)This truncates toward zero.
For LeetCode constraints, intermediate values fit in 32-bit signed integer range.
Algorithm
- Keep a shared index
iinto the string. - Define
parse()to evaluate fromiuntil)or end. - Inside
parse():- Create an empty stack.
- Set
num = 0. - Set
op = "+".
- Scan characters:
- Skip spaces.
- If the character is a digit, build the full number.
- If the character is
(, recursively parse the subexpression. - If the character is an operator,
), or the end:- Apply the previous operator
optonum. - Set
opto the current operator. - Reset
num.
- Apply the previous operator
- If the character is
), stop this recursive call.
- Return
sum(stack).
Correctness
The parser evaluates each parenthesized expression in its own recursive call. Therefore, every expression inside parentheses is fully evaluated before it is used by the outer expression.
Within one expression level, the stack enforces operator precedence. Addition and subtraction push signed values into the stack, so they are delayed until the end of the current expression. Multiplication and division immediately combine the current number with the previous stack value, so they are evaluated before surrounding additions and subtractions.
The scan processes operators from left to right. Since multiplication and division are applied immediately when encountered, operations with equal precedence are evaluated in the correct order.
Every number or parenthesized subexpression is consumed exactly once and combined with the operator before it. Therefore, the recursive parser computes the same value as the original arithmetic expression.
Complexity
Let n = len(s).
| Metric | Value | Why |
|---|---|---|
| Time | O(n) | Each character is processed once |
| Space | O(n) | The stack and recursion depth may grow linearly |
Implementation
class Solution:
def calculate(self, s: str) -> int:
i = 0
def apply(stack: list[int], op: str, num: int) -> None:
if op == "+":
stack.append(num)
elif op == "-":
stack.append(-num)
elif op == "*":
stack.append(stack.pop() * num)
else:
stack.append(int(stack.pop() / num))
def parse() -> int:
nonlocal i
stack = []
num = 0
op = "+"
while i < len(s):
ch = s[i]
if ch == " ":
i += 1
continue
if ch.isdigit():
num = 0
while i < len(s) and s[i].isdigit():
num = num * 10 + int(s[i])
i += 1
continue
if ch == "(":
i += 1
num = parse()
continue
apply(stack, op, num)
num = 0
if ch == ")":
i += 1
break
op = ch
i += 1
return sum(stack)
return parse()Code Explanation
The variable i is the current position in the string:
i = 0It is shared by recursive calls.
The helper apply applies the previous operator to the current number:
def apply(stack: list[int], op: str, num: int) -> None:For + and -, we push signed numbers.
For * and /, we combine with the previous stack value immediately.
stack.append(stack.pop() * num)
stack.append(int(stack.pop() / num))The parser begins with:
stack = []
num = 0
op = "+"The initial + means the first number should be pushed as positive.
When we see a digit, we build the full multi-digit number:
while i < len(s) and s[i].isdigit():
num = num * 10 + int(s[i])
i += 1When we see an opening parenthesis:
if ch == "(":
i += 1
num = parse()
continuethe recursive call evaluates the whole inner expression and returns its value.
When we see an operator or closing parenthesis, we apply the previous operator:
apply(stack, op, num)If the character is ), this subexpression is complete:
if ch == ")":
i += 1
breakAt the end of a parse call:
return sum(stack)returns the value of the current expression level.
Important Implementation Note
The implementation above needs one sentinel operator at the end to force the last pending number to be applied.
A simple way is to append + to the expression before parsing.
Here is the corrected full implementation:
class Solution:
def calculate(self, s: str) -> int:
s += "+"
i = 0
def apply(stack: list[int], op: str, num: int) -> None:
if op == "+":
stack.append(num)
elif op == "-":
stack.append(-num)
elif op == "*":
stack.append(stack.pop() * num)
else:
stack.append(int(stack.pop() / num))
def parse() -> int:
nonlocal i
stack = []
num = 0
op = "+"
while i < len(s):
ch = s[i]
if ch == " ":
i += 1
continue
if ch.isdigit():
num = 0
while i < len(s) and s[i].isdigit():
num = num * 10 + int(s[i])
i += 1
continue
if ch == "(":
i += 1
num = parse()
continue
apply(stack, op, num)
num = 0
if ch == ")":
i += 1
break
op = ch
i += 1
return sum(stack)
return parse()Testing
def run_tests():
s = Solution()
assert s.calculate("1+1") == 2
assert s.calculate("6-4/2") == 4
assert s.calculate("2*(5+5*2)/3+(6/2+8)") == 21
assert s.calculate("(2+6*3+5-(3*14/7+2)*5)+3") == -12
assert s.calculate("10/3") == 3
assert s.calculate("14-3/2") == 13
assert s.calculate(" 3 + 5 / 2 ") == 5
print("all tests passed")
run_tests()Test meaning:
| Test | Why |
|---|---|
"1+1" | Basic addition |
"6-4/2" | Division before subtraction |
| Complex expression | Parentheses and precedence together |
| Nested expression | Confirms recursive parsing |
"10/3" | Positive truncating division |
"14-3/2" | Division before subtraction |
| Spaces | Confirms whitespace is ignored |