Command Palette

Search for a command to run...

Linked List Cycle Detection #

Problem Statement #

Given the head of a linked list, determine if the linked list has a cycle in it.

A cycle exists in a linked list if there is some node in the list that can be reached again by continuously following the next pointer. The cycle can occur anywhere in the list, not necessarily at the end.

Return true if there is a cycle in the linked list, otherwise return false.

Challenge: Solve this without using any additional data structures (like arrays or hash tables).

Method Signature #

public static boolean hasCycle(ListNode head)

ListNode Definition #

class ListNode {
    int val;
    ListNode next;

    ListNode(int x) {
        val = x;
        next = null;
    }
}

Examples #

Example 1: Cycle exists

Input: head = [3,2,0,-4], cycle connects tail to node index 1
Output: true

Visual:
3 -> 2 -> 0 -> -4
     ↑         |
     +---------+

Explanation: There is a cycle where the tail connects back to the 2nd node (0-indexed).

Example 2: Cycle exists (simple)

Input: head = [1,2], cycle connects tail to node index 0
Output: true

Visual:
1 -> 2
↑    |
+----+

Explanation: There is a cycle where the tail connects back to the first node.

Example 3: No cycle

Input: head = [1,2,3,4,5]
Output: false

Visual:
1 -> 2 -> 3 -> 4 -> 5 -> null

Explanation: The list ends with null, so there is no cycle.

Example 4: Single node, no cycle

Input: head = [1]
Output: false

Visual:
1 -> null

Explanation: Single node pointing to null has no cycle.

Example 5: Single node with cycle

Input: head = [1], cycle connects to itself
Output: true

Visual:
1 --+
↑   |
+---+

Explanation: The single node points to itself, creating a cycle.

Test Your Solution #

public class LinkedListCycleTest {
    public static void main(String[] args) {
        // Test case 1: No cycle [1,2,3,4,5]
        ListNode test1 = new ListNode(1);
        test1.next = new ListNode(2);
        test1.next.next = new ListNode(3);
        test1.next.next.next = new ListNode(4);
        test1.next.next.next.next = new ListNode(5);
        System.out.println("Test 1 (no cycle): " + hasCycle(test1)); // Expected: false

        // Test case 2: Cycle [3,2,0,-4] with tail -> index 1
        ListNode test2 = new ListNode(3);
        test2.next = new ListNode(2);
        test2.next.next = new ListNode(0);
        test2.next.next.next = new ListNode(-4);
        test2.next.next.next.next = test2.next; // Create cycle: -4 -> 2
        System.out.println("Test 2 (has cycle): " + hasCycle(test2)); // Expected: true

        // Test case 3: Simple cycle [1,2] with tail -> index 0
        ListNode test3 = new ListNode(1);
        test3.next = new ListNode(2);
        test3.next.next = test3; // Create cycle: 2 -> 1
        System.out.println("Test 3 (simple cycle): " + hasCycle(test3)); // Expected: true

        // Test case 4: Single node, no cycle
        ListNode test4 = new ListNode(1);
        System.out.println("Test 4 (single, no cycle): " + hasCycle(test4)); // Expected: false

        // Test case 5: Single node with self cycle
        ListNode test5 = new ListNode(1);
        test5.next = test5; // Points to itself
        System.out.println("Test 5 (self cycle): " + hasCycle(test5)); // Expected: true

        // Test case 6: Empty list
        System.out.println("Test 6 (empty): " + hasCycle(null)); // Expected: false
    }

    // Your implementation goes here
    public static boolean hasCycle(ListNode head) {
        // TODO: Implement cycle detection
        return false;
    }
}
Solution

Solution: Two Pointers Approach #

public static boolean hasCycle(ListNode head) {
    // Handle edge cases
    if (head == null || head.next == null) {
        return false;
    }

    // Initialize two pointers
    ListNode slow = head;  // Moves 1 step at a time
    ListNode fast = head;  // Moves 2 steps at a time

    // Traverse with different speeds
    while (fast != null && fast.next != null) {
        slow = slow.next;         // Move slow pointer 1 step
        fast = fast.next.next;    // Move fast pointer 2 steps

        // If pointers meet, there's a cycle
        if (slow == fast) {
            return true;
        }
    }

    // Fast pointer reached end (null), no cycle
    return false;
}

Algorithm Explanation: Two Pointers Technique #

Different Speed Strategy #

Note: This algorithm is also known as Floyd's Cycle Detection or the "Tortoise and Hare" algorithm.

This algorithm uses two pointers moving at different speeds:

  • Slow pointer (tortoise): Moves 1 step at a time
  • Fast pointer (hare): Moves 2 steps at a time

Key Insight #

If there's a cycle: The fast pointer will eventually "lap" the slow pointer and they will meet inside the cycle.

If there's no cycle: The fast pointer will reach the end (null) first.

Why This Works #

Mathematical Proof:

  • If there's a cycle, both pointers will eventually enter it
  • Once both are in the cycle, the fast pointer gains 1 position on the slow pointer each iteration
  • Since the cycle is finite, the fast pointer will eventually catch up to the slow pointer

Visual Trace #

Example 1: Cycle exists

List: 3 -> 2 -> 0 -> -4
           ↑         |
           +---------+

Initial: slow = 3, fast = 3

Step 1: slow = 2, fast = 0
        slow moves 1 step (3 -> 2)
        fast moves 2 steps (3 -> 2 -> 0)

Step 2: slow = 0, fast = 2
        slow moves 1 step (2 -> 0)
        fast moves 2 steps (0 -> -4 -> 2)

Step 3: slow = -4, fast = -4
        slow moves 1 step (0 -> -4)
        fast moves 2 steps (2 -> 0 -> -4)

Step 4: slow == fast. They meet at node -4. → return true

Example 2: No cycle

List: 1 -> 2 -> 3 -> 4 -> 5 -> null

Initial: slow = 1, fast = 1

Step 1: slow = 2, fast = 3
Step 2: slow = 3, fast = 5
Step 3: slow = 4, fast = null (fast.next = null, can't move 2 steps)

Fast reached null → return false

Alternative Approaches (Less Efficient) #

This approach tries to detect cycles by limiting how many steps we take:

public static boolean hasCycleStepLimit(ListNode head) {
    ListNode current = head;
    int steps = 0;
    int maxSteps = 10000; // Arbitrary limit

    while (current != null && steps < maxSteps) {
        current = current.next;
        steps++;
    }

    // If we've taken too many steps, assume cycle
    return steps >= maxSteps;
}

Why it's not good:

  • The step limit is just a guess - it might be too small or too large
  • Not reliable for all cases
  • Doesn't actually prove a cycle exists

This approach tries to mark nodes we've already seen:

public static boolean hasCycleDestructive(ListNode head) {
    ListNode current = head;
    final int VISITED_MARKER = Integer.MIN_VALUE;

    while (current != null) {
        if (current.val == VISITED_MARKER) {
            return true; // We've seen this node before
        }
        current.val = VISITED_MARKER; // Mark as visited
        current = current.next;
    }

    return false;
}

Why it's not good:

  • Changes the original data (destructive)
  • Fails if the marker value naturally exists in the list
  • In real applications, you usually can't modify the input data

Performance Analysis #

Two Pointers Approach:

  • Time Efficiency: Linear performance in the worst case
    • If no cycle: traverse entire list once (fast pointer moves through all nodes)
    • If cycle exists: at most 2n steps where n is the number of nodes
  • Memory Efficiency: Very efficient memory usage
    • Only uses two pointer variables
    • No additional data structures needed
    • Perfect constant space usage

Key Insights #

This problem demonstrates:

  • Two pointers technique with different movement speeds
  • Cycle detection without additional memory
  • Mathematical reasoning behind algorithm correctness
  • Edge case handling (empty list, single node)
  • Elegant problem solving using simple pointer manipulation

Common Mistakes #

  1. Null pointer exceptions: Not checking if fast.next is null before moving
  2. Wrong termination condition: Not handling the case where fast reaches null
  3. Incorrect pointer initialization: Starting pointers at wrong positions
  4. Missing edge cases: Not handling empty list or single node
  5. Infinite loop: Not advancing pointers correctly

Edge Cases #

  1. Empty list: head = null → return false
  2. Single node, no cycle: [1] -> null → return false
  3. Single node, self cycle: [1] -> [1] → return true
  4. Two nodes, no cycle: [1,2] -> null → return false
  5. Two nodes with cycle: [1,2] where 2 -> 1 → return true

Step-by-Step Execution #

Detailed trace for cycle detection:

List with cycle: A -> B -> C -> D
                      ↑         |
                      +---------+

Iteration  | Slow | Fast | Notes
-----------|------|------|------------------
Initial    |  A   |  A   | Both start at head
1          |  B   |  C   | slow+1, fast+2
2          |  C   |  D   | continuing...
3          |  D   |  C   | fast cycles back
4          |  C   |  D   | slow enters cycle
5          |  D   |  C   | both in cycle
6          |  C   |  D   | getting closer...
7          |  D   |  C   | very close...
8          |  C   |  C   | MEET! → return true

Key insight: Once both pointers are in the cycle,
the distance between them decreases by 1 each step
until they meet.

Extension Challenges #

  1. Find cycle start: Return the node where the cycle begins
  2. Cycle length: Find the length of the cycle if one exists
  3. Multiple cycles: Detect if there are multiple separate cycles
  4. Remove cycle: Break the cycle by setting appropriate next pointer to null
  5. Doubly linked list: Adapt algorithm for doubly linked lists

Real-World Applications #

  • Memory leak detection: Detect circular references in object graphs
  • Infinite loop prevention: Detect cycles in state machines or workflows
  • Graph algorithms: Detect cycles in linked graph representations
  • Resource management: Prevent circular dependencies in resource allocation
  • Data validation: Ensure data structures don't have unintended cycles