DSA sheet · Stack · Expression evaluation pattern

Basic Calculator II

This is the first problem of the second stack pattern in the sheet: expression evaluation. We get a maths expression as a string, like "3+2*2", and we must compute its value ourselves, following the rule that * and / are done before + and −. The teacher first solves it with a slow but natural two-list brute force, explains a surprise (why it still passes on LeetCode), and then shows the one-pass stack solution that interviewers expect. The stack idea here (keep the last operator, settle a number only when the next operator arrives) comes back again in every calculator-style problem.

Every part below follows the same order:
① the question in simple words → ② what the constraints tell us → ③ intuition → ④ building the logic from examples → ⑤ approach steps → ⑥ code → ⑦ code line by line → ⑧ dry run → ⑨ complexity & remember

Part 0 · Stacks and expression evaluation from scratch

What is a stack?

A stack is a pile of plates. You can only put a plate on the top (push) and only take the plate from the top (pop). Looking at the top plate without removing it is called peek. The last plate you put on is the first one you take off. This rule is called LIFO: Last In, First Out.

The Python list as a stack

stack operationPythoncost
push xst.append(x)O(1)
pop the topst.pop()O(1)
peek at the topst[-1]O(1)
is it empty?not stO(1)

We always write a stack left = bottom, right = top. So [3, 4] means 3 is at the bottom and 4 is on top.

  [3, 4]      top →  | 4 |
                     | 3 |  ← bottom
                     +---+
Doubt: what happens if I pop or peek an empty stack?
→ Python raises IndexError. So whenever the stack might be empty, check if st: first. In this problem the input is always a valid expression, so the stack always has a number when we pop (we'll see why in Part B).

The pattern on this page: expression evaluation

Reading a multi-digit number from a string

The string gives digits one character at a time. For "19" we first see '1', then '9'. To build the number: start with num = 0, and for each digit do num = num * 10 + digit. So 0 → 0*10+1 = 1 → 1*10+9 = 19. Shifting left by one place (×10) makes room for the new digit.

In Java the teacher writes ch - '0' to turn the character into its number (it subtracts ASCII codes). In Python we simply write int(ch).

Python trap: division must truncate toward zero

The problem says integer division truncates toward zero: drop the decimal part. 7/2 = 3.5 → 3, and −7/2 = −3.5 → −3.

expressiontrue valuea // b (floor, rounds DOWN)int(a / b) (cuts toward 0)
7 / 23.533
−7 / 2−3.5−4−3

For positive numbers both agree. As soon as one side is negative, // goes the wrong way. In Python always write int(a / b) for this problem. In Part B a negative number really does reach the division, so this matters.


Part A · Brute force: a numbers list and an operators list

LeetCode 227

1The question in simple words

You get a string s holding a valid expression with non-negative whole numbers, the four signs + - * /, and maybe some spaces. Return its value as an integer.

"3+2*2"2*2 first = 4, then 3 + 4 = 7
" 3/2 "3/2 = 1.5 → truncate → 1 (spaces ignored)
" 3+5 / 2 "5/2 = 2, then 3 + 2 = 5

2What the constraints tell us

3Intuition: do it the way you would on paper

On paper you'd scan the expression, do every * and / first, then go back and do the + and −. The trouble: when you stand on a *, you need the number before it and the number after it. The one before you already passed, so you could have kept it somewhere. The one after you haven't read yet.

The teacher's simple fix: first split the string into two lists, one list of numbers and one list of operators. Once everything is laid out in lists, both neighbours of every operator are easy to reach by index.

index012
nums322
ops+*ops[i] sits between nums[i] and nums[i+1]

Notice: there is always one operator fewer than numbers. And operator ops[i] always works on nums[i] and nums[i+1].

4Building the logic

Step 1: split the string

Doubt 1: after the loop, why do we add num to the list once more?
→ A number is only saved when we hit an operator after it. The last number (the final 2 in "3+2*2") has no operator after it. When the loop ends it's still sitting in num, so we must append it by hand.

Step 2: first pass, only * and /

Look at each operator. If it's + or −, leave it alone and move on (BODMAS says not yet). If it's * or /, compute nums[i] * nums[i+1] (or the division).

Doubt 2: where do I keep the result? Can't I just store it in a variable result?
→ No, because the result may still be used by the next operator. In "3+2*2/2", 2*2 = 4 is not final: it still has to be divided by 2. So the teacher writes the result back into the list: put 4 at nums[i], delete nums[i+1], and delete ops[i] (that operator is done). Now the list looks as if the expression had been "3+4/2" from the start, and the next operator finds 4 as its left neighbour.
before322+*
after34+2*2 written into nums[1]; nums[2] and ops[1] removed
Doubt 3: after removing, should i move forward?
→ No. After the removal, the next operator slides into position i, and its left number is the result we just wrote at nums[i]. If we did i += 1 we would skip it. For example in "2*3*4": after 2*3 → nums [6, 4], ops [*]. The second * is now at index 0, the same i. That's why the Python code uses a while loop and only moves i forward when it leaves a + or − alone.

Step 3: second pass, + and −

Now only + and − are left. Do the exact same "combine, write back, remove" loop with + and −, left to right. When it finishes, the list has one number. That's the answer: nums[0].

Doubt 4: why two separate loops? Why not do everything in one go?
→ Because of precedence. If we added while scanning, "3+2*2" would give (3+2)*2 = 10. All the * and / must be finished before any + or − happens, so they get their own earlier pass.

5Approach steps

  1. Scan the string: skip spaces, build numbers digit by digit, and on every operator push the finished number to nums and the operator to ops, then reset num.
  2. After the scan, push the last number.
  3. Pass 1: for each operator that is * or /, compute with its two neighbours, write the result at nums[i], remove nums[i+1] and ops[i], and stay at the same i. Otherwise move on.
  4. Pass 2: do the same for + and −.
  5. Return nums[0].

6Code (Python)

Brute force with two lists
class Solution:
    def calculate(self, s: str) -> int:
        nums, ops = [], []
        num = 0
        for ch in s:
            if ch == ' ':
                continue                      # spaces mean nothing
            if ch.isdigit():
                num = num * 10 + int(ch)      # build multi-digit numbers
            else:                             # an operator
                nums.append(num)              # the number before it is complete
                ops.append(ch)
                num = 0                       # start reading a new number
        nums.append(num)                      # the last number has no operator after it

        # pass 1: * and /  (higher precedence)
        i = 0
        while i < len(ops):
            if ops[i] == '*' or ops[i] == '/':
                if ops[i] == '*':
                    result = nums[i] * nums[i + 1]
                else:
                    result = int(nums[i] / nums[i + 1])   # truncate toward zero
                nums[i] = result              # write the result back
                nums.pop(i + 1)               # the right number is used up
                ops.pop(i)                    # the operator is used up
                # do NOT move i: the next operator slid into position i
            else:
                i += 1                        # + or -: leave it for pass 2

        # pass 2: + and -
        i = 0
        while i < len(ops):
            if ops[i] == '+':
                nums[i] = nums[i] + nums[i + 1]
            else:
                nums[i] = nums[i] - nums[i + 1]
            nums.pop(i + 1)
            ops.pop(i)
        return nums[0]

In pass 2 every operator gets used, so i stays at 0 the whole time and we keep folding the list from the left.

7Code line by line

linewhat it means
if ch == ' ': continueSpaces can appear anywhere, even between digits of different numbers. They carry no meaning.
num = num * 10 + int(ch)Shift the old digits one place left and add the new digit. "411" → 4 → 41 → 411.
nums.append(num) ops.append(ch) num = 0An operator ends the current number. Save both, then reset for the next number.
nums.append(num) (after the loop)Save the last number, which no operator followed.
result = int(nums[i] / nums[i + 1])Division that cuts toward zero (see Part 0). Here everything is non-negative, so // would also work, but keeping int(a / b) everywhere is the safe habit.
nums[i] = result nums.pop(i + 1) ops.pop(i)Replace the pair by its result and delete the used operator. The lists get shorter by one.
else: i += 1A + or − is skipped in pass 1. Only now does i move.
return nums[0]After both passes, one number is left: the answer.

8Dry run: s = "3+2*2/2+1"

stepiops[i]actionnums afterops after
splitread the string[3, 2, 2, 2, 1][+, *, /, +]
pass 10+skip, i → 1[3, 2, 2, 2, 1][+, *, /, +]
pass 11*2*2 = 4, write at 1, remove[3, 4, 2, 1][+, /, +]
pass 11/4/2 = 2 (same i!)[3, 2, 1][+, +]
pass 11+skip, i → 2 = len(ops), stop[3, 2, 1][+, +]
pass 20+3+2 = 5[5, 1][+]
pass 20+5+1 = 6[6][]

Answer 6 ✓. Look at the row with /: if we had stored 4 in a separate variable and moved on, the division would have used the old 2 instead of 4. Writing back into the list is what keeps the chain correct.

9Complexity & the "why did it pass?" surprise

Doubt: with n = 3 × 10⁵, O(n²) should give TLE. Why did the teacher's submission pass, and even look fast?
→ Because of how LeetCode's tests are made. The removals only happen once per operator, not once per character. In the tests, numbers are often long ("411" is 3 characters, "10000" is 5) and many operators are + or −. So the real number of removals is small. Passing does not mean it's optimal. In an interview, call it O(n · k), worst case O(n²), and move on to the stack solution.
Remember the brute forceSplit into nums and ops (one fewer op). Pass 1: * and /, write the result back at nums[i], remove nums[i+1] and ops[i], don't move i. Pass 2: + and −. Answer = nums[0]. Removal shifts the list → O(n²) worst case.

Part B · Optimal: one pass with a stack and the "last operator"

1The question in simple words

Same question as Part A. Now we want one left-to-right scan, with no list removals.

2What the constraints tell us

3Intuition: settle a number only when the NEXT operator arrives

In the brute force we waited for + and − and only acted on * and /. Let's keep that idea but do it on the fly.

But when do we know a number is complete? A number like 411 comes digit by digit. We only know it's finished when we reach the next operator. So at every operator we settle the previous number using the previous operator. To combine two numbers we need three things:

piecewhere it lives
the left numberthe top of the stack
the operator between themthe variable op (the last operator we saw)
the right numberthe variable num (just finished)
the triggerwe are standing on the next operator

4Building the logic from "3+2*2"

  1. Start: stack = [], num = 0, op = '+'.
  2. Read 3 → num = 3.
  3. Read +. The number 3 is complete. Its sign (op) is + → push 3. Now update op = '+' (the operator we're standing on) and reset num = 0.
  4. Read 2 → num = 2.
  5. Read *. The 2 is complete. Its sign is + → push 2. Stack [3, 2]. Now op = '*', num = 0.
  6. Read 2 → num = 2.
  7. The string ends. But this last 2 has no next operator to trigger it!
Doubt 1: how do we settle the last number?
→ The teacher's trick: pretend there is an extra + at the end of the string. She runs the loop one step further (i goes up to len(s), included) and treats that extra position as a '+'. That fake operator triggers the last settlement: op is * → pop 2, 2*2 = 4, push 4. Stack [3, 4]. Sum = 7 ✓. (It doesn't matter which sign we pretend, because it's never used after that.)
Doubt 2: why push -num for minus instead of remembering the minus somewhere?
→ Because then the final step is just "add everything in the stack". 10 − 3 becomes [10, −3] → 10 + (−3) = 7. No sign bookkeeping at the end.
Doubt 3: the stack can hold a negative number. Does that break * or /?
→ It works, because the minus belongs to that number anyway. In "14-3/2" the stack is [14, −3] when we reach /. Then −3 / 2 = −1.5 → truncate → −1, and 14 + (−1) = 13 ✓ (14 − 1). But in Python, -3 // 2 is −2, which would give 12 ✗. This is exactly why we must use int(a / b).
Doubt 4: in the loop, how do we tell an operator from a space?
→ If the character is a digit, build the number. Otherwise, if it is not a space, it must be an operator (the input only has digits, spaces and the four signs). Spaces fall through both checks and are ignored.
Doubt 5: why don't we combine + and − during the scan too?
→ A + or − number might still be grabbed by a later * or /. In "3+2*2", if we added 3+2 early we'd lose the 2 that the * needs. Keeping +/− numbers separate in the stack and adding them at the very end respects BODMAS.

5Approach steps

  1. stack = [], num = 0, op = '+'.
  2. For i from 0 to len(s) (included): ch = '+' if i == len(s), else s[i].
  3. If ch is a digit → num = num * 10 + int(ch).
  4. Else if ch is not a space (so it's an operator): settle num using the old op: + → push num · − → push −num · * → push pop() * num · / → push int(pop() / num). Then op = ch, num = 0.
  5. Return the sum of the stack.

6Code (Python)

Basic Calculator II with a stack (optimal)
class Solution:
    def calculate(self, s: str) -> int:
        stack = []
        num = 0
        op = '+'                              # the first number counts as "+num"
        n = len(s)
        for i in range(n + 1):                # one extra step for the fake '+'
            ch = '+' if i == n else s[i]
            if ch.isdigit():
                num = num * 10 + int(ch)
            elif ch != ' ':                   # an operator: settle the previous number
                if op == '+':
                    stack.append(num)
                elif op == '-':
                    stack.append(-num)
                elif op == '*':
                    stack.append(stack.pop() * num)
                else:                         # op == '/'
                    stack.append(int(stack.pop() / num))   # truncate toward zero
                op = ch                       # this operator is the sign of the NEXT number
                num = 0
        answer = 0
        while stack:
            answer += stack.pop()
        return answer

The final loop can also be written return sum(stack). On screen the teacher's first run failed because the last branch was missing its divide sign. She fixed it and it passed. When you copy the four branches, check each sign carefully.

7Code line by line

linewhat it means
op = '+'The first number has no sign in front of it. Treating it as + means it simply gets pushed.
for i in range(n + 1): ch = '+' if i == n else s[i]One extra iteration with a fake '+' so the last number gets settled like all the others.
num = num * 10 + int(ch)Grow the current number one digit at a time.
elif ch != ' ':Not a digit and not a space → an operator. The number before it is complete.
stack.append(num) / stack.append(-num)Old sign + or −: this number must wait. Store it with its sign.
stack.append(stack.pop() * num)Old sign *: combine with the number just before it (the top), right now.
stack.append(int(stack.pop() / num))Old sign /: divide the top by num, cutting toward zero. The top can be negative, so // would be wrong here.
op = ch num = 0The operator we're on becomes the sign of the next number. Start a fresh number. (Forgetting op = ch is the step the teacher points out she almost skipped explaining.)
while stack: answer += stack.pop()Only signed "+/−" terms remain. Their sum is the answer.

8Dry run: s = "14-3*2/4+5"

The correct value: 3*2 = 6, 6/4 = 1 (1.5 cut down), so 14 − 1 + 5 = 18. The table shows only the steps where something happens (digits just grow num). "op" is the sign waiting for the current number.

index012345678910
s14-3*2/4+5+yellow = operators (triggers); grey = fake '+'
icurrent charop, num beforewhat we pop (and why)what we pushstack AFTER (bottom → top)op after
0–11, 4+, 0digits: num = 1 → 14–[]+
2-+, 14nothing (old op is +, 14 must wait)14[14]-
33-, 0digit: num = 3–[14]-
4*-, 3nothing (old op is −)−3[14, −3]*
52*, 0digit: num = 2–[14, −3]*
6/*, 2pop −3 (old op is *: it needs its left number)−3 × 2 = −6[14, −6]/
74/, 0digit: num = 4–[14, −6]/
8+/, 4pop −6 (old op is /)int(−6 / 4) = int(−1.5) = −1[14, −1]+
95+, 0digit: num = 5–[14, −1]+
10fake ++, 5nothing (old op is +)5[14, −1, 5]+
endsum the stack: 14 + (−1) + 518 ✓

At i = 8, Python's -6 // 4 would be −2 and the answer would be 17 ✗. The truncation rule matters.

after i = 4
14−3
after i = 6 (−3 popped, −6 pushed)
14−6
after i = 10 (fake +)
14−15

The top of each stack is red. The bottom of the box is the bottom of the stack.

The teacher's example "3+2*2" in short: at '+' push 3 → [3]; at '*' push 2 → [3, 2]; at the fake '+' op is * → pop 2, push 2×2 = 4 → [3, 4]; sum = 7.

9Complexity & remember

RememberKeep op = the sign before the current number (start with '+'). At every operator (and a fake '+' at the end) settle the old number: + push, − push −num, * push pop×num, / push int(pop/num). Then op = ch, num = 0. Answer = sum of the stack.

Part C · Revision page

Brute force (two lists)Stack (one pass)
ideasplit into nums and ops, then do * / pass, then + − passremember the last operator, settle each number when the next operator arrives
how precedence is kepttwo separate passes* and / combine immediately; + and − wait in the stack
where results gowritten back into nums[i], then removalspushed onto the stack
last numberappend after the loopfake '+' at the end
timeO(n · k), worst O(n²) (list removal shifts)O(n)
spaceO(n)O(n)
old opwhat we do with num
+push(num)
−push(-num)
*push(pop() * num)
/push(int(pop() / num))
If you remember only 5 lines 1. * and / before + and −; equal signs go left to right.
2. A number is only complete when the next operator (or the end) arrives.
3. Settle it with the previous operator: +/− push (with sign), * / combine with the top.
4. Fake '+' at the end settles the last number. Answer = sum of the stack.
5. Python: int(a / b), never //, because the top can be negative.
Mistakes to avoid ✗ using // (wrong for −3/2: gives −2, need −1)
✗ forgetting the last number (no operator after it)
✗ forgetting op = ch and num = 0 after settling
✗ treating a space as an operator
✗ in the brute force, moving i forward after a removal
✗ thinking the brute force is O(n) because it passed on LeetCode
test it yourself (paste under either solution)
s = Solution()
print(s.calculate("3+2*2"))         # 7
print(s.calculate(" 3/2 "))         # 1
print(s.calculate(" 3+5 / 2 "))     # 5
print(s.calculate("14-3/2"))        # 13  (// would give 12)
print(s.calculate("14-3*2/4+5"))    # 18
print(s.calculate("42"))            # 42

Based on this video: Basic Calculator II