Skip to content

Stack

A stack is a Last-In-First-Out (LIFO) data structure where elements are added and removed from the same end (called the top). It follows the principle that the last element added is the first one to be removed.

Features

  • LIFO ordering - Last In, First Out access pattern
  • Efficient operations - O(1) push, pop, and peek operations
  • Dynamic size - automatically grows and shrinks as needed
  • Memory efficient - uses singly linked list for optimal memory usage
  • Type safe - generic implementation with type checking
  • Iterator support - can iterate from top to bottom
  • Utility operations - search, count, reverse, and copy functionality

Time Complexities

Operation Time Complexity Description
Push O(1) Add element to top
Pop O(1) Remove element from top
Peek/Top O(1) View top element
Search O(n) Find element position
Count O(n) Count occurrences
Clear O(1) Remove all elements
Copy O(n) Create shallow copy
Reverse O(n) Reverse stack order
Size/Length O(1) Get number of elements

Space Complexity: O(n) where n is the number of elements

Basic Usage

Creating and Initializing

from algo.data_structs.stacks_queues.stack import Stack

# Create empty stack
stack = Stack()

# Check if empty
print(stack.is_empty())    # True
print(len(stack))          # 0
print(stack.size())        # 0

# Stack is generic - can specify type
int_stack = Stack[int]()
str_stack = Stack[str]()

Adding Elements (Push)

# Push individual elements
stack.push(1)
stack.push(2)
stack.push(3)

print(len(stack))          # 3
print(stack.peek())        # 3 (top element)
print(stack.top())         # 3 (alias for peek)

# Push multiple elements using extend
stack.extend([4, 5, 6])
print(stack.peek())        # 6 (last element added becomes top)
print(len(stack))          # 6

# Extend with different iterables
stack.extend((7, 8))       # Tuple
stack.extend([9, 10])      # List
print(stack.to_list())     # [10, 9, 8, 7, 6, 5, 4, 3, 2, 1] (top to bottom)

Viewing Elements

# Peek at top element without removing
top_element = stack.peek()
print(top_element)         # 10
print(len(stack))          # Still same size

# Using top() alias
top_element = stack.top()
print(top_element)         # 10

# Check if stack contains element
print(5 in stack)          # True
print(99 in stack)         # False

# Convert to list (top to bottom order)
elements = stack.to_list()
print(elements)            # [10, 9, 8, 7, 6, 5, 4, 3, 2, 1]

Removing Elements (Pop)

# Pop elements one by one (LIFO order)
popped = stack.pop()
print(popped)              # 10 (most recently added)
print(len(stack))          # 9

# Pop multiple elements
results = []
for _ in range(3):
    results.append(stack.pop())
print(results)             # [9, 8, 7]

# Pop until empty
while not stack.is_empty():
    element = stack.pop()
    print(f"Popped: {element}")

print(stack.is_empty())    # True

Clearing the Stack

# Add some elements
stack.extend([1, 2, 3, 4, 5])
print(len(stack))          # 5

# Clear all elements
stack.clear()
print(len(stack))          # 0
print(stack.is_empty())    # True

Search and Count Operations

Finding Elements

stack = Stack()
stack.extend([1, 2, 3, 2, 4, 2])  # [2, 4, 2, 3, 2, 1] (top to bottom)

# Search returns distance from top (0-based)
print(stack.search(2))     # 0 (top element)
print(stack.search(4))     # 1 (second from top)
print(stack.search(3))     # 3 (fourth from top)
print(stack.search(1))     # 5 (bottom element)
print(stack.search(99))    # -1 (not found)

# Check membership
print(2 in stack)          # True
print(99 in stack)         # False

Counting Elements

# Count occurrences of elements
print(stack.count(2))      # 3 (appears 3 times)
print(stack.count(1))      # 1 (appears once)
print(stack.count(99))     # 0 (not present)

# Count all elements
total_elements = len(stack)
print(total_elements)      # 6

Iteration and Traversal

stack = Stack()
stack.extend(['apple', 'banana', 'cherry', 'date'])

# Iterate from top to bottom
print("Stack contents (top to bottom):")
for item in stack:
    print(item)  # date, cherry, banana, apple

# Convert to list for processing
items = list(stack)
print(items)               # ['date', 'cherry', 'banana', 'apple']

# Enumerate with position
for i, item in enumerate(stack):
    print(f"Position {i}: {item}")

Working with Different Types

String Elements

words = Stack()
words.push("hello")
words.push("world")
words.push("python")

print(words.peek())        # "python"
print("world" in words)    # True
print(words.count("hello")) # 1

Numeric Elements

numbers = Stack()
numbers.extend([1, 2, 3, 4, 5])
numbers.push(3.14)
numbers.push(42)

print(numbers.peek())      # 42
print(3.14 in numbers)     # True
print(len(numbers))        # 7

Mixed Types

mixed = Stack()
mixed.push("string")
mixed.push(42)
mixed.push([1, 2, 3])      # Lists can be stored
mixed.push(True)

print(mixed.peek())        # True
print(42 in mixed)         # True
print(len(mixed))          # 4

Custom Objects

class Task:
    def __init__(self, name: str, priority: int):
        self.name = name
        self.priority = priority

    def __eq__(self, other):
        return isinstance(other, Task) and self.name == other.name

    def __str__(self):
        return f"Task({self.name}, priority={self.priority})"

tasks = Stack()
task1 = Task("Write code", 1)
task2 = Task("Test code", 2)
task3 = Task("Deploy", 3)

tasks.push(task1)
tasks.push(task2)
tasks.push(task3)

print(tasks.peek())        # Task(Deploy, priority=3)
print(task1 in tasks)      # True
print(len(tasks))          # 3

Stack Manipulation

Copying Stacks

original = Stack()
original.extend([1, 2, 3, 4, 5])

# Create a copy
copy_stack = original.copy()

# Verify they have same contents
print(copy_stack.to_list() == original.to_list())  # True
print(len(copy_stack) == len(original))             # True

# Verify they're independent
original.push(6)
print(len(original))       # 6
print(len(copy_stack))     # 5 (unchanged)

copy_stack.push(99)
print(99 in original)      # False
print(99 in copy_stack)    # True

Reversing Stacks

stack = Stack()
stack.extend([1, 2, 3, 4, 5])
print(stack.to_list())     # [5, 4, 3, 2, 1] (top to bottom)

# Reverse the stack
stack.reverse()
print(stack.to_list())     # [1, 2, 3, 4, 5] (top to bottom)
print(stack.peek())        # 1 (was bottom, now top)

String Representation

# Empty stack
empty_stack = Stack()
print(str(empty_stack))    # Stack([])

# Non-empty stack
stack = Stack()
stack.extend([1, 2, 3])
print(str(stack))          # Stack([3 <- 2 <- 1] <- top)

# Detailed representation for debugging
print(repr(stack))         # Stack(size=3, top=3)

Error Handling

stack = Stack()

# Safe operations on empty stack
print(stack.is_empty())    # True
print(len(stack))          # 0
print(stack.search(42))    # -1
print(stack.count(42))     # 0
print(42 in stack)         # False

# Operations that raise errors on empty stack
try:
    stack.pop()
except IndexError as e:
    print(f"Error: {e}")   # Error: Cannot pop from empty stack

try:
    stack.peek()
except IndexError as e:
    print(f"Error: {e}")   # Error: Cannot peek empty stack

try:
    stack.top()
except IndexError as e:
    print(f"Error: {e}")   # Error: Cannot peek empty stack

Common Patterns

Building from Iterables

# From list
stack = Stack()
stack.extend([1, 2, 3, 4, 5])

# From tuple
stack2 = Stack()
stack2.extend((1, 2, 3))

# From range
stack3 = Stack()
stack3.extend(range(10))

# From another stack
stack4 = Stack()
stack4.extend(stack)  # Copy elements from stack

LIFO Processing

# Process items in reverse order
items = ["first", "second", "third", "fourth"]
stack = Stack()

# Load items
for item in items:
    stack.push(item)

# Process in LIFO order
processed = []
while not stack.is_empty():
    item = stack.pop()
    processed.append(f"Processed: {item}")

print(processed)
# ['Processed: fourth', 'Processed: third', 'Processed: second', 'Processed: first']

Undo/Redo Operations

class SimpleEditor:
    def __init__(self):
        self.content = ""
        self.undo_stack = Stack()
        self.redo_stack = Stack()

    def add_text(self, text: str):
        # Save current state for undo
        self.undo_stack.push(self.content)
        self.redo_stack.clear()  # Clear redo history
        self.content += text

    def delete_chars(self, count: int):
        # Save current state for undo
        self.undo_stack.push(self.content)
        self.redo_stack.clear()  # Clear redo history
        self.content = self.content[:-count]

    def undo(self):
        if not self.undo_stack.is_empty():
            # Save current state for redo
            self.redo_stack.push(self.content)
            self.content = self.undo_stack.pop()

    def redo(self):
        if not self.redo_stack.is_empty():
            # Save current state for undo
            self.undo_stack.push(self.content)
            self.content = self.redo_stack.pop()

# Usage
editor = SimpleEditor()
editor.add_text("Hello")
editor.add_text(" World")
print(editor.content)      # "Hello World"

editor.undo()
print(editor.content)      # "Hello"

editor.redo()
print(editor.content)      # "Hello World"

Function Call Stack Simulation

def function_call_trace():
    call_stack = Stack()

    def trace_call(func_name: str, args: str = ""):
        call_info = f"{func_name}({args})"
        call_stack.push(call_info)
        print(f"Entering: {call_info}")
        print(f"Call stack depth: {len(call_stack)}")

    def trace_return(func_name: str):
        if not call_stack.is_empty():
            returned_func = call_stack.pop()
            print(f"Returning from: {returned_func}")
            print(f"Call stack depth: {len(call_stack)}")

    # Simulate function calls
    trace_call("main")
    trace_call("process_data", "data.txt")
    trace_call("validate_input", "user_input")
    trace_call("parse_config", "config.json")

    # Show current call stack
    print("\nCurrent call stack (top to bottom):")
    for i, call in enumerate(call_stack):
        print(f"  {i}: {call}")

    # Simulate returns
    trace_return("parse_config")
    trace_return("validate_input")
    trace_return("process_data")
    trace_return("main")

function_call_trace()

Backtracking Algorithms

def find_path_in_maze(maze, start, end):
    """Find path using stack-based backtracking."""
    path_stack = Stack()
    visited = set()

    def is_valid(x, y):
        return (0 <= x < len(maze) and 
                0 <= y < len(maze[0]) and 
                maze[x][y] == 0 and  # 0 = open, 1 = wall
                (x, y) not in visited)

    # Start the search
    path_stack.push(start)
    visited.add(start)

    while not path_stack.is_empty():
        current = path_stack.peek()

        if current == end:
            # Found the end, return path
            return path_stack.to_list()

        # Try all four directions
        x, y = current
        directions = [(0, 1), (1, 0), (0, -1), (-1, 0)]
        moved = False

        for dx, dy in directions:
            new_x, new_y = x + dx, y + dy

            if is_valid(new_x, new_y):
                path_stack.push((new_x, new_y))
                visited.add((new_x, new_y))
                moved = True
                break

        if not moved:
            # Dead end, backtrack
            path_stack.pop()

    return None  # No path found

# Example maze (0 = open, 1 = wall)
maze = [
    [0, 1, 0, 0, 0],
    [0, 1, 0, 1, 0],
    [0, 0, 0, 1, 0],
    [1, 1, 0, 0, 0],
    [0, 0, 0, 1, 0]
]

path = find_path_in_maze(maze, (0, 0), (4, 4))
if path:
    print(f"Path found: {path}")
else:
    print("No path found")

Practical Applications

Expression Evaluation (Parentheses Matching)

def is_balanced_parentheses(expression: str) -> bool:
    """Check if parentheses are balanced using a stack."""
    stack = Stack()
    pairs = {'(': ')', '[': ']', '{': '}'}

    for char in expression:
        if char in pairs:  # Opening bracket
            stack.push(char)
        elif char in pairs.values():  # Closing bracket
            if stack.is_empty():
                return False
            if pairs[stack.pop()] != char:
                return False

    return stack.is_empty()

# Test expressions
expressions = [
    "()",           # True
    "()[]{}", 
    "((()))",       # True
    "{[()]}",       # True
    "([)]",         # False
    "((())",        # False
    ")("            # False
]

for expr in expressions:
    result = "balanced" if is_balanced_parentheses(expr) else "unbalanced"
    print(f"'{expr}': {result}")

Browser History Management

class BrowserHistory:
    def __init__(self):
        self.history = Stack()
        self.forward_stack = Stack()
        self.current_page = None

    def visit(self, url: str):
        """Visit a new page."""
        if self.current_page:
            self.history.push(self.current_page)
        self.current_page = url
        self.forward_stack.clear()  # Clear forward history
        print(f"Visited: {url}")

    def back(self):
        """Go back to previous page."""
        if not self.history.is_empty():
            self.forward_stack.push(self.current_page)
            self.current_page = self.history.pop()
            print(f"Back to: {self.current_page}")
        else:
            print("No previous page")

    def forward(self):
        """Go forward to next page."""
        if not self.forward_stack.is_empty():
            self.history.push(self.current_page)
            self.current_page = self.forward_stack.pop()
            print(f"Forward to: {self.current_page}")
        else:
            print("No next page")

    def show_history(self):
        """Show browsing history."""
        print(f"Current: {self.current_page}")
        if not self.history.is_empty():
            print("History (most recent first):")
            for i, page in enumerate(self.history):
                print(f"  {i+1}: {page}")

# Usage
browser = BrowserHistory()
browser.visit("google.com")
browser.visit("github.com")
browser.visit("stackoverflow.com")
browser.show_history()

browser.back()
browser.back()
browser.forward()
browser.show_history()

Syntax Analysis

def evaluate_postfix(expression: str) -> float:
    """Evaluate postfix expression using stack."""
    stack = Stack()
    operators = {'+', '-', '*', '/', '//', '%', '**'}

    tokens = expression.split()

    for token in tokens:
        if token in operators:
            if len(stack) < 2:
                raise ValueError("Invalid expression")

            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
            elif token == '//':
                result = a // b
            elif token == '%':
                result = a % b
            elif token == '**':
                result = a ** b

            stack.push(result)
        else:
            # Operand
            try:
                number = float(token)
                stack.push(number)
            except ValueError:
                raise ValueError(f"Invalid token: {token}")

    if len(stack) != 1:
        raise ValueError("Invalid expression")

    return stack.pop()

# Test postfix expressions
expressions = [
    "3 4 +",           # 3 + 4 = 7
    "3 4 + 2 *",       # (3 + 4) * 2 = 14
    "15 7 1 1 + - / 3 *",  # 15 / (7 - (1 + 1)) * 3 = 9
]

for expr in expressions:
    try:
        result = evaluate_postfix(expr)
        print(f"'{expr}' = {result}")
    except ValueError as e:
        print(f"'{expr}' - Error: {e}")

Memory Management Simulation

class MemoryManager:
    """Simulate memory allocation using stack for free blocks."""

    def __init__(self, total_size: int):
        self.total_size = total_size
        self.allocated = {}  # address -> size
        self.free_blocks = Stack()

        # Initially, entire memory is one free block
        self.free_blocks.push((0, total_size))

    def allocate(self, size: int) -> int:
        """Allocate memory block of given size."""
        # Find suitable free block
        temp_stack = Stack()
        allocated_address = None

        while not self.free_blocks.is_empty():
            address, block_size = self.free_blocks.pop()

            if block_size >= size:
                # Use this block
                allocated_address = address
                self.allocated[address] = size

                # Return remaining space to free blocks
                if block_size > size:
                    remaining_address = address + size
                    remaining_size = block_size - size
                    temp_stack.push((remaining_address, remaining_size))

                break
            else:
                # Block too small, keep for later
                temp_stack.push((address, block_size))

        # Restore free blocks
        while not temp_stack.is_empty():
            self.free_blocks.push(temp_stack.pop())

        if allocated_address is not None:
            print(f"Allocated {size} bytes at address {allocated_address}")
            return allocated_address
        else:
            print(f"Failed to allocate {size} bytes - insufficient memory")
            return -1

    def deallocate(self, address: int):
        """Deallocate memory block at given address."""
        if address in self.allocated:
            size = self.allocated[address]
            del self.allocated[address]

            # Return block to free blocks
            self.free_blocks.push((address, size))
            print(f"Deallocated {size} bytes at address {address}")
        else:
            print(f"Invalid address {address}")

    def show_status(self):
        """Show memory status."""
        print("\nMemory Status:")
        print(f"Total size: {self.total_size}")
        print(f"Allocated blocks: {len(self.allocated)}")
        print(f"Free blocks: {len(self.free_blocks)}")

        allocated_size = sum(self.allocated.values())
        print(f"Allocated memory: {allocated_size}")
        print(f"Free memory: {self.total_size - allocated_size}")

# Usage
memory = MemoryManager(1000)
memory.show_status()

addr1 = memory.allocate(100)
addr2 = memory.allocate(200)
addr3 = memory.allocate(150)
memory.show_status()

memory.deallocate(addr2)
memory.show_status()

addr4 = memory.allocate(50)  # Should reuse freed space
memory.show_status()

Use Cases

When to Use Stacks

Good for:

  • Function call management
  • Expression evaluation and parsing
  • Undo/redo operations
  • Backtracking algorithms
  • Syntax analysis
  • Memory management
  • Browser history
  • Depth-first search traversals

Examples:

# Function call stack
call_stack = Stack()
call_stack.push("main()")
call_stack.push("process_data()")
call_stack.push("validate_input()")

# Undo operations
undo_stack = Stack()
undo_stack.push("typed 'hello'")
undo_stack.push("deleted 2 chars")
undo_stack.push("typed 'world'")

# Navigation history
history = Stack()
history.push("/home")
history.push("/profile")
history.push("/settings")

When NOT to Use Stacks

Avoid for:

  • Random access to elements (use arrays/lists)
  • FIFO processing (use queues)
  • Searching sorted data (use trees/sorted arrays)
  • Frequent middle insertions/deletions

Better alternatives:

# For random access, use list
random_access = [1, 2, 3, 4, 5]
print(random_access[2])  # Direct access to index 2

# For FIFO, use queue
from collections import deque
fifo_queue = deque()
fifo_queue.append(1)
fifo_queue.popleft()  # FIFO behavior

# For sorted operations, use sorted list or heap
import heapq
sorted_data = []
heapq.heappush(sorted_data, 3)
heapq.heappush(sorted_data, 1)

Performance Characteristics

Time Complexity Analysis

  • Push/Pop operations: O(1) - constant time
  • Peek/Top operations: O(1) - constant time
  • Search operations: O(n) - linear time
  • Memory allocation: O(1) per element (linked list)
  • Space overhead: Minimal (only node pointers)

Memory Efficiency

# Stack uses singly linked list internally
# Memory per element: data + one pointer
# No pre-allocation of fixed arrays
# Memory grows/shrinks dynamically

stack = Stack()
print(f"Empty stack memory efficient: {stack.is_empty()}")

# Memory usage scales linearly with elements
for i in range(1000):
    stack.push(i)
# Only allocates memory for actual elements

Comparison with Other Data Structures

Operation Stack List Deque Queue
Push/Add Top O(1) O(1) O(1) N/A
Pop/Remove Top O(1) O(1) O(1) N/A
Add Bottom N/A O(1) O(1) O(1)
Remove Bottom N/A O(n) O(1) O(1)
Random Access O(n) O(1) O(n) O(n)
Memory Overhead Low Medium Low Low
Use Case LIFO Random Access Both Ends FIFO

API Reference

Stack Implementation

A stack is a Last-In-First-Out (LIFO) data structure where elements are added and removed from the same end (called the top). It follows the principle that the last element added is the first one to be removed.

Key Features:

  • LIFO (Last In, First Out) ordering
  • Push operation adds element to top
  • Pop operation removes element from top
  • Peek operation views top element without removing it
  • Dynamic size allocation

Common Use Cases:

  • Function call management (call stack)
  • Expression evaluation and syntax parsing
  • Undo operations in applications
  • Backtracking algorithms
  • Browser history navigation

Time Complexities:

  • Push: O(1)
  • Pop: O(1)
  • Peek/Top: O(1)
  • Size: O(1)
  • Search: O(n)

Space Complexity: O(n)

Stack()

Bases: Generic[T]

A stack implementation using a singly linked list as the underlying data structure.

This implementation provides efficient O(1) push and pop operations by using the head of the linked list as the top of the stack.

Initialize an empty stack.

__contains__(value)

Check if a value exists in the stack.

__iter__()

Return an iterator over the stack elements.

Note: Iteration goes from top to bottom (most recent to oldest).

__len__()

Return the number of elements in the stack.

__repr__()

Return detailed string representation for debugging.

__str__()

Return string representation of the stack.

clear()

Remove all elements from the stack.

Time Complexity: O(1)

copy()

Create a shallow copy of the stack.

Time Complexity: O(n)

Returns:

Type Description
Stack[T]

A new Stack with the same elements

count(item)

Count the number of occurrences of an item in the stack.

Time Complexity: O(n)

Parameters:

Name Type Description Default
item T

Item to count

required

Returns:

Type Description
int

Number of occurrences

extend(iterable)

Push all elements from an iterable onto the stack.

Time Complexity: O(k) where k is the number of elements

Parameters:

Name Type Description Default
iterable Any

Iterable containing elements to push

required
Note

Elements are pushed in order, so the last element in the iterable will be at the top of the stack.

is_empty()

Check if the stack is empty.

Time Complexity: O(1)

Returns:

Type Description
bool

True if the stack is empty, False otherwise

peek()

Return the top element without removing it.

Time Complexity: O(1)

Returns:

Type Description
T

The top element of the stack

Raises:

Type Description
IndexError

If the stack is empty

pop()

Remove and return the top element from the stack.

Time Complexity: O(1)

Returns:

Type Description
T

The top element of the stack

Raises:

Type Description
IndexError

If the stack is empty

push(item)

Add an element to the top of the stack.

Time Complexity: O(1)

Parameters:

Name Type Description Default
item T

Element to add to the stack

required

reverse()

Reverse the stack in place.

Time Complexity: O(n)

Note

After reversal, the bottom element becomes the top element.

search(item)

Search for an item in the stack and return its distance from the top.

Time Complexity: O(n)

Parameters:

Name Type Description Default
item T

Item to search for

required

Returns:

Type Description
int

Distance from top (0-based), or -1 if not found

Note

Top element has distance 0, second element has distance 1, etc.

size()

Get the number of elements in the stack.

Time Complexity: O(1)

Returns:

Type Description
int

The number of elements in the stack

to_list()

Convert the stack to a Python list.

Time Complexity: O(n)

Returns:

Type Description
list[T]

A Python list with elements from top to bottom

top()

Alias for peek() - return the top element without removing it.

Time Complexity: O(1)

Returns:

Type Description
T

The top element of the stack

Raises:

Type Description
IndexError

If the stack is empty