Stack (LIFO)
A stack is a collection working on the LIFO (Last In, First Out) principle: the last element pushed is the first popped. The mental model is a stack of plates: you only add and remove from the top.
A stack is an abstract data type (ADT) — defined by its behaviour, not by a concrete implementation. Three core operations, all O(1):
- push(x) — put an element on top
- pop() — remove and return the top element
- peek() (or
top) — look at the top without removing it
Plus isEmpty() and size.
A stack's power lies precisely in its restrictions: no removal from the middle, no index access, no search. That restriction states the intent directly in the code and makes misuse impossible.
| Criterion | Array-based | Linked-list based |
|---|---|---|
| push | amortized O(1) — append at the end | O(1) — insert at the head |
| pop / peek | O(1) — from the end | O(1) — from the head |
| Memory | Dense; unused capacity can be wasted | An extra pointer per node |
| Growth | An O(n) copy when capacity fills (rare) | No copying at all |
| Cache locality | Good — faster in practice | Poor |
| Size limit | Bounded by memory | Bounded by memory |
| Practical choice | Default — JS `Array`, Kotlin `ArrayDeque`, Dart `List` | Only for special requirements |
The call stack is the most important real example of a stack. How a program manages function calls is exactly LIFO: a frame is pushed on call and popped on return. The most recently called function returns first.
Understanding this connection explains two things:
- Why a stack trace reads bottom-up — the topmost line is the most recent call
- Why a stack overflow happens — pushes without matching pops fill the bounded stack
And the practical consequence: every recursive algorithm can be written iteratively with an explicit stack. In tree DFS you replace the call stack with an ArrayDeque — the logic is identical, but since the memory is on the heap the depth limit disappears.
Classic use cases — a stack shows up wherever the logic is "close the most recently opened first".
- Bracket matching — push each opening bracket; on a closing one, pop and check it matches. The stack must be empty at the end. This is the core technique in compilers and linters;
O(n)time,O(n)space. - Undo — each action is pushed and undoing pops (with a second stack for redo).
- Expression evaluation — converting infix to postfix (RPN) with the shunting-yard algorithm and evaluating postfix: push operands, and on an operator pop two operands and push the result.
- DFS (depth-first search) — graph/tree traversal; unlike BFS which uses a queue, DFS uses a stack.
- Backtracking — mazes, sudoku, N-Queens: push the state, and on a dead end pop and take another path.
- The browser back button, quote/tag balancing in editors, and monotonic-stack problems (next greater element, largest rectangle in a histogram).
Interview tip. "Check whether brackets are balanced" is the most-asked stack question. A strong answer walks these steps: push opening brackets; on a closing one, first check the stack isn't empty (if it is, return false immediately), then pop and verify the type matches; and when the loop ends, the stack must be empty ("(((" is false). Naming the two edge cases — empty input is true, an extra closing bracket is false — is the care the interviewer is looking for.
A favourite follow-up: "Design a stack with an `O(1)` `getMin()`." The answer: keep a second stack — push the running minimum alongside each push, and pop from both. O(n) memory, all operations O(1). This question tests whether you think in terms of trading memory for time.
The most common mistakes: popping from an empty stack without checking; forgetting to verify the stack is empty at the end; and, when using a JS Array as a stack, reaching for shift()/unshift() (which are O(n)) — the correct pair is push()/pop().
📚 Sources and documentation
- java.util.ArrayDequeofficialdocs.oracle.com
The docs state outright that it beats Stack for stacks and LinkedList for queues.