Expression problems
Stacks parse expressions: balanced parentheses and multiple bracket kinds, converting infix to postfix and prefix with the shunting-yard method, evaluating postfix expressions and the role of operator precedence and associativity.
Learning goals
- Check bracket matching with a stack
- Convert infix to postfix and prefix
- Evaluate postfix expressions
- Apply precedence and associativity rules correctly
Prerequisites
Balanced parentheses
A string of parentheses is balanced when every opener is closed in the right order. Scan left to right: push each opener, and for each closer pop an opener (failing if the stack is empty). The string is balanced if the stack is empty at the end. For a single bracket type a counter is enough, but a stack generalises. O(n) time.
Run the balanced and unclosed-opener presets.
1static boolean balanced(String s) {2int depth = 0;3for (char c : s.toCharArray()) {4if (c == '(') depth++;5else if (c == ')' && --depth < 0) return false; // closer with nothing open6}7return depth == 0;8}
Balanced brackets with a stack
Interactive · edit the inputchar[] s · length 6
stack of openers (top is last)
Push every opener; each closer must match the most recent unclosed opener, which is exactly the top of the stack.
1static boolean isBalanced(String s) {2Deque<Character> stack = new ArrayDeque<>();3for (char c : s.toCharArray()) {4if (c == '(' || c == '[' || c == '{') stack.push(c); // remember the opener5else if (c == ')' || c == ']' || c == '}') {6if (stack.isEmpty()) return false; // a closer with nothing open7char open = stack.pop(); // the most recent opener must match8if (!matches(open, c)) return false;9}10}11return stack.isEmpty(); // leftover openers are unmatched12}
Live state
Execution trace (dry run table)Show 8 stepsHide
| Step | Line | State | What happens |
|---|---|---|---|
| 1 | L2 | Push every opener; each closer must match the most recent unclosed opener, which is exactly the top of the stack. | |
| 2 | L4 | i=0 c='{' depth=1 | '{' opens: push it. Depth 1. |
| 3 | L4 | i=1 c='[' depth=2 | '[' opens: push it. Depth 2. |
| 4 | L4 | i=2 c='(' depth=3 | '(' opens: push it. Depth 3. |
| 5 | L7 | i=3 c=')' depth=2 | ')' closes '(' from index 2: pop. Depth 2. |
| 6 | L7 | i=4 c=']' depth=1 | ']' closes '[' from index 1: pop. Depth 1. |
| 7 | L7 | i=5 c='}' depth=0 | '}' closes '{' from index 0: pop. Depth 0. |
| 8 | L11 | return=true | Every opener was closed in the right order and the stack is empty: balanced. O(n) time, O(n) space. |
Multi-bracket matching
With several bracket kinds a counter is not enough: ([)] has matching counts but wrong nesting. Push each opener and require each closer to match the kind of the top opener. Characters that are not brackets are ignored. The stack depth is the nesting depth at any point.
The wrong-kind preset ([)] fails even though each kind balances on its own.
1static boolean isBalanced(String s) {2Deque<Character> stack = new ArrayDeque<>();3for (char c : s.toCharArray()) {4if (c == '(' || c == '[' || c == '{') stack.push(c);5else if (c == ')' || c == ']' || c == '}') {6if (stack.isEmpty()) return false;7char open = stack.pop();8if ((c == ')' && open != '(') || (c == ']' && open != '[') || (c == '}' && open != '{')) return false;9}10}11return stack.isEmpty();12}
Balanced brackets with a stack
Interactive · edit the inputchar[] s · length 6
stack of openers (top is last)
Push every opener; each closer must match the most recent unclosed opener, which is exactly the top of the stack.
1static boolean isBalanced(String s) {2Deque<Character> stack = new ArrayDeque<>();3for (char c : s.toCharArray()) {4if (c == '(' || c == '[' || c == '{') stack.push(c); // remember the opener5else if (c == ')' || c == ']' || c == '}') {6if (stack.isEmpty()) return false; // a closer with nothing open7char open = stack.pop(); // the most recent opener must match8if (!matches(open, c)) return false;9}10}11return stack.isEmpty(); // leftover openers are unmatched12}
Live state
Execution trace (dry run table)Show 8 stepsHide
| Step | Line | State | What happens |
|---|---|---|---|
| 1 | L2 | Push every opener; each closer must match the most recent unclosed opener, which is exactly the top of the stack. | |
| 2 | L4 | i=0 c='{' depth=1 | '{' opens: push it. Depth 1. |
| 3 | L4 | i=1 c='[' depth=2 | '[' opens: push it. Depth 2. |
| 4 | L4 | i=2 c='(' depth=3 | '(' opens: push it. Depth 3. |
| 5 | L7 | i=3 c=')' depth=2 | ')' closes '(' from index 2: pop. Depth 2. |
| 6 | L7 | i=4 c=']' depth=1 | ']' closes '[' from index 1: pop. Depth 1. |
| 7 | L7 | i=5 c='}' depth=0 | '}' closes '{' from index 0: pop. Depth 0. |
| 8 | L11 | return=true | Every opener was closed in the right order and the stack is empty: balanced. O(n) time, O(n) space. |
Infix-to-postfix
In postfix (reverse Polish) notation operators follow their operands, so no parentheses or precedence rules are needed to evaluate it. Dijkstra's shunting-yard algorithm converts infix: operands go straight to the output; an operator first pops every stacked operator of higher precedence (or equal precedence if it is left-associative) to the output and is then pushed; ( is pushed; ) pops until the matching (. At the end pop everything. O(n).
Convert 3+4*(2-1) and watch which operators wait on the stack.
1static String toPostfix(String s) {2StringBuilder out = new StringBuilder(); Deque<Character> ops = new ArrayDeque<>();3for (char c : s.toCharArray()) {4if (Character.isDigit(c)) out.append(c);5else if (c == '(') ops.push(c);6else if (c == ')') { while (ops.peek() != '(') out.append(ops.pop()); ops.pop(); }7else {8while (!ops.isEmpty() && ops.peek() != '(' && (prec(ops.peek()) > prec(c) || (prec(ops.peek()) == prec(c) && c != '^')))9out.append(ops.pop());10ops.push(c);11}12}13while (!ops.isEmpty()) out.append(ops.pop());14return out.toString();15}16static int prec(char op) { return op == '^' ? 3 : (op == '*' || op == '/') ? 2 : 1; }
Expression conversion and evaluation with stacks
Interactive · edit the inputchar[] expression · length 9
operator stack (top is last)
output
Shunting-yard: digits go straight to the output; operators wait on a stack until a weaker operator or a closing parenthesis forces them out.
1static String toPostfix(String s) {2StringBuilder out = new StringBuilder(); Deque<Character> ops = new ArrayDeque<>();3for (char c : s.toCharArray()) {4if (Character.isDigit(c)) out.append(c); // operands go straight to the output5else if (c == '(') ops.push(c);6else if (c == ')') { while (ops.peek() != '(') out.append(ops.pop()); ops.pop(); }7else { // an operator8while (!ops.isEmpty() && ops.peek() != '(' && (prec(ops.peek()) > prec(c) || (prec(ops.peek()) == prec(c) && c != '^')))9out.append(ops.pop()); // stronger (or equal, if left-associative) goes first10ops.push(c);11}12}13while (!ops.isEmpty()) out.append(ops.pop());14return out.toString();15}16static int evalPostfix(String p) {17Deque<Integer> st = new ArrayDeque<>();18for (char c : p.toCharArray()) {19if (Character.isDigit(c)) st.push(c - '0');20else { int b = st.pop(), a = st.pop(); st.push(apply(c, a, b)); } // the right operand was pushed last21}22return st.pop();23}24// infix to prefix: reverse the string, swap '(' and ')', run toPostfix with the pop test25// 'prec(top) > prec(c) || (prec(top) == prec(c) && c == \'^\')', then reverse the result
Live state
Execution trace (dry run table)Show 12 stepsHide
| Step | Line | State | What happens |
|---|---|---|---|
| 1 | L2 | Shunting-yard: digits go straight to the output; operators wait on a stack until a weaker operator or a closing parenthesis forces them out. | |
| 2 | L4 | c='3' output=3 | 3 is an operand: append to the output. |
| 3 | L8 | c='+' output=3 | '+' binds tighter than the stack top (or the stack has no operator): push it. |
| 4 | L4 | c='4' output=34 | 4 is an operand: append to the output. |
| 5 | L8 | c='*' output=34 | '*' binds tighter than the stack top (or the stack has no operator): push it. |
| 6 | L5 | c='(' output=34 | '(' always goes on the stack. |
| 7 | L4 | c='2' output=342 | 2 is an operand: append to the output. |
| 8 | L8 | c='-' output=342 | '-' binds tighter than the stack top (or the stack has no operator): push it. |
| 9 | L4 | c='1' output=3421 | 1 is an operand: append to the output. |
| 10 | L6 | c=')' output=3421- | ')' pops operators to the output until '(' is found (-), then discards the pair. |
| 11 | L13 | output=3421-*+ | End of input: pop every remaining operator to the output. |
| 12 | L14 | return="3421-*+" | Postfix: "3421-*+". Operands keep their order and operators follow their operands. |
Infix-to-prefix
In prefix (Polish) notation each operator comes before its operands. The simple method: reverse the infix string, swap every ( with ), convert with the shunting-yard algorithm but pop only on strictly higher precedence (plus equal precedence for the right-associative ^), then reverse the result. Alternatively build an expression tree and print it in preorder.
Choose Infix to prefix in the animation.
1static String toPrefix(String infix) {2StringBuilder rev = new StringBuilder();3for (int i = infix.length() - 1; i >= 0; i--) {4char c = infix.charAt(i);5rev.append(c == '(' ? ')' : c == ')' ? '(' : c);6}7StringBuilder out = new StringBuilder(); Deque<Character> ops = new ArrayDeque<>();8for (char c : rev.toString().toCharArray()) {9if (Character.isDigit(c)) out.append(c);10else if (c == '(') ops.push(c);11else if (c == ')') { while (ops.peek() != '(') out.append(ops.pop()); ops.pop(); }12else {13while (!ops.isEmpty() && ops.peek() != '(' && (prec(ops.peek()) > prec(c) || (prec(ops.peek()) == prec(c) && c == '^')))14out.append(ops.pop());15ops.push(c);16}17}18while (!ops.isEmpty()) out.append(ops.pop());19return out.reverse().toString();20}
Expression conversion and evaluation with stacks
Interactive · edit the inputchar[] expression · length 9
operator stack (top is last)
output
Shunting-yard: digits go straight to the output; operators wait on a stack until a weaker operator or a closing parenthesis forces them out.
1static String toPostfix(String s) {2StringBuilder out = new StringBuilder(); Deque<Character> ops = new ArrayDeque<>();3for (char c : s.toCharArray()) {4if (Character.isDigit(c)) out.append(c); // operands go straight to the output5else if (c == '(') ops.push(c);6else if (c == ')') { while (ops.peek() != '(') out.append(ops.pop()); ops.pop(); }7else { // an operator8while (!ops.isEmpty() && ops.peek() != '(' && (prec(ops.peek()) > prec(c) || (prec(ops.peek()) == prec(c) && c != '^')))9out.append(ops.pop()); // stronger (or equal, if left-associative) goes first10ops.push(c);11}12}13while (!ops.isEmpty()) out.append(ops.pop());14return out.toString();15}16static int evalPostfix(String p) {17Deque<Integer> st = new ArrayDeque<>();18for (char c : p.toCharArray()) {19if (Character.isDigit(c)) st.push(c - '0');20else { int b = st.pop(), a = st.pop(); st.push(apply(c, a, b)); } // the right operand was pushed last21}22return st.pop();23}24// infix to prefix: reverse the string, swap '(' and ')', run toPostfix with the pop test25// 'prec(top) > prec(c) || (prec(top) == prec(c) && c == \'^\')', then reverse the result
Live state
Execution trace (dry run table)Show 12 stepsHide
| Step | Line | State | What happens |
|---|---|---|---|
| 1 | L2 | Shunting-yard: digits go straight to the output; operators wait on a stack until a weaker operator or a closing parenthesis forces them out. | |
| 2 | L4 | c='3' output=3 | 3 is an operand: append to the output. |
| 3 | L8 | c='+' output=3 | '+' binds tighter than the stack top (or the stack has no operator): push it. |
| 4 | L4 | c='4' output=34 | 4 is an operand: append to the output. |
| 5 | L8 | c='*' output=34 | '*' binds tighter than the stack top (or the stack has no operator): push it. |
| 6 | L5 | c='(' output=34 | '(' always goes on the stack. |
| 7 | L4 | c='2' output=342 | 2 is an operand: append to the output. |
| 8 | L8 | c='-' output=342 | '-' binds tighter than the stack top (or the stack has no operator): push it. |
| 9 | L4 | c='1' output=3421 | 1 is an operand: append to the output. |
| 10 | L6 | c=')' output=3421- | ')' pops operators to the output until '(' is found (-), then discards the pair. |
| 11 | L13 | output=3421-*+ | End of input: pop every remaining operator to the output. |
| 12 | L14 | return="3421-*+" | Postfix: "3421-*+". Operands keep their order and operators follow their operands. |
Postfix evaluation
Evaluate postfix with one stack: push operands; on an operator pop the two top values (the right operand is the one popped first), apply the operator and push the result. A valid expression leaves exactly one value. The division operator needs the operands in the right order, and Java's integer division truncates toward zero.
Choose Evaluate a postfix expression and try 23*54*+.
1static int evalPostfix(String p) {2Deque<Integer> st = new ArrayDeque<>();3for (char c : p.toCharArray()) {4if (Character.isDigit(c)) st.push(c - '0');5else {6int b = st.pop(), a = st.pop();7st.push(switch (c) { case '+' -> a + b; case '-' -> a - b; case '*' -> a * b; default -> a / b; });8}9}10return st.pop();11}
Expression conversion and evaluation with stacks
Interactive · edit the inputchar[] expression · length 9
operator stack (top is last)
output
Shunting-yard: digits go straight to the output; operators wait on a stack until a weaker operator or a closing parenthesis forces them out.
1static String toPostfix(String s) {2StringBuilder out = new StringBuilder(); Deque<Character> ops = new ArrayDeque<>();3for (char c : s.toCharArray()) {4if (Character.isDigit(c)) out.append(c); // operands go straight to the output5else if (c == '(') ops.push(c);6else if (c == ')') { while (ops.peek() != '(') out.append(ops.pop()); ops.pop(); }7else { // an operator8while (!ops.isEmpty() && ops.peek() != '(' && (prec(ops.peek()) > prec(c) || (prec(ops.peek()) == prec(c) && c != '^')))9out.append(ops.pop()); // stronger (or equal, if left-associative) goes first10ops.push(c);11}12}13while (!ops.isEmpty()) out.append(ops.pop());14return out.toString();15}16static int evalPostfix(String p) {17Deque<Integer> st = new ArrayDeque<>();18for (char c : p.toCharArray()) {19if (Character.isDigit(c)) st.push(c - '0');20else { int b = st.pop(), a = st.pop(); st.push(apply(c, a, b)); } // the right operand was pushed last21}22return st.pop();23}24// infix to prefix: reverse the string, swap '(' and ')', run toPostfix with the pop test25// 'prec(top) > prec(c) || (prec(top) == prec(c) && c == \'^\')', then reverse the result
Live state
Execution trace (dry run table)Show 12 stepsHide
| Step | Line | State | What happens |
|---|---|---|---|
| 1 | L2 | Shunting-yard: digits go straight to the output; operators wait on a stack until a weaker operator or a closing parenthesis forces them out. | |
| 2 | L4 | c='3' output=3 | 3 is an operand: append to the output. |
| 3 | L8 | c='+' output=3 | '+' binds tighter than the stack top (or the stack has no operator): push it. |
| 4 | L4 | c='4' output=34 | 4 is an operand: append to the output. |
| 5 | L8 | c='*' output=34 | '*' binds tighter than the stack top (or the stack has no operator): push it. |
| 6 | L5 | c='(' output=34 | '(' always goes on the stack. |
| 7 | L4 | c='2' output=342 | 2 is an operand: append to the output. |
| 8 | L8 | c='-' output=342 | '-' binds tighter than the stack top (or the stack has no operator): push it. |
| 9 | L4 | c='1' output=3421 | 1 is an operand: append to the output. |
| 10 | L6 | c=')' output=3421- | ')' pops operators to the output until '(' is found (-), then discards the pair. |
| 11 | L13 | output=3421-*+ | End of input: pop every remaining operator to the output. |
| 12 | L14 | return="3421-*+" | Postfix: "3421-*+". Operands keep their order and operators follow their operands. |
Operator precedence
Precedence says which operator binds tighter (* before +); associativity says what happens between operators of the same precedence: - and / are left-associative (a - b - c = (a - b) - c) while exponentiation is right-associative (2^3^2 = 2^(3^2) = 512). The shunting-yard pop condition encodes both: pop while the stack top has higher precedence, or equal precedence and the new operator is left-associative.
Compare 2^3^2 (right-associative) with 8-3-2 (left-associative) in the animation.
| Operator | Precedence | Associativity |
|---|---|---|
| ^ | 3 (highest) | right |
| * / | 2 | left |
| + - | 1 | left |
Expression conversion and evaluation with stacks
Interactive · edit the inputchar[] expression · length 9
operator stack (top is last)
output
Shunting-yard: digits go straight to the output; operators wait on a stack until a weaker operator or a closing parenthesis forces them out.
1static String toPostfix(String s) {2StringBuilder out = new StringBuilder(); Deque<Character> ops = new ArrayDeque<>();3for (char c : s.toCharArray()) {4if (Character.isDigit(c)) out.append(c); // operands go straight to the output5else if (c == '(') ops.push(c);6else if (c == ')') { while (ops.peek() != '(') out.append(ops.pop()); ops.pop(); }7else { // an operator8while (!ops.isEmpty() && ops.peek() != '(' && (prec(ops.peek()) > prec(c) || (prec(ops.peek()) == prec(c) && c != '^')))9out.append(ops.pop()); // stronger (or equal, if left-associative) goes first10ops.push(c);11}12}13while (!ops.isEmpty()) out.append(ops.pop());14return out.toString();15}16static int evalPostfix(String p) {17Deque<Integer> st = new ArrayDeque<>();18for (char c : p.toCharArray()) {19if (Character.isDigit(c)) st.push(c - '0');20else { int b = st.pop(), a = st.pop(); st.push(apply(c, a, b)); } // the right operand was pushed last21}22return st.pop();23}24// infix to prefix: reverse the string, swap '(' and ')', run toPostfix with the pop test25// 'prec(top) > prec(c) || (prec(top) == prec(c) && c == \'^\')', then reverse the result
Live state
Execution trace (dry run table)Show 12 stepsHide
| Step | Line | State | What happens |
|---|---|---|---|
| 1 | L2 | Shunting-yard: digits go straight to the output; operators wait on a stack until a weaker operator or a closing parenthesis forces them out. | |
| 2 | L4 | c='3' output=3 | 3 is an operand: append to the output. |
| 3 | L8 | c='+' output=3 | '+' binds tighter than the stack top (or the stack has no operator): push it. |
| 4 | L4 | c='4' output=34 | 4 is an operand: append to the output. |
| 5 | L8 | c='*' output=34 | '*' binds tighter than the stack top (or the stack has no operator): push it. |
| 6 | L5 | c='(' output=34 | '(' always goes on the stack. |
| 7 | L4 | c='2' output=342 | 2 is an operand: append to the output. |
| 8 | L8 | c='-' output=342 | '-' binds tighter than the stack top (or the stack has no operator): push it. |
| 9 | L4 | c='1' output=3421 | 1 is an operand: append to the output. |
| 10 | L6 | c=')' output=3421- | ')' pops operators to the output until '(' is found (-), then discards the pair. |
| 11 | L13 | output=3421-*+ | End of input: pop every remaining operator to the output. |
| 12 | L14 | return="3421-*+" | Postfix: "3421-*+". Operands keep their order and operators follow their operands. |
Practice ladder
Now try it yourself
Easy
- Is “{[()]}” balanced? Is “([)]”?
- Evaluate the postfix expression 53+2*
Medium
- Convert 8-3-2 and 2^3^2 to postfix and explain the difference
- Convert (1+2)*3-4 to prefix
Advanced
- Extend the evaluator to multi-digit numbers and unary minus
Key takeaways
- Bracket matching: push openers, pop on closers, end empty.
- Shunting-yard turns infix into postfix with one operator stack.
- Postfix evaluation: operands push, operators pop two and push the result.