Stack -- Professional / Theoretical Level¶
Table of Contents¶
- Overview
- Formal Stack ADT Axioms
- Correctness Proof: Balanced Parentheses
- Lock-Free Treiber Stack: Linearizability Proof
- Amortized Analysis of Dynamic Array Stack
- Stack-Sortable Permutations
- Catalan Numbers Connection
- Summary
Overview¶
This document covers the mathematical and theoretical foundations of stacks. It is intended for those who need to reason formally about correctness, prove properties of concurrent data structures, or understand the combinatorial theory behind stack-based sorting.
Formal Stack ADT Axioms¶
A stack can be defined axiomatically as an abstract data type with the following signature and laws.
Signature¶
Stack<T>
Operations:
empty : () -> Stack<T>
push : Stack<T> x T -> Stack<T>
pop : Stack<T> -> Stack<T> x T (partial: undefined on empty)
peek : Stack<T> -> T (partial: undefined on empty)
isEmpty : Stack<T> -> Bool
Axioms¶
For all stacks s and elements x:
-
isEmpty(empty()) = true An empty stack is empty.
-
isEmpty(push(s, x)) = false A stack with at least one pushed element is not empty.
-
pop(push(s, x)) = (s, x) Popping after a push returns the original stack and the pushed element.
-
peek(push(s, x)) = x Peeking after a push returns the most recently pushed element.
These four axioms completely characterize the stack. Any implementation satisfying them is a correct stack.
Derived Properties¶
From the axioms, we can derive:
- LIFO order: By repeated application of axiom 3, elements are removed in reverse insertion order.
- Size conservation: If
size(s) = n, thensize(push(s, x)) = n + 1andsize(fst(pop(s))) = n - 1. - Commutativity of unrelated pushes is NOT guaranteed:
push(push(s, a), b) != push(push(s, b), a)in general (the stack remembers order).
Correctness Proof: Balanced Parentheses¶
We prove that the standard stack-based algorithm correctly determines whether a string of parentheses is balanced.
Definition¶
A string w over {(, )} is balanced if and only if: 1. w has equal numbers of ( and ). 2. For every prefix of w, the number of ( is >= the number of ).
Algorithm¶
function isBalanced(w):
stack = empty()
for each character c in w:
if c == '(':
push(stack, c)
elif c == ')':
if isEmpty(stack):
return false
pop(stack)
return isEmpty(stack)
Loop Invariant¶
Let w[0..i-1] be the characters processed so far. The loop invariant is:
size(stack) = count('(', w[0..i-1]) - count(')', w[0..i-1])
and size(stack) >= 0 at every step.
Proof¶
Initialization (i = 0): Stack is empty, count of both brackets is 0. 0 = 0 - 0. Invariant holds.
Maintenance: Assume the invariant holds after processing w[0..i-1].
- Case
w[i] = '(': We push, sosize(stack)increases by 1.count('(')also increases by 1. Invariant maintained. - Case
w[i] = ')'and stack is non-empty: We pop, sosize(stack)decreases by 1.count(')')increases by 1. The differencecount('(') - count(')')decreases by 1. Invariant maintained. - Case
w[i] = ')'and stack is empty: We returnfalse. This is correct becausesize(stack) = 0meanscount('(') = count(')'), and seeing another)means prefix condition 2 is violated.
Termination: After processing all characters, if isEmpty(stack), then count('(') = count(')') and no prefix violated condition 2. The string is balanced. If the stack is non-empty, count('(') > count(')'), so the string is unbalanced.
QED.
Lock-Free Treiber Stack: Linearizability Proof¶
Linearizability¶
A concurrent data structure is linearizable if every concurrent execution is equivalent to some sequential execution where each operation appears to take effect atomically at some point between its invocation and response.
Treiber Stack Operations¶
Push(x):
loop:
oldTop = read(top)
newNode = Node(x, next=oldTop)
if CAS(top, oldTop, newNode):
return
Pop():
loop:
oldTop = read(top)
if oldTop == null: return EMPTY
newTop = oldTop.next
if CAS(top, oldTop, newTop):
return oldTop.val
Linearization Points¶
- Push: The linearization point is the successful
CAS(top, oldTop, newNode). At this instant, the element is atomically added to the stack. - Pop (success): The linearization point is the successful
CAS(top, oldTop, newTop). At this instant, the element is atomically removed. - Pop (empty): The linearization point is the
read(top)that returnsnull, provided top is still null at that moment.
Proof Sketch¶
-
Safety: Each successful CAS atomically swings the top pointer. Between the CAS of a push and the CAS of the corresponding pop, the element is visible at the top (or below other elements). No element is lost or duplicated.
-
Lock-freedom: In every execution, at least one thread makes progress. If a CAS fails, it means another thread's CAS succeeded, so global progress is guaranteed.
-
Stack ordering: The linked list rooted at
topmaintains LIFO order at all times. A push prepends to the head; a pop removes the head. Any linearization of the CAS operations produces a valid sequential stack history.
ABA Problem and Correctness¶
The basic Treiber stack is vulnerable to ABA: - Thread T1 reads top = A. - T2 pops A, pops B, pushes A back. - T1's CAS succeeds (top is A), but A.next may now be different.
Fix: Use a version-tagged pointer (pointer, version) and perform a double-width CAS. Each successful CAS increments the version, preventing ABA.
Amortized Analysis of Dynamic Array Stack¶
A dynamic array stack starts with capacity c and doubles when full. Individual pushes may cost O(n) due to copying, but the amortized cost per push is O(1).
Aggregate Method¶
Consider a sequence of n pushes starting from an empty stack with initial capacity 1.
Resizes occur at pushes 1, 2, 4, 8, 16, ..., costing 1 + 2 + 4 + 8 + ... + n/2 + n copies.
Total resize cost = 1 + 2 + 4 + ... + n <= 2n (geometric series).
Total cost of n pushes = n (for the pushes themselves) + 2n (for all resizes) = 3n.
Amortized cost per push = 3n / n = O(1).
Potential Method¶
Define the potential function: Phi(S) = 2 * size(S) - capacity(S)
For a non-resizing push: - Actual cost = 1 - Phi increases by 2 (size grows by 1) - Amortized cost = 1 + 2 = 3
For a resizing push (size = capacity = k, new capacity = 2k): - Actual cost = 1 + k (push + copy) - Before: Phi = 2k - k = k - After: Phi = 2(k+1) - 2k = 2 - Delta Phi = 2 - k - Amortized cost = (1 + k) + (2 - k) = 3
Both cases give amortized cost 3 = O(1).
Pop Amortized Analysis¶
If we shrink the array when size < capacity/4 (not capacity/2, to avoid thrashing): - Amortized cost of pop is also O(1). - The 1/4 threshold prevents the pathological case where alternating push/pop near capacity boundary causes repeated resize/shrink.
Stack-Sortable Permutations¶
A permutation p = (p1, p2, ..., pn) is stack-sortable if it can be sorted into (1, 2, ..., n) by passing elements through a single stack.
Algorithm¶
Input sequence: p1, p2, ..., pn
Output sequence: (must be 1, 2, 3, ..., n)
Stack: auxiliary storage
For each element in input:
Push it onto the stack.
While stack top equals the next expected output value:
Pop and output it.
Characterization (Knuth, 1968)¶
A permutation is stack-sortable if and only if it avoids the pattern 231.
A permutation contains the pattern 231 if there exist indices i < j < k such that p[k] < p[i] < p[j].
Example: - (2, 3, 1) contains the pattern 231 (at positions 1, 2, 3) and is NOT stack-sortable. - (2, 1, 3) avoids 231 and IS stack-sortable.
Proof Sketch¶
If 231 exists, sorting fails: Suppose p[i] < p[j] and p[k] < p[i] with i < j < k. When processing p[j], p[i] is already on the stack. p[j] > p[i], so p[j] is pushed on top of p[i]. Later, p[k] < p[i] arrives, but we need to output p[k] before p[i]. However, p[j] sits on top of p[i], blocking access. Since p[j] > p[i] > p[k], we cannot output p[j] before p[k] either. Deadlock.
If 231 is avoided, sorting succeeds: Without the 231 pattern, whenever we need to output the next value, it is either at the input head or the stack top. The proof proceeds by induction on n.
Catalan Numbers Connection¶
The number of stack-sortable permutations of length n is the n-th Catalan number:
First values: 1, 1, 2, 5, 14, 42, 132, 429, 1430, ...
Why Catalan Numbers?¶
Stack-sortable permutations correspond bijectively to several Catalan-counted objects:
-
Valid parenthesizations:
npairs of matched parentheses. Each push is(, each pop is). -
Binary trees:
n-node binary trees. The stack operations during sorting trace an in-order traversal. -
Dyck paths: Paths from
(0,0)to(2n,0)using up-steps(1,1)and down-steps(1,-1)that never go below the x-axis. Push = up-step, pop = down-step. -
Non-crossing partitions: Partitions of
{1, ..., n}where no two blocks "cross."
Generating Function¶
The ordinary generating function for Catalan numbers satisfies:
Solving: C(x) = (1 - sqrt(1 - 4x)) / (2x)
Asymptotic Growth¶
This means the fraction of all n! permutations that are stack-sortable shrinks rapidly. For large n, almost no permutation is stack-sortable with a single stack.
Two-Stack Sortable Permutations¶
With two stacks in series, the set of sortable permutations is larger but still does not cover all permutations. West (1990) showed that 2-stack sortable permutations avoid a specific set of patterns and are counted by a different sequence.
Summary¶
| Topic | Key Result |
|---|---|
| Stack ADT axioms | 4 axioms fully characterize the stack |
| Balanced parentheses proof | Loop invariant: stack size = open count - close count |
| Treiber stack linearizability | CAS is the linearization point; lock-free but ABA-vulnerable |
| Amortized dynamic array | Doubling gives O(1) amortized push via potential method |
| Stack-sortable permutations | Exactly the 231-avoiding permutations (Knuth) |
| Catalan numbers | C(n) counts stack-sortable permutations, Dyck paths, binary trees |