Topics › Linked List › Medium Problems of LL

Detect a loop in LL

Medium TUF ID 620 SDEBlind 75

Problem

You are given the head of a singly linked list. Return true if the list contains a loop, meaning that by following next pointers from the head you eventually come back to a node you have already visited. Return false if the walk reaches null.

When a loop exists, the list never ends, so “walk until null” never terminates. The whole problem is about detecting that situation in finite time. Test harnesses usually describe the loop with an index pos: the last node’s next points to the node at index pos, and pos = -1 means no loop. The method itself only receives head.

Constraints: up to 10^4 nodes in the LeetCode version. The follow-up asks for O(1) extra memory.

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

Example 1

3 -> 2 -> 0 -> -4
     ^          |
     +----------+          // pos = 1: the last node points back to the node holding 2
answer = true

Example 2

1 -> 2 -> 3 -> 4 -> 5 -> null
answer = false             // the walk reaches null

Example 3

7 -> (back to 7)           // a single node whose next is itself
answer = true

Approach 1: Brute force, remember every visited node

Intuition. In a list without a loop, a walk from the head visits every node exactly once and then hits null. In a list with a loop, the walk must eventually step onto a node it has already seen, because there are only finitely many nodes. So we record every node we pass in a hash set; the first time we try to record a node that is already there, we have proof of a loop.

Algorithm.

  1. Create an empty HashSet<ListNode>.
  2. Walk the list. For each node, try to add it to the set. If add returns false, the node was already present: return true.
  3. If the walk reaches null, return false.
static boolean hashing(ListNode head) {
    Set<ListNode> visited = new HashSet<>();
    for (ListNode cur = head; cur != null; cur = cur.next) {
        if (!visited.add(cur)) return true;         // add() is false when this node object was seen before
    }
    return false;                                   // reached null, so the list ends
}

Complexity. Time O(n): each node is added once, and the walk stops at the first repeated node, which happens within n + 1 steps. Space O(n) for the set.

HashSet.add returns false when the element is already present, which saves a separate contains call. Because ListNode does not override equals, the set compares nodes by identity. Storing values instead (Set<Integer>) would be a bug: 1 -> 1 -> 1 has repeated values but no loop.

Approach 2: Optimal, Floyd’s tortoise and hare

Intuition. Put two pointers on the head. The tortoise (slow) moves one node per step, the hare (fast) moves two.

That last argument is also why the speeds are 1 and 2. With speeds 1 and 3 the gap shrinks by two per step, and on a loop of even length an odd gap would never reach zero.

Algorithm.

  1. Set slow = head and fast = head.
  2. While fast != null and fast.next != null:
    • slow = slow.next, fast = fast.next.next.
    • If slow == fast, return true.
  3. Return false.
static boolean optimal(ListNode head) {
    ListNode slow = head, fast = head;
    while (fast != null && fast.next != null) {
        slow = slow.next;                           // 1 step
        fast = fast.next.next;                      // 2 steps
        if (slow == fast) return true;              // the hare caught the tortoise inside the loop
    }
    return false;                                   // the hare found the end of the list
}

Dry run on Example 1. Name the nodes by index: n0 = 3, n1 = 2, n2 = 0, n3 = -4, with n3.next = n1.

step slow fast same node?
0 n0 n0 start, not checked
1 n1 n2 no
2 n2 n1 (n2 → n3 → n1) no
3 n3 n3 (n1 → n2 → n3) yes, return true

On Example 2 the hare visits 1, 3, 5 and then finds fast.next == null, so the loop exits after two steps and returns false.

Complexity. Time O(n): if there is no loop the hare reaches the end in about n / 2 steps; if there is a loop of length C after a tail of length L, the tortoise enters the loop after L steps and is caught within C more, and L + C ≤ n. Space O(1): two pointers.

The order of the loop condition matters. fast != null must be tested before fast.next != null, otherwise an even-length list throws a NullPointerException on its last step. The check slow == fast is placed after moving; checking before the first move would report a loop immediately because both start on the head.

Edge cases and pitfalls