Fast & Slow Pointers
Send two runners down the same path, one twice as fast as the other. Where and whether they meet tells you about the shape of the path (its middle, whether it loops, and where the loop begins) using O(1) memory.
1. What is this pattern for?
This pattern (also called Floyd's tortoise and hare) works on anything where "go to the next thing" is well defined: a linked list (node.next), an array used as a map (i → nums[i]), or a function (n → sum of squares of digits). It answers:
- Is there a cycle? (they meet ⇔ yes)
- Where does the cycle start? (a second, equal-speed walk)
- Where's the middle? (when fast hits the end, slow is in the middle). That's the first step of many list problems: palindrome check, reorder, split for merge sort.
The alternative is a hash set of visited nodes, which needs O(n) memory. Fast & slow uses O(1).
2. How it works (bisected)
- Start both at the head.
slow = headandfast = head. - Move at different speeds. Each round:
slow = slow.next(1 step),fast = fast.next.next(2 steps). Guard withwhile fast and fast.nextso fast never steps off the end. - No cycle → fast falls off. When the loop ends, fast has walked twice as far, so
slowis at the middle (the second middle for even lengths). - Cycle → they meet. Inside the loop, the gap between them shrinks by exactly 1 every round, so fast can't jump over slow. They must land on the same node within one lap.
- Find the entrance. Put one pointer back at the head and move both 1 step at a time. They meet exactly at the cycle's first node. (Why: if the tail has length
aand they metbsteps into a loop of lengthL, then2(a+b) = a+b+kL, soa = kL − b: walkingasteps from the meeting point lands on the entrance.)
3. Seeing a problem: how to spot Fast & Slow
Trigger words
- "linked list" + "cycle", "loop", "where the cycle begins"
- "middle of the linked list", "second half", "palindrome linked list", "reorder"
- "find the duplicate" with values in
1..n, "without modifying the array", "O(1) space" - "repeat the process… does it end in 1 or loop forever?" (Happy Number)
- "circular array", "jump forward/backward by nums[i]"
Shape of the input
- A next-function:
node.next,nums[i], or a computed successor. - You can only walk forward (singly linked), and you don't know the length.
- Brute-force smell: "store every visited node in a set" or "count the length first, then walk again". The interviewer then asks for
O(1)space or one pass.
4. When to use it, and when not to
| Use Fast & Slow when… | Prefer something else when… |
|---|---|
Detecting a cycle / its start in O(1) space | Memory isn't a concern and you want simple code: a set() of visited nodes |
| Finding the middle of a singly linked list in one pass | You have random access (an array): just use n // 2 |
| Splitting a list before reversing half (palindrome, reorder) | The task is to reverse a list or parts of it: that's LinkedList Reversal (often used right after this one) |
Duplicate in 1..n without modifying the array | The pointers walk toward each other on a sorted array: that's Two Pointers |
| Cycle in an implicit "next" function | Cycles in a general graph (many edges per node): use DFS with colours, or topological sort |
Cost: O(n) time. Fast covers at most ~2n steps before falling off or meeting, and phase 2 is at most n more. O(1) extra space.
5. The Python template (memorize this)
# A) Middle of a list (the second middle when the length is even)
def middle_node(head):
# Both runners start on the first node.
slow = head
fast = head
# fast jumps 2 nodes per round, so BOTH fast and fast.next must exist.
while fast is not None and fast.next is not None:
slow = slow.next # tortoise: 1 step
fast = fast.next.next # hare: 2 steps
# fast has walked twice as far as slow, so slow is halfway.
return slow
# B) Cycle detection
def has_cycle(head):
slow = head
fast = head
while fast is not None and fast.next is not None:
slow = slow.next
fast = fast.next.next
# Compare NODES with "is", not values: two nodes may hold equal values.
if slow is fast:
return True # the hare lapped the tortoise: a loop
return False # the hare fell off the end: no loop
# C) Start of the cycle (LeetCode 142)
def cycle_start(head):
slow = head
fast = head
while fast is not None and fast.next is not None:
slow = slow.next
fast = fast.next.next
if slow is fast:
# Phase 1 is done: they met somewhere inside the loop.
# Phase 2: send one runner back to the start ...
slow = head
# ... and move BOTH one step at a time until they meet again.
while slow is not fast:
slow = slow.next
fast = fast.next
return slow # the node where they meet is the entrance
return None # no loop, so no entrance
What the code is doing, line by line:
slow = headandfast = head: both runners start on the first node. (We write two separate lines instead of the shortcutslow = fast = head.)while fast is not None and fast.next is not None:the hare is about to take two steps, so the node it stands on and the node after it must both exist. The order matters: Python checksfast is not Nonefirst and stops right there if it is false, sofast.nextis never read onNone. This is the long way of writingwhile fast and fast.next.slow = slow.nextmoves the tortoise one node;fast = fast.next.nextmoves the hare two nodes.- Template A just returns
slowwhen the loop stops. The hare walked twice as far, so the tortoise is halfway. - Template B adds
if slow is fast: return Trueafter moving.isasks "is this the very same node object?", which is what we want. Two different nodes can hold the same number. If the loop ends without a meeting, the hare found the end of the list, so we returnFalse. - Template C does the same as B, but when they meet it starts phase 2:
slow = headputs one runner back at the start, and the innerwhile slow is not fast:moves both runners one step at a time. The node where they meet again is the first node of the loop.
Dry run on the example: Template A on 1 → 2 → 3 → 4 → 5 (values shown for each node), then template C on LeetCode 142's list 3 → 2 → 0 → -4 whose tail points back to the node 2.
| Round | slow | fast | Can fast take 2 more steps? |
|---|---|---|---|
| start | 1 | 1 | yes |
| 1 | 2 | 3 | yes |
| 2 | 3 | 5 | no → stop, return slow |
Template A returns the node 3. Template C:
| Phase | Round | slow | fast | What happened |
|---|---|---|---|---|
| 1 | start | 3 | 3 | both at the head |
| 1 | 1 | 2 | 0 | different nodes |
| 1 | 2 | 0 | 2 | different nodes |
| 1 | 3 | -4 | -4 | same node: they met |
| 2 | reset | 3 | -4 | slow back to the head |
| 2 | 1 | 2 | 2 | same node: the entrance |
Template C returns the node 2, the entrance of the loop.
6. How to remember it forever
How this hook works: what “Hare laps tortoise” means
What the slogan means, phrase by phrase
- “Hare”
- The fast pointer:
fast = fast.next.next, two nodes per round. - “tortoise”
- The slow pointer:
slow = slow.next, one node per round. - “A straight road: the hare finishes while the tortoise is at halftime”
- If the list ends,
fastreaches the end whenslowhas gone half as far:slowis the middle. - “A running track: the hare laps the tortoise”
- If the list loops, the hare can't fall off the end. It gains one node per round and eventually catches the tortoise from behind:
slow is fastmeans there is a cycle. - “both stroll … and bump into each other at the gate”
- To find where the loop starts, put one pointer back at the head, leave one at the meeting point, and move both one step at a time. They meet at the loop's entrance (the "gate").
Why this picture fits the pattern: Linked lists have no length and no indexes. Two runners at different speeds learn about the shape of the list (its middle, whether it loops, where the loop starts) in one pass with O(1) memory.
The story → the code
| In the story | In the code |
|---|---|
| Tortoise / hare | slow = slow.next / fast = fast.next.next |
| The road ends | fast is None or fast.next is None |
| Hare laps tortoise | slow is fast |
| Stroll to the gate | slow = head, then both move 1 step until slow is fast |
Watch it on numbers
A list 1 → 2 → 3 → 4 → 5, where 5 points back to 3. The gate is node 3.
- Start: both on 1. Round 1: tortoise 2, hare 3. Round 2: tortoise 3, hare 5.
- Round 3: tortoise 4, hare 5 → 3 → 4. Same node: the hare lapped the tortoise, so there is a loop.
- Stroll: one walker from the head (1), one from the meeting point (4). Next: 2 and 5. Next: 3 and 3. They meet at the gate.
- Why it works: the tail is 2 steps long, and the meeting point is 1 step past the gate on a 3-node loop. Walking 2 more steps from there wraps round to the gate, exactly as 2 steps from the head do.
The trap it saves you from
Jumping two steps without checking both fast and fast.next (a crash on even-length lists), or comparing slow.val == fast.val instead of slow is fast (different nodes can hold equal values).
It still works when…
Happy Number: the "track" is the sequence of digit-square sums. If it loops without reaching 1, the hare laps the tortoise. Find the Duplicate Number: treat i → nums[i] as a list, and the gate is the duplicate.
Picture
The Greek letter ρ (rho): a tail leading into a loop. The meeting happens on the loop; the gate is where the tail joins it.
Chant
"One step, two steps. Meet? Loop. Reset one, walk both, meet at the gate."
Guard rule
"Check fast and fast.next." The fast runner looks two steps ahead before jumping.
7. LeetCode problems, solved step by step
Medium 287. Find the Duplicate Number Google Meta Amazon Cisco Microsoft
Problem: nums has n + 1 integers, each in 1..n. Exactly one value repeats (possibly many times). Find it without modifying the array and with O(1) extra space.
nums = [1,3,4,2,2]2nums = [3,1,3,4,2]3Constraints: 1 ≤ n ≤ 10⁵ · nums.length == n + 1 · 1 ≤ nums[i] ≤ n · All the integers in nums appear only once except for precisely one integer which appears two or more times.
Hint 1
Values are in 1..n and there are n + 1 of them. Treat i → nums[i] as a "next" pointer.
Hint 2
The duplicate value has two arrows pointing at it: it's the entrance of a cycle. Use Floyd's two phases.
From brute force to the pattern: A set of seen values is O(n) space; sorting modifies the array; checking every pair is O(n²). Following i → nums[i] turns the array into a linked list with a cycle whose entrance is the duplicate, which Floyd finds in O(n) time and O(1) space without changing anything.
Spot it: values are valid indices, so treat i → nums[i] as a "next" pointer. Index 0 is never a target (values ≥ 1), so it's the head of a tail. Two indices point at the duplicate value, so that node has two arrows coming in: the entrance of a cycle. "Duplicate + O(1) space + read-only" means Floyd.
Key insight: the duplicate is the cycle start. Run template C with slow = nums[slow] and fast = nums[nums[fast]].
Approach:
- Treat each index
ias a node whose next isnums[i], starting from index 0. - Phase 1:
slow = nums[slow],fast = nums[nums[fast]]until they're equal. - Phase 2: reset
slowto the start (nums[0]) and keepfastat the meeting point. - Move both one hop at a time. Where they meet is the cycle entrance, which is the duplicate.
from typing import List
class Solution:
def findDuplicate(self, nums: List[int]) -> int:
# Treat every index as a "node" and nums[index] as its "next" arrow.
# Index 0 is the head (no value is 0, so nothing points back at it).
# One hop from index 0 lands on nums[0]; both runners start there.
slow = nums[0]
fast = nums[0]
# Phase 1: slow hops once, fast hops twice, until they land together.
while True:
slow = nums[slow] # slow: 1 hop
fast = nums[fast] # fast: first hop
fast = nums[fast] # fast: second hop (same as nums[nums[fast]])
if slow == fast:
break # they met somewhere inside the loop
# Phase 2: put slow back at the start, leave fast at the meeting point.
slow = nums[0]
# Both now hop ONE step at a time; they meet at the loop entrance.
while slow != fast:
slow = nums[slow]
fast = nums[fast]
# Two different indices point at the entrance, so it is the duplicate.
return slow
What the code is doing, line by line:
slow = nums[0]andfast = nums[0]: index 0 is the "head" of our hidden linked list (no value is 0, so no arrow ever points back to index 0). Its arrow leads tonums[0], so both runners start there.while True:we don't know how many rounds phase 1 needs, so we loop "forever" and leave withbreak.- Inside the loop,
slow = nums[slow]follows one arrow.fast = nums[fast]written twice follows two arrows. That is the same asnums[nums[fast]], split into two easy lines. if slow == fast: break: they are on the same index, so they met inside the loop. We compare after moving because they start out equal.- Phase 2:
slow = nums[0]sends slow back to the start whilefaststays at the meeting point.while slow != fast:moves both one arrow at a time. return slow: where they meet is the entrance of the loop. Two different indices point to it, so that value appears twice: it is the duplicate.
Dry run on the example: nums = [1, 3, 4, 2, 2] (expected answer 2). The arrows are 0→1, 1→3, 2→4, 3→2, 4→2.
| Phase | Round | slow | fast | What happened |
|---|---|---|---|---|
| 1 | start | 1 | 1 | both start at nums[0] = 1 |
| 1 | 1 | 3 | 2 | slow: nums[1] = 3; fast: nums[1] = 3, then nums[3] = 2 |
| 1 | 2 | 2 | 2 | slow: nums[3] = 2; fast: nums[2] = 4, then nums[4] = 2 → equal, break |
| 2 | reset | 1 | 2 | slow = nums[0] = 1, fast stays |
| 2 | 1 | 3 | 4 | not equal yet |
| 2 | 2 | 2 | 2 | equal → return 2 |
The function returns 2.
Edge cases to test:
- The duplicate appears many times, e.g.
[2, 2, 2, 2]. - The smallest case,
[1, 1]. - The duplicate is at index 0.
Complexity: O(n) time, O(1) space.
Medium 142. Linked List Cycle II Google Meta Amazon Microsoft
Problem: Return the node where the cycle begins, or None if the list has no cycle. Don't modify the list.
head = [3,2,0,-4], pos = 1tail connects to node index 1head = [1,2], pos = 0tail connects to node index 0Constraints: The number of the nodes in the list is in the range [0, 10⁴]. · -10⁵ ≤ Node.val ≤ 10⁵ · pos is -1 or a valid index in the linked-list.
Hint 1
First detect the cycle with slow (1 step) and fast (2 steps).
Hint 2
After they meet, move one pointer back to the head and step both one at a time; they meet at the entrance.
From brute force to the pattern: A set of visited nodes returns the first repeat in O(n) time but O(n) space. Floyd's algorithm gets the entrance with two pointers: the distance from the head to the entrance equals the distance from the meeting point to it (modulo the loop length): O(1) space.
Spot it: "where the cycle begins" + linked list. It's template C exactly; the second visualizer above traces it.
Key insight: distance from head to entrance = distance from meeting point to entrance (mod the loop length).
Approach:
- Run Floyd: slow 1 step, fast 2 steps, until they meet or fast falls off (no cycle →
None). - Reset
slowtohead. - Move
slowandfastone step each until they're the same node. - Return that node: the cycle's entrance.
class Solution:
def detectCycle(self, head: Optional[ListNode]) -> Optional[ListNode]:
# Both runners start at the head of the list.
slow = head
fast = head
# Phase 1: slow moves 1 step, fast moves 2 steps, until they meet
# or fast runs off the end of the list.
while fast is not None and fast.next is not None:
slow = slow.next
fast = fast.next.next
if slow is fast:
# Same NODE: fast lapped slow, so the list has a cycle.
# Phase 2: send slow back to the head, keep fast where they met.
slow = head
# Move both ONE step at a time; they meet at the entrance.
while slow is not fast:
slow = slow.next
fast = fast.next
return slow
# fast reached the end of the list, so there is no cycle.
return None
What the code is doing, line by line:
slow = head,fast = head: both runners start at the head.while fast is not None and fast.next is not None:keep going only while the hare can take two steps. If it can't, the list has an end, so there is no cycle.slow = slow.next(1 step) andfast = fast.next.next(2 steps).if slow is fast:both point at the same node object, so the hare has lapped the tortoise: there is a cycle.- Phase 2:
slow = headmoves one runner back to the head;faststays at the meeting point. The math in section 2 says both are now the same distance from the entrance. while slow is not fast:move both one step at a time. When they become the same node,return slowreturns the entrance.return Noneafter the outer loop: the hare fell off the end, so there is no cycle.
Dry run on the example: LeetCode example 1, head = [3, 2, 0, -4], pos = 1 (the tail -4 points back to the node 2). Node values are shown.
| Phase | Round | slow | fast | What happened |
|---|---|---|---|---|
| 1 | start | 3 | 3 | both at the head |
| 1 | 1 | 2 | 0 | different nodes |
| 1 | 2 | 0 | 2 | different nodes |
| 1 | 3 | -4 | -4 | same node: they met |
| 2 | reset | 3 | -4 | slow back to the head |
| 2 | 1 | 2 | 2 | same node: the entrance |
The function returns the node with value 2 (index 1).
Edge cases to test:
- No cycle →
None. - The cycle starts at the head.
- A single node pointing to itself.
Complexity: O(n) time, O(1) space.
Medium 143. Reorder List Google Meta Amazon Microsoft
Problem: Rearrange L0 → L1 → … → Ln into L0 → Ln → L1 → Ln−1 → … in place.
Solve it on LeetCode ↗ · Video walkthrough ↗
head = [1,2,3,4][1,4,2,3]head = [1,2,3,4,5][1,5,2,4,3]Constraints: The number of nodes in the list is in the range [1, 5 * 10⁴]. · 1 ≤ Node.val ≤ 1000
Hint 1
The result interleaves the first half with the reversed second half.
Hint 2
Three steps: find the middle (fast/slow), reverse the second half, then weave the two halves together.
From brute force to the pattern: Copying the nodes into an array and re-linking with two indices is O(n) time but O(n) space. Middle + reverse + merge does it in place in O(n) time and O(1) space.
Spot it: you need the back half in reverse order. That's (1) find the middle with fast/slow, (2) reverse the second half (pattern 05), (3) weave.
Key insight: stop slow at the end of the first half (while fast.next and fast.next.next) so you can cut slow.next = None.
Approach:
- Find the end of the first half with fast/slow (loop while
fast.next and fast.next.next). - Cut the list after
slowand reverse the second half. - Weave: one node from the first half, then one from the reversed second half, and so on.
- The first half is equal or one longer, so stop when the second half runs out.
class Solution:
def reorderList(self, head: Optional[ListNode]) -> None:
# A list with 0 or 1 nodes is already in the right order.
if head is None or head.next is None:
return
# Step 1: find the END of the first half.
# Looking at fast.next and fast.next.next makes slow stop one node
# earlier than usual: on the last node of the first half.
slow = head
fast = head
while fast.next is not None and fast.next.next is not None:
slow = slow.next
fast = fast.next.next
# Step 2: cut the list after slow ...
second_half = slow.next
slow.next = None # the first half now ends at slow
# ... and reverse the second half (pattern 05).
previous_node = None
current_node = second_half
while current_node is not None:
next_node = current_node.next # remember the rest of the list
current_node.next = previous_node # flip this node's arrow backwards
previous_node = current_node # previous moves forward
current_node = next_node # current moves forward
# previous_node is now the first node of the reversed second half.
# Step 3: weave: one node from the first half, then one from the second.
first = head
second = previous_node
while second is not None:
first_next = first.next # save the rest of the first half
second_next = second.next # save the rest of the second half
first.next = second # first node points to second node
second.next = first_next # second node points to the next first-half node
first = first_next # step forward in the first half
second = second_next # step forward in the second half
What the code is doing, line by line:
if head is None or head.next is None: return: an empty list or a single node is already in the right order.- Step 1, find the end of the first half. Both runners start at
head. The loop conditionfast.next is not None and fast.next.next is not Nonelooks one node further ahead than the usual check, soslowstops on the last node of the first half (the first middle). - Step 2, cut.
second_half = slow.nextremembers where the second half begins.slow.next = Noneends the first half there, so now we have two separate lists. - Step 2, reverse the second half.
previous_nodestarts asNoneandcurrent_nodeat the start of the second half. Each loop round: savenext_node = current_node.next(or we'd lose the rest of the list), flip the arrow withcurrent_node.next = previous_node, then move both names one node forward. Whencurrent_nodebecomesNone,previous_nodeis the head of the reversed half. - Step 3, weave.
firstwalks the first half andsecondwalks the reversed second half. Each round we save both "next" nodes first, then linkfirst → second → first_next, and step both names forward. - The loop stops when
secondruns out. The first half is the same length or one node longer, and its last node already ends withNonebecause of the cut.
Dry run on the example: LeetCode example 2, head = [1, 2, 3, 4, 5] (expected [1, 5, 2, 4, 3]).
| Stage | Variables after the step | What the list looks like |
|---|---|---|
| 1 find middle | slow = 1, fast = 1 | start |
| 1 find middle | slow = 2, fast = 3 | slow 1 step, fast 2 steps |
| 1 find middle | slow = 3, fast = 5 | slow 1 step, fast 2 steps; now fast.next.next is None → stop (slow ends the first half) |
| 2 cut | second_half = 4 | first half: 1 → 2 → 3 | second half: 4 → 5 |
| 2 reverse | previous_node = 4, current_node = 5 | reversed so far: 4 |
| 2 reverse | previous_node = 5, current_node = None | reversed so far: 5 → 4 |
| 3 weave | first = 2, second = 4 | linked 1 → 5 → 2; list from head: 1 → 5 → 2 → 3 |
| 3 weave | first = 3, second = None | linked 2 → 4 → 3; list from head: 1 → 5 → 2 → 4 → 3 |
Final list: 1 → 5 → 2 → 4 → 3.
Edge cases to test:
- One or two nodes → unchanged.
- Odd length: the middle node ends up last.
- Forgetting to cut the first half off (set its tail's
nexttoNone) creates a cycle.
Complexity: O(n) time, O(1) space.
Medium 457. Circular Array Loop Google
Problem: From index i you jump nums[i] steps (wrapping around). Is there a cycle of length > 1 where every jump goes the same direction?
nums = [2,-1,1,2,2]truenums = [-1,-2,-3,-4,-5,6]falseConstraints: 1 ≤ nums.length ≤ 5000 · -1000 ≤ nums[i] ≤ 1000 · nums[i] != 0
Hint 1
From each index, jump by nums[i] (wrapping around). A valid loop moves in one direction only and has length > 1.
Hint 2
Run fast/slow from each start, stopping if the direction changes or a self-loop appears. Mark dead-end paths so they're never re-walked.
From brute force to the pattern: Simulating from every start with a visited set is O(n²). Fast/slow pointers detect a loop from a start in O(n) with O(1) space, and marking failed paths as 0 keeps the total at O(n).
Spot it: "circular" + "jump by nums[i]" defines a next function (i + nums[i]) % n. Detect a cycle with fast/slow, with extra rules.
Key insight: abort a run as soon as fast would step onto a node of the opposite direction or a self-loop (next(j) == j). Afterwards, zero out the path you walked so each index is explored once overall: O(n).
Approach:
- For each unvisited start
i, remember its direction (forward ifnums[i] > 0). - Run slow/fast with
next(i) = (i + nums[i]) % n, but only while fast's next two steps keep that direction and aren't self-loops. - If
slow == fast, a valid cycle exists →True. - Otherwise zero out every index on this same-direction path so it's never explored again.
- If no start finds a cycle, return
False.
class Solution:
def next_index(self, nums: List[int], index: int) -> int:
# Jump nums[index] steps. "% size" wraps around the circle;
# in Python the result of % is always between 0 and size - 1,
# even when the jump is negative.
size = len(nums)
return (index + nums[index]) % size
def is_legal_step(self, nums: List[int], index: int, forward: bool) -> bool:
# 1) 0 means "already explored, leads nowhere": not legal.
if nums[index] == 0:
return False
# 2) It must move in the same direction as the run we started.
moves_forward = nums[index] > 0
if moves_forward != forward:
return False
# 3) Jumping back onto itself is a loop of length 1: not allowed.
if self.next_index(nums, index) == index:
return False
return True
def circularArrayLoop(self, nums: List[int]) -> bool:
size = len(nums)
for start in range(size):
# Skip positions an earlier run already proved useless.
if nums[start] == 0:
continue
# Remember this run's direction (True = forward, False = backward).
forward = nums[start] > 0
# Floyd: slow makes 1 jump per round, fast makes 2 jumps.
slow = start
fast = start
while True:
# fast may only jump if its current spot is legal ...
if not self.is_legal_step(nums, fast, forward):
break
fast_after_one_jump = self.next_index(nums, fast)
# ... and the spot after its first jump is legal too.
if not self.is_legal_step(nums, fast_after_one_jump, forward):
break
slow = self.next_index(nums, slow)
fast = self.next_index(nums, fast_after_one_jump)
if slow == fast:
return True # met inside a valid loop
# No loop from this start. Zero out the same-direction path we walked
# so that no later start walks it again (this keeps the total O(n)).
index = start
while nums[index] != 0:
moves_forward = nums[index] > 0
if moves_forward != forward:
break # the path turned around: stop
following = self.next_index(nums, index) # compute BEFORE zeroing
nums[index] = 0
index = following
return False
What the code is doing, line by line:
next_index(nums, index)is the "next pointer": jumpnums[index]steps and wrap around with% size. In Python,%always gives a number from0tosize - 1, even for negative jumps (for example-1 % 5is4).is_legal_step(nums, index, forward)answers "may a valid loop pass through this index?" in three checks: it is not already zeroed out, it moves in the same direction as this run (moves_forward != forwardmeans it would turn around), and it does not jump onto itself (a loop of length 1 is not allowed).- The main
for start in range(size):tries every index as a starting point and skips indices that are already0.forward = nums[start] > 0records this run's direction. - The
while True:loop is Floyd's race with extra rules. Before fast jumps, both the spot it stands on and the spot after its first jump must be legal. If either isn't,breakends this run. Otherwise slow makes one jump, fast makes two (fast_after_one_jump, then one more), andif slow == fast: return True, because only legal steps were taken, so this is a valid loop. - If the run ended without a meeting, the second
whileloop walks the same path again and sets each same-direction index to0. We computefollowingbefore zeroing, becausenext_indexneeds the old value. Zeroing means "dead end, never explore again", which keeps the total workO(n). - If no start finds a loop,
return False.
Dry run on the example: LeetCode example 1, nums = [2, -1, 1, 2, 2] (expected True: the loop 0 → 2 → 3 → 0 only moves forward).
| start | slow | fast | nums | What happened |
|---|---|---|---|---|
| 0 | 0 | 0 | [2, -1, 1, 2, 2] | new run, forward = True |
| 0 | 2 | 3 | [2, -1, 1, 2, 2] | fast jumped via 2 to 3; slow != fast |
| 0 | 3 | 2 | [2, -1, 1, 2, 2] | fast jumped via 0 to 2; slow != fast |
| 0 | 0 | 0 | [2, -1, 1, 2, 2] | fast jumped via 3 to 0; slow == fast → return True |
The function returns True.
Edge cases to test:
- A self-loop (
nums[i] % n == 0) doesn't count. - A loop that mixes forward and backward moves doesn't count.
- Large jumps, so take
% n(and handle negatives).
Complexity: O(n) time, O(1) space (it modifies the input as a visited marker).
Easy 141. Linked List Cycle Start here Google Meta Nvidia Amazon Cisco Microsoft
Problem: Does the linked list contain a cycle?
Solve it on LeetCode ↗ · Video walkthrough ↗
head = [3,2,0,-4], pos = 1truehead = [1,2], pos = 0trueConstraints: The number of the nodes in the list is in the range [0, 10⁴]. · -10⁵ ≤ Node.val ≤ 10⁵ · pos is -1 or a valid index in the linked-list.
Hint 1
If there's a loop, a faster runner eventually laps a slower one.
Hint 2
Move slow one step and fast two steps; if they ever point at the same node, there's a cycle.
From brute force to the pattern: Storing every visited node in a set detects a repeat in O(n) time but O(n) space. Two pointers at different speeds detect the cycle with O(1) space.
Spot it: the warm-up. Template B; the follow-up "O(1) memory" rules out the visited-set solution.
Approach:
- Start both runners at the head:
slow = headandfast = head. - While
fastandfast.nextexist: slow takes 1 step, fast takes 2. - If they become the same node, there's a cycle.
- If fast falls off the end, there isn't.
class Solution:
def hasCycle(self, head: Optional[ListNode]) -> bool:
# Both runners start at the head.
slow = head
fast = head
# fast needs two valid nodes ahead of it to take its 2 steps.
while fast is not None and fast.next is not None:
slow = slow.next # 1 step
fast = fast.next.next # 2 steps
if slow is fast:
return True # same NODE: fast lapped slow
return False # fast reached the end: no loop
What the code is doing, line by line:
slow = head,fast = head: both runners start at the head.while fast is not None and fast.next is not None:continue only while the hare has two nodes in front of it. An empty list makes this false right away.slow = slow.nexttakes 1 step;fast = fast.next.nexttakes 2 steps.if slow is fast: return True: checked after moving (at the start they are trivially equal). Being on the same node means the hare came around the loop and caught up.return False: the loop ended because the hare reached the end of the list, and a list with an end has no cycle.
Dry run on the example: LeetCode example 1, head = [3, 2, 0, -4], pos = 1 (the tail points back to the node 2).
| Round | slow | fast | slow is fast? |
|---|---|---|---|
| start | 3 | 3 | (not checked before moving) |
| 1 | 2 | 0 | no |
| 2 | 0 | 2 | no |
| 3 | -4 | -4 | yes → return True |
The function returns True.
Edge cases to test:
- An empty list → False.
- A single node with no cycle.
- A cycle back to the head.
Complexity: O(n) time, O(1) space.
8. Common mistakes
fast.next.next without checking fast and fast.next first. Always guard with while fast and fast.next.head, so slow is fast is true immediately. Move first, then compare.is (identity) for linked lists.while fast and fast.next gives the second middle (good for "return the middle"). while fast.next and fast.next.next gives the first middle (good for splitting into halves).9. Self-check quiz
A list has no cycle. When fast can no longer move, where is slow?
Slow and fast just met inside a cycle. How do you find where the cycle begins?
a = kL − b: the head and the meeting point are the same distance from the entrance.Find the repeated number in [3,1,3,4,2] (values in 1..n) without modifying it, O(1) space.
O(n) space. Treating values as next-pointers makes the duplicate the entrance of a cycle.Once slow enters the cycle, how soon is it guaranteed that fast catches it?
10. Practice list (in order)
| # | Problem | Level | Data structure | Twist |
|---|---|---|---|---|
| 876 | Middle of the Linked List | Easy | Warm-up: template A | |
| 141 | Linked List Cycle | Easy | Template B | |
| 202 | Happy Number | Easy | Implicit "next" = sum of digit squares | |
| 234 | Palindrome Linked List (O(1) space) | Easy | Middle + reverse half + compare | |
| 142 | Linked List Cycle II | Medium | Phase 2: find the entrance | |
| 287 | Find the Duplicate Number | Medium | Array as a linked list | |
| 2095 | Delete the Middle Node of a Linked List | Medium | Stop one before the middle | |
| 143 | Reorder List | Medium | Middle + reverse + weave | |
| 148 | Sort List | Medium | Fast/slow split for merge sort | |
| 457 | Circular Array Loop | Medium | Direction rule + mark dead paths | |
| 2130 | Maximum Twin Sum of a Linked List | Medium | Middle + reverse second half | |
| 19 | Remove Nth Node From End of List | Medium | Same speed, fixed gap: fast starts n nodes ahead | |
| 442 | Find All Duplicates in an Array | Medium | Index as a "seen" flag: flip the sign at index |x| − 1 | |
| 41 | First Missing Positive | Hard | Cyclic placement: put each value v at index v − 1, then find the first misfit |
Worked solutions for the practice list
Problems already solved in section 7 link back up there. Every other problem from the table is solved here.
Easy 876. Middle of the Linked List Start here Google Meta Amazon Microsoft
Problem: Return the middle node of a linked list (the second middle when the length is even).
Solve it on LeetCode ↗ · Video walkthrough ↗
head = [1,2,3,4,5][3,4,5]head = [1,2,3,4,5,6][4,5,6]Constraints: The number of nodes in the list is in the range [1, 100]. · 1 ≤ Node.val ≤ 100
Hint 1
Count the nodes, then walk half way? Can you do it in one pass?
Hint 2
Move fast two steps for each step of slow; when fast reaches the end, slow is in the middle.
From brute force to the pattern: Counting the length and then walking n / 2 steps is two passes; copying into an array uses O(n) space. Fast/slow finds the middle in one pass with O(1) space.
Spot it: "Middle" + singly linked list → template A.
Approach:
- Start both runners at the head:
slow = headandfast = head. - While
fastandfast.next: slow takes 1 step, fast takes 2. - Return
slow.
class Solution:
def middleNode(self, head: Optional[ListNode]) -> Optional[ListNode]:
# Both runners start at the head.
slow = head
fast = head
# Keep going while fast can still take 2 steps.
while fast is not None and fast.next is not None:
slow = slow.next # 1 step
fast = fast.next.next # 2 steps
# fast is at (or just past) the end, so slow is in the middle.
# For an even length this is the SECOND of the two middle nodes.
return slow
What the code is doing, line by line:
slow = head,fast = head: both runners start at the head.while fast is not None and fast.next is not None:keep going while the hare can take two steps.slow = slow.next(1 step),fast = fast.next.next(2 steps).return slow: the hare is at the last node (odd length) or just past it (None, even length). Having walked half as far, the tortoise is in the middle. For an even length that is the second of the two middle nodes, which is what LeetCode wants.
Dry run on the example: LeetCode example 2, head = [1, 2, 3, 4, 5, 6] (expected the node 4).
| Round | slow | fast | Can fast take 2 more steps? |
|---|---|---|---|
| start | 1 | 1 | yes |
| 1 | 2 | 3 | yes |
| 2 | 3 | 5 | yes |
| 3 | 4 | None | no → stop, return slow |
The function returns the node 4 (so the answer list is [4, 5, 6]).
Edge cases to test:
- Even length → return the second middle.
- A single node.
- Two nodes → the second.
Complexity: O(n) time, O(1) space.
Easy 202. Happy Number Start here Google Meta Amazon Cisco Microsoft
Problem: Keep replacing n with the sum of the squares of its digits. If you reach 1, n is happy; otherwise the numbers loop forever. Is n happy?
Solve it on LeetCode ↗ · Video walkthrough ↗
n = 19truen = 2falseConstraints: 1 ≤ n ≤ 2³¹ - 1
Hint 1
Repeatedly replacing n by the sum of the squares of its digits either reaches 1 or loops forever.
Hint 2
Detect the loop with a set, or with fast/slow pointers on this "next number" function.
From brute force to the pattern: Following the sequence while storing seen numbers in a set works (O(log n) steps per number) but uses memory. Treating "next = sum of digit squares" as a linked list lets Floyd's fast/slow detect the cycle in O(1) space.
Spot it: "Loops forever" + a well-defined next value → cycle detection on numbers instead of nodes.
Approach:
- Define
sum_of_digit_squares(x)= sum of the squared digits ofx(the "next pointer"). slow = n,fast = sum_of_digit_squares(n).- Advance slow by 1 and fast by 2 until
fast == 1orslow == fast. - Happy exactly when
fast == 1.
class Solution:
def sum_of_digit_squares(self, number: int) -> int:
# This is the implicit "next pointer": 19 -> 1*1 + 9*9 = 82.
total = 0
while number > 0:
digit = number % 10 # the last digit
total = total + digit * digit # add its square
number = number // 10 # chop the last digit off
return total
def isHappy(self, n: int) -> bool:
# slow starts at n; fast starts one step ahead so the loop can run.
slow = n
fast = self.sum_of_digit_squares(n)
# Stop when fast reaches 1 (happy) or catches slow (stuck in a loop).
while fast != 1 and slow != fast:
slow = self.sum_of_digit_squares(slow) # slow: 1 step
fast = self.sum_of_digit_squares(fast) # fast: first step
fast = self.sum_of_digit_squares(fast) # fast: second step
# Happy exactly when the chain reached 1.
return fast == 1
What the code is doing, line by line:
sum_of_digit_squares(number)is the "next pointer".number % 10gives the last digit, we add its square tototal, andnumber // 10(whole-number division) chops that digit off. Whennumberreaches 0, every digit has been used.- In
isHappy,slow = nandfast = sum_of_digit_squares(n): fast starts one step ahead so thatslow != fastis true and the loop can begin. while fast != 1 and slow != fast:stop when fast reaches 1 (happy, and 1 maps to itself forever) or when fast catches slow (we are stuck in a loop that doesn't contain 1).- Inside: slow takes one step, fast takes two steps (two separate calls, one per line).
return fast == 1:Trueif we stopped because we reached 1,Falseif we stopped because the runners met.
Dry run on the example: LeetCode example 1, n = 19 (expected True). First, one call of the helper, sum_of_digit_squares(19):
| number | digit = number % 10 | total | number // 10 |
|---|---|---|---|
| 19 | 9 | 81 | 1 |
| 1 | 1 | 82 | 0 |
The helper returns 82. Now the race in isHappy(19):
| Round | slow | fast | Steps taken |
|---|---|---|---|
| start | 19 | 82 | fast = sum_of_digit_squares(19) = 82 |
| 1 | 82 | 100 | slow: 19 → 82; fast: 82 → 68 → 100 |
| 2 | 68 | 1 | slow: 82 → 68; fast: 100 → 1 → 1 |
fast reached 1, so the function returns True.
Edge cases to test:
n = 1→ True immediately.n = 7→ True (it takes a few steps).n = 2→ False (falls into the 4 → 16 → … loop).
Complexity: Each step is O(log n) digits, and the sequence quickly drops below a few hundred, so the time is effectively O(log n). O(1) space.
Medium 2095. Delete the Middle Node of a Linked List Google Amazon Microsoft
Problem: Delete the node at index ⌊n/2⌋ and return the head.
head = [1,3,4,7,1,2,6][1,3,4,1,2,6]head = [1,2,3,4][1,2,4]Constraints: The number of nodes in the list is in the range [1, 10⁵]. · 1 ≤ Node.val ≤ 10⁵
Hint 1
To delete the middle you need the node before it.
Hint 2
Start fast one step ahead (at head.next.next) so that slow stops just before the middle.
From brute force to the pattern: Counting the nodes and walking to n // 2 − 1 takes two passes. An offset fast/slow pair stops one node early in a single pass.
Spot it: To unlink a node you need the node before it. Use fast & slow, but give fast a one-round head start so slow stops one node early.
Approach:
- If there's only one node, return
None. slow = head,fast = head.next.next(fast starts two steps ahead).- While
fast and fast.next: slow takes 1 step, fast takes 2. slowis now just before the middle:slow.next = slow.next.next.
class Solution:
def deleteMiddle(self, head: Optional[ListNode]) -> Optional[ListNode]:
# A single node IS the middle; deleting it leaves an empty list.
if head.next is None:
return None
# Give fast a head start of one round (2 nodes).
# Then slow stops on the node just BEFORE the middle.
slow = head
fast = head.next.next
while fast is not None and fast.next is not None:
slow = slow.next
fast = fast.next.next
# Unlink the middle: slow now points past it.
middle = slow.next
slow.next = middle.next
return head
What the code is doing, line by line:
if head.next is None: return None: with a single node, that node is the middle; deleting it leaves an empty list.slow = head,fast = head.next.next: fast gets a one-round head start (two nodes). That shifts where slow stops by one node, so slow ends on the node just before the middle, which is the node we need in order to unlink the middle. (head.next.nextis safe: we knowhead.nextexists; it may beNone, which the loop handles.)- The usual loop:
while fast is not None and fast.next is not None:slow 1 step, fast 2 steps. middle = slow.nextnames the node to delete, andslow.next = middle.nextmakes the list skip over it.return head: the head itself never changes (the list has at least 2 nodes here).
Dry run on the example: LeetCode example 1, head = [1, 3, 4, 7, 1, 2, 6] (n = 7, so delete index 3, the node 7). The value 1 appears twice, so each node also shows its position.
| Round | slow | fast | Note |
|---|---|---|---|
| start | 1 (node #1) | 4 (node #3) | fast starts two nodes ahead |
| 1 | 3 (node #2) | 1 (node #5) | slow 1 step, fast 2 steps |
| 2 | 4 (node #3) | 6 (node #7) | slow 1 step, fast 2 steps; fast cannot take 2 more steps → stop |
| unlink | 4 (node #3) | 6 (node #7) | middle = 7 (node #4); slow.next = 1 (node #5); list: 1 → 3 → 4 → 1 → 2 → 6 |
The function returns 1 → 3 → 4 → 1 → 2 → 6.
Edge cases to test:
- A single node → empty list.
- Two nodes → delete the second.
- Even length → delete node
n // 2(0-indexed).
Complexity: O(n) time, O(1) space.
Easy 234. Palindrome Linked List (O(1) space) Google Meta Nvidia Amazon Microsoft
Problem: Do the list's values read the same forwards and backwards? Follow-up: use O(1) extra space.
head = [1,2,2,1]truehead = [1,2]falseConstraints: The number of nodes in the list is in the range [1, 10⁵]. · 0 ≤ Node.val ≤ 9
Hint 1
Copying the values into an array and comparing with its reverse uses O(n) space. How can you avoid that?
Hint 2
Find the middle, reverse the second half in place, then compare the two halves node by node.
From brute force to the pattern: An array copy or a stack gives O(n) space. Middle-finding plus an in-place reversal of the second half compares the halves with O(1) extra space (and you can reverse it back afterwards).
Spot it: You need to compare the first half with the second half read backwards → find the middle with fast/slow, then reverse half (pattern 05).
Approach:
- Fast/slow:
slowlands on the first node of the second half. - Reverse the list from
slowonward. - Walk from
headand from the reversed half together until the reversed half ends. - Any mismatch →
False; otherwiseTrue.
class Solution:
def isPalindrome(self, head: Optional[ListNode]) -> bool:
# Step 1: slow walks to the first node of the second half.
slow = head
fast = head
while fast is not None and fast.next is not None:
slow = slow.next
fast = fast.next.next
# Step 2: reverse the list from slow to the end.
previous_node = None
current_node = slow
while current_node is not None:
next_node = current_node.next # remember the rest
current_node.next = previous_node # flip the arrow
previous_node = current_node # move both forward
current_node = next_node
# previous_node is now the head of the reversed second half.
# Step 3: compare the first half with the reversed second half.
left = head
right = previous_node
while right is not None: # the second half is never longer
if left.val != right.val:
return False # a mismatch: not a palindrome
left = left.next
right = right.next
return True
What the code is doing, line by line:
- Step 1: the usual fast/slow loop (
while fast is not None and fast.next is not None) leavesslowon the first node of the second half (the second middle). - Step 2: reverse the list from
slowto the end. Each round savesnext_node, flipscurrent_node.nextto point backwards, and movesprevious_nodeandcurrent_nodeone node forward. Afterwardsprevious_nodeis the head of the reversed second half. - Step 3:
leftstarts atheadandrightat the reversed half. Whilerightis notNone, compareleft.valwithright.val. Any difference meansreturn False; otherwise move both one node. return True: every pair matched. We loop onrightbecause the second half is never longer than the first.
Dry run on the example: LeetCode example 1, head = [1, 2, 2, 1] (expected True). Values repeat, so each node also shows its position.
| Stage | Variables | What happened |
|---|---|---|
| 1 middle | slow = 1 (node #1), fast = 1 (node #1) | start |
| 1 middle | slow = 2 (node #2), fast = 2 (node #3) | slow 1 step, fast 2 steps |
| 1 middle | slow = 2 (node #3), fast = None | slow 1 step, fast 2 steps |
| 2 reverse | previous_node = 2 (node #3), current_node = 1 (node #4) | reversed half so far: 2 |
| 2 reverse | previous_node = 1 (node #4), current_node = None | reversed half so far: 1 → 2 |
| 3 compare | left = 1 (node #1), right = 1 (node #4) | equal → move both |
| 3 compare | left = 2 (node #2), right = 2 (node #3) | equal → move both |
| 3 compare | left = 2 (node #3), right = None | right half used up → return True |
The function returns True.
Edge cases to test:
- A single node → True.
- Even and odd lengths.
- Two nodes with different values → False.
Complexity: O(n) time, O(1) space. (It leaves the second half reversed; reverse it back if the caller cares.)
Medium 148. Sort List Google Meta Amazon
Problem: Sort a linked list in O(n log n) time.
Solve it on LeetCode ↗ · Video walkthrough ↗
head = [4,2,1,3][1,2,3,4]head = [-1,5,3,4,0][-1,0,3,4,5]Constraints: The number of nodes in the list is in the range [0, 5 * 10⁴]. · -10⁵ ≤ Node.val ≤ 10⁵
Hint 1
Merge sort works well on linked lists because merging needs no extra arrays.
Hint 2
Split at the middle with fast/slow, sort each half recursively, then merge the two sorted halves.
From brute force to the pattern: Copying values into an array and calling sort() uses O(n) space. Merge sort on the list itself is O(n log n) time with only the recursion stack; the split uses fast/slow pointers.
Spot it: Merge sort suits linked lists because it never needs random access; fast/slow finds the split point.
Approach:
- Base case: 0 or 1 node is already sorted.
slow = head,fast = head.next; advance until fast ends.slowis now the end of the first half.- Cut after
slowand sort both halves recursively. - Merge the two sorted halves (helper
merge_two_sorted_lists) using a dummy node and atailpointer.
class Solution:
def sortList(self, head: Optional[ListNode]) -> Optional[ListNode]:
# Base case: 0 or 1 node is already sorted.
if head is None or head.next is None:
return head
# Find the END of the first half. fast starts one node ahead,
# which makes slow stop on the last node of the first half.
slow = head
fast = head.next
while fast is not None and fast.next is not None:
slow = slow.next
fast = fast.next.next
# Cut the list into two halves.
second_half = slow.next
slow.next = None
# Sort each half (recursion), then merge the two sorted halves.
sorted_first = self.sortList(head)
sorted_second = self.sortList(second_half)
return self.merge_two_sorted_lists(sorted_first, sorted_second)
def merge_two_sorted_lists(self, left: Optional[ListNode], right: Optional[ListNode]) -> Optional[ListNode]:
# A dummy node in front means we never special-case the first node.
dummy = ListNode()
tail = dummy # tail = last node of the merged list
while left is not None and right is not None:
# Take the smaller front node and attach it after tail.
if left.val <= right.val:
tail.next = left
left = left.next
else:
tail.next = right
right = right.next
tail = tail.next
# One list ran out; attach whatever is left of the other one.
if left is not None:
tail.next = left
else:
tail.next = right
return dummy.next
What the code is doing, line by line:
if head is None or head.next is None: return head: the base case. A list of 0 or 1 nodes is already sorted, and it is what stops the recursion.slow = head,fast = head.next: fast starts one node ahead, so when the loop endsslowis on the last node of the first half. (If both started athead, a 2-node list would never shrink and the recursion would never end.)second_half = slow.nextthenslow.next = Nonecuts the list into two separate halves.sorted_first = self.sortList(head)andsorted_second = self.sortList(second_half): recursion. Trust that each call returns its half sorted.merge_two_sorted_lists(left, right): adummynode sits in front of the answer so the first real node needs no special case;tailis the last node of the merged list so far. Each round attaches the smaller of the two front nodes aftertailand moves that list forward. (<=keeps equal values in their original order.)- When one list runs out, the
if left is not None ... else ...attaches the rest of the other list in one go, because it is already sorted.return dummy.nextskips the dummy.
Dry run on the example: LeetCode example 1, head = [4, 2, 1, 3] (expected [1, 2, 3, 4]). Each row is one event in the order it happens; depth 0 is the first call.
| Recursion depth | Event | Lists |
|---|---|---|
| 0 | split | 4 → 2 → 1 → 3 ⇒ 4 → 2 | 1 → 3 (slow stopped on 2) |
| 1 | split | 4 → 2 ⇒ 4 | 2 (slow stopped on 4) |
| 2 | base case | 4 is already sorted |
| 2 | base case | 2 is already sorted |
| 1 | merge | 4 + 2 = 2 → 4 |
| 1 | split | 1 → 3 ⇒ 1 | 3 (slow stopped on 1) |
| 2 | base case | 1 is already sorted |
| 2 | base case | 3 is already sorted |
| 1 | merge | 1 + 3 = 1 → 3 |
| 0 | merge | 2 → 4 + 1 → 3 = 1 → 2 → 3 → 4 |
The function returns 1 → 2 → 3 → 4.
Edge cases to test:
- Empty or single-node list.
- Already sorted or reverse sorted.
- Duplicates and negative values.
Complexity: O(n log n) time, O(log n) recursion stack.
Medium 2130. Maximum Twin Sum of a Linked List Amazon
Problem: In a list of even length n, node i and node n−1−i are twins. Return the largest twin sum.
head = [5,4,2,1]6head = [4,2,2,3]7Constraints: The number of nodes in the list is an even integer in the range [2, 10⁵]. · 1 ≤ Node.val ≤ 10⁵
Hint 1
Node i pairs with node n − 1 − i: the first half pairs with the second half read backwards.
Hint 2
Find the middle, reverse the second half, then walk both halves together summing pairs.
From brute force to the pattern: Copying values to an array and summing a[i] + a[n − 1 − i] is O(n) space. Middle + reverse + walk does it in place with O(1) space.
Spot it: Pair the first half with the second half read backwards → middle (fast/slow) + reverse the second half.
Approach:
- Fast/slow:
slowlands on the first node of the second half. - Reverse the second half.
- Walk from
headand from the reversed half together, adding twin values. - Return the maximum sum.
class Solution:
def pairSum(self, head: Optional[ListNode]) -> int:
# Step 1: slow walks to the first node of the second half.
slow = head
fast = head
while fast is not None and fast.next is not None:
slow = slow.next
fast = fast.next.next
# Step 2: reverse the second half.
previous_node = None
current_node = slow
while current_node is not None:
next_node = current_node.next # remember the rest
current_node.next = previous_node # flip the arrow
previous_node = current_node # move both forward
current_node = next_node
# Step 3: left walks forward from the head, right walks forward through
# the reversed half, which means "backwards" through the original list.
best_sum = 0
left = head
right = previous_node
while right is not None:
twin_sum = left.val + right.val
if twin_sum > best_sum:
best_sum = twin_sum # a new best pair
left = left.next
right = right.next
return best_sum
What the code is doing, line by line:
- Step 1: the usual fast/slow loop leaves
slowon the first node of the second half (the length is even, so this is noden/2). - Step 2: reverse the second half with
previous_node/current_node/next_node, exactly like in 234.previous_nodeends on the old last node. - Step 3:
leftwalks forward from the head whilerightwalks the reversed half, which visits the original list from the back. Soleftandrightare always twins. twin_sum = left.val + right.val;if twin_sum > best_sum: best_sum = twin_sumkeeps the largest sum seen so far (all values are positive, so starting from 0 is safe).return best_sumonce the reversed half is used up.
Dry run on the example: LeetCode example 1, head = [5, 4, 2, 1] (expected 6).
| Stage | Variables | What happened |
|---|---|---|
| 1 middle | slow = 5, fast = 5 | start |
| 1 middle | slow = 4, fast = 2 | slow 1 step, fast 2 steps |
| 1 middle | slow = 2, fast = None | slow 1 step, fast 2 steps |
| 2 reverse | previous_node = 2, current_node = 1 | reversed half so far: 2 |
| 2 reverse | previous_node = 1, current_node = None | reversed half so far: 1 → 2 |
| 3 twin sums | left = 5, right = 1 | twin_sum = 5 + 1 = 6 → new best_sum = 6 |
| 3 twin sums | left = 4, right = 2 | twin_sum = 4 + 2 = 6 → best_sum stays 6 |
| 3 twin sums | right = None | loop ends → return 6 |
The function returns 6.
Edge cases to test:
- Two nodes → their sum.
- All equal values.
- The maximum pair is in the middle.
Complexity: O(n) time, O(1) space.
Medium 19. Remove Nth Node From End of List Google Meta Amazon Microsoft
Problem: Remove the n-th node from the end of the linked list and return its head, in one pass.
Solve it on LeetCode ↗ · Video walkthrough ↗
head = [1,2,3,4,5], n = 2[1,2,3,5]head = [1], n = 1[]Constraints: The number of nodes in the list is sz. · 1 ≤ sz ≤ 30 · 0 ≤ Node.val ≤ 100 · 1 ≤ n ≤ sz
Hint 1
If fast starts n nodes ahead of slow, where is slow when fast falls off the end?
Hint 2
Start slow at a dummy node before the head so it stops right before the node to delete.
From brute force to the pattern: Counting the length, then walking to position length − n, is two passes. A fixed gap between two pointers finds the node in one pass; the dummy node handles deleting the head.
Spot it: "N-th from the end" without knowing the length → two runners. Here they move at the same speed but with a head start of n, so when the leader falls off the end, the follower stands just before the target.
Key insight: A dummy node before head means the node before the target always exists, even when the target is the head itself (n equal to the length).
Approach:
- Put a
dummynode in front ofhead. - Move
fastforwardnnodes fromhead. - Start
slowatdummy, then move both one step at a time untilfastisNone. slowis now just before the node to delete: setslow.next = slow.next.next.- Return
dummy.next(the head may have changed).
class Solution:
def removeNthFromEnd(self, head: Optional[ListNode], n: int) -> Optional[ListNode]:
# a fake node before head, so removing the head needs no special case
dummy = ListNode(0, head)
fast = head
# give fast a head start of n nodes
for _ in range(n):
fast = fast.next
slow = dummy
# move together; the gap between them stays n
while fast is not None:
fast = fast.next
slow = slow.next
# slow is right before the node to remove: skip over it
slow.next = slow.next.next
return dummy.next
What the code is doing, line by line:
dummy = ListNode(0, head): a placeholder whosenextis the real head.for _ in range(n): fast = fast.next:fastwalksnnodes ahead. (_means we don't need the loop counter.)slow = dummy:slowstarts one node before the head. That extra step is what leaves it in front of the target, not on it.while fast is not None:move both one step per round; the gap ofn + 1positions between them never changes.- When
fastisNone(just past the last node),slowisn + 1positions back: the node before the target. slow.next = slow.next.next: unlink the target.return dummy.nexthandles the case where the old head was removed.
Dry run on the example:
head = [1, 2, 3, 4, 5], n = 2 (LeetCode Example 1). Returns [1, 2, 3, 5].
| phase | fast | slow | note |
|---|---|---|---|
| head start | 3 | — | fast moved 2 nodes: 1 → 2 → 3 |
| start | 3 | dummy | move together |
| round 1 | 4 | 1 | |
| round 2 | 5 | 2 | |
| round 3 | None | 3 | fast fell off: stop |
| unlink | 3 | 3.next = 5, node 4 is gone → [1, 2, 3, 5] |
Edge cases to test:
- Remove the head (
nequals the length). - A single-node list → empty.
- Remove the last node (
n = 1).
Complexity: O(L) time (one pass over the list), O(1) space.
Medium 442. Find All Duplicates in an Array Google
Problem: Values are in 1..n and each appears once or twice. Return all values that appear twice, in O(n) time and O(1) extra space.
nums = [4,3,2,7,8,2,3,1][2,3]nums = [1,1,2][1]Constraints: n == nums.length · 1 ≤ n ≤ 10⁵ · 1 ≤ nums[i] ≤ n · Each element in nums appears once or twice.
Hint 1
Values are between 1 and n. Could the array itself remember which values you have seen?
Hint 2
Use the sign of nums[x − 1] as a "seen x" flag.
From brute force to the pattern: A hash set finds duplicates in O(n) but uses O(n) space; sorting changes the order and costs O(n log n). Using the sign at each value's home index as a marker gives O(n) time with O(1) extra space.
Spot it: Values are valid indices (x → x − 1), the same structure Fast & Slow uses in Find the Duplicate Number. Here we use each value's home index as a marker: make it negative the first time; finding it already negative means a duplicate.
Why not another pattern? A set or counter is O(n) extra space; sorting is O(n log n). Floyd's cycle finding returns only one duplicate, not all of them.
Approach:
- For each
xinnums, look athome = abs(x) − 1(useabsbecause earlier steps may have negatedx). - If
nums[home]is already negative,abs(x)was seen before: record it. - Otherwise negate
nums[home]to markabs(x)as seen.
class Solution:
def findDuplicates(self, nums: List[int]) -> List[int]:
duplicates = []
for i in range(len(nums)):
value = abs(nums[i]) # the sign may already be used as a marker
home = value - 1 # the index that represents this value
if nums[home] < 0:
duplicates.append(value) # its marker is set: seen before
else:
nums[home] = -nums[home] # set the marker
return duplicates
What the code is doing, line by line:
abs(nums[i]): the stored number may have been negated as a marker, but its magnitude is the real value.home = value - 1: values1..nmap onto indices0..n−1.- A negative number at
homemeansvaluewas already marked once. - Negating the number at
homeuses the sign bit as a free boolean, so no extra memory is needed.
Dry run: use the Step through the solution player under the code. It runs this exact code on Example 1 and shows every line with the variables at that moment.
Edge cases to test:
- No duplicates →
[]. - Every value appears twice.
[1, 1]→[1].
Complexity: O(n) time, O(1) extra space (besides the output).
Hard 41. First Missing Positive Google Meta Amazon Microsoft Netflix
Problem: Return the smallest positive integer that is not in nums, in O(n) time and O(1) extra space.
nums = [1,2,0]3nums = [3,4,-1,1]2Constraints: 1 ≤ nums.length ≤ 10⁵ · -2³¹ ≤ nums[i] ≤ 2³¹ - 1
Hint 1
The answer is between 1 and n + 1. Can the array's own indices act as a "seen" table?
Hint 2
Try to put value v at index v − 1 by swapping; afterwards, scan for the first index that doesn't hold i + 1.
From brute force to the pattern: Checking 1, 2, 3, … with a linear search each time is O(n²); a set is O(n) time but O(n) space; sorting is O(n log n). Because the answer lies in 1..n + 1, each value can be swapped into its own index, and one more scan finds the gap: O(n) time, O(1) space.
Spot it: The answer is always in 1..n + 1 (n numbers can cover at most 1..n). So the array itself can be used as the "seen" table: index v − 1 is the home of value v. Following i → nums[i] − 1 and swapping values home is the cyclic-indices trick, a cousin of fast & slow on i → nums[i] (Find the Duplicate Number).
Why not another pattern? A hash set gives O(n) time but O(n) space, and sorting is O(n log n); both break the constraints. Binary search doesn't apply because the input isn't sorted.
Key insight: Swap each value v in 1..n into position v − 1 until every position holds its own value or a value that has no home (≤ 0, > n, or a duplicate). Then the first index i with nums[i] != i + 1 gives the answer i + 1. Each swap puts one value home for good, so there are at most n swaps in total.
Approach:
- For each index
i: whilenums[i]is in1..nand its homenums[i] − 1doesn't already hold the same value, swap it home. - Values outside
1..n, and duplicates, are left where they are. - Scan again: the first index
iwithnums[i] != i + 1meansi + 1is missing. - If every position is correct, the answer is
n + 1.
class Solution:
def firstMissingPositive(self, nums: List[int]) -> int:
n = len(nums)
for i in range(n):
# keep sending nums[i] to its home (index value - 1) while that's possible
while 1 <= nums[i] <= n and nums[nums[i] - 1] != nums[i]:
home = nums[i] - 1 # where this value belongs
nums[i], nums[home] = nums[home], nums[i] # move it home
for i in range(n):
if nums[i] != i + 1:
return i + 1 # the first position without its own value
return n + 1 # 1..n are all present
What the code is doing, line by line:
for i in range(n): visit each position once.while 1 <= nums[i] <= n and nums[nums[i] - 1] != nums[i]: the value has a home inside the array, and that home doesn't already contain it (this also stops duplicates from swapping forever).home = nums[i] - 1then the swap: the value goes home, and whatever was there comes toito be checked next round.- Second loop: the first index whose value isn't
i + 1reveals the smallest missing positive. return n + 1: every number1..nwas present.
Dry run: use the Step through the solution player under the code. It runs this exact code on Example 1 and shows every line with the variables at that moment.
Edge cases to test:
- All non-positive, e.g.
[-1, -2]→ 1. - Already
1..n, e.g.[1, 2, 3]→ 4. - Duplicates, e.g.
[1, 1]→ 2 (the!=check prevents an endless swap).
Complexity: O(n) time (each swap fixes one value permanently, so at most n swaps in total), O(1) extra space.