Stacks
On This Lesson 11 topics
Lesson Overview
A stack is the simplest useful data structure, and one of the most common. It is built on a rule: you may only add to the top and only remove from the top. The picture is a stack of plates in a canteen. You put a clean plate on top, and you take the top plate. You do not pull one from the middle.
The rule sounds like a limitation, but it is the point. Because every operation touches only the top, every operation is O(1), and the order of removal is always "newest first". Many problems have exactly that shape: undo, going back in a browser, and matching brackets.
By the end you will use a stack through Java's ArrayDeque, write a bracket checker, and know why the old Stack class is avoided.
Why this matters: the call stack that runs your Java methods is a stack, and so is the undo history of nearly every editor. Recognizing "newest first" in a problem tells you which structure to reach for.
This lesson builds on Lessons 1 to 3 (arrays and linked nodes can both implement a stack) and on the Queues and deques lesson of the Collections module (BEG-030).
Learning Objectives
By the end of this lesson you will be able to:
- Define a stack and LIFO (last in, first out).
- Use
push,pop, andpeek, and state that each is O(1). - Use
ArrayDequeas a stack, and explain whyDequeis preferred overStack. - Predict what
popandpeekdo on an empty stack. - Implement a bracket-matching checker with a stack.
- Recognize problems that need LIFO order.
Written Explanation
The three operations
| Operation | Meaning | Cost |
|---|---|---|
push(x) | Put x on top | O(1) |
pop() | Remove and return the top item | O(1) |
peek() | Look at the top item without removing it | O(1) |
Worked trace:
push(1) stack (top on the left): 1
push(2) stack: 2 1
pop() -> 2 stack: 1
peek() -> 1 stack: 1 (unchanged)The last item in (2) was the first out. That is LIFO: last in, first out.
Java: use ArrayDeque
The JDK documentation for the Deque interface says deques can be used as LIFO stacks, and that this interface should be used in preference to the legacy Stack class. It maps the stack operations to push, pop, and peek. ArrayDeque is the usual implementation, and its documentation says it is likely to be faster than Stack when used as a stack.
Deque<Integer> stack = new ArrayDeque<>();
stack.push(1);
stack.push(2);
System.out.println(stack.pop()); // 2
System.out.println(stack.peek()); // 1Declare the variable as Deque<Integer> (the interface) and create an ArrayDeque. This follows the habit from the Collections module: code against the interface, choose the implementation in one place.
Two behaviors on an empty stack, which you must know:
pop()throwsNoSuchElementException.peek()returnsnull(it does not throw).
Also, ArrayDeque refuses null items: push(null) throws NullPointerException. That is why peek() can use null to mean "empty" without ambiguity.
Why not java.util.Stack?
Stack is a legacy class from the earliest versions of Java. It is built on Vector, which was designed so that its methods are synchronized, and it exposes list operations such as get(i) that break the "only touch the top" idea. The Deque documentation steers you away from it. In new code, use ArrayDeque.
Implementing a stack yourself
An array stack is an array plus size where the top is data[size - 1]; push is a dynamic-array append (Lesson 2) and pop removes the last item, so both are O(1) (push is amortized). A linked stack is a linked list where push is addFirst and pop is removeFirst (Lesson 3), so both are O(1) worst case. The lab asks you to build one of them.
T pop() {
if (size == 0) throw new NoSuchElementException("stack is empty");
T top = (T) data[--size];
data[size] = null; // let the removed object be collected
return top;
}Problems that are secretly stacks
- Undo: the last action is the first undone.
- Back button: the last page visited is the first you return to.
- The call stack: when
a()callsb(),bmust finish first. In BEG-038 you saw the stack of frames;StackOverflowErrorhappens when it grows too deep. - Matching brackets and nested structures: the most recently opened bracket must be closed first.
- Backtracking: try a path, and if it fails go back to the previous choice.
Bracket matching
Idea for "{[()]}": scan left to right.
- On an opener
(,[,{: push it. - On a closer: if the stack is empty, it is unbalanced; else pop and check that the popped opener matches this closer.
- At the end the stack must be empty; leftovers are unclosed openers.
static boolean balanced(String s) {
Deque<Character> stack = new ArrayDeque<>();
for (char c : s.toCharArray()) {
if (c == '(' || c == '[' || c == '{') {
stack.push(c);
} else if (c == ')' || c == ']' || c == '}') {
if (stack.isEmpty()) return false;
char open = stack.pop();
if ((c == ')' && open != '(') || (c == ']' && open != '[')
|| (c == '}' && open != '{')) return false;
}
}
return stack.isEmpty();
}One pass, O(1) per character, so O(n) overall. Deque<Character> works because char is boxed to Character automatically.
Examples
Example 1: push, pop, peek, and the empty stack (simple)
Deque<Integer> stack = new ArrayDeque<>();
stack.push(1);
stack.push(2);
System.out.println(stack.pop());
System.out.println(stack.peek());
stack.pop();
System.out.println(stack.peek());
stack.pop(); // on an empty stackReal output (the last line is the exception, which the program caught and printed):
2
1
null
pop on empty: java.util.NoSuchElementExceptionCheck the empty case with isEmpty() before pop, or accept the exception when empty means a bug.
Example 2: an undo history (practical)
Deque<String> history = new ArrayDeque<>();
history.push("typed A");
history.push("typed B");
history.push("deleted line");
System.out.println("undo: " + history.pop());
System.out.println("undo: " + history.pop());Output:
undo: deleted line
undo: typed BEach action is pushed as it happens. Undo pops the newest one. After two undos, only "typed A" remains. A real editor stores actions or snapshots rather than strings, but the shape is identical.
Example 3: validating bracket strings (realistic)
A configuration or expression checker calls balanced on each input. Running the method from above gave:
"{[()]}" -> true
"([)]" -> false
"((" -> false
"())" -> false
"" -> trueLook at why each result happens:
"([)]": the)meets[on top, a mismatch."((": the loop ends with two openers still on the stack."())": the second)finds the stack empty."": nothing to match, so the stack is trivially empty: balanced.
This is the real task behind many parsers, from JSON and HTML to arithmetic.
Backend Insight
You will meet stacks in these places:
- Stack traces. When an exception prints a trace, it is listing the call stack, from the most recent call down. Reading one is reading a stack.
- Parsers and validators. Anything that reads nested text (JSON, XML, templates, SQL) tracks nesting, usually with an explicit stack or with recursion, which uses the call stack.
- Undo and rollback. Features that let a user step backward often store operations in a stack.
- Depth control. Recursion that never stops fills the call stack and ends in
StackOverflowError. Converting a deep recursion into a loop with your ownArrayDequeis a standard fix, because your stack lives on the heap and can grow larger (up to available memory).
When you choose a stack, ask: "Do I always need the most recent item first?" If yes, it is a stack.
Common Mistakes
Mistake 1: Using java.util.Stack in new code
- Incorrect:
Stack<Integer> s = new Stack<>(); - Correct:
Deque<Integer> s = new ArrayDeque<>();
Mistake 2: Popping an empty stack
pop() on an empty ArrayDeque throws. Check isEmpty() first, as the bracket checker does before popping a closer.
Mistake 3: Forgetting the final emptiness check
If the loop finishes without a mismatch, "((" would look fine. The checker must return stack.isEmpty(), not true.
Mistake 4: Mixing up the iteration order
Printing an ArrayDeque used as a stack lists items from the top down: after push(1); push(2); push(3) it shows [3, 2, 1]. This is correct but surprising if you expect insertion order.
Exercises
Try each exercise yourself before opening the solutions.
Level A: Foundation
A1. After push(5), push(8), push(3), pop(), pop(), what does peek() return?
Show solution A1
A1. After the pushes the stack is 3, 8, 5 (top first). Two pops remove 3 and 8. peek() returns 5.
A2. What does pop() do on an empty ArrayDeque, and what does peek() do?
Show solution A2
A2. pop() throws NoSuchElementException; peek() returns null.
A3. Choose stack or not-a-stack: (a) the pages you can step back through, (b) the nearest unclosed { in code, (c) people paying at a till.
Show solution A3
A3. (a) stack, (b) stack, (c) not a stack: they are served in arrival order, which is a queue (next lesson).
Level B: Application
B1. Using the checker idea, say whether "[{}]", "{[}]", and "}{" are balanced, and why.
Show solution B1
B1. "[{}]": balanced; { pushed on top of [, } closes {, ] closes [, stack empty. "{[}]": not balanced; } meets [ on top. "}{": not balanced; the first } finds an empty stack.
B2. Write static String reverse(String s) using a Deque<Character>. What is its cost?
Show solution B2
B2.
static String reverse(String s) {
Deque<Character> stack = new ArrayDeque<>();
for (char c : s.toCharArray()) stack.push(c);
StringBuilder sb = new StringBuilder();
while (!stack.isEmpty()) sb.append(stack.pop());
return sb.toString();
}reverse("stack") returned kcats (real run). One push and one pop per character: O(n).
B3. Show the stack contents after each token while evaluating the postfix expression 3 4 + 2 * (numbers are pushed; an operator pops two numbers, applies itself, and pushes the result).
Show solution B3
B3. Tokens: 3 gives 3; 4 gives 4 3 (top first); + pops 4 and 3, pushes 7, giving 7; 2 gives 2 7; * pops 2 and 7, pushes 14, giving 14. The answer is 14.
Level C: Challenge
C1. Write static int evalPostfix(String expr) for expressions like "10 2 8 * + 3 -", with tokens separated by spaces and the operators +, -, *. Careful: for -, the second pop is the left operand.
Show solution C1
C1.
static int evalPostfix(String expr) {
Deque<Integer> stack = new ArrayDeque<>();
for (String token : expr.split(" ")) {
if (token.equals("+") || token.equals("-") || token.equals("*")) {
int b = stack.pop(); // right operand
int a = stack.pop(); // left operand
if (token.equals("+")) stack.push(a + b);
else if (token.equals("-")) stack.push(a - b);
else stack.push(a * b);
} else {
stack.push(Integer.parseInt(token));
}
}
return stack.pop();
}"3 4 + 2 *" gave 14 and "10 2 8 * + 3 -" gave 23 (real run). The second pop is the left operand because it was pushed earlier. The cost is O(n).
C2. Implement ArrayStack<T> with push, pop (throws NoSuchElementException if empty), peek (returns null if empty), and isEmpty, on a growing Object[]. Why set the freed slot to null on pop?
Show solution C2
C2.
static class ArrayStack<T> {
private Object[] data = new Object[2];
private int size;
void push(T item) {
if (size == data.length) data = Arrays.copyOf(data, size * 2);
data[size++] = item;
}
@SuppressWarnings("unchecked")
T pop() {
if (size == 0) throw new NoSuchElementException("stack is empty");
T top = (T) data[--size];
data[size] = null;
return top;
}
@SuppressWarnings("unchecked")
T peek() { return size == 0 ? null : (T) data[size - 1]; }
boolean isEmpty() { return size == 0; }
}Pushing a, b, c then popping twice returned c, b, left a on top, and a pop on the empty stack threw "stack is empty" (real run). Setting the slot to null means the array no longer holds a reference to the removed object, so the garbage collector can free it. The cast is unchecked because Object[] cannot be generic; that is why the annotation is there.
Mini Challenge
Write a tiny text-editor undo model. Keep a StringBuilder text and a Deque<String> of earlier versions. type(String s) pushes the current text and then appends s; undo() pops and restores, doing nothing if the history is empty.
- Type "Hello", then " world", then "!".
- Print the text after each step.
- Undo twice and print again.
Check your work: you expect Hello, Hello world, Hello world!, and after two undos Hello. Note the cost of type in memory: copying whole strings is simple but not how a real editor does it.
Summary & Next Lesson
Key takeaways
- A stack adds and removes at the top only: LIFO;
push,pop,peekare O(1). - In Java use
Deque<T> stack = new ArrayDeque<>(), not the legacyStackclass. pop()on empty throwsNoSuchElementException;peek()on empty returnsnull.- Bracket matching: push openers, pop on closers and compare, and require an empty stack at the end.
- Undo, back, the call stack, and nested parsing are all stack problems.
Next lesson
Next is "Queues". A queue flips the rule: items leave from the front in arrival order, like a line at a shop.
Self-check
- What does LIFO mean?
- Which three operations does a stack offer and what do they cost?
- Why is
ArrayDequepreferred overStack? - What does
peek()return on an emptyArrayDeque? - Why does a bracket checker return
stack.isEmpty()at the end?
Show answers
- Last in, first out: the newest item comes out first.
push,pop,peek; each is O(1).- The JDK documentation recommends
Dequeover the legacyStackclass, andArrayDequeis likely to be faster. null.- Leftover openers mean the string is unbalanced, as in
"((".
References
- Oracle, Java SE 21 API, java.util.Deque. Supports: a deque can be used as a LIFO stack, it should be used in preference to the legacy
Stackclass, andpush,pop, andpeekact on the beginning of the deque. - Oracle, Java SE 21 API, java.util.ArrayDeque. Supports: null elements are prohibited, and it is likely to be faster than
Stackwhen used as a stack. - Outputs were produced by running the code on JDK 21. The statement that
Stackis built onVectorwith synchronized methods is from general Java knowledge and was not re-checked on a fetched page for this lesson.
Practice
Apply this lesson with the module's lab, exercises and quiz.
Marking a lesson complete is optional and you can undo it any time. Your progress stays in this browser.