Stack in Python
What is a Stack?
A stack is a data structure operating on the Last In, First Out (LIFO) principle. This means that the last element added to the stack is first removed. Consider a stack of plates:
- The last placed plate is the first one you remove.
- You can only come to the top plate without dislodging anything else.
Key Operations of a Stack
- Push:
- Adds an element to the top of the stack.
- Example: If the stack is
[10, 20]and you push30, the stack becomes[10, 20, 30].
2. Pop:
- Removes and returns the top element of the stack.
- Example: If the stack is
[10, 20, 30]and you pop, it returns30and the stack becomes[10, 20].
3. Peek (or Top):
- Retrieves the top element of the stack without removing it.
- Example: If the stack is
[10, 20, 30], the top element is30.
4. IsEmpty:
- Checks if the stack contains any element.
- Example: If the stack is
[], it is empty; otherwise, it is not.
Why Use a Stack?
Stacks are simple yet powerful and are widely used in:
- Programming: Managing function calls, undo/redo actions.
- Algorithms: DFS (Depth-First Search), evaluating mathematical expressions.
- Data Processing: Balance symbols (parentheses, brackets), reverse a word.
Implementing Stacks in Python
Python does not have an in-built class Stack, however you can develop one using:
- Lists, basic and direct.
collections.deque(optimized for add/remove operations).queue.LifoQueue(thread-safe for multi-threaded programs).
1. Stack Using Lists
Lists in Python support methods like append() and pop() to mimic stack behavior. However, they are not specifically designed for stacks.
Code Example:
stack = [] # Initialize an empty stack
# Push elements onto the stack
stack.append(10) # Stack: [10]
stack.append(20) # Stack: [10, 20]
stack.append(30) # Stack: [10, 20, 30]
print("Stack after pushes:", stack)
# Pop an element from the stack
popped_element = stack.pop() # Removes 30
print("Popped element:", popped_element) # Outputs: 30
print("Stack after pop:", stack) # Outputs: [10, 20]
# Peek the top element
if stack:
top_element = stack[-1] # Get the last element
print("Top element:", top_element) # Outputs: 20
# Check if the stack is empty
print("Is the stack empty?", len(stack) == 0) # Outputs: False
Limitations:
- Lists are dynamic arrays. When they resize, operations such as
append()may take longer. - Inefficiency: It suffers performance degradation upon many resize or big stack.
2. Stack Using
A deque is a double-ended queue, which is ideal for stacks because:
- It is optimized for appending and popping elements from both ends.
- Implemented as a doubly linked list, hence making operations quite fast.
Code Example:
from collections import deque
stack = deque() # Initialize an empty stack
# Push elements
stack.append(10) # Stack: deque([10])
stack.append(20) # Stack: deque([10, 20])
stack.append(30) # Stack: deque([10, 20, 30])
print("Stack after pushes:", stack)
# Pop an element
popped_element = stack.pop() # Removes 30
print("Popped element:", popped_element) # Outputs: 30
print("Stack after pop:", stack) # Outputs: deque([10, 20])
# Peek the top element
if stack:
top_element = stack[-1]
print("Top element:", top_element) # Outputs: 20
# Check if the stack is empty
print("Is the stack empty?", len(stack) == 0) # Outputs: False
Advantages:
- Fast and memory-efficient for stack operations.
- More appropriate for stacks than
list.
3. Stack Using queue.LifoQueue
The queue.LifoQueue class provides a thread-safe stack implementation. This is especially useful in multi-threaded environments.
Code Example:
from queue import LifoQueue
stack = LifoQueue() # Initialize an empty stack
# Push elements
stack.put(10) # Stack: [10]
stack.put(20) # Stack: [10, 20]
stack.put(30) # Stack: [10, 20, 30]
print("Stack size after pushes:", stack.qsize()) # Outputs: 3
# Pop an element
popped_element = stack.get() # Removes 30
print("Popped element:", popped_element) # Outputs: 30
print("Stack size after pop:", stack.qsize()) # Outputs: 2
# Check if the stack is empty
print("Is the stack empty?", stack.empty()) # Outputs: False
Advantages:
- Thread-safe for concurrent programs.
- Built-in methods (put(), get(), empty()) make it robust.
Disadvantages:
- Slightly slower due to thread-safety overhead.
Choosing the Right Implementation
- Use a
listfor basic stack operations in single-threaded, non-performance-critical applications. - Use
dequefor better performance and memory efficiency in most cases. - Use
LifoQueuewhen working in multi-threaded environments.
Real-World Applications of Stacks
- Expression Evaluation:
- Parsing and evaluating mathematical expressions.
- Example: Infix to postfix conversion.
- Function Call Stack:
- Stacks are used in programming languages to track the function calls and local variables.
- Example: Recursive function calls.
- Backtracking:
- Algorithms such as depth-first search use stacks to traverse the paths.
- Example: Maze solving or puzzles.
- Undo Operations:
- Text editors maintain a stack to keep track of the user actions to provide the undo/redo feature.
- Balanced Parentheses:
- It is used to check if an expression contains balanced brackets or parentheses.
- For instance:
((a + b) * c)is balanced but(a + b))isn’t.
Conclusion
- A stack is a versatile and widely-used data structure.
- In Python:
listis simple but less efficient for large-scale operations.dequeis optimized and more efficient.LifoQueueis for thread-safe applications.