772. Basic Calculator III 🔒
Problem Description
You need to implement a calculator that can evaluate mathematical expressions given as strings. The expressions can contain:
- Non-negative integers (0, 1, 2, ...)
- Four basic arithmetic operators:
+
,-
,*
,/
- Parentheses:
(
and)
The calculator must follow standard mathematical rules:
- Operations inside parentheses are evaluated first
- Multiplication and division have higher precedence than addition and subtraction
- Operations of the same precedence are evaluated left to right
- Integer division should truncate toward zero (e.g.,
5/2 = 2
,-5/2 = -2
)
For example:
"2+3*4"
should return14
(not 20, because multiplication happens first)"(2+3)*4"
should return20
(parentheses override normal precedence)"10/3"
should return3
(integer division truncates)
The solution uses a recursive depth-first search approach with a stack. It processes the expression character by character, handling:
- Building multi-digit numbers by accumulating digits
- Recursively evaluating sub-expressions within parentheses
- Applying operators based on their precedence - multiplication and division are applied immediately by modifying the last value on the stack, while addition and subtraction push new values onto the stack
- Returning the sum of all values in the stack when a closing parenthesis is encountered or the expression ends
The key insight is that by applying *
and /
immediately but deferring +
and -
operations to the stack, the solution naturally handles operator precedence without needing explicit precedence rules.
Intuition
When evaluating mathematical expressions, we face two main challenges: handling operator precedence and dealing with parentheses. Let's think about how we naturally solve "2+3*4"
on paper - we recognize that multiplication must happen before addition, giving us 2+12=14
.
The key insight is that we can handle precedence by treating operators differently. For addition and subtraction, we can defer the calculation by storing values on a stack. For multiplication and division, we need to apply them immediately to respect their higher precedence.
Consider the expression "2+3*4-5"
. As we scan through:
- See
2
and+
: push2
to stack → stack:[2]
- See
3
and*
: push3
to stack → stack:[2, 3]
- See
4
and-
: since previous operator was*
, we pop3
, multiply by4
, push back12
→ stack:[2, 12]
- See
5
and end: push-5
to stack → stack:[2, 12, -5]
- Sum the stack:
2+12-5 = 9
Notice how multiplication modifies the last value on the stack immediately, while addition/subtraction just add new values. This naturally preserves the correct order of operations.
For parentheses, we recognize that (expression)
is essentially a sub-problem. When we encounter (
, we can recursively evaluate everything inside until we find the matching )
, treating that entire sub-expression as a single number. This recursive approach elegantly handles nested parentheses too.
The beauty of this solution is that by combining these two ideas - using a stack with immediate application of high-precedence operators and recursive evaluation of parentheses - we get a clean algorithm that handles all cases without explicitly tracking operator precedence levels or building expression trees.
Solution Approach
The solution uses a recursive depth-first search (DFS) with a stack-based approach to handle operator precedence. Let's walk through the implementation:
Data Structures:
- A
deque
to efficiently process characters from left to right - A stack (
stk
) to store intermediate results - Variables to track the current number being built (
num
) and the previous operator (sign
)
Main Algorithm Flow:
-
Initialize State: Start with
num = 0
,sign = "+"
, and an empty stackstk
. -
Process Characters One by One:
- If the character is a digit, build the multi-digit number:
num = num * 10 + int(c)
- If the character is
(
, recursively calldfs()
to evaluate the sub-expression and treat the result asnum
- If the character is an operator (
+-*/
), closing parenthesis)
, or we've reached the end of the expression, apply the previous operator
- If the character is a digit, build the multi-digit number:
-
Apply Operators Based on Precedence:
match sign: case "+": stk.append(num) # Defer addition case "-": stk.append(-num) # Convert subtraction to adding negative case "*": stk.append(stk.pop() * num) # Apply immediately case "/": stk.append(int(stk.pop() / num)) # Apply immediately with truncation
The key insight:
+
and-
push values to the stack for later summation, while*
and/
immediately modify the last stack value. -
Handle Parentheses:
-
Final Result: After processing all characters, return
sum(stk)
which gives the final result.
Example Walkthrough for "2*(3+4)"
:
- Start with main expression
- Process
2
, then see*
- Push
2
to stack, setsign = "*"
- Encounter
(
, recursively evaluate"3+4)"
- In recursion: push
3
, then push4
, hit)
, return7
- In recursion: push
- Back in main:
num = 7
, apply*
: pop2
, push2*7=14
- Return sum of stack:
14
The elegance lies in how the stack naturally maintains evaluation order while the recursive calls handle nested expressions seamlessly.
Ready to land your dream job?
Unlock your dream job with a 5-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's trace through the expression "6+2*3-4"
to see how the algorithm works:
Initial State:
num = 0
,sign = "+"
,stk = []
Step 1: Process '6'
- Character is digit:
num = 6
- Move to next character '+'
Step 2: Process '+'
- Character is operator, so apply previous sign ('+')
- Execute:
stk.append(6)
→stk = [6]
- Reset:
num = 0
,sign = "+"
Step 3: Process '2'
- Character is digit:
num = 2
- Move to next character '*'
Step 4: Process '*'
- Character is operator, so apply previous sign ('+')
- Execute:
stk.append(2)
→stk = [6, 2]
- Reset:
num = 0
,sign = "*"
Step 5: Process '3'
- Character is digit:
num = 3
- Move to next character '-'
Step 6: Process '-'
- Character is operator, so apply previous sign ('*')
- Execute:
stk.append(stk.pop() * 3)
→stk.pop()
gives 2, then2 * 3 = 6
- Result:
stk = [6, 6]
- Reset:
num = 0
,sign = "-"
Step 7: Process '4'
- Character is digit:
num = 4
- Reached end of expression
Step 8: End of Expression
- Apply previous sign ('-')
- Execute:
stk.append(-4)
→stk = [6, 6, -4]
Step 9: Calculate Final Result
- Return
sum(stk) = 6 + 6 + (-4) = 8
The key observation: multiplication was applied immediately when we encountered the '-' after '3', modifying the last stack value from 2 to 6. Addition and subtraction operations were deferred by pushing values to the stack, which we sum at the end. This ensures correct operator precedence without explicit priority rules.
Solution Implementation
1from collections import deque
2from typing import Deque
3
4class Solution:
5 def calculate(self, s: str) -> int:
6 def evaluate_expression(char_queue: Deque[str]) -> int:
7 """
8 Recursively evaluate mathematical expression from a queue of characters.
9 Handles +, -, *, / operations and parentheses.
10
11 Args:
12 char_queue: A deque containing characters of the expression
13
14 Returns:
15 The evaluated result as an integer
16 """
17 current_number = 0
18 operator = "+"
19 operand_stack = []
20
21 while char_queue:
22 char = char_queue.popleft()
23
24 # Build multi-digit numbers
25 if char.isdigit():
26 current_number = current_number * 10 + int(char)
27
28 # Handle nested parentheses with recursion
29 if char == "(":
30 current_number = evaluate_expression(char_queue)
31
32 # Process operator or end of expression
33 if char in "+-*/)" or not char_queue:
34 # Apply the previous operator with the current number
35 if operator == "+":
36 operand_stack.append(current_number)
37 elif operator == "-":
38 operand_stack.append(-current_number)
39 elif operator == "*":
40 operand_stack.append(operand_stack.pop() * current_number)
41 elif operator == "/":
42 # Integer division with truncation toward zero
43 operand_stack.append(int(operand_stack.pop() / current_number))
44
45 # Reset for next operation
46 current_number = 0
47 operator = char
48
49 # Exit recursion when closing parenthesis is found
50 if char == ")":
51 break
52
53 # Sum all values in the stack to get final result
54 return sum(operand_stack)
55
56 # Convert string to deque and start evaluation
57 return evaluate_expression(deque(s))
58
1class Solution {
2 /**
3 * Performs mathematical operation based on the given operator
4 * @param b Second operand
5 * @param op Operator character (+, -, *, /)
6 * @param a First operand
7 * @return Result of the operation a op b
8 */
9 private int operate(int b, char op, int a) {
10 switch (op) {
11 case '+':
12 return a + b;
13 case '-':
14 return a - b;
15 case '*':
16 return a * b;
17 case '/':
18 return a / b;
19 default:
20 return 0;
21 }
22 }
23
24 /**
25 * Evaluates a mathematical expression string containing +, -, *, /, and parentheses
26 * @param s The expression string to evaluate
27 * @return The calculated result of the expression
28 */
29 public int calculate(String s) {
30 // Initialize operator priority map
31 int[] priority = new int[256];
32 priority['+'] = 1;
33 priority['-'] = 1;
34 priority['*'] = 2;
35 priority['/'] = 2;
36 priority['('] = 0;
37 priority[')'] = 0;
38
39 Stack<Character> operatorStack = new Stack<>(); // Stack to store operators
40 Stack<Integer> operandStack = new Stack<>(); // Stack to store operands
41 int n = s.length();
42
43 // Process each character in the expression
44 for (int i = 0; i < n; i++) {
45 char currentChar = s.charAt(i);
46
47 // Skip whitespace
48 if (currentChar == ' ') {
49 continue;
50 }
51
52 // Process numeric values
53 if (Character.isDigit(currentChar)) {
54 int number = currentChar - '0';
55
56 // Handle multi-digit numbers
57 while (i + 1 < n && Character.isDigit(s.charAt(i + 1))) {
58 i++;
59 number = number * 10 + (s.charAt(i) - '0');
60 }
61
62 operandStack.push(number);
63 }
64 // Process operators and parentheses
65 else {
66 // Push operator if stack is empty, it's an opening parenthesis,
67 // or it has higher priority than stack top
68 if (operatorStack.isEmpty() || currentChar == '(' ||
69 priority[currentChar] > priority[operatorStack.peek()]) {
70
71 // Handle unary operators at the beginning
72 if (operandStack.isEmpty() && (currentChar == '-' || currentChar == '+')) {
73 operandStack.push(0);
74 }
75
76 operatorStack.push(currentChar);
77
78 // Handle unary operators after opening parenthesis
79 if (currentChar == '(') {
80 int j = i;
81 while (j + 1 < n) {
82 if (s.charAt(j + 1) == '-' || s.charAt(j + 1) == '+') {
83 operandStack.push(0);
84 break;
85 }
86 if (s.charAt(j + 1) != ' ') {
87 break;
88 }
89 j++;
90 }
91 }
92 }
93 // Process closing parenthesis
94 else if (currentChar == ')') {
95 // Evaluate all operations until matching opening parenthesis
96 char topOperator = operatorStack.pop();
97
98 while (topOperator != '(') {
99 int secondOperand = operandStack.pop();
100 int firstOperand = operandStack.pop();
101
102 operandStack.push(operate(secondOperand, topOperator, firstOperand));
103
104 topOperator = operatorStack.pop();
105 }
106 }
107 // Process operators with lower or equal priority
108 else if (priority[currentChar] <= priority[operatorStack.peek()]) {
109 // Evaluate all higher or equal priority operations
110 char topOperator = operatorStack.peek();
111
112 while (!operatorStack.isEmpty() &&
113 priority[currentChar] <= priority[operatorStack.peek()] &&
114 topOperator != '(') {
115
116 operatorStack.pop();
117 int secondOperand = operandStack.pop();
118 int firstOperand = operandStack.pop();
119
120 operandStack.push(operate(secondOperand, topOperator, firstOperand));
121
122 if (!operatorStack.isEmpty()) {
123 topOperator = operatorStack.peek();
124 } else {
125 break;
126 }
127 }
128
129 operatorStack.push(currentChar);
130 }
131 }
132 }
133
134 // Process any remaining operations in the stack
135 while (!operatorStack.isEmpty()) {
136 char topOperator = operatorStack.pop();
137
138 int secondOperand = operandStack.pop();
139 int firstOperand = operandStack.pop();
140
141 operandStack.push(operate(secondOperand, topOperator, firstOperand));
142 }
143
144 // Return the final result
145 return operandStack.peek();
146 }
147}
148
1class Solution {
2public:
3 /**
4 * Performs mathematical operation based on the given operator
5 * @param b Second operand
6 * @param op Operator character (+, -, *, /)
7 * @param a First operand
8 * @return Result of the operation a op b
9 */
10 int operate(int b, char op, int a) {
11 switch (op) {
12 case '+':
13 return a + b;
14 case '-':
15 return a - b;
16 case '*':
17 return a * b;
18 case '/':
19 return a / b;
20 default:
21 return 0;
22 }
23 }
24
25 /**
26 * Evaluates a mathematical expression string containing +, -, *, /, and parentheses
27 * @param s The expression string to evaluate
28 * @return The calculated result of the expression
29 */
30 int calculate(string s) {
31 // Initialize operator priority map
32 int priority[256] = {0};
33 priority['+'] = 1;
34 priority['-'] = 1;
35 priority['*'] = 2;
36 priority['/'] = 2;
37 priority['('] = 0;
38 priority[')'] = 0;
39
40 stack<char> operatorStack; // Stack to store operators
41 stack<int> operandStack; // Stack to store operands
42 int n = s.size();
43
44 // Process each character in the expression
45 for (int i = 0; i < n; i++) {
46 char currentChar = s[i];
47
48 // Skip whitespace
49 if (currentChar == ' ') {
50 continue;
51 }
52
53 // Process numeric values
54 if (isdigit(currentChar)) {
55 int number = currentChar - '0';
56
57 // Handle multi-digit numbers
58 while (i + 1 < n && isdigit(s[i + 1])) {
59 i++;
60 number = number * 10 + (s[i] - '0');
61 }
62
63 operandStack.push(number);
64 }
65 // Process operators and parentheses
66 else {
67 // Push operator if stack is empty, it's an opening parenthesis,
68 // or it has higher priority than stack top
69 if (operatorStack.empty() || currentChar == '(' ||
70 priority[currentChar] > priority[operatorStack.top()]) {
71
72 // Handle unary operators at the beginning
73 if (operandStack.empty() && (currentChar == '-' || currentChar == '+')) {
74 operandStack.push(0);
75 }
76
77 operatorStack.push(currentChar);
78
79 // Handle unary operators after opening parenthesis
80 if (currentChar == '(') {
81 int j = i;
82 while (j + 1 < n) {
83 if (s[j + 1] == '-' || s[j + 1] == '+') {
84 operandStack.push(0);
85 break;
86 }
87 if (s[j + 1] != ' ') {
88 break;
89 }
90 j++;
91 }
92 }
93 }
94 // Process closing parenthesis
95 else if (currentChar == ')') {
96 // Evaluate all operations until matching opening parenthesis
97 char topOperator = operatorStack.top();
98 operatorStack.pop();
99
100 while (topOperator != '(') {
101 int secondOperand = operandStack.top();
102 operandStack.pop();
103 int firstOperand = operandStack.top();
104 operandStack.pop();
105
106 operandStack.push(operate(secondOperand, topOperator, firstOperand));
107
108 topOperator = operatorStack.top();
109 operatorStack.pop();
110 }
111 }
112 // Process operators with lower or equal priority
113 else if (priority[currentChar] <= priority[operatorStack.top()]) {
114 // Evaluate all higher or equal priority operations
115 char topOperator = operatorStack.top();
116
117 while (!operatorStack.empty() &&
118 priority[currentChar] <= priority[operatorStack.top()] &&
119 topOperator != '(') {
120
121 operatorStack.pop();
122 int secondOperand = operandStack.top();
123 operandStack.pop();
124 int firstOperand = operandStack.top();
125 operandStack.pop();
126
127 operandStack.push(operate(secondOperand, topOperator, firstOperand));
128
129 if (!operatorStack.empty()) {
130 topOperator = operatorStack.top();
131 } else {
132 break;
133 }
134 }
135
136 operatorStack.push(currentChar);
137 }
138 }
139 }
140
141 // Process any remaining operations in the stack
142 while (!operatorStack.empty()) {
143 char topOperator = operatorStack.top();
144 operatorStack.pop();
145
146 int secondOperand = operandStack.top();
147 operandStack.pop();
148 int firstOperand = operandStack.top();
149 operandStack.pop();
150
151 operandStack.push(operate(secondOperand, topOperator, firstOperand));
152 }
153
154 // Return the final result
155 return operandStack.top();
156 }
157};
158
1/**
2 * Performs mathematical operation based on the given operator
3 * @param b - Second operand
4 * @param op - Operator character (+, -, *, /)
5 * @param a - First operand
6 * @returns Result of the operation a op b
7 */
8function operate(b: number, op: string, a: number): number {
9 switch (op) {
10 case '+':
11 return a + b;
12 case '-':
13 return a - b;
14 case '*':
15 return a * b;
16 case '/':
17 return Math.floor(a / b); // Integer division
18 default:
19 return 0;
20 }
21}
22
23/**
24 * Evaluates a mathematical expression string containing +, -, *, /, and parentheses
25 * @param s - The expression string to evaluate
26 * @returns The calculated result of the expression
27 */
28function calculate(s: string): number {
29 // Initialize operator priority map
30 const priority: { [key: string]: number } = {
31 '+': 1,
32 '-': 1,
33 '*': 2,
34 '/': 2,
35 '(': 0,
36 ')': 0
37 };
38
39 const operatorStack: string[] = []; // Stack to store operators
40 const operandStack: number[] = []; // Stack to store operands
41 const n = s.length;
42
43 // Process each character in the expression
44 for (let i = 0; i < n; i++) {
45 const currentChar = s[i];
46
47 // Skip whitespace
48 if (currentChar === ' ') {
49 continue;
50 }
51
52 // Process numeric values
53 if (/\d/.test(currentChar)) {
54 let number = parseInt(currentChar);
55
56 // Handle multi-digit numbers
57 while (i + 1 < n && /\d/.test(s[i + 1])) {
58 i++;
59 number = number * 10 + parseInt(s[i]);
60 }
61
62 operandStack.push(number);
63 }
64 // Process operators and parentheses
65 else {
66 // Push operator if stack is empty, it's an opening parenthesis,
67 // or it has higher priority than stack top
68 if (operatorStack.length === 0 || currentChar === '(' ||
69 priority[currentChar] > priority[operatorStack[operatorStack.length - 1]]) {
70
71 // Handle unary operators at the beginning
72 if (operandStack.length === 0 && (currentChar === '-' || currentChar === '+')) {
73 operandStack.push(0);
74 }
75
76 operatorStack.push(currentChar);
77
78 // Handle unary operators after opening parenthesis
79 if (currentChar === '(') {
80 let j = i;
81 while (j + 1 < n) {
82 if (s[j + 1] === '-' || s[j + 1] === '+') {
83 operandStack.push(0);
84 break;
85 }
86 if (s[j + 1] !== ' ') {
87 break;
88 }
89 j++;
90 }
91 }
92 }
93 // Process closing parenthesis
94 else if (currentChar === ')') {
95 // Evaluate all operations until matching opening parenthesis
96 let topOperator = operatorStack.pop()!;
97
98 while (topOperator !== '(') {
99 const secondOperand = operandStack.pop()!;
100 const firstOperand = operandStack.pop()!;
101
102 operandStack.push(operate(secondOperand, topOperator, firstOperand));
103
104 topOperator = operatorStack.pop()!;
105 }
106 }
107 // Process operators with lower or equal priority
108 else if (priority[currentChar] <= priority[operatorStack[operatorStack.length - 1]]) {
109 // Evaluate all higher or equal priority operations
110 let topOperator = operatorStack[operatorStack.length - 1];
111
112 while (operatorStack.length > 0 &&
113 priority[currentChar] <= priority[operatorStack[operatorStack.length - 1]] &&
114 topOperator !== '(') {
115
116 operatorStack.pop();
117 const secondOperand = operandStack.pop()!;
118 const firstOperand = operandStack.pop()!;
119
120 operandStack.push(operate(secondOperand, topOperator, firstOperand));
121
122 if (operatorStack.length > 0) {
123 topOperator = operatorStack[operatorStack.length - 1];
124 } else {
125 break;
126 }
127 }
128
129 operatorStack.push(currentChar);
130 }
131 }
132 }
133
134 // Process any remaining operations in the stack
135 while (operatorStack.length > 0) {
136 const topOperator = operatorStack.pop()!;
137
138 const secondOperand = operandStack.pop()!;
139 const firstOperand = operandStack.pop()!;
140
141 operandStack.push(operate(secondOperand, topOperator, firstOperand));
142 }
143
144 // Return the final result
145 return operandStack[operandStack.length - 1];
146}
147
Time and Space Complexity
Time Complexity: O(n)
The algorithm processes each character in the input string exactly once. The main operations performed are:
- Traversing through the deque with
q.popleft()
- each character is visited once - Digit parsing with
num = num * 10 + int(c)
-O(1)
per digit - Stack operations (
append
,pop
) -O(1)
per operation - The recursive call for parentheses doesn't add extra iterations, it just continues processing the same string
- The final
sum(stk)
operation isO(k)
wherek
is the number of elements in the stack, butk ≤ n
Since each character is processed exactly once with constant-time operations, the overall time complexity is O(n)
where n
is the length of the input string.
Space Complexity: O(n)
The space usage comes from:
- The deque created from the input string -
O(n)
- The stack
stk
used in each recursive call - in the worst case (all additions/subtractions), it can holdO(n)
elements - The recursive call stack depth - in the worst case with deeply nested parentheses like
((((1))))
, the depth can beO(n)
The dominant factor is the deque and the potential recursive depth, both of which can be O(n)
in the worst case. Therefore, the overall space complexity is O(n)
.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Integer Division Truncation Issues
Pitfall: Using //
(floor division) instead of int(a/b)
for integer division can give incorrect results for negative numbers.
# Incorrect - floor division rounds down (toward negative infinity)
-5 // 2 # Returns -3 (wrong!)
# Correct - truncation toward zero
int(-5 / 2) # Returns -2 (correct!)
Why it matters: The problem specifically requires truncation toward zero, not floor division. For negative results, these operations differ:
- Floor division:
-5 // 2 = -3
(rounds to negative infinity) - Truncation:
int(-5 / 2) = -2
(rounds toward zero)
Solution: Always use int(operand_stack.pop() / current_number)
for division operations.
2. Whitespace Handling
Pitfall: The current implementation doesn't handle spaces in the input string, which will cause incorrect parsing.
# This will fail: "2 + 3 * 4" # Spaces will break number parsing
Solution: Add a condition to skip whitespace characters:
while char_queue: char = char_queue.popleft() # Skip whitespace if char == " ": continue # Rest of the logic...
3. Empty Expression or Invalid Input
Pitfall: The code doesn't handle edge cases like empty strings or expressions starting with operators.
# These cases might cause errors: "" # Empty string "*5" # Starting with operator "()" # Empty parentheses
Solution: Add validation and default handling:
def calculate(self, s: str) -> int:
# Handle empty string
if not s or not s.strip():
return 0
# Continue with normal processing...
4. Division by Zero
Pitfall: The code doesn't check for division by zero, which will raise an exception.
"5/0" # Will raise ZeroDivisionError "10/(3-3)" # Also results in division by zero
Solution: Add a check before performing division:
elif operator == "/":
if current_number == 0:
# Handle division by zero - could return 0, infinity, or raise custom error
raise ValueError("Division by zero")
operand_stack.append(int(operand_stack.pop() / current_number))
5. Character Validation
Pitfall: Invalid characters in the expression aren't handled and might cause unexpected behavior.
"2+3a" # Invalid character 'a' "2++3" # Consecutive operators
Solution: Add character validation:
if char not in "0123456789+-*/() ":
raise ValueError(f"Invalid character: {char}")
These pitfalls are common in expression evaluation problems and addressing them makes the solution more robust for real-world use cases.
Which of the following is a min heap?
Recommended Readings
Stack Intro Imagine you have a pile of books on your desk If you want to add a new book you place it on top If you want to read a book you take it from the top And if you simply want to see which book is on the top you
Recursion Recursion is one of the most important concepts in computer science Simply speaking recursion is the process of a function calling itself Using a real life analogy imagine a scenario where you invite your friends to lunch https assets algo monster recursion jpg You first call Ben and ask
Math for Technical Interviews How much math do I need to know for technical interviews The short answer is about high school level math Computer science is often associated with math and some universities even place their computer science department under the math faculty However the reality is that you
Want a Structured Path to Master System Design Too? Don’t Miss This!