Python Lecture 15: Advanced Data Structures - Building Efficient Systems
Welcome to an exciting lecture that bridges the gap between basic Python and computer science fundamentals! You've mastered Python's built-in data structures - lists, dictionaries, tuples, and sets. Today, we're exploring advanced data structures - specialized structures like stacks, queues, linked lists, and trees that form the foundation of efficient algorithms and complex systems. Understanding these structures is crucial for writing performant code and solving sophisticated programming challenges.
These data structures aren't just theoretical concepts - they're used everywhere in real software. Your browser's back button uses a stack. Print job queues use queues. File systems use trees. Social networks use graphs. Operating systems use all of these. Understanding advanced data structures makes you a better programmer by helping you choose the right structure for each problem, understand how libraries and frameworks work internally, and optimize your code for performance.
By the end of this comprehensive lecture, you'll understand what each data structure is conceptually, how to implement them in Python, when to use each structure, and how they enable efficient algorithms. We'll build these structures from scratch to understand their mechanics, then explore practical applications. This knowledge elevates you from a Python programmer to a computer scientist who understands algorithmic efficiency. Let's dive into the world of advanced data structures!
Understanding Stacks - Last In, First Out (LIFO)
A stack is a linear data structure that follows the Last-In-First-Out (LIFO) principle. Think of it like a stack of plates - you can only add or remove plates from the top. The last plate you put on is the first one you take off. This simple concept is surprisingly powerful and appears throughout computing.
Stack Operations: Stacks support two primary operations: push (add to top) and pop (remove from top). Additional operations include peek (look at top without removing) and is_empty (check if stack is empty). These operations are all O(1) - constant time, meaning they take the same time regardless of stack size. This efficiency makes stacks useful for many algorithms.
Real-World Stack Applications: Stacks are everywhere: your browser's back/forward buttons (each page is pushed onto a stack), undo/redo functionality (each action is pushed onto a stack), function call tracking (the call stack - each function call is pushed, returned calls are popped), expression evaluation (converting infix to postfix notation), and recursive algorithm implementation. Understanding stacks helps you understand how computers execute programs.
Why Stacks Matter: Stacks naturally model any process where you need to reverse order or backtrack. When you need to process the most recent item first, a stack is your tool. The LIFO property makes certain algorithms elegant and efficient - algorithms that would be complex with other structures become simple with stacks.
# Stack implementation using Python list
class Stack:
"""Stack data structure - LIFO"""
def __init__(self):
self.items = []
def push(self, item):
"""Add item to top of stack"""
self.items.append(item)
def pop(self):
"""Remove and return top item"""
if not self.is_empty():
return self.items.pop()
raise IndexError("Pop from empty stack")
def peek(self):
"""Return top item without removing"""
if not self.is_empty():
return self.items[-1]
raise IndexError("Peek from empty stack")
def is_empty(self):
"""Check if stack is empty"""
return len(self.items) == 0
def size(self):
"""Return number of items"""
return len(self.items)
def __str__(self):
"""String representation"""
return f"Stack({self.items})"
# Using the stack
stack = Stack()
# Push items
stack.push(10)
stack.push(20)
stack.push(30)
print(f"Stack: {stack}")
# Peek at top
print(f"Top item: {stack.peek()}")
# Pop items (LIFO order)
print(f"Popped: {stack.pop()}") # 30
print(f"Popped: {stack.pop()}") # 20
print(f"Stack now: {stack}")
# Practical example: Undo functionality
class TextEditor:
def __init__(self):
self.text = ""
self.undo_stack = Stack()
def type(self, new_text):
"""Add text and save state for undo"""
self.undo_stack.push(self.text)
self.text += new_text
def undo(self):
"""Undo last change"""
if not self.undo_stack.is_empty():
self.text = self.undo_stack.pop()
def get_text(self):
return self.text
# Using text editor
editor = TextEditor()
editor.type("Hello ")
editor.type("World!")
print(f"Text: {editor.get_text()}") # Hello World!
editor.undo()
print(f"After undo: {editor.get_text()}") # Hello
Real-World Application - Expression Evaluation: When evaluating mathematical expressions like "3 + 4 * 2", computers use stacks. Operators and operands are pushed onto stacks, then popped and evaluated following precedence rules. This is how calculators and programming language interpreters work internally. Understanding stacks reveals how code execution works!
Understanding Queues - First In, First Out (FIFO)
A queue is a linear data structure that follows the First-In-First-Out (FIFO) principle. Like a line at a store - the first person in line is the first person served. Items are added at the back (enqueue) and removed from the front (dequeue). This ordering is perfect for fair scheduling and processing tasks in order.
Queue Operations: The main operations are enqueue (add to back), dequeue (remove from front), peek (look at front), and is_empty. Like stacks, these operations are O(1) when implemented efficiently. The key difference from stacks is FIFO vs LIFO - this changes everything about when and how you use them.
Real-World Queue Applications: Queues model any waiting or scheduling system: print job queues (first print request is handled first), task scheduling in operating systems, breadth-first search in graphs, handling asynchronous requests, message queues in distributed systems, and customer service lines. Any time you need to process things in the order they arrived, queues are your tool.
# Queue implementation using Python list
class Queue:
"""Queue data structure - FIFO"""
def __init__(self):
self.items = []
def enqueue(self, item):
"""Add item to back of queue"""
self.items.append(item)
def dequeue(self):
"""Remove and return front item"""
if not self.is_empty():
return self.items.pop(0)
raise IndexError("Dequeue from empty queue")
def peek(self):
"""Return front item without removing"""
if not self.is_empty():
return self.items[0]
raise IndexError("Peek from empty queue")
def is_empty(self):
"""Check if queue is empty"""
return len(self.items) == 0
def size(self):
"""Return number of items"""
return len(self.items)
def __str__(self):
"""String representation"""
return f"Queue({self.items})"
# Using the queue
queue = Queue()
# Enqueue items
queue.enqueue("Task 1")
queue.enqueue("Task 2")
queue.enqueue("Task 3")
print(f"Queue: {queue}")
# Dequeue items (FIFO order)
print(f"Processing: {queue.dequeue()}") # Task 1
print(f"Processing: {queue.dequeue()}") # Task 2
print(f"Queue now: {queue}")
# Practical example: Print queue
class PrintQueue:
def __init__(self):
self.queue = Queue()
def add_job(self, document):
"""Add print job to queue"""
self.queue.enqueue(document)
print(f"Added to print queue: {document}")
def print_next(self):
"""Print next document"""
if not self.queue.is_empty():
doc = self.queue.dequeue()
print(f"Printing: {doc}")
else:
print("Print queue is empty")
def view_queue(self):
"""Show waiting documents"""
return f"Waiting to print: {self.queue.items}"
# Using print queue
printer = PrintQueue()
printer.add_job("Report.pdf")
printer.add_job("Letter.docx")
printer.add_job("Photo.jpg")
print(printer.view_queue())
printer.print_next() # Prints Report.pdf (first in)
printer.print_next() # Prints Letter.docx (second in)
Deques - Double-Ended Queues
A deque (pronounced "deck") is a double-ended queue where you can add or remove items from both ends. It combines the capabilities of stacks and queues, making it more flexible. Python provides an efficient deque implementation in the collections module.
# Using Python's deque from collections
from collections import deque
# Creating a deque
dq = deque([1, 2, 3])
# Add to right (like queue)
dq.append(4)
print(f"After append: {dq}")
# Add to left
dq.appendleft(0)
print(f"After appendleft: {dq}")
# Remove from right (like stack)
dq.pop()
print(f"After pop: {dq}")
# Remove from left (like queue)
dq.popleft()
print(f"After popleft: {dq}")
# Practical use: Browser history
class Browser:
def __init__(self):
self.history = deque()
self.current_page = None
def visit(self, url):
"""Visit new page"""
if self.current_page:
self.history.append(self.current_page)
self.current_page = url
print(f"Visiting: {url}")
def back(self):
"""Go back"""
if self.history:
self.current_page = self.history.pop()
print(f"Back to: {self.current_page}")
else:
print("No history available")
# Using browser
browser = Browser()
browser.visit("google.com")
browser.visit("github.com")
browser.visit("stackoverflow.com")
browser.back() # Back to github.com
browser.back() # Back to google.com
📚 Related Python Tutorials:
Linked Lists - Dynamic Memory Structures
A linked list is a linear data structure where elements (nodes) are connected via references (links). Unlike arrays/lists where elements are stored contiguously in memory, linked list nodes can be anywhere in memory, connected by pointers. This enables efficient insertion and deletion but slower access.
Why Linked Lists Matter: Lists (arrays) require contiguous memory and are expensive to insert/delete from the middle (everything must shift). Linked lists solve this - insertion and deletion are O(1) if you have a reference to the location. The tradeoff is O(n) access time - you must traverse from the head. Understanding these tradeoffs helps you choose the right structure.
# Node class
class Node:
"""Single node in linked list"""
def __init__(self, data):
self.data = data
self.next = None
# Linked List class
class LinkedList:
"""Singly linked list"""
def __init__(self):
self.head = None
def append(self, data):
"""Add node to end"""
new_node = Node(data)
if not self.head:
self.head = new_node
return
current = self.head
while current.next:
current = current.next
current.next = new_node
def prepend(self, data):
"""Add node to beginning"""
new_node = Node(data)
new_node.next = self.head
self.head = new_node
def delete(self, data):
"""Delete first node with data"""
if not self.head:
return
if self.head.data == data:
self.head = self.head.next
return
current = self.head
while current.next:
if current.next.data == data:
current.next = current.next.next
return
current = current.next
def display(self):
"""Display all nodes"""
elements = []
current = self.head
while current:
elements.append(str(current.data))
current = current.next
return " -> ".join(elements)
# Using linked list
ll = LinkedList()
ll.append(1)
ll.append(2)
ll.append(3)
print(f"List: {ll.display()}")
ll.prepend(0)
print(f"After prepend: {ll.display()}")
ll.delete(2)
print(f"After delete: {ll.display()}")
Algorithm Complexity - Big O Notation
Understanding data structure performance requires understanding Big O notation - a way to describe how algorithm runtime grows as input size increases. This is crucial for choosing the right data structure for your problem.
Common Complexities:
• O(1) - Constant: Same time regardless of size (stack push/pop)
• O(log n) - Logarithmic: Doubles input, adds one step (binary search)
• O(n) - Linear: Time grows proportionally with size (list traversal)
• O(n log n) - Linearithmic: Efficient sorting algorithms
• O(n²) - Quadratic: Nested loops over data (bubble sort)
• O(2ⁿ) - Exponential: Doubles time when input grows by 1 (recursive fibonacci)
# Performance comparison
import time
# List operations
print("=== List Performance ===")
my_list = list(range(100000))
# Access - O(1)
start = time.time()
item = my_list[50000]
print(f"Access time: {time.time() - start:.6f}s")
# Insert at beginning - O(n)
start = time.time()
my_list.insert(0, -1)
print(f"Insert at start: {time.time() - start:.6f}s")
# Append - O(1)
start = time.time()
my_list.append(100000)
print(f"Append: {time.time() - start:.6f}s")
# Dictionary operations
print("\n=== Dictionary Performance ===")
my_dict = {i: i*2 for i in range(100000)}
# Access - O(1)
start = time.time()
value = my_dict[50000]
print(f"Access time: {time.time() - start:.6f}s")
# Insert - O(1)
start = time.time()
my_dict[100000] = 200000
print(f"Insert: {time.time() - start:.6f}s")
Practical Applications and When to Use Each Structure
Use Stacks When: You need LIFO behavior, implementing undo/redo, parsing expressions, backtracking algorithms, function call management.
Use Queues When: You need FIFO behavior, task scheduling, breadth-first search, handling requests in order, print spooling.
Use Linked Lists When: Frequent insertions/deletions at arbitrary positions, unknown or changing size, implementing other structures (stacks, queues).
Use Lists When: Random access needed, size is relatively stable, simple sequential access.
Use Dictionaries When: Fast key-based lookup needed, mapping relationships, caching/memoization.
Summary and Data Structure Mastery
Advanced data structures are the foundation of efficient algorithms. You've learned:
✓ Stack implementation and LIFO behavior
✓ Queue implementation and FIFO behavior
✓ Deques for double-ended operations
✓ Linked list concepts and implementation
✓ Big O notation for performance analysis
✓ When to use each data structure
✓ Real-world applications
Choose Wisely: The right data structure makes algorithms elegant and efficient. The wrong choice makes them clunky and slow. Always consider: What operations will be most frequent? What are the performance requirements? What are the memory constraints? This thinking separates good programmers from great ones.
Final Challenge: Build a task management system using multiple data structures: stack for undo, queue for task processing order, dictionary for fast task lookup by ID, and linked list for priority ordering. Implement add task, complete task, undo, and view queue. This integrates everything you've learned about data structures!
Congratulations on completing the Python course! You've built a solid foundation from basic syntax to advanced data structures. Continue practicing, building projects, and exploring specialized topics like web development, data science, or machine learning. Keep coding!

