A stack is a linear data structure that follows the Last-In-First-Out (LIFO) principle. The most recently added element is the first one to be removed - like a stack of plates where you can only add or remove from the top.

Core Operations

Stacks support two primary operations:

1. Push - Add an element to the top 2. Pop - Remove the element from the top

Additional operations:

  • Peek/Top - View the top element without removing it
  • IsEmpty - Check if the stack is empty
  • Size - Get the number of elements
graph TD
    A[Empty Stack] -->|Push 1| B[Stack: 1]
    B -->|Push 2| C[Stack: 1, 2]
    C -->|Push 3| D[Stack: 1, 2, 3]
    D -->|Pop| E[Returns 3, Stack: 1, 2]
    E -->|Pop| F[Returns 2, Stack: 1]

    style A fill:#f0f0f0
    style D fill:#e8f5e9
    style E fill:#fff4e1

Visual Representation

Visual stack operations:

Initial state:
┌─────────────┐
│   (empty)   │
└─────────────┘

After push(5):
┌─────────────┐
│      5      │ ← top
└─────────────┘

After push(10):
┌─────────────┐
│     10      │ ← top
├─────────────┤
│      5      │
└─────────────┘

After push(15):
┌─────────────┐
│     15      │ ← top
├─────────────┤
│     10      │
├─────────────┤
│      5      │
└─────────────┘

After pop() → returns 15:
┌─────────────┐
│     10      │ ← top
├─────────────┤
│      5      │
└─────────────┘

After peek() → returns 10 (doesn't remove):
┌─────────────┐
│     10      │ ← top (still here)
├─────────────┤
│      5      │
└─────────────┘

LIFO Principle

Last-In-First-Out means the order of removal is the reverse of the order of insertion:

Push order: 1 → 2 → 3 → 4
Pop order:  4 → 3 → 2 → 1

Like stacking books:
┌────────┐
│ Book 4 │ ← Added last, removed first
├────────┤
│ Book 3 │
├────────┤
│ Book 2 │
├────────┤
│ Book 1 │ ← Added first, removed last
└────────┘

This contrasts with:

  • Queue (FIFO): First-In-First-Out (like a line of people)
  • Deque: Can add/remove from both ends
  • Priority Queue: Ordered by priority, not insertion order

Implementation Approaches

Array-Based Stack

class ArrayStack
  def initialize
    @items = []
  end
 
  def push(item)
    @items.push(item)
  end
 
  def pop
    @items.pop
  end
 
  def peek
    @items.last
  end
 
  def empty?
    @items.empty?
  end
 
  def size
    @items.length
  end
end

Advantages:

  • Simple implementation
  • Fast access (O(1) for all operations)
  • Cache-friendly (contiguous memory)

Disadvantages:

  • May need resizing (amortized O(1))
  • Wasted space if over-allocated

Linked-List-Based Stack

class Node
  attr_accessor :value, :next
 
  def initialize(value)
    @value = value
    @next = nil
  end
end
 
class LinkedStack
  def initialize
    @top = nil
    @size = 0
  end
 
  def push(value)
    node = Node.new(value)
    node.next = @top
    @top = node
    @size += 1
  end
 
  def pop
    return nil if @top.nil?
 
    value = @top.value
    @top = @top.next
    @size -= 1
    value
  end
 
  def peek
    @top&.value
  end
 
  def empty?
    @top.nil?
  end
 
  def size
    @size
  end
end

Advantages:

  • No resizing needed
  • Efficient memory use (no over-allocation)
  • Dynamic size

Disadvantages:

  • Extra memory for pointers
  • Less cache-friendly (scattered memory)
  • Slightly more complex implementation

Time Complexity

All primary stack operations are O(1) - constant time:

OperationArray-BasedLinked-List-Based
PushO(1)*O(1)
PopO(1)O(1)
PeekO(1)O(1)
IsEmptyO(1)O(1)
SizeO(1)O(1)

*Amortized O(1) for array-based (occasional resizing)

Real-World Applications

1. Function Call Stack

Programming languages use stacks to manage function calls:

def factorial(n)
  return 1 if n <= 1
  n * factorial(n - 1)
end
 
factorial(3)
 
# Call stack:
# ┌──────────────┐
# │ factorial(1) │ ← top (executes first)
# ├──────────────┤
# │ factorial(2) │
# ├──────────────┤
# │ factorial(3) │
# └──────────────┘
#
# Return stack (unwinding):
# factorial(1) returns 1
# factorial(2) returns 2 * 1 = 2
# factorial(3) returns 3 * 2 = 6

See recursion and call stack for deeper exploration.

2. Undo/Redo Functionality

Text editors and applications use stacks for undo/redo:

User actions:
1. Type "Hello"
2. Type " World"
3. Delete " World"

Undo stack:
┌──────────────────┐
│ Delete " World"  │ ← Undo this first
├──────────────────┤
│ Type " World"    │
├──────────────────┤
│ Type "Hello"     │
└──────────────────┘

3. Expression Evaluation

Stacks evaluate mathematical expressions and parse syntax:

Expression: 2 + 3 * 4

Using stack (postfix evaluation):
Push 2 → [2]
Push 3 → [2, 3]
Push 4 → [2, 3, 4]
Pop 4, 3, multiply, push 12 → [2, 12]
Pop 12, 2, add, push 14 → [14]

See reverse Polish notation for more on expression evaluation.

4. Virtual Machines

YARV and other stack-based VMs use stacks as their primary execution mechanism:

# Ruby code: 5 + 3
# YARV instructions:
push 5    # Stack: [5]
push 3    # Stack: [5, 3]
add       # Stack: [8]

See YARV stack instructions for detailed examples.

5. Browser History

Web browsers use stacks for back/forward navigation:

Visit sequence: Home → Page1 → Page2 → Page3

Back stack:
┌─────────┐
│ Page3   │ ← Current page
├─────────┤
│ Page2   │ ← Back goes here
├─────────┤
│ Page1   │
├─────────┤
│ Home    │
└─────────┘

6. Syntax Parsing

Compilers use stacks to parse balanced expressions:

def is_balanced(expression)
  stack = []
  pairs = {'(' => ')', '[' => ']', '{' => '}'}
 
  expression.each_char do |char|
    if pairs.key?(char)
      stack.push(char)
    elsif pairs.value?(char)
      return false if stack.empty?
      return false if pairs[stack.pop] != char
    end
  end
 
  stack.empty?
end
 
is_balanced("([{}])")  # => true
is_balanced("([)]")    # => false

Stack Overflow

When a stack grows beyond its allocated size, a stack overflow occurs:

def infinite_recursion
  infinite_recursion  # Never returns, keeps pushing stack frames
end
 
infinite_recursion
# => SystemStackError: stack level too deep

This is why the popular programming site is called “Stack Overflow” - it references a common programming error!

Causes:

  • Infinite or excessive recursion
  • Very deep recursion without tail-call optimization
  • Allocating large arrays on the stack (in languages like C)

Prevention:

  • Use iteration instead of deep recursion
  • Implement tail recursion when possible
  • Increase stack size (language/platform dependent)
  • Move data to heap instead of stack

Stack vs Heap

Stacks are often contrasted with the heap in memory management:

AspectStackHeap
AllocationAutomaticManual
DeallocationAutomatic (LIFO)Manual or GC
Access patternLIFO onlyRandom access
SpeedFasterSlower
SizeLimitedLarge
LifetimeFunction scopeArbitrary
Memory layout in a typical program:

┌─────────────────┐ High addresses
│     Stack       │ ← Grows downward
│       ↓         │
│                 │
│   (free space)  │
│                 │
│       ↑         │
│     Heap        │ ← Grows upward
├─────────────────┤
│   Static Data   │
├─────────────────┤
│   Code/Text     │
└─────────────────┘ Low addresses

Advanced Stack Operations

Reverse a String

def reverse_string(str)
  stack = []
  str.each_char { |c| stack.push(c) }
 
  result = ""
  result += stack.pop until stack.empty?
  result
end
 
reverse_string("hello")  # => "olleh"

Check Palindrome

def is_palindrome(str)
  stack = []
  str.each_char { |c| stack.push(c) }
 
  str.each_char do |char|
    return false if char != stack.pop
  end
 
  true
end
 
is_palindrome("racecar")  # => true

Infix to Postfix Conversion

Converting 2 + 3 * 4 to 2 3 4 * + using a stack (used by compilers):

def infix_to_postfix(expression)
  stack = []
  output = []
  precedence = {'+' => 1, '-' => 1, '*' => 2, '/' => 2}
 
  expression.split.each do |token|
    if token =~ /\d+/  # Operand
      output << token
    elsif precedence[token]  # Operator
      while !stack.empty? && precedence[stack.last].to_i >= precedence[token]
        output << stack.pop
      end
      stack.push(token)
    end
  end
 
  output.concat(stack.reverse)
  output.join(' ')
end
 
infix_to_postfix("2 + 3 * 4")  # => "2 3 4 * +"