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.

Key takeaways
  • 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.
The core insight

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 −1-1, 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.

Figure
Array-backed stack: state before and after two operations
State 1: after push(A), push(B), push(C) A B C · · index: 0 1 2 3 4 top = 2 (points at C) State 2: after pop() removes C A B (stale) top = 1 (points at B)

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 O(n)O(n) to copy nn 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 nn pushes starting from empty, the total copying work is
1+2+4+⋯+n  ≤  2n=O(n),1 + 2 + 4 + \cdots + n \;\le\; 2n = O(n),

so the amortized cost per push is O(n)/n=O(1)O(n)/n = O(1). Pop never needs to reallocate in this simplified version (a further optimization is to shrink the array when it becomes at most 1/41/4 full, halving it — this preserves O(1)O(1) amortized pop as well, while avoiding oscillation between growing and shrinking at exactly half-full).

Space complexity. Both implementations use O(n)O(n) space for nn elements. The array stack may have up to 2n2n allocated slots right after a doubling (a constant-factor overhead); the linked-list stack uses exactly nn 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 II: > After any sequence of operations, the elements at indices 0,1,…,top0, 1, \dots, \text{top} 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 II — there are no elements, and "most recent" is vacuously undefined, matching `isEmpty() = true`.
  • Inductive step (push): assume II 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 II holds again.
  • Inductive step (pop): assume II holds and the stack is non-empty. By II, 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 II for the remaining elements, whose relative order and "most recent" status are untouched.

Because II 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 O(n)O(n) 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 O(n)O(n) for "is xx 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.

StepToken readActionStack 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)
10end of inputstack 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 II 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 O(n)O(n) single pass, using O(n)O(n) 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

PitfallWhat goes wrongFix
Popping an empty stackReading `A[top]` when `top = -1` accesses invalid memory or returns garbageAlways 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 slotFix one convention (commonly "index of top element, −1-1 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 changeTreat pointer update as inseparable from the data write — same line or same atomic step
Resizing without copying old elementsNew bigger array is allocated but old contents aren't copied in, silently losing dataAlways copy indices `0..top` into the new array before discarding the old one
Using a stack where order doesn't matterForcing LIFO access onto a problem that needs FIFO (e.g., BFS) produces wrong traversal orderUse 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 deallocatedExplicitly 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
FL
Friedrich L. Bauer
1924–2015 · German computer scientist, co-inventor of the stack principle for computation

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.

History
1946
von Neumann's stored-program architecture makes the idea of a manipulable "memory of instructions and data" concrete, setting the stage for explicit runtime structures.
1955–1957
Friedrich Bauer and Klaus Samelson formalize the stack ("Keller-Prinzip") for automatic translation of arithmetic expressions, filing a foundational patent.
1957–1960
Early ALGOL implementations adopt stack-based mechanisms for managing nested procedure calls and local variables, cementing the "call stack."
1960
s: Edsger Dijkstra's work on structured programming and shunting-yard–style expression parsing popularizes stack-based algorithms for parsing and evaluation.
1970
s–present: Stacks become a standard chapter in every algorithms curriculum and a built-in or standard-library type in virtually every programming language.

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.

Before this

Where this leads