Facebook Pixel

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 return 14 (not 20, because multiplication happens first)
  • "(2+3)*4" should return 20 (parentheses override normal precedence)
  • "10/3" should return 3 (integer division truncates)

The solution uses a recursive depth-first search approach with a stack. It processes the expression character by character, handling:

  1. Building multi-digit numbers by accumulating digits
  2. Recursively evaluating sub-expressions within parentheses
  3. 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
  4. 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.

Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

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 +: push 2 to stack → stack: [2]
  • See 3 and *: push 3 to stack → stack: [2, 3]
  • See 4 and -: since previous operator was *, we pop 3, multiply by 4, push back 12 → 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.

Learn more about Stack, Recursion and Math patterns.

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:

  1. Initialize State: Start with num = 0, sign = "+", and an empty stack stk.

  2. 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 call dfs() to evaluate the sub-expression and treat the result as num
    • If the character is an operator (+-*/), closing parenthesis ), or we've reached the end of the expression, apply the previous operator
  3. 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.

  4. Handle Parentheses:

    • When encountering (, recursively evaluate the sub-expression
    • When encountering ), break the current loop and return the sum of the stack
    • This naturally handles nested parentheses through recursion
  5. 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, set sign = "*"
  • Encounter (, recursively evaluate "3+4)"
    • In recursion: push 3, then push 4, hit ), return 7
  • Back in main: num = 7, apply *: pop 2, push 2*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 Evaluator

Example 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, then 2 * 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 is O(k) where k is the number of elements in the stack, but k ≤ 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 hold O(n) elements
  • The recursive call stack depth - in the worst case with deeply nested parentheses like ((((1)))), the depth can be O(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.

Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

Which of the following is a min heap?


Recommended Readings

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More