Stacks & Applications
LIFO formal specification, array vs linked implementations, Dijkstra's Shunting-Yard expression parsing, and balanced brackets.
Reinforce your mental model: run, pause, and inspect live pointers with our interactive visualizer:
Topics Covered:
30. Stack Abstract Data Type (LIFO) & Core Operations • Static & Dynamic Array Stack Implementations (Amortized Analysis) • Linked List Stack • Hardware Call Stack & Activation Records • Expression Evaluation (Infix, Prefix, Postfix, Shunting-Yard Algorithm) • Balanced Delimiter Matching (Dyck Language & Formal Induction Proof) • Production Multi-Language Implementations
The stack is one of computing's most fundamental restricted-access linear abstractions, operating under the strict Last-In, First-Out (LIFO) principle. All element insertions and removals occur at a single designated boundary: the top. Beyond its utility as a software container, the stack is embedded directly into computer architecture as the CPU execution call stack, powering subroutine invocation, local variable scoping, and recursion. This chapter examines array and pointer-linked stack implementations, activation record lifecycles, balanced bracket validation, postfix expression evaluation, and Dijkstra's Shunting-Yard parsing algorithm.
Learning Objectives
#- Formalize the Stack Abstract Data Type (ADT) interface and enforce strict Last-In, First-Out (LIFO) invariants.
- Compare contiguous dynamic-array stack backing buffers against heap pointer-linked implementations in terms of allocation latency and memory overhead.
- Trace hardware activation records, frame pointers (
%rbp), and return addresses on the physical CPU call stack to diagnose stack overflow and buffer overflow conditions. - Prove the correctness of linear-time balanced delimiter matching over the Dyck Language using mathematical induction.
- Implement postfix (Reverse Polish Notation) arithmetic evaluation and Dijkstra's Shunting-Yard algorithm for converting infix expressions to postfix.
- Master robust production implementations across C++, Python, and Java with full error handling and cache consciousness.
Topic 30: Stacks (LIFO) & Applications
#1. Conceptual Architecture & The LIFO Invariant
A Stack restricts element access to a single boundary called Top:
- Push: Places an element onto the top of the container.
- Pop: Removes and returns the element currently residing at the top.
- Peek / Top: Inspects the value of the top element without mutating state.
Interactive Simulations:
Experiment with LIFO dynamics live in the Interactive Stack Push Simulator and the Interactive Stack Pop Simulator.
2. The Stack Abstract Data Type (ADT) Interface
#| Operation | Description | Array Best | Array Worst | Array Amortized | Linked List | Auxiliary Space |
|---|---|---|---|---|---|---|
Push(x) | Insert element at TOP | (resize) | ||||
Pop() | Remove and return element at TOP | |||||
Peek() | Read value at TOP without removing | |||||
IsEmpty() | Returns true if size is 0 | |||||
Size() | Return current count of elements |
3. Implementation Paradigms: Contiguous Array vs. Linked List
#Paradigm A: Contiguous Array Implementation
Maintains a backing array storage[] and an integer offset topIndex:
- Empty State:
topIndex = -1. - Push(): Check bounds;
topIndex += 1; storage[topIndex] = x;. - Pop(): Check for underflow (
topIndex == -1);val = storage[topIndex]; topIndex -= 1; return val;.
| Array Index | 0 | 1 | 2 | 3 | 4 |
|---|---|---|---|---|---|
| Stored Value | 10 | 20 | 30 | [EMPTY] | [EMPTY] |
| Role / Marker | Bottom | Interior | topIndex = 2 (TOP) | Available | Available |
Amortized Cost: When backed by a geometrically doubling dynamic array, Push incurs occasional reallocations, but achieves amortized cost across any sequence of operations while maintaining exceptional CPU L1/L2 cache locality.
Paradigm B: Linked-List-Based Stack
Maintains a pointer to the head node:
Push(x): Allocate new node, setnewNode.next = top,top = newNode.Pop(): Retrievetop.data, advancetop = top.next, free old node.
| Node Position | Virtual Heap Address | Node Payload | next Pointer Target | Architectural Role |
|---|---|---|---|---|
| Node 3 | 0x30A0 | 30 | 0x2050 | top (Head of List) |
| Node 2 | 0x2050 | 20 | 0x1010 | Intermediate Frame |
| Node 1 | 0x1010 | 10 | NULL | Bottom of Stack |
Guaranteed Strict Bound: Every single operation executes in guaranteed worst-case time without memory reallocation pauses, but consumes of pointer and padding overhead per element.
4. Hardware Symbiosis: The CPU Call Stack & Activation Records
In von Neumann computer architecture, subroutine execution is governed by the hardware call stack located in the upper region of the process virtual address space. On x86-64 / AMD64 architectures:
- The stack grows downward from higher memory addresses toward lower memory addresses.
- The
%rsp(Stack Pointer) register holds the memory address of the current top of the stack. - The
%rbp(Base / Frame Pointer) register anchors the base of the current subroutine activation record.
Call Stack Lifecycle:
- Prologue: When a function is called (
callq):- The CPU pushes
%rip(return address) onto the stack. - The callee executes:
pushq %rbp; movq %rsp, %rbp; subq $N, %rsp;to allocate bytes of local stack memory.
- The CPU pushes
- Epilogue: Before returning (
retq):- The callee executes:
movq %rbp, %rsp; popq %rbp; retq;. - The CPU pops
%ripinto the program counter and resumes the caller seamlessly.
- The callee executes:
- Stack Overflow: If recursive invocations exceed the OS stack ceiling (typically 8 MB on Linux, 1 MB on Windows),
%rspcollides with the memory guard page, triggering an immediateSIGSEGVfault.
5. Application 1: Balanced Delimiter Matching & Formal Proof
#The balanced parenthesis problem requires validating whether a string over alphabet belongs to the Dyck Language .
Grammar of :
Correctness Invariant & Induction Proof:
- Loop Invariant: Prior to scanning index , the stack contains the exact sequence of unmatched open delimiters in the order they were opened.
- Base Case (): The stack is empty. An empty prefix has no unmatched delimiters. Invariant holds.
- Inductive Step: Assume invariant holds for prefix .
- If , it must eventually match a future closing bracket of the same type. Pushing onto the stack preserves the invariant.
- If , by the Dyck grammar, the most recently opened unmatched delimiter must be its exact counterpart. Peeking at
TOP:- If
stk.IsEmpty(), an unopen closing delimiter exists invalid. - If
stk.Top() != match(S[i]), delimiters are improperly nested invalid. - If
stk.Top() == match(S[i]), popping the top successfully completes the inner Dyck reduction . The invariant is preserved for .
- If
- Termination: At string conclusion (), . Any remaining element represents an unclosed delimiter.
6. Application 2: Expression Parsing & Dijkstra's Shunting-Yard Algorithm
#Mathematical expressions can be formalized in three distinct notations:
- Infix: (human-readable; requires operator precedence and parentheses).
- Prefix (Polish): (operator precedes operands).
- Postfix (Reverse Polish Notation / RPN): (operands precede operator; zero parentheses required).
Step-by-Step Conversion Trace: 3 + 4 * 2 / ( 1 - 5 )
| Token | Operator Stack Action | Operator Stack (Bottom Top) | Postfix Output Queue |
|---|---|---|---|
3 | None (Operand) | [] | 3 |
+ | Push('+') | ['+'] | 3 |
4 | None (Operand) | ['+'] | 3, 4 |
* | Push('*') () | ['+', '*'] | 3, 4 |
2 | None (Operand) | ['+', '*'] | 3, 4, 2 |
/ | Pop * (equal precedence), then Push('/') | ['+', '/'] | 3, 4, 2, * |
( | Push('(') | ['+', '/', '('] | 3, 4, 2, * |
1 | None (Operand) | ['+', '/', '('] | 3, 4, 2, * , 1 |
- | Push('-') | ['+', '/', '(', '-'] | 3, 4, 2, * , 1 |
5 | None (Operand) | ['+', '/', '(', '-'] | 3, 4, 2, * , 1, 5 |
) | Pop until ( | ['+', '/'] | 3, 4, 2, * , 1, 5, - |
| End | Pop remaining operators | [] | 3, 4, 2, * , 1, 5, -, /, + |
7. Concrete Production Implementations
#A. C++20 Cache-Conscious Templated Dynamic Stack
#include <iostream>
#include <vector>
#include <stdexcept>
#include <string>
template <typename T>
class ArrayStack {
private:
T* buffer_;
size_t capacity_;
size_t size_;
void resize(size_t new_capacity) {
T* new_buffer = new T[new_capacity];
for (size_t i = 0; i < size_; ++i) {
new_buffer[i] = std::move(buffer_[i]);
}
delete[] buffer_;
buffer_ = new_buffer;
capacity_ = new_capacity;
}
public:
explicit ArrayStack(size_t initial_capacity = 8)
: buffer_(new T[initial_capacity]), capacity_(initial_capacity), size_(0) {}
~ArrayStack() {
delete[] buffer_;
}
// Disable copy for RAII safety
ArrayStack(const ArrayStack&) = delete;
ArrayStack& operator=(const ArrayStack&) = delete;
// Enable move semantics
ArrayStack(ArrayStack&& other) noexcept
: buffer_(other.buffer_), capacity_(other.capacity_), size_(other.size_) {
other.buffer_ = nullptr;
other.capacity_ = 0;
other.size_ = 0;
}
void push(const T& item) {
if (size_ == capacity_) {
resize(capacity_ * 2);
}
buffer_[size_++] = item;
}
void push(T&& item) {
if (size_ == capacity_) {
resize(capacity_ * 2);
}
buffer_[size_++] = std::move(item);
}
T pop() {
if (empty()) {
throw std::underflow_error("Stack underflow: cannot pop from empty stack.");
}
T val = std::move(buffer_[--size_]);
if (size_ > 0 && size_ <= capacity_ / 4 && capacity_ > 8) {
resize(capacity_ / 2);
}
return val;
}
[[nodiscard]] const T& top() const {
if (empty()) {
throw std::underflow_error("Stack is empty.");
}
return buffer_[size_ - 1];
}
[[nodiscard]] bool empty() const noexcept { return size_ == 0; }
[[nodiscard]] size_t size() const noexcept { return size_; }
};B. Python 3 Production Shunting-Yard & Postfix Evaluator
from typing import List
def shunting_yard(tokens: List[str]) -> List[str]:
"""Converts an infix token list into Reverse Polish Notation (RPN)."""
precedence = {'+': 1, '-': 1, '*': 2, '/': 2, '^': 3}
right_associative = {'^'}
output: List[str] = []
op_stack: List[str] = []
for token in tokens:
if token.isnumeric() or (token.startswith('-') and token[1:].isnumeric()):
output.append(token)
elif token == '(':
op_stack.append(token)
elif token == ')':
while op_stack and op_stack[-1] != '(':
output.append(op_stack.pop())
if not op_stack:
raise ValueError("Mismatched parentheses in expression.")
op_stack.pop() # Discard '('
elif token in precedence:
curr_p = precedence[token]
while (op_stack and op_stack[-1] != '(' and
(precedence.get(op_stack[-1], 0) > curr_p or
(precedence.get(op_stack[-1], 0) == curr_p and token not in right_associative))):
output.append(op_stack.pop())
op_stack.append(token)
else:
raise ValueError(f"Unrecognized token: {token}")
while op_stack:
op = op_stack.pop()
if op in {'(', ')'}:
raise ValueError("Mismatched parentheses in expression.")
output.append(op)
return output
def evaluate_postfix(rpn_tokens: List[str]) -> float:
"""Evaluates an RPN token list in O(n) time using an operand stack."""
stack: List[float] = []
for token in rpn_tokens:
if token.isnumeric() or (token.startswith('-') and token[1:].isnumeric()):
stack.append(float(token))
else:
if len(stack) < 2:
raise ValueError("Malformed RPN expression.")
b = stack.pop()
a = stack.pop()
if token == '+': stack.append(a + b)
elif token == '-': stack.append(a - b)
elif token == '*': stack.append(a * b)
elif token == '/': stack.append(a / b)
elif token == '^': stack.append(a ** b)
if len(stack) != 1:
raise ValueError("Invalid RPN evaluation: excess operands.")
return stack[0]8. Key Takeaways
#- LIFO Discipline: Stacks enforce restricted access where all operations occur in time at the
TOPboundary. - Array vs. Linked List: Contiguous dynamic arrays achieve superior CPU cache locality with amortized push; linked lists guarantee strict worst-case push with pointer overhead.
- Execution Call Stack: The CPU utilizes an internal stack to manage activation records, return pointers, and local scoping during function execution.
- Parsing Foundations: Stacks are the core computational engine driving syntax tree generation, delimiter balancing, and Reverse Polish Notation (RPN) compilers via the Shunting-Yard algorithm.
Academic Attribution & References
#- Cormen, T. H., Leiserson, C. E., Rivest, R. L., & Stein, C. (2022). Introduction to Algorithms (4th ed.), Chapter 10: Elementary Data Structures. MIT Press.
- Dijkstra, E. W. (1961). Making a Translator for ALGOL 60. ALGOL Bulletin, 10, 10-11.
- Aho, A. V., Lam, M. S., Sethi, R., & Ullman, J. D. (2006). Compilers: Principles, Techniques, and Tools (2nd ed.), Chapter 4: Syntax Analysis. Addison-Wesley.
- Sedgewick, R., & Wayne, K. (2011). Algorithms (4th ed.), Section 1.3: Bags, Queues, and Stacks. Addison-Wesley.