Stacks & Queues

A stack hands back the newest item first and a queue the oldest, both in O(1). That order alone solves brackets, expression evaluation and BFS.

BeginnerRead first: Big-O & Complexity

How It Works

Stacks and queues are lists you may only touch at the ends. A stack adds and removes at the same end, so the last thing in is the first thing out (LIFO), like a pile of plates. A queue adds at the back and removes from the front, so things leave in the order they arrived (FIFO), like a line at a counter. That restriction is the point: every operation is O(1), and the order things come out often does the work for you.

stack: push and pop at the top

    push   pop
      |     ^
      v     |
   |           |
   |     3     |  <- top
   |     2     |
   |     1     |
   +-----------+

queue: enqueue at the back, dequeue at the front

   dequeue <- [ 1 | 2 | 3 ] <- enqueue
              front   back
Add 1, 2 and 3 to each. The stack gives back 3 first, and the queue gives back 1.

A Stack and a Queue, Side by Side

Step 1 of 8

stack

stack order

queue

queue order

  • Just added or removed
  • Handed back
  • Behind head, already dequeued

Both start empty. You'll add 1, 2 and 3 to each, then take everything back out. They differ only in which end you remove from.

All 8 steps as text
  1. Both start empty. You'll add 1, 2 and 3 to each, then take everything back out. They differ only in which end you remove from.
  2. Push 1 onto the stack and enqueue it at the back of the queue. Both are O(1), since nothing else moves.
  3. Add 2. It goes on top of 1 in the stack and behind 1 in the queue.
  4. Add 3. Both hold 1, 2, 3 now, but the stack removes from the end you just added to, and the queue from the other end.
  5. Pop the top, 3, the newest value. The queue's front is the oldest, 1. Dequeue it by moving head past it, which is O(1) with no shifting.
  6. Next, the stack gives back 2 and the queue gives back 2. Each time, the stack takes the newest value left and the queue the oldest.
  7. Last out: 1 from the stack, the first value you pushed, and 3 from the queue, the last one you enqueued.
  8. Both are empty, since head is past the queue's last slot. The stack handed back [3, 2, 1], newest first, and the queue [1, 2, 3], in arrival order.

Reach for a stack when the most recent unfinished thing is the one you have to deal with next: matching brackets, undo history, evaluating an expression, or exploring as deep as you can before backing up. The call stack is itself a stack, which is why any recursive function can be rewritten as a loop with an explicit one. Reach for a queue when things must be handled in the order they arrive: breadth-first search, task scheduling, or buffering a stream.

Every language gives you both, but check which end is cheap. Adding or removing at the end of an array is O(1), so arrays make good stacks. Removing from the front is where it goes wrong. JavaScript's shift() and Python's list.pop(0) move every remaining element down one slot, so each call is O(n). A queue needs a structure built for it, or an index that marks where the front is.

When to Reach for It

  • Brackets, tags or anything else that opens and closes in nested order.
  • You need the most recent item that hasn't been resolved yet, as in undo or simplifying a file path.
  • There's an expression to evaluate, especially one in postfix (Reverse Polish) form.
  • Things must be handled in arrival order, or level by level, as in breadth-first search.
  • You want to replace recursion with a loop. An explicit stack does the call stack's job.

Complexity

OperationTimeSpace
Push, pop or peek on a stackO(1), amortized for array-backed stacksO(n) for n items
Enqueue, dequeue or peek on a queue or dequeO(1), amortized for array-backed queuesO(n) for n items
Remove from the front of an array (shift(), pop(0))O(n)O(1)
Search for a valueO(n)O(1)

The Template

Stack and Queue Basics

Both functions add the same values and differ only in which end they remove from. The stack hands [1, 2, 3] back as [3, 2, 1], and the queue hands it back unchanged. Each loop peeks first and then removes, the same check-then-act step you'll use whenever a decision depends on the top or the front.

Here's what each language gives you:

  • TypeScript: an array is the stack, with push, pop and stack[stack.length - 1] to peek. For a queue, push onto an array and advance a head index instead of calling shift().
  • Python: a list is the stack, with append, pop() and stack[-1]. For a queue, use collections.deque with append, popleft() and queue[0].
  • Go: slices do both. Pop with stack[:len(stack)-1] and dequeue with queue[1:], which reslices without copying anything.
  • C#: Stack<T> has Push, Pop and Peek, and Queue<T> has Enqueue, Dequeue and Peek.
  • Java: ArrayDeque does both. Use push, pop and peek as a stack, or offer, poll and peek as a queue. Java's own docs recommend it over the old Stack class, and it's usually faster than LinkedList as a queue.

Why not shift()? A JavaScript array keeps its elements at indices 0 to length - 1 with no gaps. shift() removes index 0, so every remaining element has to move down one slot to keep that true, which is n - 1 moves per call. Dequeuing n items that way costs O(n²) in total. Engines speed up some cases, mostly small arrays, but you can't count on it. Advancing a head index is O(1) every time. Python's list.pop(0) has the same problem, which is why deque exists.

Time O(1) per operationSpace O(n)

from collections import deque


def stack_order(values: list[int]) -> list[int]:
    """A stack adds and removes at the same end, so values come back newest first.
    Pushes every value, then peeks at and pops the top until the stack is empty."""
    stack: list[int] = []
    for value in values:
        stack.append(value)  # Push onto the top (the end).
    order = []
    while stack:
        top = stack[-1]  # Peek: read the top without removing it.
        stack.pop()  # Pop: remove the top. O(1), since nothing else moves.
        order.append(top)
    return order


def queue_order(values: list[int]) -> list[int]:
    """A queue adds at the back and removes from the front, so values come back in
    arrival order. Enqueues every value, then peeks at and dequeues the front."""
    queue: deque[int] = deque()  # list.pop(0) would be O(n). A deque pops either end in O(1).
    for value in values:
        queue.append(value)  # Enqueue at the back.
    order = []
    while queue:
        front = queue[0]  # Peek: read the front without removing it.
        queue.popleft()  # Dequeue: remove the front. O(1).
        order.append(front)
    return order


if __name__ == "__main__":
    print(stack_order([1, 2, 3]))  # [3, 2, 1]
    print(queue_order([1, 2, 3]))  # [1, 2, 3]
    print(stack_order([]))  # []
    print(queue_order([]))  # []
// A stack adds and removes at the same end, so values come back newest first.
// Pushes every value, then peeks at and pops the top until the stack is empty.
export function stackOrder(values: number[]): number[] {
  const stack: number[] = [];
  for (const value of values) stack.push(value); // Push onto the top (the end).
  const order: number[] = [];
  while (stack.length > 0) {
    const top = stack[stack.length - 1]; // Peek: read the top without removing it.
    stack.pop(); // Pop: remove the top. O(1), since nothing else moves.
    order.push(top);
  }
  return order;
}

// A queue adds at the back and removes from the front, so values come back in
// arrival order. Enqueues every value, then peeks at and dequeues the front.
export function queueOrder(values: number[]): number[] {
  const queue: number[] = [];
  let head = 0; // Index of the front. queue.shift() would be O(n), so move this instead.
  for (const value of values) queue.push(value); // Enqueue at the back.
  const order: number[] = [];
  while (head < queue.length) {
    const front = queue[head]; // Peek: read the front without removing it.
    head++; // Dequeue: step past the front. O(1).
    order.push(front);
  }
  return order;
}

console.log(JSON.stringify(stackOrder([1, 2, 3]))); // [3,2,1]
console.log(JSON.stringify(queueOrder([1, 2, 3]))); // [1,2,3]
console.log(JSON.stringify(stackOrder([]))); // []
console.log(JSON.stringify(queueOrder([]))); // []
package main

import "fmt"

// stackOrder pushes every value, then peeks at and pops the top until the
// stack is empty. A stack adds and removes at the same end, so values come
// back newest first.
func stackOrder(values []int) []int {
	stack := []int{}
	for _, value := range values {
		stack = append(stack, value) // Push onto the top (the end).
	}
	order := []int{}
	for len(stack) > 0 {
		top := stack[len(stack)-1]   // Peek: read the top without removing it.
		stack = stack[:len(stack)-1] // Pop: shrink the slice by one. O(1).
		order = append(order, top)
	}
	return order
}

// queueOrder enqueues every value, then peeks at and dequeues the front until
// the queue is empty. A queue adds at the back and removes from the front, so
// values come back in arrival order.
func queueOrder(values []int) []int {
	queue := []int{}
	for _, value := range values {
		queue = append(queue, value) // Enqueue at the back.
	}
	order := []int{}
	for len(queue) > 0 {
		front := queue[0] // Peek: read the front without removing it.
		queue = queue[1:] // Dequeue: reslice past the front. O(1), nothing is copied.
		order = append(order, front)
	}
	return order
}

func main() {
	fmt.Println(stackOrder([]int{1, 2, 3})) // [3 2 1]
	fmt.Println(queueOrder([]int{1, 2, 3})) // [1 2 3]
	fmt.Println(stackOrder([]int{}))        // []
	fmt.Println(queueOrder([]int{}))        // []
}
using System;
using System.Collections.Generic;

public class Solution
{
    // A stack adds and removes at the same end, so values come back newest first.
    // Pushes every value, then peeks at and pops the top until the stack is empty.
    public static List<int> StackOrder(int[] values)
    {
        var stack = new Stack<int>();
        foreach (int value in values) stack.Push(value); // Push onto the top.
        var order = new List<int>();
        while (stack.Count > 0)
        {
            int top = stack.Peek(); // Peek: read the top without removing it.
            stack.Pop(); // Pop: remove the top. O(1), since nothing else moves.
            order.Add(top);
        }
        return order;
    }

    // A queue adds at the back and removes from the front, so values come back in
    // arrival order. Enqueues every value, then peeks at and dequeues the front.
    public static List<int> QueueOrder(int[] values)
    {
        var queue = new Queue<int>();
        foreach (int value in values) queue.Enqueue(value); // Enqueue at the back.
        var order = new List<int>();
        while (queue.Count > 0)
        {
            int front = queue.Peek(); // Peek: read the front without removing it.
            queue.Dequeue(); // Dequeue: remove the front. O(1).
            order.Add(front);
        }
        return order;
    }

    public static void Main()
    {
        Console.WriteLine(Format(StackOrder(new[] { 1, 2, 3 }))); // [3, 2, 1]
        Console.WriteLine(Format(QueueOrder(new[] { 1, 2, 3 }))); // [1, 2, 3]
        Console.WriteLine(Format(StackOrder(new int[0]))); // []
        Console.WriteLine(Format(QueueOrder(new int[0]))); // []
    }

    static string Format(List<int> values) => "[" + string.Join(", ", values) + "]";
}
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Deque;
import java.util.List;

public class Solution {
    // A stack adds and removes at the same end, so values come back newest first.
    // Pushes every value, then peeks at and pops the top until the stack is empty.
    static List<Integer> stackOrder(int[] values) {
        Deque<Integer> stack = new ArrayDeque<>();
        for (int value : values) stack.push(value); // Push onto the top.
        List<Integer> order = new ArrayList<>();
        while (!stack.isEmpty()) {
            int top = stack.peek(); // Peek: read the top without removing it.
            stack.pop(); // Pop: remove the top. O(1), since nothing else moves.
            order.add(top);
        }
        return order;
    }

    // A queue adds at the back and removes from the front, so values come back in
    // arrival order. Enqueues every value, then peeks at and dequeues the front.
    static List<Integer> queueOrder(int[] values) {
        Deque<Integer> queue = new ArrayDeque<>();
        for (int value : values) queue.offer(value); // Enqueue at the back.
        List<Integer> order = new ArrayList<>();
        while (!queue.isEmpty()) {
            int front = queue.peek(); // Peek: read the front without removing it.
            queue.poll(); // Dequeue: remove the front. O(1).
            order.add(front);
        }
        return order;
    }

    public static void main(String[] args) {
        System.out.println(stackOrder(new int[] {1, 2, 3})); // [3, 2, 1]
        System.out.println(queueOrder(new int[] {1, 2, 3})); // [1, 2, 3]
        System.out.println(stackOrder(new int[] {})); // []
        System.out.println(queueOrder(new int[] {})); // []
    }
}

Worked Problems

Each solution stays folded until you open it, so you can try the problem first. Every one is a complete program in Python, TypeScript, Go, C# and Java, run and checked against its expected output.

Valid Parentheses

EASYLeetCode 20

Problem

You're given a string made only of the characters ()[]{}. Return whether it's balanced: every opening bracket is closed by a bracket of the same type, and pairs close in the right order, so they nest instead of crossing.

Example: ([]){} is valid. ([)] is not, because ) arrives while [ is still open.

Show Solution

Approach

The most recently opened bracket has to close first, which is exactly the order a stack gives you. Push each opener. When a closer arrives, pop the top and check that it's the matching opener. If the stack is empty, or the top is the wrong type, the string is invalid.

char  stack
(     (
[     ( [
]     (       ] closes [
)     empty   ) closes (
{     {
}     empty   } closes {
end   empty   valid
The stack is written bottom to top. Each closer pops the most recent opener.

Matching Brackets With a Stack

Step 1 of 8

s

stack

  • Current character
  • Open, waiting for its closer
  • Matched pair

Read s left to right with an empty stack. Each opener waits on the stack until its closer arrives.

All 8 steps as text
  1. Read s left to right with an empty stack. Each opener waits on the stack until its closer arrives.
  2. ( is an opener, so push it. It waits for a ).
  3. [ is an opener too. It goes on top of (, so it has to close first.
  4. ] is a closer. Pop the top, [: it matches, so the pair closes. ( is on top again.
  5. ) is a closer. Pop the top, (: it matches, so the pair closes. Nothing is open now.
  6. { is an opener, so push it. It waits for a }.
  7. } is a closer. Pop the top, {: it matches, so the pair closes. Nothing is open now.
  8. The scan is done and the stack is empty, so every opener was closed in order. ([]){} is valid: return true.

Two failures are easy to miss. A closer can arrive when nothing is open, as in ], so check for an empty stack before you pop. And openers can be left over at the end, as in ((, so the string is only valid if the stack ends up empty.

Time O(n)Space O(n)

# Each closing bracket and the opener it must match.
PAIRS = {")": "(", "]": "[", "}": "{"}


def is_valid(s: str) -> bool:
    """Whether every bracket in `s` is closed by the same type, in the right order."""
    stack: list[str] = []
    for ch in s:
        opener = PAIRS.get(ch)
        if opener is None:
            stack.append(ch)  # An opener waits on the stack for its closer.
        elif not stack or stack.pop() != opener:
            return False  # A closer must match the most recent unclosed opener.
    return not stack  # Anything left was never closed.


if __name__ == "__main__":
    print(is_valid("([]){}"))  # True
    print(is_valid("([)]"))  # False
    print(is_valid("(("))  # False
    print(is_valid("]"))  # False
// Each closing bracket and the opener it must match.
const PAIRS = new Map([
  [")", "("],
  ["]", "["],
  ["}", "{"],
]);

// Whether every bracket in s is closed by the same type, in the right order.
export function isValid(s: string): boolean {
  const stack: string[] = [];
  for (const ch of s) {
    const opener = PAIRS.get(ch);
    if (opener === undefined) {
      stack.push(ch); // An opener waits on the stack for its closer.
    } else if (stack.length === 0 || stack.pop() !== opener) {
      return false; // A closer must match the most recent unclosed opener.
    }
  }
  return stack.length === 0; // Anything left was never closed.
}

console.log(isValid("([]){}")); // true
console.log(isValid("([)]")); // false
console.log(isValid("((")); // false
console.log(isValid("]")); // false
package main

import "fmt"

// pairs maps each closing bracket to the opener it must match.
var pairs = map[rune]rune{')': '(', ']': '[', '}': '{'}

// isValid reports whether every bracket in s is closed by the same type, in
// the right order.
func isValid(s string) bool {
	stack := []rune{}
	for _, ch := range s {
		opener, isCloser := pairs[ch]
		if !isCloser {
			stack = append(stack, ch) // An opener waits on the stack for its closer.
		} else if len(stack) == 0 || stack[len(stack)-1] != opener {
			return false // A closer must match the most recent unclosed opener.
		} else {
			stack = stack[:len(stack)-1]
		}
	}
	return len(stack) == 0 // Anything left was never closed.
}

func main() {
	fmt.Println(isValid("([]){}")) // true
	fmt.Println(isValid("([)]"))   // false
	fmt.Println(isValid("(("))     // false
	fmt.Println(isValid("]"))      // false
}
using System;
using System.Collections.Generic;

public class Solution
{
    // Each closing bracket and the opener it must match.
    static readonly Dictionary<char, char> Pairs = new() { [')'] = '(', [']'] = '[', ['}'] = '{' };

    // Whether every bracket in s is closed by the same type, in the right order.
    public static bool IsValid(string s)
    {
        var stack = new Stack<char>();
        foreach (char ch in s)
        {
            if (!Pairs.TryGetValue(ch, out char opener))
            {
                stack.Push(ch); // An opener waits on the stack for its closer.
            }
            else if (stack.Count == 0 || stack.Pop() != opener)
            {
                return false; // A closer must match the most recent unclosed opener.
            }
        }
        return stack.Count == 0; // Anything left was never closed.
    }

    public static void Main()
    {
        Console.WriteLine(IsValid("([]){}")); // True
        Console.WriteLine(IsValid("([)]")); // False
        Console.WriteLine(IsValid("((")); // False
        Console.WriteLine(IsValid("]")); // False
    }
}
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.Map;

public class Solution {
    // Each closing bracket and the opener it must match.
    static final Map<Character, Character> PAIRS = Map.of(')', '(', ']', '[', '}', '{');

    // Whether every bracket in s is closed by the same type, in the right order.
    static boolean isValid(String s) {
        Deque<Character> stack = new ArrayDeque<>();
        for (char ch : s.toCharArray()) {
            Character opener = PAIRS.get(ch);
            if (opener == null) {
                stack.push(ch); // An opener waits on the stack for its closer.
            } else if (stack.isEmpty() || !stack.pop().equals(opener)) {
                return false; // A closer must match the most recent unclosed opener.
            }
        }
        return stack.isEmpty(); // Anything left was never closed.
    }

    public static void main(String[] args) {
        System.out.println(isValid("([]){}")); // true
        System.out.println(isValid("([)]")); // false
        System.out.println(isValid("((")); // false
        System.out.println(isValid("]")); // false
    }
}

Evaluate Reverse Polish Notation

MEDIUMLeetCode 150

Problem

In Reverse Polish Notation every operator comes after its two operands, so 3 4 + means 3 + 4, and parentheses are never needed. Given the tokens of a valid expression that uses +, -, * and / on integers, return its value. Division truncates toward zero.

Example: the tokens 2 1 + 3 * mean (2 + 1) * 3, which is 9.

Show Solution

Approach

Read the tokens left to right with a stack of numbers. Push each number. When an operator arrives, its operands are the two most recent values, so pop them, apply the operator, and push the result. At the end, the one value left is the answer.

token  stack
2      2
1      2 1
+      3        2 + 1
3      3 3
*      9        3 * 3
Each operator pops two values and pushes one. The last value left is the answer.

Order matters for - and /. The right operand was pushed last, so it comes off first: pop b, then a, and compute a - b. Division needs care too. Truncating toward zero is what integer division already does in Go, C# and Java. In TypeScript, / keeps the fraction, so wrap it in Math.trunc. In Python, // rounds down instead (-7 // 2 is -4), so use int(a / b).

Time O(n)Space O(n)

OPERATORS = {"+", "-", "*", "/"}


def eval_rpn(tokens: list[str]) -> int:
    """Evaluates an expression in Reverse Polish Notation, where each operator
    comes after its two operands. Division truncates toward zero."""
    stack: list[int] = []
    for token in tokens:
        if token in OPERATORS:
            # The right operand was pushed last, so it comes off first.
            b = stack.pop()
            a = stack.pop()
            stack.append(apply(a, b, token))
        else:
            stack.append(int(token))
    return stack.pop()  # A valid expression leaves exactly one value.


def apply(a: int, b: int, op: str) -> int:
    if op == "+":
        return a + b
    if op == "-":
        return a - b
    if op == "*":
        return a * b
    return int(a / b)  # Not a // b, which rounds down: -7 // 2 is -4, not -3.


if __name__ == "__main__":
    print(eval_rpn(["2", "1", "+", "3", "*"]))  # 9
    print(eval_rpn(["4", "13", "5", "/", "+"]))  # 6
    print(eval_rpn(["10", "6", "9", "3", "+", "-11", "*", "/", "*", "17", "+", "5", "+"]))  # 22
    print(eval_rpn(["-7", "2", "/"]))  # -3
const OPERATORS = new Set(["+", "-", "*", "/"]);

// Evaluates an expression in Reverse Polish Notation, where each operator
// comes after its two operands. Division truncates toward zero.
export function evalRPN(tokens: string[]): number {
  const stack: number[] = [];
  for (const token of tokens) {
    if (OPERATORS.has(token)) {
      // The right operand was pushed last, so it comes off first.
      const b = stack.pop()!;
      const a = stack.pop()!;
      stack.push(apply(a, b, token));
    } else {
      stack.push(Number(token));
    }
  }
  return stack.pop()!; // A valid expression leaves exactly one value.
}

function apply(a: number, b: number, op: string): number {
  switch (op) {
    case "+":
      return a + b;
    case "-":
      return a - b;
    case "*":
      return a * b;
    default:
      return Math.trunc(a / b); // Not Math.floor, which rounds down: -3.5 would become -4.
  }
}

console.log(evalRPN(["2", "1", "+", "3", "*"])); // 9
console.log(evalRPN(["4", "13", "5", "/", "+"])); // 6
console.log(evalRPN(["10", "6", "9", "3", "+", "-11", "*", "/", "*", "17", "+", "5", "+"])); // 22
console.log(evalRPN(["-7", "2", "/"])); // -3
package main

import (
	"fmt"
	"strconv"
)

var operators = map[string]bool{"+": true, "-": true, "*": true, "/": true}

// evalRPN evaluates an expression in Reverse Polish Notation, where each
// operator comes after its two operands. Division truncates toward zero.
func evalRPN(tokens []string) int {
	stack := []int{}
	for _, token := range tokens {
		if operators[token] {
			// The right operand was pushed last, so it comes off first.
			b, a := stack[len(stack)-1], stack[len(stack)-2]
			stack = stack[:len(stack)-2]
			stack = append(stack, apply(a, b, token))
		} else {
			n, _ := strconv.Atoi(token) // Every other token is a valid integer.
			stack = append(stack, n)
		}
	}
	return stack[len(stack)-1] // A valid expression leaves exactly one value.
}

func apply(a, b int, op string) int {
	switch op {
	case "+":
		return a + b
	case "-":
		return a - b
	case "*":
		return a * b
	default:
		return a / b // Integer division in Go already truncates toward zero.
	}
}

func main() {
	fmt.Println(evalRPN([]string{"2", "1", "+", "3", "*"}))                                             // 9
	fmt.Println(evalRPN([]string{"4", "13", "5", "/", "+"}))                                            // 6
	fmt.Println(evalRPN([]string{"10", "6", "9", "3", "+", "-11", "*", "/", "*", "17", "+", "5", "+"})) // 22
	fmt.Println(evalRPN([]string{"-7", "2", "/"}))                                                      // -3
}
using System;
using System.Collections.Generic;

public class Solution
{
    static readonly HashSet<string> Operators = new() { "+", "-", "*", "/" };

    // Evaluates an expression in Reverse Polish Notation, where each operator
    // comes after its two operands. Division truncates toward zero.
    public static int EvalRpn(string[] tokens)
    {
        var stack = new Stack<int>();
        foreach (string token in tokens)
        {
            if (Operators.Contains(token))
            {
                // The right operand was pushed last, so it comes off first.
                int b = stack.Pop();
                int a = stack.Pop();
                stack.Push(Apply(a, b, token));
            }
            else
            {
                stack.Push(int.Parse(token));
            }
        }
        return stack.Pop(); // A valid expression leaves exactly one value.
    }

    static int Apply(int a, int b, string op) => op switch
    {
        "+" => a + b,
        "-" => a - b,
        "*" => a * b,
        _ => a / b, // Integer division in C# already truncates toward zero.
    };

    public static void Main()
    {
        Console.WriteLine(EvalRpn(new[] { "2", "1", "+", "3", "*" })); // 9
        Console.WriteLine(EvalRpn(new[] { "4", "13", "5", "/", "+" })); // 6
        Console.WriteLine(EvalRpn(new[] { "10", "6", "9", "3", "+", "-11", "*", "/", "*", "17", "+", "5", "+" })); // 22
        Console.WriteLine(EvalRpn(new[] { "-7", "2", "/" })); // -3
    }
}
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.Set;

public class Solution {
    static final Set<String> OPERATORS = Set.of("+", "-", "*", "/");

    // Evaluates an expression in Reverse Polish Notation, where each operator
    // comes after its two operands. Division truncates toward zero.
    static int evalRPN(String[] tokens) {
        Deque<Integer> stack = new ArrayDeque<>();
        for (String token : tokens) {
            if (OPERATORS.contains(token)) {
                // The right operand was pushed last, so it comes off first.
                int b = stack.pop();
                int a = stack.pop();
                stack.push(apply(a, b, token));
            } else {
                stack.push(Integer.parseInt(token));
            }
        }
        return stack.pop(); // A valid expression leaves exactly one value.
    }

    static int apply(int a, int b, String op) {
        return switch (op) {
            case "+" -> a + b;
            case "-" -> a - b;
            case "*" -> a * b;
            default -> a / b; // Integer division in Java already truncates toward zero.
        };
    }

    public static void main(String[] args) {
        System.out.println(evalRPN(new String[] {"2", "1", "+", "3", "*"})); // 9
        System.out.println(evalRPN(new String[] {"4", "13", "5", "/", "+"})); // 6
        System.out.println(evalRPN(new String[] {"10", "6", "9", "3", "+", "-11", "*", "/", "*", "17", "+", "5", "+"})); // 22
        System.out.println(evalRPN(new String[] {"-7", "2", "/"})); // -3
    }
}

Common Pitfalls

  • Popping or peeking an empty stack. Check first, as when a closing bracket arrives before any opener.
  • Using shift() or list.pop(0) to dequeue. Each call is O(n), so a BFS over n nodes quietly becomes O(n²).
  • Popping operands in the wrong order. In a b -, b comes off the stack first, so compute a - b, not b - a.
  • Forgetting to check that the stack is empty at the end. (( never fails a match, but it's still invalid.
  • Using Python's // when a problem says to truncate toward zero. -7 // 2 is -4, not -3.

Practice

More problems to try on your own.