Stacks
Imagine an undo button in a text editor, a browser's back button. Or the way a compiler checks that every open parenthesis has a matching close. Each of these depends on remembering things in reverse order of arrival. The most recent action is the first one you want to reverse. This "last thing in, first thing out" discipline is exactly what a stack formalizes. It turns out to be one of the most quietly ubiquitous ideas in computing. A stack is deceptively simple — push an item on top, pop the top item off. Yet this single restriction is what makes call frames in every programming language work. It's what lets a parser check balanced brackets in linear time. And it's what powers depth-first search across graphs and trees. The restriction is the point. By refusing random access and only ever touching the top, a stack can guarantee every operation runs in constant time. There is no shifting, searching, or rebalancing. This article builds the stack from first principles as an abstract data type (ADT). This is a contract of operations independent of implementation. Then it shows two concrete realizations: a resizable array and a singly linked list. Both achieve O(1) push and pop. The difference lies in memory layout, cache behavior, and how they handle growth. We will also make precise the invariant that a "top pointer" (or top index) must maintain. Nearly every subtle stack bug is really an invariant violation in disguise. By the end you will be able to implement a stack correctly from scratch in either style. You will be able to reason about amortized cost when the underlying array resizes. You will trace a stack through a real balanced-parentheses check. And you will see how the stack idea foreshadows recursion, Tree Traversals, and even the union-find structure used in Disjoint-Set Union-Find.
- A stack is a LIFO (last-in, first-out) abstract data type with two core operations: `push` and `pop`.
- Both the array-backed and linked-list-backed implementations achieve O(1) time per operation (amortized for dynamic arrays).
- The correctness of any stack implementation rests on one invariant: the top pointer always identifies exactly the most recently pushed, not-yet-popped element.
- Array stacks are cache-friendly and memory-compact; linked-list stacks avoid resizing pauses and never waste unused capacity.
- Stacks underlie recursion (the call stack), expression evaluation, backtracking, and depth-first traversal.
- Classic bugs are almost all invariant violations: popping an empty stack, forgetting to update top, or off-by-one indexing.
A stack is nothing but a single pointer (the top) plus a rule that every operation touches only what that pointer names — and that rule alone guarantees constant-time push and pop.
The idea
A stack is an abstract data type that stores a sequence of elements and supports insertion and removal at only one end, called the top. The two defining operations are `push(x)`, which adds `x` to the top, and `pop()`, which removes and returns the top element. This access pattern is called LIFO: Last In, First Out. A helper operation `peek()` (or `top()`) returns the top element without removing it, and `isEmpty()` reports whether the stack has any elements. The key conceptual move is separating the interface (what operations exist, and what they mean) from the implementation (how the elements are actually stored in memory). CLRS-style abstract data types are defined by their behavioral contract, not by a specific data layout. A stack can be built on top of a dynamic array (as in Arrays and Dynamic Arrays) or on top of a singly linked list; both satisfy the same LIFO contract, but they differ in memory locality, worst-case single-operation cost, and how they behave under resizing. Central to every correct implementation is an invariant: a property that must hold true before and after every operation. For a stack, the invariant is that the top pointer (an index into an array, or a reference to a linked-list head) always refers to exactly the most recently pushed element that has not yet been popped — and if the stack is empty, top signals "no such element" (an index of , or a null reference). Every `push` and `pop` must update this pointer in lockstep with adding or removing the element, or the structure silently corrupts: a stale top, a dangling reference, or a phantom empty check.
How it works
Array-based stack. Maintain a backing array `A` of capacity `cap` and an integer `top` denoting the index of the last used slot (`top = -1` means empty). `push` writes to `A[top+1]` and increments `top`; `pop` reads `A[top]` and decrements `top`. When `top+1` would exceed `cap`, the array is reallocated to twice its size (a dynamic array resize), and all existing elements are copied over — this is the same doubling trick used in dynamic arrays generally.
Linked-list-based stack. Maintain a pointer `head` to a singly linked list, where `head` represents the top of the stack. `push` allocates a new node whose `next` field points to the old `head`, then reassigns `head` to the new node. `pop` reads `head.value`, reassigns `head` to `head.next`, and (in languages without garbage collection) frees the old head node. No resizing ever occurs, because nodes are allocated individually. ```pseudocode // ---------- Array-based stack ---------- structure ArrayStack: A[0 .. cap-1] // backing array cap // current capacity top // index of top element; -1 if empty ArrayStack.init(initialCap = 4): cap <- initialCap A <- new array of size cap top <- -1 ArrayStack.isEmpty(): return top == -1 ArrayStack.push(x): if top + 1 == cap: // would overflow: grow cap <- 2 * cap A' <- new array of size cap for i from 0 to top: A'[i] <- A[i] A <- A' top <- top + 1 A[top] <- x ArrayStack.pop(): if isEmpty(): error "pop from empty stack" x <- A[top] A[top] <- undefined // optional: drop the reference top <- top - 1 return x ArrayStack.peek(): if isEmpty(): error "peek on empty stack" return A[top]
// ---------- Linked-list-based stack ---------- structure Node: value next structure ListStack: head // reference to top node; null if empty ListStack.init(): head <- null ListStack.isEmpty(): return head == null ListStack.push(x): n <- new Node(value = x, next = head) head <- n ListStack.pop(): if isEmpty(): error "pop from empty stack" x <- head.value head <- head.next return x ListStack.peek(): if isEmpty(): error "peek on empty stack" return head.value ``` Both implementations expose exactly the same interface — `push`, `pop`, `peek`, `isEmpty` — so any client code (a parser, a DFS routine, an expression evaluator) can be written against the ADT alone, without knowing which realization backs it.
Complexity & correctness
Time complexity.
- Linked-list stack: `push` and `pop` are O(1) worst case, always. Each touches only `head` and one node — no traversal, no dependency on stack size.
- Array stack: `push` and `pop` are O(1) amortized, and O(1) worst case except during a resize, when a single `push` costs to copy existing elements into the new array. Using the standard doubling argument (identical to dynamic arrays): if the array doubles whenever full, then across any sequence of pushes starting from empty, the total copying work is
so the amortized cost per push is . Pop never needs to reallocate in this simplified version (a further optimization is to shrink the array when it becomes at most full, halving it — this preserves amortized pop as well, while avoiding oscillation between growing and shrinking at exactly half-full).
Space complexity. Both implementations use space for elements. The array stack may have up to allocated slots right after a doubling (a constant-factor overhead); the linked-list stack uses exactly nodes but each node carries the overhead of a `next` pointer, which is often larger in practice for small elements (e.g., a stack of integers wastes relatively more memory per element as a linked list than as an array).
Correctness argument. The proof that these implementations satisfy the LIFO contract is an invariant proof. Define the invariant : > After any sequence of operations, the elements at indices of the array (or the nodes reachable from `head` in the list) are exactly the currently-stored elements, in the order they were pushed, with `A[top]` (or `head.value`) equal to the most recently pushed element not yet popped.
- Base case: an empty stack (`top = -1`, or `head = null`) trivially satisfies — there are no elements, and "most recent" is vacuously undefined, matching `isEmpty() = true`.
- Inductive step (push): assume holds before `push(x)`. The operation places `x` immediately after the current top (index `top+1`, or as the new `head` whose `next` is the old `head`) and then updates the top pointer to reference it. Since `x` was just added and no other element moved, `x` is now the most recent, and holds again.
- Inductive step (pop): assume holds and the stack is non-empty. By , the current top is the most recently pushed element; removing it and moving the top pointer to the previous element (`top - 1`, or `head.next`) restores for the remaining elements, whose relative order and "most recent" status are untouched.
Because holds initially and is preserved by every operation, it holds after any sequence of operations — this is exactly why `peek()` always returns the correct element and why `pop()` always undoes the most recent `push()` that hasn't already been undone.
Tradeoffs. Array stacks win on cache locality (contiguous memory, better for CPU prefetching) and lower per-element memory overhead, but pay for occasional resize spikes — a real concern in latency-sensitive systems, where an push can violate a real-time deadline even though it is rare. Linked-list stacks give a hard worst-case guarantee per operation and never waste unused capacity, but every node is a separate heap allocation, hurting cache behavior and adding pointer overhead. Neither implementation supports efficient random access or search — both are for "is in the stack," which is by design: a stack that let you peek at the middle would no longer be a stack.
Worked example
Consider using a stack to check whether the string `"( a ( b ) [ c ] )"` has correctly balanced and nested brackets — a canonical stack application. The algorithm: scan left to right; push every opening bracket; on a closing bracket, pop and check that it matches.
| Step | Token read | Action | Stack after action (bottom → top) |
|---|---|---|---|
| 1 | `(` | push `(` | `(` |
| 2 | `a` | ignore (not a bracket) | `(` |
| 3 | `(` | push `(` | `( (` |
| 4 | `b` | ignore | `( (` |
| 5 | `)` | pop → got `(`, matches `)` ✓ | `(` |
| 6 | `[` | push `[` | `( [` |
| 7 | `c` | ignore | `( [` |
| 8 | `]` | pop → got `[`, matches `]` ✓ | `(` |
| 9 | `)` | pop → got `(`, matches `)` ✓ | (empty) |
| 10 | end of input | stack is empty → balanced | (empty) |
Each `pop` in this trace relies on the stack invariant: at step 5, the top of the stack is guaranteed to be the innermost still-open bracket, because that is precisely the most recently pushed, not-yet-popped element — exactly what the invariant guarantees. If the input had instead been `"( a ( b ] )"`, step 5 would pop `(` while reading `]`, the mismatch would be detected immediately, and the algorithm would report the string as unbalanced in the same single pass, using stack space in the worst case (all opens, no closes, e.g. `"((((("`). This same "match against the top" pattern generalizes directly to evaluating postfix (reverse Polish) expressions, implementing undo/redo, and — via an explicit stack replacing the implicit call stack — converting a recursive tree traversal into an iterative one.
Common pitfalls & traps
| Pitfall | What goes wrong | Fix |
|---|---|---|
| Popping an empty stack | Reading `A[top]` when `top = -1` accesses invalid memory or returns garbage | Always check `isEmpty()` before `pop`/`peek`, or have `pop` raise a clear error |
| Off-by-one on `top` | Using `top` as a count in one place and as a last index in another causes writes to the wrong slot | Fix one convention (commonly "index of top element, if empty") and use it consistently everywhere |
| Forgetting to update `top`/`head` | Element is logically added/removed but the pointer isn't moved, so the stack "forgets" the change | Treat pointer update as inseparable from the data write — same line or same atomic step |
| Resizing without copying old elements | New bigger array is allocated but old contents aren't copied in, silently losing data | Always copy indices `0..top` into the new array before discarding the old one |
| Using a stack where order doesn't matter | Forcing LIFO access onto a problem that needs FIFO (e.g., BFS) produces wrong traversal order | Use a queue for FIFO needs; reserve the stack for genuinely last-in-first-out semantics |
| Not freeing popped nodes (linked-list) | In manual-memory languages, popped nodes leak if not explicitly deallocated | Explicitly free/delete the old head node after reading its value |
Pioneers & history
The stack as an explicit data structure emerged alongside the earliest work on programming language implementation, expression evaluation, and formal automata theory, where it became inseparable from the very definition of what a computer program's execution is.
Origins · 1 minFriedrich L. Bauer and how this was discovered
Bauer, working in Germany in the mid-1950s, is widely credited (alongside Klaus Samelson) with formalizing the stack ("Keller" in German, meaning cellar) as a mechanism for evaluating arithmetic expressions and managing subroutine calls — an idea patented in 1957 and foundational to how every modern compiler manages function calls today.
Where this matters
Every function call in every programming language relies on a call stack: each call pushes a new stack frame holding local variables, parameters, and a return address, and returning from the function pops that frame — which is exactly why deep unbounded recursion causes a "stack overflow." Compilers and interpreters use explicit stacks to evaluate expressions (converting infix to postfix via the shunting-yard algorithm, then evaluating postfix with a stack), to manage nested scopes, and to implement virtual machines (the Java Virtual Machine and many bytecode interpreters are explicitly stack-based). Browsers use a stack for back-button history and undo systems in editors use one for undo/redo. Balanced-bracket and syntax checking in parsers, backtracking search (maze solving, Sudoku solvers, and general depth-first search), and iterative simulations of recursive algorithms all lean on the stack's LIFO discipline. The stack is also the conceptual seed for Tree Traversals (pre-order/in-order/post-order can be done recursively via the implicit call stack or iteratively via an explicit one) and shares its "simple pointer, strong invariant" design philosophy with the union-find structure at the heart of Disjoint-Set Union-Find.
Ask the tutor: How would you adapt this stack implementation to support a `getMin()` operation in O(1) time?
Is a stack the same thing as the call stack in a running program?
Related but not identical. The call stack is a specific, language-runtime-managed stack that stores stack frames — one per active function call — each holding local variables, parameters, and a return address. It is a real-world instance of the abstract stack data type described here, built into how CPUs and language runtimes execute code, typically using a dedicated region of memory with a hardware or runtime-maintained stack pointer. The stack ADT in this article is the general concept; the call stack is one famous, automatic application of it that happens whenever you write ordinary function calls, including recursive ones.
Why not just always use a dynamic array and skip the linked-list version?
Both are valid; the choice is about tradeoffs, not correctness. Array-backed stacks are usually preferred in practice because contiguous memory is far more cache-friendly, and modern hardware rewards locality heavily. Linked-list stacks avoid the occasional O(n) resize spike and never over-allocate unused capacity, which matters in real-time systems with strict per-operation latency bounds, or when elements are large and copying them during a resize is expensive. In garbage-collected, high-level languages, the array version (e.g., a dynamic array or vector) is almost always the default choice.
How is a stack different from a queue?
A stack removes the most recently added element first (LIFO); a queue removes the earliest added element first (FIFO). Structurally, they can look similar — both are linear sequences supporting insertion and removal — but a queue needs efficient access at both ends (enqueue at the back, dequeue at the front), whereas a stack only ever touches one end. This single difference in access pattern changes which algorithms each structure naturally supports: stacks suit depth-first exploration and backtracking, while queues suit breadth-first exploration and fair scheduling.
Can a stack support search or access to elements in the middle?
Not efficiently, and that's by design. Both implementations described here only expose operations on the top element in O(1); finding or accessing an arbitrary element requires popping everything above it (destroying the stack's state) or, in the array version, direct indexing that violates the ADT's contract (and is not part of the interface). If you need efficient arbitrary access alongside stack-like behavior, you want a different structure entirely, such as a dynamic array used directly, or a deque if you need access at both ends.
What happens if you push onto a "full" array stack without resizing?
If the implementation doesn't check for capacity, `push` writes past the end of the allocated array — a buffer overflow. In managed languages this typically throws an index-out-of-bounds exception; in unmanaged languages (C, for instance) it is undefined behavior, potentially corrupting adjacent memory, which historically has been a serious security vulnerability class. Correct implementations must always check `top + 1 == cap` before writing and trigger a resize (or reject the push) rather than writing unconditionally.
Does shrinking the array stack back down matter for correctness or just efficiency?
Purely efficiency, not correctness — a stack that never shrinks its backing array is still perfectly correct, just potentially memory-wasteful if it grows large and then empties out. A common optimization is to halve the array's capacity when the count of elements falls to one quarter of capacity (not one half, to avoid immediately re-growing on the next push and thrashing back and forth). This preserves O(1) amortized cost for both push and pop while bounding wasted space to a constant factor of the current element count.
How does a stack relate to recursion, and can I always replace recursion with an explicit stack?
Every recursive function call is implicitly managed by the language's call stack, so any recursive algorithm can, in principle, be rewritten iteratively using an explicit stack that manually stores whatever the call frame would have stored (parameters, local state, and a marker for "where to resume"). This transformation is exactly how recursive algorithms like depth-first search or Tree Traversals are converted into iterative form to avoid stack-overflow risk on very deep recursion, or to give explicit control over memory usage in performance-critical or embedded systems.