The fast-slow pointer algorithm looks deceptively simple. Two pointers, one loop, a couple of assignments. You could write it on a napkin. But there is a reason interviewers love asking about it: the algorithm has three distinct failure modes, and each one traces back to a single careless line in the loop condition. Getting the logic right is not about being clever. It is about having seen the bugs before they show up in your code.
Think of this as a detective story. You are about to watch three crimes play out in slow motion, and your job is to predict what goes wrong before the animation reveals it.
Bug 1: The Crash. The loop guard is while (fast.next) -- it checks whether the next node exists, but it never checks whether fast itself is still a valid node. Consider a 5-node list: [0] -> [1] -> [2] -> [3] -> [4] -> null. Fast starts at node 0 and jumps by 2 each iteration. After step 1, fast is at node 2. After step 2, fast is at node 4, the last node. Now the loop checks fast.next -- that is null, so the loop should exit. But what if fast had jumped past node 4? On a list where the length is just right, fast.next.next lands fast on null itself, not on a node. The guard then evaluates null.next, which is a null pointer dereference. In JavaScript that is a TypeError: Cannot read property 'next' of null. In C++ it is a segfault. The root cause: the guard checks one step ahead (fast.next) but never asks “is fast itself still pointing at something real?”
Bug 2: The Spiral. This one is more insidious because the code does not crash -- it just never finishes. The bug is in the loop body, not the guard: fast = fast.next instead of fast = fast.next.next. Both pointers now move at speed 1. On a linear list this is fine -- slow and fast reach the end at different times, and the algorithm just returns the wrong midpoint. But on a cyclic list, the pointers enter the ring and chase each other forever. The gap between them never changes. If slow enters the cycle 3 nodes behind fast, it stays 3 nodes behind forever, because both advance by 1. The 2x speed ratio is not a stylistic choice. It is the mechanism that guarantees the gap shrinks by exactly 1 per step, which means collision in at most C steps (where C is the cycle length). Remove the speed difference and you remove the convergence.
Bug 3: The Near Miss. This is the subtlest failure and the one most people miss in interviews. The guard is while (fast.next && fast.next.next) -- it looks more careful than the standard while (fast && fast.next) because it checks two nodes ahead instead of one. On a 6-node even list [0] -> [1] -> [2] -> [3] -> [4] -> [5] -> null, trace through: fast starts at 0, jumps to 2, then to 4. Now the guard checks fast.next (node 5, truthy) and fast.next.next (null, falsy). The guard exits. Slow has only moved from 0 to 1 to 2 -- it stops at node 2. But the conventional “left-middle” of a 6-node list is node 3 (the upper of the two middle candidates), not node 2. The overly strict guard stopped the loop one iteration too early. Slow ended up one node short. The standard guard while (fast && fast.next) would have let the loop run one more time: fast at 4, fast is truthy, fast.next (node 5) is truthy, the loop runs, fast jumps to null, slow advances to 3. Correct answer.
Why does this matter? Because in a real interview, the difference between a working solution and a segfault often comes down to one boolean expression in the while condition. The candidates who get this right are not smarter -- they have seen the bugs. They know what each guard clause is defending against. By the end of this screen, you will too.
Three bugs. Three predictions. Watch closely.
Bug 1: The Crash
This code checks fast.next but not fast itself. On a 5-node list, what happens when fast reaches the end?
You have now witnessed all three failure modes: the crash when fast is null and you ask for .next, the infinite spiral when both pointers move at the same speed, and the off-by-one when an overly strict guard exits the loop one iteration too early.
Every one of those bugs traces back to the same root cause -- an incomplete or incorrect while condition. The fix is a single expression: fast && fast.next. But these six characters are not a magic incantation you memorize and paste. Each token in the expression serves a specific defensive purpose, like layers in a shield. Understanding why each token is there means you will never get it wrong, even under interview pressure.
Let's break the expression apart, left to right, the same way JavaScript evaluates it.
Token 1: fast -- This is the null boundary guard. Its job is to prevent Bug 1. Before the loop body runs, the program needs to confirm that fast is pointing at a real node, not at null. When does fast become null? On a linear list, every time fast = fast.next.next executes, fast jumps two nodes forward. If the list has an odd number of nodes, fast eventually lands on null (it steps past the last node). Without the fast check, the next line of the loop body would try to read fast.next -- but null has no .next property. The fast token is the first line of defense: it catches the case where fast has already left the building.
Token 2: && -- This is the short-circuit operator, and it is the glue that makes the entire guard safe. JavaScript evaluates && left to right. If the left operand is falsy, the right operand is never evaluated. This is not an optimization hint -- it is a language specification guarantee, defined in ECMAScript since the first edition. If fast is null (falsy), JavaScript stops immediately, never touches fast.next, never dereferences anything. This is why the order matters. fast.next && fast would crash on null because fast.next is evaluated first -- and if fast is null, that is the dereference you were trying to prevent.
To make the evaluation sequence concrete, here is exactly what happens when fast is null:
&&: the identifier fast. It resolves to null.null is falsy. The && operator short-circuits.fast.next) is never evaluated. Not deferred, not lazy-loaded, not evaluated and discarded. Literally never touched.while condition is falsy. The loop exits.And here is what happens when fast is a valid node but fast.next is null (the even-length list termination):
fast. It resolves to a node object. Objects are truthy.&& operator does not short-circuit because the left side was truthy. It proceeds to evaluate the right side.fast.next. It resolves to null.null is falsy. The while condition is falsy. The loop exits.Notice the asymmetry: the first check (fast) only needs to be falsy to exit. The second check (fast.next) only needs to be falsy to exit. But the second check is only safe to evaluate because the first check already confirmed that fast is not null. This is the essence of short-circuit-based safety -- each check simultaneously serves as a guard for the next check and as a termination condition in its own right.
Token 3: fast.next -- This is the lookahead guard. Its job is to prevent Bug 3 and to ensure the loop body can safely execute fast = fast.next.next. Think about what happens inside the loop: fast needs to jump two nodes. That means fast.next must exist (so you can read its .next), and fast.next.next can be anything -- even null, because after the jump, the next iteration's guard will catch that. The fast.next check ensures the first of those two steps is valid. Without it, fast.next.next would crash when fast is on the last node of a list (where fast.next is null, and null.next is a dereference error).
Now, what doesn't this guard do? It does not check fast.next.next. That might seem like an oversight, but it is actually the key insight. By checking only one step ahead, the guard lets the loop run one more iteration on even-length lists than the stricter fast.next && fast.next.next guard would. That extra iteration is exactly what pushes slow to the correct “upper middle” position on even-length lists. The guard is not being sloppy -- it is being precisely calibrated.
Now build it yourself. You have six tokens. Three are correct; three are traps. Place them in the right order and watch the shields appear on the linked list.
The guard works. But how it works depends on something you might not expect: whether the list has an odd or even number of nodes. This is where the distinction between “correct” and “subtle” lives, and it is the detail that separates candidates who can write the algorithm from those who truly understand its termination behavior.
Let's trace through both cases manually and see exactly where the two guards -- fast && fast.next (standard) and fast.next && fast.next.next (strict) -- diverge.
Odd-length list: 7 nodes. The list is [0] -> [1] -> [2] -> [3] -> [4] -> [5] -> [6] -> null. Both pointers start at node 0.
slow moves to 1, fast moves to 2.slow moves to 2, fast moves to 4.slow moves to 3, fast moves to 6.fast is at node 6 (truthy). fast.next is null. Standard guard: fast (truthy) && fast.next (null, falsy) -- exits. Strict guard: fast.next (null, falsy) -- exits immediately. Both guards exit at the same point. slow is at node 3, which is the exact middle of a 7-node list (3 nodes on each side). No controversy. No disagreement. Both guards are correct.Why do they agree? Because on an odd-length list, fast always lands on the last node on its final jump, and fast.next is null. The short-circuit behavior is identical for both guards because the first falsy value they encounter is in the same position.
Even-length list: 6 nodes. The list is [0] -> [1] -> [2] -> [3] -> [4] -> [5] -> null. Both pointers start at node 0.
slow moves to 1, fast moves to 2.slow moves to 2, fast moves to 4.fast is at node 4.
fast && fast.next: fast is node 4 (truthy). fast.next is node 5 (truthy). Both checks pass. The loop runs. slow advances to 3, fast advances to null (node 5's next is null, so fast.next.next is... wait. fast is at node 4, fast.next is node 5, fast.next.next is null. So fast = fast.next.next sets fast to null).fast.next && fast.next.next: fast.next is node 5 (truthy). fast.next.next is null (falsy). The guard exits immediately. The loop does NOT run this iteration. slow stays at node 2.slow is at node 3, fast is null. The standard guard now checks fast (null, falsy), short-circuits, exits. Final answer: slow = 3.slow is at node 2. Final answer: slow = 2.Node 3 vs node 2. One is the “upper middle” (the right candidate of the two center nodes). The other is the “lower middle” (the left candidate). For a 6-node list with nodes 0-5, the two middle candidates are nodes 2 and 3. Which one is “correct” depends on the problem -- but the standard convention in most LeetCode problems and textbooks is the upper middle (node 3), which is what fast && fast.next returns.
The root cause of the difference is the short-circuit evaluation point. When fast lands on node 4 (two nodes from the end), the standard guard still sees two truthy values (fast and fast.next) and lets the loop proceed. The strict guard looks further ahead (fast.next.next) and sees null, so it bails. That one extra iteration moves slow from 2 to 3.
The prediction gates below will make this concrete -- you will see both guards run on the same list, side by side.
Two guards run on a 7-node (ODD) list.
On an ODD-length list, will both guards find the correct middle?
You have seen how the guard handles normal-length lists -- 6 nodes, 7 nodes, lists long enough that the loop runs multiple iterations. But what about the degenerate inputs that show up in interviews and at the top of every edge-case checklist? An empty list. A single node. A list with exactly two nodes.
These cases are where the && operator's short-circuit evaluation stops being an abstract concept and becomes the concrete mechanism that prevents your code from crashing. When JavaScript encounters fast && fast.next, it evaluates left to right and stops the moment it hits a falsy value. It never evaluates the rest. This is not an optimization -- it is a guarantee baked into the ECMAScript specification (section 13.13 in the 2024 edition, if you are curious), and the entire reason the guard survives null inputs without a separate if statement.
Case 1: Empty list (head = null). The function starts with let fast = head, so fast is null. The while condition evaluates fast -- that is null, which is falsy. Short-circuit: JavaScript stops right there. It never touches fast.next. It never enters the loop body. The function falls through to return slow, which is also null (since slow was initialized to head, and head is null). The function returns null. No crash. No error. Just a clean return of “there is no middle node because there is no list.”
Here is the evaluation trace in full:
fast resolves to null.null is falsy. && short-circuits.fast.next is never evaluated.while condition is falsy. Loop body is skipped entirely.return slow returns null.Case 2: Single node (head = [0] -> null). Now fast is the node [0], which is a truthy object. JavaScript does NOT short-circuit -- it proceeds to evaluate the right side. fast.next is null (the single node's next pointer). null is falsy. The && expression as a whole is falsy. The loop does not run. slow stays at node 0 -- which is the correct midpoint of a one-element list (the only element is the middle).
The evaluation trace:
fast resolves to node 0 (an object, truthy).&& does NOT short-circuit. Proceeds to right operand.fast.next resolves to null.null is falsy. while condition is falsy.slow remains at node 0.return slow returns node 0.Notice the difference from Case 1: in the empty list, short-circuit happened on the first operand. In the single node case, short-circuit happened on the second operand. The guard has two layers, and each layer caught a different case.
Case 3: Two nodes (head = [0] -> [1] -> null). This is the interesting one because the loop actually runs. Initial state: fast = head = node 0, fast.next = node 1. The guard evaluates: fast is node 0 (truthy), fast.next is node 1 (truthy). Both checks pass. The loop body executes.
Inside the loop: slow = slow.next moves slow from node 0 to node 1. fast = fast.next.next moves fast from node 0 to... node 1's next, which is null. So fast becomes null.
Back to the guard: fast is null. Falsy. Short-circuit. Loop exits. slow is at node 1.
Is this correct? For a 2-node list [0] -> [1] -> null, the “left-middle” convention would give node 0, and the “upper-middle” convention gives node 1. The standard fast && fast.next guard gives node 1 (the upper middle), which matches the typical LeetCode expectation for LC 876.
The evaluation trace for the full cycle:
fast = node 0 (truthy). fast.next = node 1 (truthy). Enter loop.slow moves 0 -> 1. fast moves 0 -> null (via fast.next.next).fast = null (falsy). Short-circuit. Exit loop.return slow returns node 1.Three edge cases, zero crashes, and the guard handled each one through the same two-check short-circuit mechanism.
Predict each one below.
null head
The list is completely empty.
With head = null, what does the guard fast && fast.next evaluate to?
You have seen the bugs, forged the guard, tested it on odd and even lists, and dissected the edge cases. Now comes the transfer: can you take everything you have learned and apply it to actual code?
Below are two functions -- findMiddle and hasCycle -- with blanks where the critical expressions should be. These are not arbitrary fill-in-the-blank exercises. Every blank maps directly to an insight you discovered on a previous screen, and every distractor is a bug you have already encountered.
findMiddle has four blanks. The first two are the guard: while (____ && ____). You know the first slot is fast (the null boundary check from Bug 1) and the second is fast.next (the lookahead from Bug 3). Together they form the two-layer shield: first confirm fast is not null, then confirm there is a node ahead to jump through. If either check fails, the && short-circuits and the loop exits cleanly.
The third blank is slow's step: slow = ____. This must be slow.next -- one step forward, every iteration. Slow's job is simple: it walks the list at half the speed of fast. It never jumps, never skips, never looks ahead. It just advances one node at a time, and when the loop exits, wherever slow has landed is the midpoint.
The fourth blank is fast's step: fast = ____. This must be fast.next.next -- two steps forward. This is the expression that creates the 2:1 speed ratio. If you write fast.next instead, you recreate Bug 2's same-speed spiral. If you write fast.next.next.next, fast would jump three steps and you would need a different guard entirely (checking two nodes ahead instead of one). The fast.next.next expression is safe because the guard already confirmed that fast.next exists -- and fast.next.next is allowed to be null, because the next iteration's guard check will catch that.
hasCycle shares the same guard (both algorithms face the same null-dereference risk) but adds a collision check inside the loop body. The guard blanks are identical: fast && fast.next. Why the same guard? Because the crash risk is the same. In cycle detection, if the list is not cyclic, fast will eventually reach null. The guard must prevent the crash in that non-cycle case just as it does in findMiddle. And if the list is cyclic, fast never reaches null, so the guard always passes, and termination comes from the collision check instead.
The new blank is the equality check: if (____). The correct answer is slow === fast -- pointer identity, not value equality. Two different nodes might hold the same value (imagine a list [3] -> [7] -> [3] -> [7] with a cycle), but cycle detection requires confirming that both pointers have converged on the exact same node object in memory. slow.val === fast.val would produce false positives -- it would report a “cycle” whenever two nodes happened to share a value. slow.next === fast would check adjacency (is slow one step behind fast?), not collision. slow === fast.next tests whether slow is one hop ahead of fast, which is a different relationship entirely.
Pay attention to the distractors. They are not random. Each wrong option maps directly to one of the bugs you investigated:
fast.next.next in the guard position? That is Bug 3's overly strict guard that caused the off-by-one on even lists.fast.next as the fast pointer step? That is Bug 2's same-speed spiral that chased forever in a cycle.slow or slow.next in the guard position? The guard protects fast, not slow. Slow always trails safely behind -- it never hits null first because it moves at half the speed.null anywhere in the guard? You are guarding against null, not guarding with null. A null literal in a boolean context is always falsy -- the loop would never run.The blanks are the code bridge. Every slot connects back to a specific insight you discovered in the first five screens. Fill them from understanding, not memorization.
Fill in findMiddle with the correct guard and steps:
You built the guard, tested it across list lengths and edge cases, and defended two real functions with it. This final screen asks you to explain why -- not just recall the pattern, but articulate the causal chain from guard clause to prevented failure.
Question 1: Why does fast come before fast.next? This is about evaluation order, and the answer traces back to the short-circuit semantics you explored throughout this lesson. In the expression fast && fast.next, JavaScript evaluates left to right. If fast is null (falsy), the && operator immediately returns the falsy value without ever looking at the right operand. That means fast.next -- which would crash if fast were null -- is never executed.
Now imagine you reversed the order: fast.next && fast. The left operand is fast.next. If fast is null, evaluating fast.next is a null pointer dereference. The crash happens before the && operator has a chance to short-circuit. The order is not a convention or a style preference. It is a hard safety requirement. The first operand in a short-circuit expression must be the cheaper and safer check, because it is always evaluated, and it determines whether the more dangerous check is even attempted.
This principle generalizes beyond linked lists. Any time you write a && a.b or obj && obj.property, you are relying on the same short-circuit guarantee. Optional chaining (a?.b) does the same thing with less syntax, but in a while condition, the explicit && form communicates the two separate termination conditions more clearly.
Question 2: What goes wrong with fast = fast.next? If fast moves at the same speed as slow (1 step per iteration), the relative speed between the two pointers is zero. On a linear list, this is just wrong -- slow reaches the end instead of the middle, and the “midpoint” returned is actually the tail. But on a cyclic list, the consequences are catastrophic. Both pointers enter the cycle and chase each other at the same speed. The gap between them never changes. If slow enters the cycle 3 nodes behind fast, it stays 3 nodes behind forever. The loop runs indefinitely.
Why does fast = fast.next.next fix this? Because the relative speed becomes 2 - 1 = 1. The gap between the pointers shrinks by exactly 1 every iteration. This means collision is guaranteed in at most C steps (where C is the cycle length), regardless of the initial gap. The 2x speed ratio is not an arbitrary choice -- it is the only integer ratio where the gap change divides every possible cycle length (because a gap change of 1 divides everything). A 3x ratio would change the gap by 2 per step, and on cycles with even length, the gap would oscillate instead of converging to zero.
Question 3: Does the guard handle a two-node list? You might think two nodes is “too small” to be interesting, but it is the smallest input that actually exercises the loop body (empty and single-node lists skip the loop entirely). With head = [0] -> [1] -> null: fast is node 0 (truthy), fast.next is node 1 (truthy). The guard passes. The loop runs once. Inside: slow advances from 0 to 1. fast advances from 0 to null (via fast.next.next, which is node 1.next = null). Back at the guard: fast is null. Short-circuit. Loop exits. slow is at node 1, the correct upper-middle of a two-element list.
The key observation: on the two-node list, the loop runs exactly once and fast's jump lands it on null, triggering the fast check (the first layer of the guard). On the single-node list, the loop runs zero times because fast.next is null, triggering the second layer. The two layers of the guard do not do the same job -- each one catches a different class of termination.
Prove that you understand all three layers of the shield.
Why must the guard check fast before checking fast.next?