Skip to main content

Detecting Cycles in Linked Lists Using Floyd's Algorithm

Learn to detect cycles in singly linked lists using the fast-slow pointer technique with O(1) space complexity.

Cycle detection in linked lists is a classic algorithm problem that demonstrates the power of the two-pointer technique. This approach efficiently identifies loops in data structures while maintaining optimal space complexity.

1. Problem Restatement

Clarifying questions to ask the interviewer:

  1. Can the input linked list be modified (i.e., can we change node values or pointers)?
  2. What is the expected time complexity constraint? Are we looking for O(n) or O(n log n)?
  3. What is the maximum size of the linked list we might encounter?
  4. Should we return the node where the cycle begins or just a boolean indicating presence of cycle?
  5. Are there any constraints on memory usage (e.g., can we use O(n) space)?

Restated problem: Given the head of a singly linked list, determine whether there is a cycle in the list. A cycle exists when a node's next pointer eventually points back to a previously visited node, creating a loop in the structure.

Input/output specification:

  • Input: ListNode head - the first node of a singly linked list
  • Output: boolean - true if there is a cycle, false otherwise

Constraints and assumptions:

  • The linked list can have 0 to 10^4 nodes
  • Each node's value is in the range [-10^5, 10^5]
  • The next pointer can point to any node in the list (including itself or null)
  • We cannot modify the structure of the linked list
  • We should avoid using excessive memory (preferably O(1) space)

Data shape and hidden structure: The problem involves a linear data structure (linked list) that can contain cycles, which creates a graph-like structure where nodes form paths and potentially loops. The key insight is that cycles introduce repeated references in what would otherwise be a tree-like structure.

Key assumptions:

  • We're working with a standard singly-linked list where each node has val and next fields
  • The cycle detection should work regardless of where the cycle begins in the list
  • We're not allowed to modify the structure or use additional data structures like hash sets for tracking visited nodes (unless explicitly permitted)

2. Pattern Recognition

The most likely pattern here is the Floyd's Cycle Detection Algorithm (also known as the "tortoise and hare" approach).

The wording that signals this pattern includes:

  • "Cycle in a linked list" - explicitly mentions cycle detection
  • The problem asks to determine if a cycle exists in the linked list structure
  • The constraint that we can only use O(1) space suggests we need to avoid storing references

This problem fits the cycle detection pattern because:

  • We're dealing with a linked list where nodes can point to other nodes in the list
  • The classic "cycle detection" problem is a well-known pattern that has a specific optimal solution
  • The requirement for O(1) space usage is a key indicator that we need to use the two-pointer technique rather than hash maps or other data structures

The core insight needed to solve it is that if there's a cycle in the linked list, we can detect it using two pointers moving at different speeds. If they meet at any point, there's a cycle; if one reaches the end, there isn't.

This pattern is fundamental in linked list problems and appears in many variations including cycle detection, finding the start of a cycle, and detecting cycles in graphs represented as linked lists.

3. Approach Evolution

My first instinct is to use a brute force approach where I traverse the linked list and keep track of visited nodes in a HashSet. This would be O(n) time and O(n) space, but that's not optimal for the follow-up constraint of O(1) space. Let me think about how to optimize this...

The brute force approach would work but has significant space overhead. If I store each visited node in a HashSet, I can detect cycles by checking if I've seen the current node before. However, this uses O(n) extra space which violates the follow-up constraint.

I notice that if I use two pointers moving at different speeds, I can detect cycles without extra space. This is the classic Floyd's Cycle Detection algorithm - also known as the "tortoise and hare" approach. The key insight is that if there's a cycle, the fast pointer will eventually "lap" the slow pointer inside the cycle.

Let me think about this more carefully. If there's a cycle in the linked list, and I have two pointers moving at different speeds (one moving 1 step at a time, the other 2 steps), they will eventually meet inside the cycle. This is because the fast pointer will "lap" the slow pointer, and since they're both moving in the same direction, they'll meet at some point.

The key optimization here is to use the Floyd's cycle detection algorithm with two pointers - one moving twice as fast as the other. This way, I maintain O(1) space complexity while achieving O(n) time complexity.

When I think about the invariants that must hold:

  1. If there's a cycle, the fast pointer will eventually catch up to the slow pointer
  2. The fast pointer moves 2 steps for every 1 step of the slow pointer
  3. If there's no cycle, the fast pointer will reach the end of the list
  4. The relative positions between the pointers will change in a predictable way that allows us to detect meeting

This approach is much more elegant than storing visited nodes, and it's the standard solution for this problem that most interviewers expect to see.

4. Data Structure Choice

For the linked list cycle detection problem, I'll use a two-pointer approach with specific data structures:

Primary Data Structures:

  1. ListNode pointers - The core data structure representing the linked list nodes
  2. Two fast/slow pointers - Two references to ListNode objects that traverse at different speeds

Why Each Data Structure Is Needed:

  • The ListNode pointers are fundamental to representing the linked list structure and traversing through nodes
  • The fast/slow pointer technique is specifically designed for cycle detection in linked lists - it's mathematically proven to work and requires no additional space beyond the two pointers

Time and Space Impact:

  • Time complexity: O(n) where n is the number of nodes in the list
  • Space complexity: O(1) - only using two pointer variables regardless of input size
  • The algorithm uses constant extra space, which is optimal for this problem

Helper Structures: No additional helper structures are needed beyond the two pointer references. The solution leverages the inherent structure of the linked list and mathematical properties of cycle detection.

This approach is superior to alternatives like hash sets or arrays because it maintains optimal space complexity while still providing correct detection of cycles in O(n) time. The two-pointer technique is the standard and most efficient solution for this specific problem pattern in linked list analysis.

5. Algorithm Walkthrough

Let me trace through the Floyd's Cycle Detection algorithm with a concrete example:

Example: 1 -> 2 -> 3 -> 4 -> 2 (cycle from 4 back to 2)

Step-by-step execution:

  1. Initialization: slow = 1, fast = 1
  2. First iteration:
    • Move slow: slow = slow.next → 2
    • Move fast: fast = fast.next.next → 3
    • Check: slow != fast → Continue
  3. Second iteration:
    • Move slow: slow = slow.next → 3
    • Move fast: fast = fast.next.next → 4
    • Check: slow != fast → Continue
  4. Third iteration:
    • Move slow: slow = slow.next → 4
    • Move fast: fast = fast.next.next → 2
    • Check: slow != fast → Continue
  5. Fourth iteration:
    • Move slow: slow = slow.next → 2
    • Move fast: fast = fast.next.next → 3
    • Check: slow != fast → Continue
  6. Fifth iteration:
    • Move slow: slow = slow.next → 3
    • Move fast: fast = fast.next.next → 4
    • Check: slow != fast → Continue
  7. Sixth iteration:
    • Move slow: slow = slow.next → 4
    • Move fast: fast = fast.next.next → 2
    • Check: slow == fast → Return true

Key insight: The fast pointer moves twice as fast, so when the slow pointer enters the cycle, the fast pointer will eventually "lap" it and catch up.

Edge case handling:

  • Empty list or single node: fast.next == null or fast.next.next == null → Return false immediately
  • Two-node cycle: Algorithm correctly detects the loop in 2 iterations

The algorithm terminates because either:

  1. Fast pointer reaches end → No cycle
  2. Fast and slow pointers meet → Cycle detected

This approach works because the relative speed difference ensures that if a cycle exists, the fast pointer will eventually "lap" the slow pointer inside the cycle.

6. Java Solution

/**
 * Definition for singly-linked list.
 * class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode(int x) {
 *         val = x;
 *         next = null;
 *     }
 * }
 */
public class Solution {
    public boolean hasCycle(ListNode head) {
        // Handle edge cases: empty list or single node
        if (head == null || head.next == null) {
            return false;
        }
        
        // Initialize two pointers
        ListNode slow = head;
        ListNode fast = head;
        
        // Traverse the list with different speeds
        while (fast != null && fast.next != null) {
            slow = slow.next;          // Move one step
            fast = fast.next.next;     // Move two steps
            
            // If pointers meet, there's a cycle
            if (slow == fast) {
                return true;
            }
        }
        
        // If we reach here, no cycle exists
        return false;
    }
}

This solution implements Floyd's Cycle Detection Algorithm (also known as the "tortoise and hare" algorithm). The key insight is that if there's a cycle in the linked list, a fast pointer moving twice as fast as a slow pointer will eventually catch up to the slow pointer inside the cycle. If there's no cycle, the fast pointer will reach the end of the list. The algorithm uses O(1) space and runs in O(n) time, making it optimal for this problem.

7. Complexity Analysis

Time Complexity: O(n)

The algorithm uses Floyd's Cycle Detection approach with two pointers moving at different speeds. The fast pointer traverses the list at 2x the speed of the slow pointer, but in terms of asymptotic analysis, both pointers visit each node at most once. The fast pointer will either reach the end of the list or catch up to the slow pointer within a finite number of steps, ensuring that we visit at most n nodes where n is the number of nodes in the list.

Space Complexity: O(1)

The solution uses only two pointer variables (slow and fast) regardless of the input size. No additional data structures are created that scale with input size, making it constant space complexity. The algorithm operates in-place without requiring any auxiliary memory proportional to the input.

Why These Bounds Are Correct

The time complexity is linear because each node is visited at most twice in the worst case - once by the slow pointer and once by the fast pointer. The space complexity is constant because we only maintain a fixed number of variables (two pointers) regardless of the size of the input linked list.

This solution is optimal for the problem as it achieves linear time with constant space - there's no way to determine cycle existence without examining each node at least once, and we cannot use less than constant space since we need to track our position in the list.

The solution is near-optimal because:

  • We cannot do better than O(n) time since we must examine the list to detect cycles
  • We cannot do better than O(1) space since we must maintain state to track our position
  • The algorithm is deterministic and handles all edge cases correctly

8. Edge Cases & Pitfalls

Empty List and Single Node

The most fundamental edge case is an empty list (head == null) or a single node with no cycle. These should return false immediately. A common pitfall is forgetting to check for null head, which leads to NullPointerException.

Self-Loop (Single Node Cycle)

When a list has only one node that points to itself, this creates a cycle of length 1. This is a valid edge case that must be handled correctly by the algorithm - it should detect this as a cycle.

Cycle at Beginning

When the cycle starts at the first node (head), it's still a valid case. The algorithm must correctly identify that the fast pointer will eventually catch up to the slow pointer.

Very Large Lists

With very large lists, integer overflow in cycle detection algorithms (if using indices) could be an issue. However, Floyd's cycle detection uses pointer traversal, so this is not a concern.

Implementation Pitfalls

Incorrect Loop Termination:

// Wrong - can cause infinite loop or premature termination
while (fast != null && fast.next != null) {
    // ...
}

Should be:

// Correct - ensures both pointers can advance safely
while (fast != null && fast.next != null) {
    // ...
}

Pointer Initialization Errors:

  • Initializing both pointers to head instead of head and head.next
  • Not handling the case where fast.next becomes null but fast is not

Race Condition in Multi-threaded Environments: While not typically a concern in this problem, if the list structure is being modified concurrently, the cycle detection might give incorrect results.

Common Mistake: Misunderstanding Cycle Detection

Some candidates incorrectly assume that if a node is visited twice, it's a cycle. However, the correct approach uses two pointers moving at different speeds to detect cycles without tracking visited nodes.

Memory Access Issues

In systems with strict memory constraints, the algorithm should not require additional space beyond O(1) for the two pointers.

Boundary Condition Handling

When head.next == null, we have a single node with no cycle. The algorithm must correctly handle this case without trying to access fast.next.next when fast.next is null.

9. Alternative Approaches

There are several alternative approaches to solving the linked list cycle problem, each with different trade-offs:

1. Hash Set Approach: Store visited nodes in a HashSet and check for duplicates. This approach uses O(n) extra space but is intuitive and easy to implement. It's O(n) time complexity and works well for small lists or when space is not a constraint.

2. Two-Pointer with Different Speeds: Instead of using slow and fast pointers moving at 1x and 2x speed, we could use different ratios like 1x and 3x. This still detects cycles but with more complex mathematical proofs for correctness. The time complexity remains O(n) but with higher constant factors.

3. Node Value Modification: Modify node values to mark visited nodes (e.g., set to a special value like Integer.MAX_VALUE). This approach is O(1) space but modifies the input data, which violates the constraint that we shouldn't mutate the input. This is acceptable in some contexts but not ideal for production systems.

4. Floyd's Cycle Detection with Different Starting Points: Instead of starting both pointers at the head, we could start one at head and another at head.next. However, this requires more careful edge case handling and doesn't provide any significant advantage over the standard approach.

The chosen solution using Floyd's cycle detection algorithm is preferred because it:

  • Uses O(1) space complexity
  • Doesn't mutate input data
  • Has optimal O(n) time complexity
  • Is well-understood and proven to work correctly for all valid inputs

The hash set approach is simpler to understand but uses extra space, while the node modification approach violates input constraints. The two-pointer with different speeds adds unnecessary complexity without benefits in most cases.

10. Test Cases

public class ListNode {
    int val;
    ListNode next;
    ListNode() {}
    ListNode(int val) { this.val = val; }
    ListNode(int val, ListNode next) { this.val = val; this.next = next; }
}

public class Solution {
    public boolean hasCycle(ListNode head) {
        if (head == null || head.next == null) {
            return false;
        }
        
        ListNode slow = head;
        ListNode fast = head;
        
        while (fast != null && fast.next != null) {
            slow = slow.next;
            fast = fast.next.next;
            
            if (slow == fast) {
                return true;
            }
        }
        
        return false;
    }
}

Sample Test Cases

1. Empty List

// Input: []
// Expected Output: false
ListNode head = null;
assert !solution.hasCycle(head);

2. Single Node with No Cycle

// Input: [1]
// Expected Output: false
ListNode head = new ListNode(1);
assert !solution.hasCycle(head);

3. Single Node with Self-Loop

// Input: [1] -> 1 (cycle back to itself)
// Expected Output: true
ListNode head = new ListNode(1);
head.next = head;
assert solution.hasCycle(head);

4. Normal List Without Cycle

// Input: 1 -> 2 -> 3 -> null
// Expected Output: false
ListNode head = new ListNode(1);
head.next = new ListNode(2);
head.next.next = new ListNode(3);
assert !solution.hasCycle(head);

5. List With Cycle

// Input: 1 -> 2 -> 3 -> 4 -> 2 (cycle back to node 2)
// Expected Output: true
ListNode head = new ListNode(1);
head.next = new ListNode(2);
head.next.next = new ListNode(3);
head.next.next.next = new ListNode(4);
head.next.next.next.next = head.next; // Creates cycle
assert solution.hasCycle(head);

6. Two-Node Cycle

// Input: 1 -> 2 -> 1 (cycle back to node 1)
// Expected Output: true
ListNode head = new ListNode(1);
head.next = new ListNode(2);
head.next.next = head; // Creates cycle
assert solution.hasCycle(head);

7. Large List With Cycle

// Input: 1 -> 2 -> 3 -> ... -> 1000 -> 500 (cycle back to node 500)
// Expected Output: true
ListNode head = new ListNode(1);
ListNode current = head;
for (int i = 2; i <= 1000; i++) {
    current.next = new ListNode(i);
    current = current.next;
}
// Create cycle from last node to 500th node
ListNode cycleStart = head;
for (int i = 0; i < 499; i++) {
    cycleStart = cycleStart.next;
}
current.next = cycleStart;
assert solution.hasCycle(head);

11. Final Summary

The core insight is that Floyd's Cycle Detection Algorithm uses two pointers moving at different speeds to detect cycles in O(1) space complexity. By maintaining the invariant that if a cycle exists, the fast pointer will eventually catch up to the slow pointer, we can efficiently detect cycles without requiring additional memory for tracking visited nodes. This solution is optimal for the given constraints and demonstrates the elegance of mathematical insights in algorithm design.

The approach works because:

  1. If there's no cycle, the fast pointer reaches the end
  2. If there's a cycle, the fast pointer will eventually "lap" the slow pointer inside the cycle
  3. The mathematical relationship ensures this detection happens within O(n) time and O(1) space

This is the most efficient solution for cycle detection in a singly linked list, as it cannot be improved upon in terms of space complexity without additional constraints on the input structure.

Was this article helpful?

Topics in this article

More from DSA Problems

  • DSA Patterns

    Fast Slow Pointer Algorithm Patterns in Java

    Comprehensive guide to fast/slow pointer techniques for linked list cycle detection, middle element finding, and related algorithmic problems with Java implementations.

  • DSA Patterns

    Detecting Cycles in Linked Lists and Graphs

    Comprehensive guide to cycle detection algorithms including Floyd's Tortoise and Hare, DFS-based approaches, and practical implementation strategies for FAANG interviews.

  • DSA Problems

    Remove Nth Node From End of List Java Solution

    Complete Java implementation for removing the nth node from the end of a linked list with optimal time and space complexity.

  • DSA Patterns

    Remove Nth Node from End of Linked List

    Complete Java solution for removing the nth node from the end of a linked list using two-pointer technique with O(n) time and O(1) space complexity.

Stay in the Loop

Get curated DSA patterns, coding problems, frontend and backend LLD/HLD, and AI/ML System Design insights delivered straight to your inbox. No spam, just focused interview and engineering content.