Valid Brackets and Expression Evaluation
26 min · Intermediate
Need help? Ask a tutor on WhatsApp (opens in a new tab)Brackets in a code line open and close in a nested order, just like function calls. A stack keeps the open ones in order, so it checks them in one pass. The same idea evaluates postfix and plain arithmetic.
The last opener must close first
In {[()]} the bracket opened last is (, and it must close first. A stack remembers the openers. Each closer must match the top.
Example 1 · Brute force: remove pairs again and again
def is_valid_slow(text): while "()" in text or "[]" in text or "{}" in text: text = text.replace("()", "").replace("[]", "").replace("{}", "") return text == ""
print(is_valid_slow("{[()]}"))print(is_valid_slow("([)]"))True False
What this code does
- Removing touching pairs repeatedly leaves an empty text when the brackets are valid.
- Each pass scans the text and there can be n / 2 passes, so the time is O(n squared). The stack does it in one pass.
Valid brackets in code
Push every opener. For a closer, the stack must not be empty and its top must be the matching opener. At the end the stack must be empty.
def is_valid(text): pairs = {")": "(", "]": "[", "}": "{"} stack = [] for ch in text: if ch in "([{": stack.append(ch) elif ch in pairs: if not stack or stack.pop() != pairs[ch]: return False return not stackExample 2 · Valid brackets with a stack
def is_valid(text): pairs = {")": "(", "]": "[", "}": "{"} stack = [] for ch in text: if ch in "([{": stack.append(ch) elif ch in pairs: if not stack or stack.pop() != pairs[ch]: return False return not stack
for text in ["{[()]}", "([)]", "((", "())", "total(a[1]) + b"]: print(text, is_valid(text)){[()]} True
([)] False
(( False
()) False
total(a[1]) + b TrueWhat this code does
([)]fails because)meets[on top.((fails because the stack is not empty at the end.- Letters and spaces are ignored, so the same function checks a code line.
Example 3 · Watch the stack grow and shrink
stack = []for ch in "{[()]}": if ch in "([{": stack.append(ch) else: stack.pop() print(ch, "".join(stack)){ {
[ {[
( {[(
) {[
] {
}What this code does
- The depth rises to three and falls back to zero. This is the pattern drawn in the picture.
- Each printed line shows the openers that are still waiting.
Example 4 · Minimum brackets to add
def min_add(text): open_count = 0 added = 0 for ch in text: if ch == "(": open_count += 1 elif open_count: open_count -= 1 else: added += 1 return added + open_count
print(min_add("())"))print(min_add("((("))print(min_add("()()"))1 3 0
What this code does
- With one bracket type, a counter replaces the stack. An unmatched
)needs an opener, and each left over(needs a closer. - This is O(n) time and O(1) space. With several bracket types you need the real stack.
A dict maps each closer to its opener. The function runs in O(n) time and uses O(n) space for the stack.
Postfix expressions
Postfix notation writes an operator after its two operands, so 3 4 + means 3 plus 4. It needs no brackets and one stack can evaluate it from left to right.
In 3 4 + 2 * each operator comes after its two numbers. Read left to right, push numbers, and let an operator pop two numbers and push the result.
def eval_postfix(tokens): stack = [] for token in tokens: if token in "+-*": right = stack.pop() left = stack.pop() if token == "+": stack.append(left + right) elif token == "-": stack.append(left - right) else: stack.append(left * right) else: stack.append(int(token)) return stack[0]Example 5 · Evaluate a postfix expression
def eval_postfix(tokens): stack = [] for token in tokens: if token in "+-*": right = stack.pop() left = stack.pop() if token == "+": stack.append(left + right) elif token == "-": stack.append(left - right) else: stack.append(left * right) else: stack.append(int(token)) return stack[0]
print(eval_postfix("3 4 + 2 *".split()))print(eval_postfix("10 2 8 * + 3 -".split()))14 23
What this code does
3 4 + 2 *is (3 + 4) * 2 = 14.10 2 8 * + 3 -is 10 + 16 - 3 = 23.- Each token is pushed or used once, so the time is O(n).
Example 6 · The order of operands matters
stack = [10, 4]right = stack.pop()left = stack.pop()print(left - right)print(right - left)6 -6
What this code does
- The first pop is the right operand. For
10 4 -the answer is 10 - 4 = 6, not -6. - The same rule holds for division. Always take the right operand first.
The first value popped is the right operand. Subtraction and division depend on that order.
Infix with + - * /
For 2+3*4, multiplication must happen before addition. Keep a stack of terms to add. A * or / changes the top term at once. A + or - pushes a new signed term. The answer is the sum of the stack.
def calc(text): stack, num, op = [], 0, "+" for ch in text + "+": if ch.isdigit(): num = num * 10 + int(ch) elif ch != " ": if op in "+-": stack.append(num if op == "+" else -num) elif op == "*": stack.append(stack.pop() * num) else: stack.append(int(stack.pop() / num)) num, op = 0, ch return sum(stack)Example 7 · Calculator for + - * /
def calc(text): stack, num, op = [], 0, "+" for ch in text + "+": if ch.isdigit(): num = num * 10 + int(ch) elif ch != " ": if op == "+": stack.append(num) elif op == "-": stack.append(-num) elif op == "*": stack.append(stack.pop() * num) else: stack.append(int(stack.pop() / num)) num, op = 0, ch return sum(stack)
print(calc("2+3*4"))print(calc(" 14 - 6 / 4 "))print(calc("100*2-50"))14 13 150
What this code does
- The extra
+at the end flushes the last number. Spaces are skipped. 14 - 6 / 4is 14 - 1 = 13 because the division truncates.
int(a / b) truncates toward zero. This is fine for small numbers. Floor division // would round -7 / 2 down to -4.
Next step
Check your understanding
11 quick questions