Understanding Linked List Cycles
A linked list cycle occurs when a node’s next pointer references an earlier node in the list, creating a loop. This means traversing the list will never reach a null terminator, leading to infinite iteration. Detecting cycles is a foundational skill in data structure interviews and real-world software development, especially when dealing with complex pointer-based data flows.
What Is a Linked List Cycle?
In a singly linked list, each node typically contains a value and a reference to the next node. A cycle exists if starting from the head and following next pointers, you eventually return to a node already visited. The cycle can involve the entire list (a circular linked list) or just a subset of nodes (a "tail" cycle where the last node points back to an interior node).
Why Detecting Cycles Matters
Cycle detection is critical for preventing infinite loops in list traversal, avoiding memory leaks in manual memory management, and ensuring correctness in graph algorithms that use linked structures. Many real-world scenarios like detecting deadlocks in resource allocation graphs or finding loops in state machines rely on the same principles. In coding interviews, it's a classic test of pointer manipulation and algorithmic thinking.
Multiple Solutions for Cycle Detection
🚀 Deploy your AI agent in 10 minutes
Managed Hermes hosting. Zero DevOps. 100M tokens/mo included.
Try it free →We will explore three approaches, with a focus on the two most robust: a hash set method and Floyd's Tortoise and Hare algorithm. Each solution is presented with complete code, complexity analysis, and practical considerations.
Approach 1: Hash Set (Visited Nodes)
The simplest mental model: traverse the list and store each encountered node in a hash set. Before adding a node, check if it already exists in the set. If it does, a cycle is present. This approach works for both singly and doubly linked lists without modifying the original structure.
class ListNode:
def __init__(self, val=0, next=None):
self.val = val
self.next = next
def has_cycle_hash(head: ListNode) -> bool:
"""
Returns True if a cycle exists in the linked list, False otherwise.
Uses O(n) time and O(n) extra space.
"""
visited = set()
current = head
while current:
if current in visited:
return True
visited.add(current)
current = current.next
return False
Complexity Analysis:
- Time: O(n) – each node is processed once. Lookup and insertion in the hash set are O(1) on average.
- Space: O(n) – in the worst case (no cycle or cycle at the very end), we store all n nodes.
This method is straightforward and easy to debug, but the linear space requirement can be a bottleneck for large lists or memory-constrained environments.
Approach 2: Floyd's Tortoise and Hare (Two Pointers)
Floyd's cycle-finding algorithm uses two pointers moving at different speeds: a slow pointer ("tortoise") that moves one step at a time, and a fast pointer ("hare") that moves two steps. If a cycle exists, the fast pointer will eventually lap the slow pointer and they will meet inside the cycle. If the fast pointer reaches a null value, no cycle exists. This method uses constant extra space.
def has_cycle_floyd(head: ListNode) -> bool:
"""
Detects a cycle using Floyd's Tortoise and Hare algorithm.
O(n) time, O(1) space.
"""
slow = fast = head
while fast and fast.next:
slow = slow.next
fast = fast.next.next
if slow == fast:
return True
return False
Complexity Analysis:
- Time: O(n) – in the worst case, the fast pointer traverses the list once and then meets the slow pointer within a few extra steps inside the cycle.
- Space: O(1) – only two pointer variables are used, regardless of list size.
This algorithm is widely preferred in production code and interviews because of its optimal space efficiency. It also forms the basis for finding the exact starting node of the cycle (see below).
Approach 3: Modifying Node Structure (Not Recommended)
A third approach temporarily marks nodes by adding a boolean flag (e.g., visited) to the node structure, or by altering the node’s value or next pointer to a sentinel. While it can achieve O(1) extra space if the flag is a bit within an existing field, it violates the principle of not modifying the input unless explicitly allowed. It also fails if the list is read-only or shared across threads. Therefore, it’s generally avoided.
Going Further: Finding the Start of the Cycle
Detecting the presence of a cycle is often just the first step. In many problems (like LeetCode's "Linked List Cycle II"), you must return the node where the cycle begins. Floyd's algorithm can be extended elegantly to locate that node without extra memory.
After the slow and fast pointers first meet, reset one pointer (e.g., slow) to the head and keep the other at the meeting point. Then move both one step at a time. The node where they meet again is the exact starting node of the cycle. This works because of the mathematical relationship between the distances traveled.
def detect_cycle_start(head: ListNode) -> ListNode | None:
"""
Returns the starting node of the cycle if one exists, otherwise None.
Uses Floyd's algorithm + one extra traversal.
"""
slow = fast = head
# Phase 1: detect meeting point
while fast and fast.next:
slow = slow.next
fast = fast.next.next
if slow == fast:
# Phase 2: find cycle start
slow = head
while slow != fast:
slow = slow.next
fast = fast.next
return slow
return None
Complexity: Still O(n) time and O(1) space, since phase 2 requires at most one pass through the non-cyclical prefix.
Complexity Analysis Summary
Below is a comparison of the three main approaches for cycle detection (not including the start-finding extension):
- Hash Set: Time O(n), Space O(n). Simple, easy to understand, but memory-intensive.
- Floyd's Algorithm: Time O(n), Space O(1). Optimal for space, requires careful pointer management.
- Node Modification: Time O(n), Space O(1) if flag uses existing memory, but destructive and unsafe for shared data.
For production systems where linked lists may be large and memory is a concern, Floyd's algorithm is the clear winner. If code clarity and maintainability are prioritized over space (and list size is moderate), the hash set method remains a valid choice.
Best Practices and Edge Cases
- Handle empty lists: Always check if
headisNone. All presented solutions naturally handle this by returningFalseorNone. - Single-node lists: A single node that points to itself forms a cycle. The algorithms correctly detect this (fast pointer’s
fast.nextexists butfast.next.nextequals itself, meeting slow). - Non-cyclical lists ending in
None: The while conditions in Floyd's approach (while fast and fast.next) prevent null pointer exceptions. - Read-only constraints: When the list cannot be modified (e.g., immutable data or thread-shared structures), avoid node modification approaches. Prefer Floyd's or hash set.
- Performance tuning: In high-performance scenarios, Floyd's algorithm minimizes cache misses because it uses only two pointers. The hash set method may cause frequent memory allocations and hash computations.
- Testing: Unit tests should cover: no cycle, cycle at beginning, cycle in middle, cycle at last node, very long lists, and edge cases like
Nonehead.
Conclusion
Linked list cycle detection is a classic problem that beautifully illustrates trade-offs between time, space, and code complexity. The hash set approach offers immediate clarity, while Floyd's Tortoise and Hare delivers optimal space efficiency and forms the basis for advanced cycle analysis like locating the cycle's start. Understanding both methods and their complexities equips you to choose the right tool for the context—whether building a robust data processing pipeline, acing a technical interview, or debugging pointer errors in low-level code. Mastering these algorithms strengthens your ability to think critically about pointer-based data structures and their real-world implications.