Introduction

Stack is abstract data structure enforcing Last-In-First-Out (LIFO) access. Last element pushed is first popped. Real-world analogy: stack of plates (add to top, remove from top). Applications: expression evaluation, DFS, undo functionality, function call stack.

Restricted access: can only interact with top element. Simpler than general lists, enables efficient implementations, natural for specific problems. Fundamental building block in compilers (expression parsing), operating systems (call stacks), algorithms (backtracking).

Implementation: array-based (fixed/dynamic size) or linked list. Array: better cache, simpler. Linked list: unbounded size, allocation on demand. Choice depends on expected size and performance requirements.

"Stacks are elegant in their simplicity and powerful in their applications. LIFO discipline enables solutions to problems that seem intractable without it." -- Algorithm design principles

LIFO Principle and Stack Properties

LIFO Behavior

Last element pushed first to pop. Sequence: push 1, push 2, push 3. Pop order: 3, 2, 1. Reversing sequence.

Stack visualization (top on right):
Push 1: [1]
Push 2: [1, 2]
Push 3: [1, 2, 3] ← top
Pop: [1, 2], removed 3
Pop: [1], removed 2
Pop: [], removed 1

Properties

  • Access restricted: Only top element accessible
  • Insertion/Deletion: Both at top (single location)
  • Order preservation: LIFO order maintained exactly
  • No random access: Can't access middle elements

Empty and Full States

Empty: no elements. Operations on empty: pop() raises error, peek() raises error. Full (fixed-size): no more space. Push to full raises error. Resizing (dynamic) expands automatically.

Stack Interface: Push, Pop, Peek

Core Operations

Operation Description Return Value Time
push(value) Add element to top None (or modified stack) O(1)
pop() Remove and return top element Top element O(1)
peek() / top() Return top without removing Top element O(1)
is_empty() Check if stack empty Boolean O(1)
size() Return number of elements Integer O(1)

Error Handling

Pop/peek on empty: raise exception (underflow). Push on full (fixed-size): raise exception (overflow). Modern stacks dynamic (no overflow). Defensive: check is_empty() before pop().

Safe popping:
if not stack.is_empty():
 value = stack.pop()
else:
 print("Stack empty, cannot pop")

Array-Based Implementation

Structure

class Stack:
 def __init__(self, capacity):
 self.items = [None] * capacity
 self.top = -1 # Index of top element, -1 means empty

 def push(self, value):
 if self.top == len(self.items) - 1:
 # Resize: double capacity
 self.items.extend([None] * len(self.items))
 self.top += 1
 self.items[self.top] = value

 def pop(self):
 if self.top == -1:
 raise IndexError("Stack underflow")
 value = self.items[self.top]
 self.items[self.top] = None # Clear reference
 self.top -= 1
 return value

 def peek(self):
 if self.top == -1:
 raise IndexError("Stack empty")
 return self.items[self.top]

 def is_empty(self):
 return self.top == -1

 def size(self):
 return self.top + 1

Analysis

Push: O(1) amortized (occasional O(n) resize). Pop, peek: O(1). Space: O(n) for n elements (plus some capacity overhead). Array-based simpler, faster than linked list (better cache).

Dynamic Resizing

When full, allocate array 2x size. Copy elements, update top pointer. Amortized O(1) per push (resizing rare). Python lists, Java ArrayLists implement this.

Linked List Implementation

Structure

class Node:
 def __init__(self, value):
 self.value = value
 self.next = None

class Stack:
 def __init__(self):
 self.top = None # Head of linked list

 def push(self, value):
 new_node = Node(value)
 new_node.next = self.top
 self.top = new_node

 def pop(self):
 if self.top is None:
 raise IndexError("Stack underflow")
 value = self.top.value
 self.top = self.top.next
 return value

 def peek(self):
 if self.top is None:
 raise IndexError("Stack empty")
 return self.top.value

 def is_empty(self):
 return self.top is None

Analysis

Push, pop, peek: O(1) (no traversal, operate at head). Space: O(n) plus pointer overhead. No resizing overhead, unbounded growth. Slower than array in practice (cache, pointer dereferencing).

Comparison

Array: faster (cache locality), simpler code. Linked list: no resizing, unbounded. Modern preference: array-based (dynamic arrays available in most languages).

Time and Space Complexity

Operation Array Linked List
Push O(1) amortized O(1)
Pop O(1) O(1)
Peek O(1) O(1)
Space O(n) + capacity O(n) + pointer overhead

Practical Performance

Array-based faster: cache locality. Linked list: pointer traversal overhead. For small stacks, array negligible difference. For large or frequent operations, array significantly faster.

Applications: Expression Parsing

Balanced Parentheses

Check if expression balanced: push '(' on open, pop and match on close. Final: stack must be empty.

def is_balanced(expression):
 stack = Stack()
 pairs = {'(': ')', '[': ']', '{': '}'}
 for char in expression:
 if char in pairs:
 stack.push(char)
 elif char in pairs.values():
 if stack.is_empty() or pairs[stack.pop()] != char:
 return False
 return stack.is_empty()

Infix to Postfix Conversion

Convert "3 + 4 * 2 / (1 - 5)" to postfix "3 4 2 * 1 5 - / +". Stack precedence tracking. Shunting-yard algorithm uses stack for operators.

Expression Evaluation

Evaluate postfix: push operands, pop two and apply operator when encountered. "3 4 + 5 *" = (3+4)*5 = 35. Stack naturally evaluates postfix.

def evaluate_postfix(tokens):
 stack = Stack()
 for token in tokens:
 if token in {'+', '-', '*', '/'}:
 b = stack.pop()
 a = stack.pop()
 if token == '+': result = a + b
 elif token == '-': result = a - b
 elif token == '*': result = a * b
 elif token == '/': result = a // b
 stack.push(result)
 else:
 stack.push(int(token))
 return stack.pop()

Depth-First Search (DFS)

Iterative DFS Using Stack

Push start vertex, pop and visit, push unvisited neighbors. Stack naturally implements DFS (vs. BFS uses queue).

def dfs(graph, start):
 visited = set()
 stack = Stack()
 stack.push(start)
 while not stack.is_empty():
 vertex = stack.pop()
 if vertex not in visited:
 print(vertex)
 visited.add(vertex)
 for neighbor in graph[vertex]:
 if neighbor not in visited:
 stack.push(neighbor)

Graph Traversal Applications

Topological sort, strongly connected components, cycle detection. All naturally use stacks (or recursion, which uses call stack).

Function Calls and Recursion

Call Stack

Every function call pushed as frame (local variables, return address). Return pops frame, resumes caller. Recursion: nested frames. Deep recursion: stack overflow (too many frames).

Call stack example (factorial):
factorial(4):
 Call factorial(3):
 Call factorial(2):
 Call factorial(1):
 Base case, return 1
 Return 2*1 = 2
 Return 3*2 = 6
 Return 4*6 = 24

Stack during deepest: [main, fact(4), fact(3), fact(2), fact(1)]

Recursion Limits

Python: sys.setrecursionlimit() (default ~1000). Stack overflow if exceed. Iterative solutions (using explicit stack) avoid recursion limit.

Converting Recursion to Iteration

Maintain explicit stack: simulate function calls. More code but no recursion depth limit. Example: tree traversal with explicit stack mimics recursive traversal.

Backtracking and Undo Operations

Undo Functionality

Each action pushed as state. Undo: pop previous state, restore. Stack naturally models undo (LIFO).

Backtracking Algorithms

N-Queens, Sudoku solver, maze solving. Try move, push state. If invalid, pop and backtrack. Stack maintains solution path.

def solve_maze_backtrack():
 stack = Stack()
 stack.push(start_position)
 while not stack.is_empty():
 pos = stack.pop()
 if pos == goal:
 return True
 for neighbor in neighbors(pos):
 if is_valid(neighbor):
 stack.push(neighbor)
 return False

Advanced Stack Problems

Largest Rectangle in Histogram

Given histogram heights, find max rectangle area. Stack stores indices of increasing heights. Popping when larger height encountered enables O(n) solution.

Stock Span Problem

For each price, find consecutive preceding days with lower price. Stack of (price, span) pairs. O(n) vs. naive O(n^2).

Trapping Rain Water

Given height array, compute water trapped between bars. Stack or two-pass approach. Uses monotonic stack (stack maintains decreasing height).

Min/Max Stack

Stack supporting push, pop, find-min, find-max all O(1). Maintain parallel min/max stacks. Each element paired with min/max so far.

References

  • Cormen, T. H., Leiserson, C. E., Rivest, R. L., and Stein, C. "Introduction to Algorithms." MIT Press, 3rd edition, 2009.
  • Knuth, D. E. "The Art of Computer Programming, Volume 1: Fundamental Algorithms." Addison-Wesley, 3rd edition, 1997.
  • Sedgewick, R., and Wayne, K. "Algorithms, Part I." Addison-Wesley, 4th edition, 2011.
  • Skiena, S. S. "The Algorithm Design Manual." Springer, 2nd edition, 2008.
  • Lafore, R. "Data Structures and Algorithms in Java." Sams Publishing, 2nd edition, 2002.