Detect a Loop in a Linked List: Floyd's Algorithm Explained
Cycle detection in a linked list is one of those problems that sounds academic until you realise it's the kind of bug that crashes production systems. A rogue pointer in a linked list can cause your traversal code to spin forever, pegging a CPU at 100% with no stack overflow to warn you — just a frozen process. Memory allocators, OS schedulers, and graph traversal algorithms all rely on linked structures, and a cycle in any of them is catastrophic.
The core problem is deceptively simple: you have a linked list and you need to know whether following the next pointers will eventually lead you to null (healthy list) or trap you in an infinite circle (corrupted list). Naively iterating and hoping to reach null will hang your program if a cycle exists. You need a smarter strategy — one that doesn't require extra memory proportional to the list size.
By the end of this article you'll understand Floyd's Cycle Detection algorithm from first principles, be able to implement it cleanly in Java, extend it to find exactly where the cycle begins, and confidently explain the mathematical intuition behind it in an interview. You'll also see the two most common implementation mistakes that cause subtle bugs even when the core idea is right.
Why the Naive Approach Breaks — and What We Really Need
The most obvious idea is to use a HashSet: visit each node, store its reference, and if you ever see the same reference twice you've found a cycle. It works. But it costs O(n) extra space — you're storing every node reference you've seen. For a list with millions of nodes, that's a meaningful memory hit, and in constrained environments (embedded systems, kernel code) it's a non-starter.
The other naive approach — setting a maximum iteration count — is fragile. How do you pick the limit? Too small and you get false positives on valid long lists. Too large and a cycle still burns time before you catch it.
What you actually need is an algorithm that uses O(1) space — constant memory regardless of list size — and runs in O(n) time. That's exactly the contract Floyd's Cycle Detection algorithm (also called the tortoise and hare algorithm) delivers. The insight is elegant: instead of remembering where you've been, you use two pointers moving at different speeds and let physics — or rather, modular arithmetic — do the work for you.
Understanding this trade-off (O(n) space HashSet vs O(1) space Floyd's) is precisely what interviewers probe for. Knowing Floyd's exists isn't enough; knowing why you'd choose it is what separates junior from senior.
import java.util.HashSet; import java.util.Set; public class NaiveLoopDetection { // A single node in our linked list static class Node { int value; Node next; Node(int value) { this.value = value; this.next = null; } } /** * Naive approach: store every visited node reference in a HashSet. * If we encounter a node we've already stored, a cycle exists. * Time: O(n) — we visit each node at most once * Space: O(n) — we store up to n node references */ public static boolean hasCycleNaive(Node head) { Set<Node> visitedNodes = new HashSet<>(); Node current = head; while (current != null) { // If this node's reference is already in the set, we've looped back if (visitedNodes.contains(current)) { return true; // cycle detected } visitedNodes.add(current); // mark this node as visited current = current.next; // move to the next node } return false; // reached null — no cycle } public static void main(String[] args) { // Build a list: 1 -> 2 -> 3 -> 4 -> 5 -> null (no cycle) Node head = new Node(1); head.next = new Node(2); head.next.next = new Node(3); head.next.next.next = new Node(4); head.next.next.next.next = new Node(5); System.out.println("List with no cycle: " + hasCycleNaive(head)); // false // Now create a cycle: node 5's next points back to node 3 // List becomes: 1 -> 2 -> 3 -> 4 -> 5 -> (back to 3) head.next.next.next.next.next = head.next.next; // node5.next = node3 System.out.println("List with cycle: " + hasCycleNaive(head)); // true } }
List with cycle: true
Floyd's Cycle Detection — The Tortoise and the Hare
Floyd's algorithm uses two pointers: tortoise moves one step at a time, hare moves two steps at a time. If there's no cycle, the hare reaches null and you're done. If there is a cycle, the hare enters it first and starts lapping the tortoise. Eventually — and this is the key — they must meet inside the cycle.
Why must they meet? Think of it this way: once both pointers are inside the cycle of length k, the hare gains exactly one step on the tortoise per iteration. So the gap between them decreases by one each step. Starting from any gap, you'll hit zero (they meet) in at most k steps. The meeting is guaranteed.
This gives you O(n) time and O(1) space — no HashSet, no counters, just two pointers. That's the win.
One important implementation detail: you must check hare != null AND hare.next != null before advancing the hare two steps. Skipping either check causes a NullPointerException on lists with an even number of nodes — a gotcha that trips up almost every first implementation.
Once they meet, you can go further: find the exact node where the cycle starts. That's the real power of Floyd's, and it's what most tutorials skip.
public class FloydCycleDetection { static class Node { int value; Node next; Node(int value) { this.value = value; this.next = null; } } /** * Floyd's Cycle Detection (Tortoise and Hare). * Time: O(n) — tortoise visits each node at most once before meeting * Space: O(1) — only two pointers, no extra data structures * * @return the meeting node inside the cycle, or null if no cycle exists */ public static Node detectCycle(Node head) { if (head == null || head.next == null) { return null; // empty or single-node list can't have a cycle } Node tortoise = head; // moves 1 step per iteration Node hare = head; // moves 2 steps per iteration while (hare != null && hare.next != null) { tortoise = tortoise.next; // 1 step hare = hare.next.next; // 2 steps // If they meet, a cycle exists — return the meeting point if (tortoise == hare) { return tortoise; } } // hare hit null — no cycle return null; } /** * Find the node where the cycle STARTS (not just whether one exists). * * Mathematical proof sketch: * Let F = distance from head to cycle start * C = length of the cycle * h = distance from cycle start to meeting point (inside cycle) * * When they meet: tortoise travelled F + h * hare travelled F + h + n*C (lapped the cycle n times) * Since hare travels 2x as far: 2(F + h) = F + h + n*C * Simplifying: F = n*C - h * This means: a pointer starting at head AND a pointer starting at the * meeting point, both moving one step at a time, will meet exactly at * the cycle start node. */ public static Node findCycleStart(Node head) { Node meetingPoint = detectCycle(head); if (meetingPoint == null) { return null; // no cycle } Node pointerFromHead = head; // starts at the beginning Node pointerFromMeeting = meetingPoint; // starts at the meeting point // Both move one step at a time — they converge at the cycle start while (pointerFromHead != pointerFromMeeting) { pointerFromHead = pointerFromHead.next; pointerFromMeeting = pointerFromMeeting.next; } return pointerFromHead; // this is the cycle start node } public static void main(String[] args) { // Build list: 10 -> 20 -> 30 -> 40 -> 50 // ^ | // |______________| (cycle starts at node 30) Node head = new Node(10); Node node20 = new Node(20); Node node30 = new Node(30); // <-- cycle start Node node40 = new Node(40); Node node50 = new Node(50); head.next = node20; node20.next = node30; node30.next = node40; node40.next = node50; node50.next = node30; // cycle: 50 points back to 30 // --- Detect cycle --- Node meeting = detectCycle(head); if (meeting != null) { System.out.println("Cycle detected! Meeting point value: " + meeting.value); } else { System.out.println("No cycle found."); } // --- Find cycle start --- Node cycleStart = findCycleStart(head); if (cycleStart != null) { System.out.println("Cycle starts at node with value: " + cycleStart.value); } // --- Verify on a clean list --- Node cleanHead = new Node(1); cleanHead.next = new Node(2); cleanHead.next.next = new Node(3); System.out.println("Clean list cycle detected: " + (detectCycle(cleanHead) != null)); } }
Cycle starts at node with value: 30
Clean list cycle detected: false
Finding Cycle Length — The Bonus Move Most Candidates Miss
Once you can detect a cycle and find its start node, measuring the cycle's length is trivial — and it's a common follow-up question that catches people off guard.
After finding the cycle start node, simply walk from that node one step at a time, counting steps until you return to the same node. Since you're guaranteed to be inside the cycle, you'll complete exactly one full loop and your step count is the cycle length.
This is useful in real scenarios too: imagine a round-robin task scheduler implemented as a circular linked list. Knowing the number of tasks (cycle length) lets you calculate time slots correctly without re-traversing the entire structure from scratch.
Notice how the three operations — detect, find start, measure length — compose naturally. Each one builds on the previous. That compositional design is intentional. In interviews, demonstrating that you see these as related operations rather than isolated tricks signals strong algorithmic thinking.
Time complexity remains O(n) overall. The cycle-length measurement adds at most O(k) where k ≤ n, so the bound doesn't change.
public class CycleLengthFinder { static class Node { int value; Node next; Node(int value) { this.value = value; this.next = null; } } // --- Reuse Floyd's detection from earlier --- public static Node detectCycle(Node head) { if (head == null || head.next == null) return null; Node tortoise = head; Node hare = head; while (hare != null && hare.next != null) { tortoise = tortoise.next; hare = hare.next.next; if (tortoise == hare) return tortoise; } return null; } public static Node findCycleStart(Node head) { Node meetingPoint = detectCycle(head); if (meetingPoint == null) return null; Node fromHead = head; Node fromMeeting = meetingPoint; while (fromHead != fromMeeting) { fromHead = fromHead.next; fromMeeting = fromMeeting.next; } return fromHead; } /** * Measures the number of nodes in the cycle. * Strategy: start at the cycle's first node, walk until we return to it. * The step count equals the cycle length. */ public static int measureCycleLength(Node head) { Node cycleStart = findCycleStart(head); if (cycleStart == null) { return 0; // no cycle } int cycleLength = 1; // we count the start node itself Node walker = cycleStart.next; // begin one step ahead // Walk until we lap back to cycleStart while (walker != cycleStart) { walker = walker.next; cycleLength++; } return cycleLength; } public static void main(String[] args) { // Build: 5 -> 10 -> 15 -> 20 -> 25 -> 30 // ^ | // |__________________| (cycle of length 4: 15->20->25->30->15) Node head = new Node(5); Node node10 = new Node(10); Node node15 = new Node(15); // cycle start Node node20 = new Node(20); Node node25 = new Node(25); Node node30 = new Node(30); head.next = node10; node10.next = node15; node15.next = node20; node20.next = node25; node25.next = node30; node30.next = node15; // close the cycle at node15 System.out.println("Cycle detected : " + (detectCycle(head) != null)); System.out.println("Cycle starts at: " + findCycleStart(head).value); System.out.println("Cycle length : " + measureCycleLength(head) + " nodes"); // Bonus: no-cycle case Node cleanList = new Node(1); cleanList.next = new Node(2); cleanList.next.next = new Node(3); System.out.println("Clean list cycle length: " + measureCycleLength(cleanList)); } }
Cycle starts at: 15
Cycle length : 4 nodes
Clean list cycle length: 0
| Aspect | HashSet Approach | Floyd's Algorithm |
|---|---|---|
| Time Complexity | O(n) | O(n) |
| Space Complexity | O(n) — stores all node refs | O(1) — just two pointers |
| Can Find Cycle Start? | Yes — the first duplicate found | Yes — requires second phase |
| Can Find Cycle Length? | Yes — counts stored refs in cycle | Yes — walk from cycle start |
| Handles Null Head? | Yes — loop never executes | Yes — explicit null check needed |
| Code Complexity | Simple — straightforward logic | Moderate — non-obvious math |
| Best Used When | Clarity matters more than memory | Memory is constrained (O(1) required) |
| Interview Preference | Acceptable as first answer | Expected as optimised solution |
🎯 Key Takeaways
- Floyd's algorithm detects a cycle in O(n) time and O(1) space — the hare gains exactly one step on the tortoise per iteration inside the cycle, so meeting is mathematically guaranteed.
- The meeting point is NOT the cycle start — you need a second phase: reset one pointer to head, keep the other at the meeting point, advance both one step at a time, and they'll converge exactly at the cycle start.
- Always guard against
hare.next == nullbefore callinghare.next.next— missing this single null check causes NullPointerExceptions on non-cyclic lists with an even node count. - Cycle length is a free bonus: once you have the cycle start node, count steps back to it in one pass — useful for round-robin schedulers, circular buffer implementations, and graph analysis.
⚠ Common Mistakes to Avoid
- ✕Mistake 1: Not checking
hare.next != nullbefore callinghare.next.next— This causes a NullPointerException on any list with an even number of nodes, because the hare lands on the last node (not null) and then tries to call.nexton null. Fix: always writewhile (hare != null && hare.next != null)— both guards are required. - ✕Mistake 2: Returning
tortoise == hareimmediately at the start when both begin athead— Since both pointers start at the same node, your first equality check fires before either pointer moves, and you incorrectly report a cycle on every non-empty list. Fix: advance both pointers before the equality check, or use a do-while loop that moves first and checks after. - ✕Mistake 3: Assuming the meeting point is the cycle start — The meeting point is somewhere inside the cycle, but almost never at the start (unless F=0, meaning the cycle starts at head). Fix: after detecting the meeting point, reset one pointer to head and advance both one step at a time until they meet — that convergence point is the true cycle start.
Interview Questions on This Topic
- QCan you explain why Floyd's algorithm is guaranteed to detect a cycle — not just that it does, but the mathematical reason the hare must eventually catch the tortoise?
- QAfter using Floyd's algorithm to find a meeting point inside the cycle, how do you find the node where the cycle *begins*? Walk me through the proof of why resetting one pointer to head works.
- QWhat happens if you use a hare that moves 3 steps instead of 2 — does the algorithm still work? What changes?
Frequently Asked Questions
What is the time and space complexity of Floyd's cycle detection algorithm?
Floyd's algorithm runs in O(n) time because in the worst case the tortoise visits each node once before the pointers meet. The space complexity is O(1) because you only use two pointer variables regardless of list size — no extra data structures are needed.
Can Floyd's algorithm detect a cycle if the cycle starts at the very first node (the head)?
Yes. When the cycle starts at head, the distance F (from head to cycle start) is zero. In the second phase, resetting one pointer to head means both pointers are already at the cycle start immediately — they meet on the first comparison, correctly identifying head as the cycle start.
Why does the second phase of Floyd's algorithm (resetting one pointer to head) find the cycle start?
The math works out to F = nC - h, where F is the distance from head to cycle start, C is the cycle length, and h is the distance from cycle start to the meeting point. This means a pointer walking from head and a pointer walking from the meeting point cover the same logical distance before reaching the cycle start, so they arrive there simultaneously.
Written and reviewed by senior developers with real-world experience across enterprise, startup and open-source projects. Every article on TheCodeForge is written to be clear, accurate and genuinely useful — not just SEO filler.