Imagine you are walking down a long hallway. Each room has exactly one door leading to the next room, and you walk through them in order: room 0, room 1, room 2, and so on. At the very end, the last door opens onto a brick wall — null — and your walk is over. This is a singly linked list. Every node has a .next pointer, and the last node's .next is null. You traverse it, you reach the end, you stop. Simple. Predictable. Every linked list problem you have solved so far has this property.
But now suppose someone sneaks in overnight and replaces that final brick wall with a portal. Instead of hitting null, the last door quietly drops you back into room 3 — a room you already visited five rooms ago. You walk through room 3 again, then room 4, then room 5, then room 6, then room 7, and then... the portal. Back to room 3. Again. You are trapped in an infinite loop, and the worst part is that nothing about the rooms themselves tells you anything has gone wrong. Room 3 looks the same the second time as it did the first. There is no sign on the wall that says “you have been here before.” You just keep walking, forever, with no way to know you are trapped.
This is what a cycle does to a linked list. Structurally, it means some node's .next pointer does not point forward to unvisited territory — it points backward to a node already in the chain. The list no longer has an end. There is no null terminator. Any algorithm that naively walks forward, following .next pointers until it finds null, will spin forever. Consider what this means in code:
let current = headwhile (current !== null) { // process current... current = current.next}This standard traversal assumes termination. In a cyclic list, current never becomes null, and the while loop never exits. Your program hangs. In an interview, this is a silent bug — no crash, no error, just an infinite loop that eats your remaining time.
The shape these lists form is called a rho (the Greek letter ρ). Picture it: a straight tail of unique nodes leading into a closed ring. Nodes 0, 1, 2 form the tail. Nodes 3, 4, 5, 6, 7 form the cycle. Node 7's .next points back to node 3 instead of to null. Every node in the tail is visited exactly once. Every node in the cycle is visited infinitely many times. The question is not whether a single pointer gets trapped — it always does — but how to detect that you are trapped when no single node carries a “visited” flag.
Tap through the list below and watch a pointer walk forward, one node at a time. The first few steps feel completely normal — just a pointer marching through a linked list. Pay close attention to what happens when it reaches node 7. That is the moment the structure reveals its true shape.
A pointer starts at node 0. Tap to walk it forward.
You just witnessed the fundamental problem: a single pointer cannot detect a cycle. It enters the loop, walks the same nodes over and over, and has no memory of where it has been. Every step looks identical to the one before. The pointer has no “I was here” marker, no counter that says “you have visited this node twice,” no way to distinguish the first lap from the hundredth. It is a goldfish swimming in a bowl, endlessly surprised by the same plastic castle.
This is the wall we need to break through. In the next screen, you will see a beautifully simple idea that solves this problem using nothing but two pointers moving at different speeds — no extra memory, no visited flags, no hash sets.
Here is an analogy that will anchor the rest of this module. Two runners are on a circular track. Runner A jogs at a steady pace — one lap every 5 minutes. Runner B sprints at exactly double speed — one lap every 2.5 minutes. They both start at the same point on the track. Runner B immediately pulls ahead and opens a gap. Your intuition says: “B is faster, so B is pulling away. They will never meet again.”
But the track is circular. “Pulling away” and “catching up from behind” are the same thing on a ring. Runner B is not escaping — they are lapping. And every time B gains ground on A, the distance between them (measured along the track in the direction B must travel to reach A) shrinks. Not randomly. Not occasionally. Every single step.
Let us make this precise with numbers. Suppose the circular track has 5 stations numbered 0 through 4. Both runners start at different stations (slow at 0, fast at 1, so the gap from fast to slow going clockwise is 4). Each “step,” slow advances 1 station and fast advances 2 stations:
Look at the gap column: 4, 3, 2, 1, 0. It counts down by exactly 1 every step.
This is not a coincidence — it is arithmetic. Each step, slow advances by 1 and fast advances by 2. So fast gains 2 - 1 = 1 position on slow per step.
The gap, measured as the clockwise distance from fast to slow, decreases by exactly 1 every time. It does not oscillate. It does not plateau. It counts down like a timer, and when it hits 0, the two pointers occupy the same node.
This property — the gap shrinks by exactly 1 per step — is the invariant that makes Floyd's cycle detection work. It means collision is not probabilistic. It is not “likely” or “eventual.” It is mathematically guaranteed for any cycle of any length. A cycle of length 5? Gap starts at most 4, counts down to 0 in 4 steps. A cycle of length 1000? Gap starts at most 999, counts down to 0 in 999 steps. The number of steps scales with the cycle length, but convergence is certain.
But do not take the math on faith — experience it. The simulation below places two pointers on a 5-node cycle ring. At each step, you will predict what happens to the gap before watching it play out. Pay attention to the gap arc that connects the two pointers: it should shrink by exactly one node segment each time. If it does anything else, something is wrong with the theory.
A slow pointer (1 step) and a fast pointer (2 steps) are both in a cycle. Will they ever meet?
That countdown you just watched — 4 → 3 → 2 → 1 → 0 — is the heartbeat of every fast/slow pointer algorithm. The gap closes by 1 per step because of the speed differential: fast_speed - slow_speed = 2 - 1 = 1. This means the relative speed between the pointers is always 1 node per step, regardless of the absolute speeds. In the cycle detection context, this relative speed of 1 is what guarantees collision. The fast pointer effectively approaches the slow pointer from behind at 1 step per iteration, which means it cannot overshoot. It cannot leapfrog. It closes in one node at a time until the distance is zero. This is why Floyd's algorithm uses speed 2 specifically — but the next screen will make that reason even sharper by showing you what happens when you try a different speed.
You just established that at speed 2x, the gap between fast and slow shrinks by exactly 1 every step. The countdown is smooth and inevitable: 4, 3, 2, 1, 0. Collision guaranteed. So a natural engineering instinct kicks in: if 2x closes the gap by 1, then 3x should close it by 2. Twice the convergence rate! The pointers should collide in half the time, right? It sounds like a strict improvement — why would anyone settle for 2x when 3x is available?
Let us test that intuition with concrete numbers. Consider a 6-node cycle (nodes 0 through 5). Slow starts at node 0, fast starts at node 1, giving an initial gap of 5 (measured clockwise from fast to slow). With 3x speed, each step moves slow by 1 and fast by 3. The gap changes by 3 - 1 = 2 per step. Watch the countdown:
The gap went 5, 3, 1, 5, 3, 1, 5, 3, 1... — it oscillates forever and never hits 0. The fast pointer leapfrogged right over the slow pointer. At the moment of closest approach (gap = 1), the next step decreased the gap by 2, sending it to -1, which wraps around to 5. The pointers flew past each other like two cars on a highway passing in opposite directions.
This is not a special case. It is a fundamental consequence of modular arithmetic. For the gap to reach exactly 0, we need the gap decrease per step (which equals fast_speed - slow_speed) to divide the current gap evenly. More precisely, the gap decreases by (speed - 1) each step, and it reaches 0 exactly when (speed - 1) divides the cycle length. At 3x, the decrease is 2, and 2 does not divide every possible cycle length. It divides 2, 4, 6, 8 — the even numbers — but not 3, 5, 7, 9. On a 5-node cycle with 3x speed, the gap goes 4, 2, 0 — collision! But on a 6-node cycle, it oscillates. You cannot know the cycle length in advance (that is the whole point of detecting it), so you cannot know whether 3x will work.
The mathematical beauty of 2x is precisely this: the gap decrease is 2 - 1 = 1, and 1 divides every positive integer. It does not matter if the cycle has 3 nodes, 7 nodes, 100 nodes, or 10,000 nodes. A countdown by 1 always reaches 0. There is no cycle length that can make the gap oscillate or skip. This is the universal guarantee that no other speed provides. Speed 4x has gap decrease 3 (fails on cycle length 4, where the gap goes 3, 0 — works — but also on cycle length 5 where it goes 4, 1, 3, 0 — wait, does it?). Speed 5x has gap decrease 4. The analysis becomes complicated and depends on gcd(speed - 1, cycle_length). Only speed = 2 makes the analysis trivially simple: gap change 1 divides everything. Always. Period.
The lesson here extends beyond Floyd's algorithm. In algorithm design, the simplest correct solution often beats the “cleverer” one. Speed 3x is faster per step, but it is not universally correct. Speed 2x is slower per step, but it works on every input. When correctness is the constraint, simplicity wins.
Try it yourself below. You will step through both speeds on the same 6-node cycle. Start with 3x and watch the gap refuse to reach 0. Then switch to 2x and feel the smooth, inevitable countdown.
What you just experienced is the complete justification for the 2x speed choice in Floyd's algorithm. It is not arbitrary. It is not a convention. It is the only integer speed multiplier that guarantees collision on every possible cycle length. The gap-closing invariant — decrease by 1 per step — is the strongest guarantee available, and it comes from the simplest possible speed differential. This is one of those rare cases in computer science where the optimal solution is also the most elegant: two pointers, speeds 1 and 2, collision guaranteed. But so far, you have been working with pure cycles — rings with no entrance ramp. Real linked lists are messier. The next screen introduces the complication that makes the algorithm practical.
Every cycle you have seen so far has been a clean ring — every node is part of the loop, and both pointers start inside it. But that is not how real linked lists work. In practice, a cyclic linked list has a tail: a straight sequence of nodes that leads into the cycle but is not itself part of the loop. The list looks like the Greek letter rho (ρ): a long straight stroke feeding into a closed circle.
Consider a concrete example. You have 9 nodes labeled 0 through 8. Nodes 0, 1, 2, 3, 4 form the tail — a normal linked list segment where each node points to the next. Node 4 points to node 5, which is the cycle entry point. Nodes 5, 6, 7, 8 form the cycle: 5 ⟶ 6 ⟶ 7 ⟶ 8 ⟶ 5. The list has a tail of length 5 and a cycle of length 4.
Now here is the concern. Both pointers start at the head (node 0). During the tail phase, the fast pointer races ahead at 2x speed while the slow pointer plods along at 1x. By the time the slow pointer reaches node 2, the fast pointer is already at node 4. A step later, fast is at node 6 — inside the cycle — while slow is still at node 3, trudging through the tail. The fast pointer enters the cycle long before the slow pointer does. When slow finally reaches the cycle entry (node 5), fast is already somewhere deep inside the loop, potentially on the opposite side. The neat “both start at the same point on the ring” setup from Screen 2 is gone. Does this scramble the gap-closing invariant?
Let us trace through it carefully. The tail is 5 nodes long. Fast enters the cycle after ceil(5/2) = 3 steps (since it moves 2 nodes per step). At that point, fast is at some position inside the cycle, and slow is still in the tail. For the next few steps, both pointers advance — slow is still walking the tail, and fast is looping inside the cycle. Eventually, slow reaches node 5 (the cycle entry). At that exact moment, fast is at some node inside the cycle, and we can measure the gap between them.
Here is the critical insight: the gap-closing rule does not care about the tail. The tail is just a runway. Both pointers traverse it linearly — fast gets through it faster, slow takes longer, but both eventually enter the cycle. The moment both pointers are on the ring, the familiar rule kicks in: the gap decreases by 1 per step. The tail length affects when collision happens (longer tail = more total steps before both pointers are in the cycle), but it never affects whether collision happens. Once both pointers are inside the loop, the math is identical to what you saw on Screen 2.
Think of it with the running track analogy. Two runners are driving to the same circular track. Runner B arrives first, starts running laps, and is already mid-lap when Runner A arrives. A starts running from wherever she enters the track. At that moment, B might be half a lap ahead. But it does not matter — from the instant both are on the track, the gap closes at the same rate. The late arrival only determines the initial gap, not whether convergence happens.
Watch the simulation below. Both pointers start at node 0 and race through a 5-node tail, then observe the familiar gap-closing pattern once they enter the 4-node cycle.
The tail changes nothing about the invariant — it only adds a prelude. The gap-closing arithmetic is blind to how the pointers arrived inside the cycle; it only cares that they are both there, moving at speeds 1 and 2. This robustness is what makes Floyd's algorithm work on any linked list shape, not just clean rings. Whether the tail has 0 nodes or 10,000 nodes, whether the cycle has 3 nodes or 3,000 nodes — once both pointers enter the cycle, collision follows from the same countdown. You now have every conceptual piece: the single-pointer trap, the gap-closing invariant, the 2x speed proof, and the tail irrelevance. All that remains is to translate this understanding into code.
You have spent four screens building a deep physical intuition for cycle detection: a single pointer gets trapped (Screen 1), two pointers at different speeds must collide (Screen 2), speed 2x specifically guarantees collision on any cycle length (Screen 3), and the tail before the cycle does not affect the invariant (Screen 4). Now it is time to connect that intuition to the actual code you would write in an interview.
Floyd's cycle detection algorithm — Phase 1, the detection step — is remarkably compact. The entire function is 8 lines of TypeScript. But every line maps directly to something you experienced visually in the previous screens. Here is the skeleton with three critical blanks for you to fill:
function hasCycle(head: ListNode | null): boolean { let slow = head let fast = head while (________) { // Blank 1: the loop guard slow = slow.next fast = ________ // Blank 2: fast advance if (________) return true // Blank 3: collision check } return false}Let us walk through what each blank needs to do, connecting back to your visual experiences.
Blank 1 — the while guard. Remember Screen 1: when a list has no cycle, the pointer reaches null and stops. The guard must protect against this. If fast is null, the list ended — no cycle. If fast.next is null, then fast.next.next would crash (you cannot call .next on null). So the guard must check both: fast && fast.next. This does double duty: it prevents null pointer crashes on acyclic lists, and it provides the exit condition. If the loop completes without returning true, we fall through to return false — the list is cycle-free.
Blank 2 — the fast advance. This is the 2x speed that creates the gap-closing invariant. Slow moves one step (slow = slow.next), so fast must move two steps (fast = fast.next.next). You saw this differential in action on Screen 2: each step, the gap between fast and slow shrinks by 2 - 1 = 1. The expression fast.next.next is safe because the guard already verified that both fast and fast.next are non-null.
Blank 3 — the collision check. This is the moment you watched play out on Screens 2, 3, and 4: when the gap reaches 0, both pointers occupy the same node. We check slow === fast — reference equality, not value equality. Two different nodes could have the same .val but be at different positions; reference equality ensures we are checking that the pointers literally point to the same object in memory. This is the collision detection that makes the algorithm work.
fast && fast.next→○ → ○ → ∅null boundary guardfast.next.next→○ →→ ○2-step jumpslow === fast→● ≡ ●collision checkNotice what is not in the code. There is no hash set. There is no “visited” flag on each node. There is no extra memory allocation at all — just two pointer variables. The space complexity is O(1). The time complexity is O(n), because the slow pointer visits each node at most once in the tail, and the gap-closing in the cycle takes at most cycle_length steps. This is why Floyd's algorithm is preferred in interviews over the hash set approach when O(1) space is required.
Fill in the three blanks below. For each one, think back to the interaction that taught you why that specific expression is correct. The while guard connects to Screen 1 (what stops the pointer when there is no cycle). The fast advance connects to Screen 2 (the 2x speed differential). The collision check connects to the collision you saw on Screens 2, 3, and 4.
You now have a working implementation of Floyd's cycle detection. Eight lines. Two pointers. O(1) space. The while guard handles termination for acyclic lists. The two .next calls create the speed differential that makes the gap-closing invariant work. And the if (slow === fast) check catches the collision that you now know is mathematically guaranteed. But this is only Phase 1 — detection. It answers “does a cycle exist?” It does not answer “where does the cycle start?” That requires Phase 2, which is a separate algorithm. For now, the important thing is that you understand why this code works, not just what it does. Every line is a consequence of the invariant you discovered through direct interaction.
Floyd's cycle detection looks like a linked list trick. It was invented for linked lists. It is usually taught in the context of linked lists. But the core idea — “send two iterators at different speeds through a repeating sequence and check for collision” — is far more general than it first appears. Any time you have a function whose output feeds back as input, you have an implicit linked list, and Floyd's algorithm applies.
Consider the happy number problem (LC 202). You take any positive integer, sum the squares of its digits, and repeat. For example: 19 ⟶ 1² + 9² = 82 ⟶ 8² + 2² = 68 ⟶ 6² + 8² = 100 ⟶ 1² + 0² + 0² = 1. When you reach 1, the number is “happy.” But not every number reaches 1. Take 2: 2 ⟶ 4 ⟶ 16 ⟶ 37 ⟶ 58 ⟶ 89 ⟶ 145 ⟶ 42 ⟶ 20 ⟶ 4. It loops back to 4 and cycles forever. The question is: given any integer, will it reach 1 or enter a cycle? There is no linked list anywhere in this problem. But the function f(n) = sum of squared digits of n takes an input and produces an output, and that output becomes the next input. This is exactly node.next. You can apply Floyd's: let slow compute f(x) once per step, and fast compute f(f(x)) once per step. If they collide, there is a cycle (not happy). If slow ever reaches 1, the number is happy.
Or consider finding the duplicate in an array (LC 287). You have an array of n + 1 integers where every value is in the range [1, n]. By the pigeonhole principle, at least one value is duplicated. The challenge is to find it in O(1) extra space (no hash sets) and without modifying the array. The trick: treat the array as a function where f(i) = nums[i]. Since all values are in [1, n] and the array has n + 1 slots, following this function from index 0 creates a sequence that must eventually cycle (it cannot visit more than n + 1 distinct indices before repeating). The duplicate value is the cycle entry point. Floyd's algorithm finds it.
The mental model to carry forward: Floyd's works on any system with deterministic transitions (given the current state, the next state is fixed), a finite state space (so the sequence must eventually revisit a state), and no memory budget for visited-state tracking (otherwise a hash set is simpler). When all three hold, the gap-closing invariant applies. It is not a linked-list fact — it is an arithmetic fact about sequences with cycles.
One caveat: Phase 1 only tells you that a cycle exists. It does not tell you where the cycle starts. The collision node is generally not the entry. Finding the entry requires Phase 2 (LC 142): reset one pointer to the head, then advance both at speed 1 until they meet.
The four scenarios below test whether you have internalized the principle deeply enough to transfer it to unfamiliar territory. Each one describes a system with deterministic transitions and a finite state space, and asks you to reason about whether and how Floyd's applies.
You have a function f(x) that maps integers to integers. You suspect calling f repeatedly will eventually revisit a value. Which technique detects this?
You have now completed the full arc of Floyd's cycle detection: from the single-pointer trap, through the gap-closing invariant, the 2x speed proof, the tail irrelevance, the code bridge, and finally the transfer to problems that have no linked list at all. The core insight is not “use two pointers at different speeds” — it is why that works. The gap between two pointers moving at speeds 1 and 2 on any cyclic sequence decreases by exactly 1 per step. That decrease by 1 is the strongest possible guarantee: it divides every cycle length, so collision is inevitable regardless of the structure's shape. This is the invariant that makes Floyd's algorithm correct, and it is the idea worth remembering long after you have forgotten the code.