A stack is a fundamental data structure used extensively in computer science and software engineering. It follows the last in, first out (LIFO) principle, where the last element added to the stack is the first one removed. Stacks have numerous applications, including managing function calls, implementing browsers’ back button, parsing, and more. Mastering stacks is an essential skill for any Python developer.
In this comprehensive guide, you will learn how to implement a stack data structure in Python using arrays, linked lists, or Python’s built-in list type. We will cover the key stack operations like push, pop, and peek with example code snippets. You will also learn various applications of stacks that demonstrate their utility. By the end, you will have a solid grasp of stacks in Python to ace technical coding interviews and solve real-world problems.
Table of Contents
Open Table of Contents
Overview of Stacks
A stack is an ordered collection of elements where additions and deletions are restricted to one end called the “top”. The opposite end of the stack is called the “base”. The basic operations are:
- Push: Add an element to the top of the stack
- Pop: Remove the element from the top of the stack
- Peek: Access the element at the top without removing it
The push and pop operations follow a LIFO order. The peek operation retrieves the top element without modifying the stack.
Stack Push and Pop Operations (Image Source: educative.io)
Some key properties of stacks:
- Stacks have a LIFO data structure
- The topmost element is the one that was added most recently
- Insertion and deletion happen only at one end called the “top”
- Peek operation allows reading the top element without removal
- Key operations are push, pop, and peek
- Simple implementation but very powerful applications
Common uses of stacks:
- Managing function calls
- Implementing undo/redo features
- Syntax parsing such as validating parenthesis in code
- Implementing browsers’ back and forward buttons
- Routing algorithms
Next, we will explore various ways to implement a stack data structure in Python focusing on the key push, pop, and peek operations.
Implementing a Stack in Python
There are several ways to implement a stack in Python:
- Using a Python list
- Using a Python deque
- Using a namedtuple
- Using a custom class
Let’s look at each of these implementations focusing on the push, pop, and peek operations.
Stack Implementation with Python List
The easiest way to implement a stack is using a Python list. Lists provide an ordered collection that can be modified easily. We can restrict inserts and deletes to one end of the list to emulate a stack.
Here is an example Stack class implemented using Python’s built-in list
:
class Stack:
def __init__(self):
self.stack = []
def push(self, item):
self.stack.append(item)
def pop(self):
if len(self.stack) > 0:
return self.stack.pop()
else:
return None
def peek(self):
if len(self.stack) > 0:
return self.stack[-1]
else:
return None
To use this stack:
stack = Stack()
stack.push(1)
stack.push(2)
print(stack.peek()) # Prints 2
print(stack.pop()) # Prints 2
The append()
and pop()
methods make insertion and deletion from the end of the list very efficient in O(1) constant time complexity. However, lists can expand dynamically so it utilizes more memory.
Stack Implementation with Python deque
Python’s deque
class from the collections module provides a double ended queue that enables efficient appends and pops from both ends. This makes it a good alternative for stack implementation.
Here is an example using deque
:
from collections import deque
class Stack:
def __init__(self):
self.container = deque()
def push(self, val):
self.container.append(val)
def pop(self):
return self.container.pop()
def peek(self):
return self.container[-1]
To use:
stack = Stack()
stack.push(1)
print(stack.peek()) # 1
The deque
allows efficient O(1) inserts and deletes from both ends. But it takes more memory than a list implementation.
Stack Implementation with namedtuple
Python’s namedtuple
from the collections module can be used to add object orientation to a stack implementation.
Here is an example with namedtuple
:
from collections import namedtuple
Stack = namedtuple('Stack', ['data', 'next'])
class Stack:
def __init__(self):
self.top = None
def push(self, item):
self.top = Stack(data=item, next=self.top)
def pop(self):
if self.top:
data = self.top.data
self.top = self.top.next
return data
else:
return None
def peek(self):
if self.top:
return self.top.data
else:
return None
To use:
stack = Stack()
stack.push(1)
print(stack.peek())
The namedtuple
provides attributes to access the data and next pointer fields. But it utilizes more memory than the earlier implementations.
Stack Class Implementation
For full object orientation, we can implement a Stack class from scratch without using Python’s built-in types.
Here is an example custom Stack class implementation:
class StackNode:
def __init__(self, data):
self.data = data
self.next = None
class Stack:
def __init__(self):
self.top = None
def push(self, item):
node = StackNode(item)
node.next = self.top
self.top = node
def pop(self):
if self.top is None:
return None
else:
data = self.top.data
self.top = self.top.next
return data
def peek(self):
if self.top is None:
return None
else:
return self.top.data
To use:
stack = Stack()
stack.push(1)
print(stack.peek())
This provides full class encapsulation and modularity at the cost of more memory usage.
So in summary, for stack implementation:
- Python list is the easiest and uses least memory
- deque provides efficient pops from both ends
- namedtuple adds object orientation
- Custom class gives full encapsulation
Pick the implementation that works best for your use case based on memory vs. performance tradeoffs.
Stack Applications and Examples
Now that we have explored various ways to implement a stack data structure in Python, let’s look at some common use cases and examples that demonstrate the power of stacks.
Balancing Symbols in Arithmetic Expressions
Stacks can be used to check if an arithmetic expression has balanced parenthesis, curly braces and square brackets.
When traversing the expression from left to right, we push each opening symbol onto the stack. When closing symbols are encountered, we compare them with the top of the stack to check if they match. If a matching opening symbol is found, it is popped off the stack.
At the end, if the stack is empty, the expression is balanced. Else it is unbalanced.
Here is an example implementation:
def balance_check(expression):
stack = []
opening = ['(', '{', '[']
closing = [')', '}', ']']
for char in expression:
if char in opening:
stack.append(char)
elif char in closing:
pos = closing.index(char)
if ((len(stack) > 0) and
(opening[pos] == stack[len(stack)-1])):
stack.pop()
else:
return False
if len(stack) == 0:
return True
else:
return False
print(balance_check('[{()}]')) # True
print(balance_check('[{]}')) # False
This uses the stack to check for matching pairs efficiently in O(n) time complexity.
Evaluating Postfix Expressions
Stacks can evaluate postfix or reverse polish notation expressions where each operator follows its operands.
We scan the expression from left to right. Operands are pushed onto the stack. When an operator is encountered, the required number of operands are popped and the operator is applied. Finally, the output is the element remaining on the stack.
Here is a Python code snippet to evaluate postfix expressions:
def evaluate_postfix(expression):
stack = []
for char in expression:
if char.isdigit():
stack.append(int(char))
else:
rhs = stack.pop()
lhs = stack.pop()
if char == '+':
stack.append(lhs + rhs)
elif char == '-':
stack.append(lhs - rhs)
elif char == '*':
stack.append(lhs * rhs)
elif char == '/':
stack.append(lhs / rhs)
return stack.pop()
print(evaluate_postfix('35*62/+4-')) # 14
This uses the stack to efficiently compute prefix expressions.
Reversing a String
We can use a stack to reverse a given string.
The algorithm is:
- Push all characters of the string onto a stack
- Pop characters from the stack and append them to an output string
This will reverse the order of characters.
Here is an example implementation:
def reverse_string(text):
stack = []
for char in text:
stack.append(char)
reversed_text = ''
while stack:
reversed_text += stack.pop()
return reversed_text
print(reverse_string('algorithm')) # mhtirogla
This leverages the LIFO order of stacks to efficiently reverse text.
Implementing a Queue using Stacks
Queues follow FIFO order while stacks follow LIFO. But we can use two stacks to implement a queue.
The idea is to:
- Use one stack for insertion at the end of queue
- Use the second stack as a buffer to reverse order when removing from front
Here is a Python code example:
class QueueViaStacks:
def __init__(self):
self.in_stack = []
self.out_stack = []
def enqueue(self, item):
self.in_stack.append(item)
def dequeue(self):
if not self.out_stack:
while self.in_stack:
self.out_stack.append(self.in_stack.pop())
return self.out_stack.pop()
This leverages two stacks to achieve FIFO queue behavior.
Above were some examples of how stacks enable solving various computer science problems elegantly.
Summary
To summarize, here are the key points about implementing stacks in Python:
-
Stacks are LIFO data structures for managing function calls, undo/redo, parsing etc.
-
Key stack operations are push, pop, and peek.
-
Can implement stacks using Python lists, deques, namedtuples or custom classes.
-
Python lists provide simple but memory inefficient implementations.
-
Deques allow efficient O(1) pops from both ends.
-
Namedtuples add object orientation to stack nodes.
-
Custom classes enable full encapsulation.
-
Stacks are used for bracket matching, evaluating postfix expressions, reversing strings etc.
-
Queues can be implemented using two stacks.
Stacks are fundamental data structures that have numerous applications. I hope this comprehensive guide provided you with a solid grounding on implementing stacks in Python along with common use cases. The key is mastering the push, pop and peek operations using any of the approaches described.
Happy coding with stacks!