Python
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax and Data Types
-
Section 3: Collections
-
39: Set Operations: Union, Intersection, Difference
-
Section 4: Control Flow
-
Section 5: Functions
-
Section 6: Turtle Graphics and Early Practice Projects
-
Section 7: Working with Files and I/O
-
Section 8: Regular Expressions
-
Section 9: Object-Oriented Python
-
Section 10: Error Handling
-
Section 11: Modules and Packages
-
Section 12: Iterators, Generators, and Functional Tools
-
Section 13: Decorators and Metaprogramming
-
Section 14: Concurrency and Parallelism
-
Section 15: Working with Dates, Times, and Numbers
-
Section 16: Standard Library Deep Dive I: Data Structures
-
Section 17: Standard Library Deep Dive II: System and Introspection
-
Section 18: Standard Library Deep Dive III: Security and Encoding
-
Section 19: Standard Library Deep Dive IV: Text and Data Utilities
-
Section 20: Networking and Web Basics
-
Section 21: Working with Databases
-
Section 22: Testing and Quality
-
Section 23: Advanced Typing
-
Section 24: Context Managers and Resource Handling
-
Section 25: Text, Unicode, and Binary Data
-
Section 26: More Functional and Iteration Tools
-
Section 27: Data Validation and Configuration
-
Section 28: Working with Images and Media
-
Section 29: Property-Based and Documentation Testing
-
Section 30: Packaging and Deployment
-
Section 31: Performance and Internals
-
Section 32: Design Patterns in Python
-
Section 33: GUI Programming
-
Section 34: Security Basics
-
Section 35: Data Structures and Algorithms
-
Section 36: Practical Projects
-
Section 37: Capstone Projects
-
Section 38: Interview and Algorithm Practice
-
Section 39: Writing Idiomatic Python
458: Whiteboard Practice: Fast and Slow Pointers
A few years ago, I was reviewing a PR for a junior dev who was building a custom dependency graph for a build system. The goal was to ensure there were no circular dependencies—basically, making sure Package A didn't depend on Package B, which depended back on Package A. He had implemented a check using a massive hash set to keep track of every node he'd visited. It worked, but as the graph grew to thousands of nodes, the memory overhead started to lag the entire CI pipeline. When I asked him if he could do it without the extra memory, he looked at me like I was asking him to solve cold fusion. That's when we sat down at the whiteboard to talk about the "Tortoise and the Hare."
The Logic of Different Speeds
The fast and slow pointer technique, formally known as Floyd's Cycle-Finding Algorithm, is a bit of a brain-bender the first time you see it, but it's incredibly elegant. Instead of using a separate data structure to remember where you've been, you use two pointers moving through the sequence at different speeds. I like to think of it as a race track. If you have two runners and the track is a straight line, the fast runner just disappears into the distance. But if the track is a loop, the fast runner will eventually lap the slow runner.
In Python, we implement this by initializing two variables (usually called slow and fast) to the head of the list. In each iteration of a while loop, the slow pointer moves one step forward, and the fast pointer moves two. If the fast pointer ever hits a None value, you know there's no cycle. But if slow == fast at any point? You've found your loop. It's a purely mathematical certainty that the fast pointer will "catch" the slow one if a cycle exists, and the best part is that it happens in $O(1)$ space. No sets, no dictionaries, just two references.
def has_cycle(head):
slow = head
fast = head
while fast and fast.next:
slow = slow.next # Move 1 step
fast = fast.next.next # Move 2 steps
if slow == fast:
return True
return False
Finding the Midpoint in One Pass
While cycle detection is the classic "interview" use case, I actually use this pattern more often to find the middle of a linked list. Normally, you'd be tempted to iterate through the whole list to count the elements, divide by two, and then iterate again to reach the middle. That's two passes. It's not a disaster, but it's inefficient.
By using the same fast and slow pointer logic, you can find the middle in a single pass. Because the fast pointer moves exactly twice as fast as the slow one, by the time the fast pointer reaches the end of the list, the slow pointer is exactly halfway there. I've found this particularly useful when implementing merge sort on linked lists, where splitting the list in half is the first requirement of the divide-and-conquer step.
def find_middle(head):
slow = fast = head
while fast and fast.next:
slow = slow.next
fast = fast.next.next
# When fast reaches the end, slow is at the middle
return slow
One quick tip: always be careful with your while condition. If you only check while fast:, your fast.next.next call will throw an AttributeError the moment you hit the tail of the list. Always check both fast and fast.next to keep your code from crashing on lists with an even number of elements.
📋 Practical Task
Exercise: The Circular Reference Detector
You are tasked with writing a utility for a memory management tool. You need to implement a function detect_loop(node) that determines if a singly linked list contains a cycle. To make this a real challenge, you must do this without using any additional data structures (no lists, sets, or dictionaries) to track visited nodes.
Below is a basic Node class for your implementation. Your goal is to return True if a loop exists and False otherwise.
class Node:
def __init__(self, value):
self.value = value
self.next = None
def detect_loop(node):
# Your code here
pass
# Test Case 1: Linear List (No Loop)
node1 = Node(1)
node2 = Node(2)
node3 = Node(3)
node1.next = node2
node2.next = node3
print(f"Test 1: {detect_loop(node1)}") # Expected: False
# Test Case 2: Circular List (Loop)
node_a = Node("A")
node_b = Node("B")
node_c = Node("C")
node_a.next = node_b
node_b.next = node_c
node_c.next = node_a # Cycle created here
print(f"Test 2: {detect_loop(node_a)}") # Expected: True
There are no comments for now.