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
- Part A · Brute force: a numbers list and an operators list
- Part B · Optimal: one pass with a stack and the "last operator"
- Part C · Revision page
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 operation | Python | cost |
|---|---|---|
| push x | st.append(x) | O(1) |
| pop the top | st.pop() | O(1) |
| peek at the top | st[-1] | O(1) |
| is it empty? | not st | O(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
+---+
→ 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
- Operands are the numbers (3, 2, 2). Operators are the signs (+, −, *, /).
- Operator precedence means "which sign goes first". * and / have higher precedence than + and −. In school this is the BODMAS rule (Brackets, Orders, Division, Multiplication, Addition, Subtraction). So
3+2*2is 3 + 4 = 7, not 5 * 2 = 10. - Signs with the same precedence are done left to right:
8/2*2= 4 * 2 = 8. - The stack trick: keep a numbers stack. Numbers that must wait (because a + or − is in front of them) sit in the stack. When a * or / shows up, we take the top number out, combine it right away, and put the result back. At the very end, everything left in the stack is just added up.
- The last sign idea: we remember the operator that came before the current number. That operator decides what we do with the number.
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.
| expression | true value | a // b (floor, rounds DOWN) | int(a / b) (cuts toward 0) |
|---|---|---|---|
| 7 / 2 | 3.5 | 3 | 3 |
| −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.
- Follow precedence: * and / before + and −.
- Division truncates toward zero.
- Spaces mean nothing. Ignore them.
- You may not use a built-in that evaluates a string as maths (like Python's
eval). We build it ourselves.
2What the constraints tell us
- 1 ≤ s.length ≤ 3 × 10⁵ → the string is never empty, and it can be long.
- The teacher's usual rule of thumb: around 10⁸ simple operations per second is the limit. Between 10⁸ and 10⁹ the code is very slow, and beyond 10⁹ it surely gives TLE (Time Limit Exceeded). Even 10⁷ can fail if each step is heavy.
- An O(n²) solution here would be (3 × 10⁵)² = 9 × 10¹⁰ → far beyond the limit. So we need about O(n).
- The expression is always valid, and every intermediate result fits in a 32-bit integer → no need to handle broken input or overflow (Python ints never overflow anyway).
- The numbers are non-negative, and there is no unary minus like
"-3+2".
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.
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
- A space → skip it (
continue). - A digit → grow the current number:
num = num * 10 + int(ch). - An operator → the number we were building is complete. Put it in
nums, put the operator inops, and resetnum = 0to start reading the next number.
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).
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.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].
→ 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
- Scan the string: skip spaces, build numbers digit by digit, and on every operator push the finished number to
numsand the operator toops, then resetnum. - After the scan, push the last number.
- Pass 1: for each operator that is * or /, compute with its two neighbours, write the result at
nums[i], removenums[i+1]andops[i], and stay at the samei. Otherwise move on. - Pass 2: do the same for + and −.
- Return
nums[0].
6Code (Python)
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
| line | what it means |
|---|---|
| if ch == ' ': continue | Spaces 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 = 0 | An 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 += 1 | A + 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"
| step | i | ops[i] | action | nums after | ops after |
|---|---|---|---|---|---|
| split | read the string | [3, 2, 2, 2, 1] | [+, *, /, +] | ||
| pass 1 | 0 | + | skip, i → 1 | [3, 2, 2, 2, 1] | [+, *, /, +] |
| pass 1 | 1 | * | 2*2 = 4, write at 1, remove | [3, 4, 2, 1] | [+, /, +] |
| pass 1 | 1 | / | 4/2 = 2 (same i!) | [3, 2, 1] | [+, +] |
| pass 1 | 1 | + | skip, i → 2 = len(ops), stop | [3, 2, 1] | [+, +] |
| pass 2 | 0 | + | 3+2 = 5 | [5, 1] | [+] |
| pass 2 | 0 | + | 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
- At first glance: one loop to split, one pass, another pass → 3n → O(n)? No.
- Removing from the middle of a list is not O(1). If the list is [2, 3, 4, 5, 6, 7] and we remove 3, every element after it shifts one step left and gets a new index (4 was at index 2, now at 1; 5 was at 3, now at 2, …). That shifting is O(n).
- So each * or / costs O(n). If there are k such operators, pass 1 is O(n · k). In the worst case (like
"1*2/3*4/5…") about half the characters are operators, so k ≈ n/2 → O(n²). Pass 2 also removes, with the same worst case. - Space O(n) for the two lists.
→ 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.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
- n up to 3 × 10⁵ → O(n) is what we aim for. One scan plus one final sum is O(n).
- The expression is valid, so every operator has a number on each side. That guarantees the stack is never empty when we pop for * or / (the number before the * was pushed already).
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.
- Think of every number as carrying the sign written in front of it.
"3+2*2"is really+3 +2 *2. The first number has no sign, so we pretend it has a +. That's why the operator variable starts as'+'. - A number with + or − in front of it can't be combined yet (a * may come later and grab it). So we just push it on a stack:
+numor-num. Storing the sign inside the number means at the end we can simply add everything. - A number with * or / in front of it must be combined now, with the number just before it. That number is the top of the stack. Pop it, combine, push the result back.
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:
| piece | where it lives |
|---|---|
| the left number | the top of the stack |
| the operator between them | the variable op (the last operator we saw) |
| the right number | the variable num (just finished) |
| the trigger | we are standing on the next operator |
4Building the logic from "3+2*2"
- Start:
stack = [],num = 0,op = '+'. - Read 3 → num = 3.
- Read +. The number 3 is complete. Its sign (op) is + → push 3. Now update
op = '+'(the operator we're standing on) and resetnum = 0. - Read 2 → num = 2.
- Read *. The 2 is complete. Its sign is + → push 2. Stack [3, 2]. Now
op = '*', num = 0. - Read 2 → num = 2.
- The string ends. But this last 2 has no next operator to trigger it!
→ 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.)-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.
→ 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).→ 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.
→ 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
stack = [],num = 0,op = '+'.- For i from 0 to len(s) (included):
ch = '+'if i == len(s), elses[i]. - If ch is a digit →
num = num * 10 + int(ch). - 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. - Return the sum of the stack.
6Code (Python)
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 answerThe 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
| line | what 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 = 0 | The 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.
| i | current char | op, num before | what we pop (and why) | what we push | stack AFTER (bottom → top) | op after |
|---|---|---|---|---|---|---|
| 0–1 | 1, 4 | +, 0 | digits: num = 1 → 14 | – | [] | + |
| 2 | - | +, 14 | nothing (old op is +, 14 must wait) | 14 | [14] | - |
| 3 | 3 | -, 0 | digit: num = 3 | – | [14] | - |
| 4 | * | -, 3 | nothing (old op is −) | −3 | [14, −3] | * |
| 5 | 2 | *, 0 | digit: num = 2 | – | [14, −3] | * |
| 6 | / | *, 2 | pop −3 (old op is *: it needs its left number) | −3 × 2 = −6 | [14, −6] | / |
| 7 | 4 | /, 0 | digit: num = 4 | – | [14, −6] | / |
| 8 | + | /, 4 | pop −6 (old op is /) | int(−6 / 4) = int(−1.5) = −1 | [14, −1] | + |
| 9 | 5 | +, 0 | digit: num = 5 | – | [14, −1] | + |
| 10 | fake + | +, 5 | nothing (old op is +) | 5 | [14, −1, 5] | + |
| end | sum the stack: 14 + (−1) + 5 | 18 ✓ | ||||
At i = 8, Python's -6 // 4 would be −2 and the answer would be 17 ✗. The truncation rule matters.
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
- Time O(n): one scan over the string, each character handled once. Every push and pop is O(1), and the final sum touches each stack item once.
- Space O(n): in the worst case (all + and −) every number sits in the stack. The brute force also used O(n) for two lists, so we gave up nothing and gained a lot of speed.
- The teacher's interview tip: the stack solution is the truly optimal one, and it's the one the interviewer expects you to explain, with a proper dry run.
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) | |
|---|---|---|
| idea | split into nums and ops, then do * / pass, then + − pass | remember the last operator, settle each number when the next operator arrives |
| how precedence is kept | two separate passes | * and / combine immediately; + and − wait in the stack |
| where results go | written back into nums[i], then removals | pushed onto the stack |
| last number | append after the loop | fake '+' at the end |
| time | O(n · k), worst O(n²) (list removal shifts) | O(n) |
| space | O(n) | O(n) |
| old op | what we do with num |
|---|---|
| + | push(num) |
| − | push(-num) |
| * | push(pop() * num) |
| / | push(int(pop() / num)) |
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.// (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
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")) # 42Based on this video: Basic Calculator II