Builds on Fast & Slow Pointers, which you haven’t finished yet.
Fixed-Offset Pointers (Nth Node From End)
Your input is a linked list's head plus an integer n. Delete the node that sits n positions from the end of the list, then return the (possibly new) head.
Follow up: Can you find and delete that node in one pass, without first counting the list's length?
Fixed-Offset Pointers
list + n ➔ lead n steps ahead of trail ➔ move both together, trail stops n steps behind lead (Fixed Gap)n nodes from the end, in one pass with O(1) extra space.| 1 | from typing import Optional |
| 2 | |
| 3 | class ListNode: |
| 4 | def __init__(self, val: int = 0, next: Optional["ListNode"] = None): |
| 5 | self.val = val |
| 6 | self.next = next |
| 7 | |
| 8 | def remove_nth_from_end(head: Optional[ListNode], n: int) -> Optional[ListNode]: |
| 9 | """Remove Nth Node From End of List: keep lead n steps ahead of trail, so trail stops just before the target. |
| 10 | L = number of nodes in the list. |
| 11 | Time: O(L) | Space: O(1) |
| 12 | """ |
| 13 | dummy = ListNode(0, head) # Every real node, the head too, now has a node before it |
| 14 | lead = trail = dummy # Both start on dummy, so lead is 0 steps ahead of trail |
| 15 | |
| 16 | # 1. Open The Gap: |
| 17 | # Moving lead n nodes forward while trail stays put leaves lead exactly n steps ahead of trail. |
| 18 | for _ in range(n): |
| 19 | lead = lead.next |
| 20 | |
| 21 | # 2. Move Both Pointers Together: |
| 22 | # Each pass moves lead and trail one node, so lead stays n steps ahead. lead stops on the last node, which puts trail on the node just before the n-th node from the end. |
| 23 | while lead.next: |
| 24 | lead = lead.next |
| 25 | trail = trail.next |
| 26 | |
| 27 | # 3. Unlink The Target: |
| 28 | # trail.next is the n-th node from the end. Skipping it removes it, and when it is the head, trail is dummy, so the head needs no special case. |
| 29 | trail.next = trail.next.next |
| 30 | |
| 31 | # 4. Return Through The Dummy, Not head: |
| 32 | # head itself may have been removed, so dummy.next, not head, is always the first node of the result. |
| 33 | return dummy.next |
n steps, lead is exactly n steps ahead of trail, and every pass of the loop moves both by one node, so the gap is still n.A singly linked list has no link back, so you cannot walk from its end. Move lead forward n nodes while trail stays on dummy. Now lead is n steps ahead of trail. Then move both one node per loop pass. When lead is the last node, trail is n steps behind it, on the node just before the one to remove.
The first loop sets the gap to n. Each pass of the second loop moves lead and trail by one node, so the gap stays n. For a list of L nodes, lead ends on node L and trail ends on node L - n. The node after trail is the n-th node from the end. The code never computes L. When n equals L, trail stays on dummy, so the head is removed like any other node.
The trap is the pair of the gap size and the loop test. The loop while lead.next stops lead on the last node, so the gap must be n, set with range(n). If you write while lead instead, lead runs one node further, to None, and the gap must be n + 1. Mixing the two leaves trail one node away from the right place, and the code removes the wrong node.
See it run
Remove Nth Node From End of List: remove_nth_from_end([1, 2, 3, 4, 5, 6, 7, 8], n = 3)
lead dummytrail dummygap 0
Each circle is a node, and D is the dummy node. Arrows are next links. A removed node disappears from the picture.
leadthe pointer that moves first and stops on the last nodetrailthe pointer that stays n steps behind leadlead+trailboth pointers on the same node, so the gap is 0targetthe node that trail.next points at, which the code removesnew linkthe link that was just written
dummy.next = head, and lead and trail both start on the dummy node D. The gap is 0, and n is 3.
Gotcha 1 of 4
- The trap
Gotcha 1: Writing
while lead:instead ofwhile lead.next:Mistakewhile lead:Fixwhile lead.next:What happens: The gap of
nsteps is sized for a loop that stops withleadon the last node. The testwhile lead:letsleadmove one more node, toNone. Thentrailalso moves one more node and stops on the node to remove, not on the node before it. The linetrail.next = trail.next.nextthen removes the node after the right one. Whenn = 1,trailends on the last node,trail.nextisNone, andtrail.next.nextraises an AttributeError.Example: head = [1, 2, 3, 4, 5], n = 2Mistake: returns [1, 2, 3, 4]Fix: returns [1, 2, 3, 5]
- The target is the middle, a fixed fraction of the length, as in Middle of the Linked List (LC 876).
fastmoves two nodes whileslowmoves one, so the distance between them grows instead of staying atn. See Fast & Slow Pointers - You need a result about every element inside a window whose size the input fixes, as in Find All Anagrams in a String (LC 438). Here you need only the one node that sits
nsteps behindlead. See Fixed Window
Deep Dive
What this card adds to Fast & Slow Pointers
Fast & Slow Pointers moves two pointers at different speeds, so the distance between them grows. This card moves both pointers at the same speed, so the distance between them never changes.
The distance has a fixed size n. It is set once, before the main loop: lead moves n nodes while trail stays on dummy. After that, lead and trail move one node per pass, and the gap stays n.
A singly linked list has only next links and no stored length. So you cannot walk back from the end. The gap replaces the walk back: when lead reaches the end, trail is already n nodes behind it.
Tracing [1, 2, 3, 4, 5, 6, 7, 8] with n = 3
lead and trail both start on dummy. The loop for _ in range(n) runs three times, so lead ends on node 3 while trail stays on dummy. The gap is 3.
Then while lead.next runs five times. Each pass moves lead and trail by one node, and the gap stays 3. lead stops on node 8, the last node. trail stops on node 5.
trail.next is node 6, the third node from the end. The line trail.next = trail.next.next links 5 to 7, so the result is [1, 2, 3, 4, 5, 7, 8]. For Example 1 of Remove Nth Node From End of List (LC 19), [1, 2, 3, 4, 5] with n = 2, the same code returns [1, 2, 3, 5].
Why trail stops on the node before the target
Let L be the number of nodes, and count dummy as position 0. After the first loop, lead is on position n and trail is on position 0. The second loop runs until lead is on position L, which is L - n passes. So trail ends on position L - n.
Position L - n + 1 is the n-th node from the end. It is the node after trail, so trail.next = trail.next.next removes it. The code never computes L: the end of the list stops lead, and the gap places trail.
When n equals L, the second loop runs zero times, and trail stays on dummy. The target is the head, and it is removed by the same line. The head needs no special case, because dummy is a real node in front of it.
The trap, the cost and a related problem
The loop test and the gap size belong together. With while lead.next, lead stops on the last node, and the gap is n. With while lead, lead runs one node further, to None, and the gap must be n + 1. For the traced list and n = 3, while lead with the gap n removes node 7, and the result is [1, 2, 3, 4, 5, 6, 8]. The last step of the board shows this.
lead moves L nodes and trail moves L - n nodes, so the time is O(L). The extra memory is dummy, lead and trail, so the space is O(1). Counting the length first and then walking L - n nodes does the same number of moves. The fixed gap finds the node in one pass.
Rotate List (LC 61) uses the same gap. It first counts the nodes and replaces k with k % L, because k can be larger than L. Then lead moves k nodes ahead, and both pointers move until lead is on the last node. trail is then on the new tail. The code sets lead.next to the old head, the new head is trail.next, and trail.next becomes None.
60-Second Pitch
"I need the node n from the end, and the list stores no length. So I keep two pointers n nodes apart. Both start on a dummy node in front of the head. I move lead forward n times. Then I move lead and trail together while lead.next exists. The gap stays n. When lead is on the last node, trail is on the node just before the target, so trail.next = trail.next.next removes it. Because I start on the dummy node, removing the head works with the same line. I return dummy.next. The time is O(L) in one pass, and the space is O(1)."
Common Questions
- What is the time and space complexity?
The time is O(L), where
Lis the number of nodes:leadmovesLnodes andtrailmovesL - nnodes. The space isO(1):dummy,leadandtrail.- Why use a dummy node?
The target can be the head, when
nequalsL. The head has no node before it, and the code needs the node before the target.dummyis that node, so the same removal line works for every target.- Is this faster than counting the length first?
Both are O(L). Counting the length moves one pointer
Lnodes, and the second walk movesL - nnodes. The fixed gap makes the same number of moves in a single loop, so the list is read once. It matches the one-pass follow-up of Remove Nth Node From End of List (LC 19).- What changes if
ncan be larger than the list length? The first loop would move
leadpast the end, toNone, andlead.nextwould fail. Check the length first, or stop the first loop whenleadbecomesNone. Remove Nth Node From End of List (LC 19) promises thatnis at most the length.- How is this different from Fast & Slow Pointers?
In Fast & Slow Pointers,
fastmoves two nodes whileslowmoves one, so the distance grows. It finds the middle or a cycle. Here both pointers move one node, so the distance staysn. It finds a node a fixed number of nodes from the end.
SWE Reasoning
Spot it
- The problem names a node by its distance from the end of a singly linked list.
- The list stores no length, and the problem asks for one pass or
O(1)extra space. - The answer needs the node before the target, because you must change its
nextlink. - Two positions in the list must stay a known number of nodes apart.
The invariant, step by step
dummy = ListNode(0, head), andlead = trail = dummy. The gap is 0.for _ in range(n): lead = lead.nextmovesleadnnodes ahead. The gap isn.while lead.nextmovesleadandtrailone node each. The gap is stillnafter every pass.- The loop stops when
leadis on the last node.trailisnnodes behind it, on the node before the target. trail.next = trail.next.nextremoves the target. When the target is the head,trailisdummy.return dummy.nextreturns the first node of the result.
Traps
while lead:with a gap ofn.leadruns on toNone,trailends one node too far, and the node after the target is removed. Withn = 1,trail.next.nextraises an AttributeError.- Starting both pointers on
head. WhennequalsL, no node is before the head, and the pointers cannot stop in the right place. - Returning
head. If the head was removed,headstill points at the removed node. - Moving
trailduring the first loop. The gap then stays 0 and the pointers never separate.
Practice
Builds on Fast & Slow Pointers, which you haven’t finished yet.
Fixed-Offset Pointers (Nth Node From End)
Your input is a linked list's head plus an integer n. Delete the node that sits n positions from the end of the list, then return the (possibly new) head.
Follow up: Can you find and delete that node in one pass, without first counting the list's length?
Fixed-Offset Pointers
list + n ➔ lead n steps ahead of trail ➔ move both together, trail stops n steps behind lead (Fixed Gap)n nodes from the end, in one pass with O(1) extra space.| 1 | from typing import Optional |
| 2 | |
| 3 | class ListNode: |
| 4 | def __init__(self, val: int = 0, next: Optional["ListNode"] = None): |
| 5 | self.val = val |
| 6 | self.next = next |
| 7 | |
| 8 | def remove_nth_from_end(head: Optional[ListNode], n: int) -> Optional[ListNode]: |
| 9 | """Remove Nth Node From End of List: keep lead n steps ahead of trail, so trail stops just before the target. |
| 10 | L = number of nodes in the list. |
| 11 | Time: O(L) | Space: O(1) |
| 12 | """ |
| 13 | dummy = ListNode(0, head) # Every real node, the head too, now has a node before it |
| 14 | lead = trail = dummy # Both start on dummy, so lead is 0 steps ahead of trail |
| 15 | |
| 16 | # 1. Open The Gap: |
| 17 | # Moving lead n nodes forward while trail stays put leaves lead exactly n steps ahead of trail. |
| 18 | for _ in range(n): |
| 19 | lead = lead.next |
| 20 | |
| 21 | # 2. Move Both Pointers Together: |
| 22 | # Each pass moves lead and trail one node, so lead stays n steps ahead. lead stops on the last node, which puts trail on the node just before the n-th node from the end. |
| 23 | while lead.next: |
| 24 | lead = lead.next |
| 25 | trail = trail.next |
| 26 | |
| 27 | # 3. Unlink The Target: |
| 28 | # trail.next is the n-th node from the end. Skipping it removes it, and when it is the head, trail is dummy, so the head needs no special case. |
| 29 | trail.next = trail.next.next |
| 30 | |
| 31 | # 4. Return Through The Dummy, Not head: |
| 32 | # head itself may have been removed, so dummy.next, not head, is always the first node of the result. |
| 33 | return dummy.next |
n steps, lead is exactly n steps ahead of trail, and every pass of the loop moves both by one node, so the gap is still n.A singly linked list has no link back, so you cannot walk from its end. Move lead forward n nodes while trail stays on dummy. Now lead is n steps ahead of trail. Then move both one node per loop pass. When lead is the last node, trail is n steps behind it, on the node just before the one to remove.
The first loop sets the gap to n. Each pass of the second loop moves lead and trail by one node, so the gap stays n. For a list of L nodes, lead ends on node L and trail ends on node L - n. The node after trail is the n-th node from the end. The code never computes L. When n equals L, trail stays on dummy, so the head is removed like any other node.
The trap is the pair of the gap size and the loop test. The loop while lead.next stops lead on the last node, so the gap must be n, set with range(n). If you write while lead instead, lead runs one node further, to None, and the gap must be n + 1. Mixing the two leaves trail one node away from the right place, and the code removes the wrong node.
See it run
Remove Nth Node From End of List: remove_nth_from_end([1, 2, 3, 4, 5, 6, 7, 8], n = 3)
lead dummytrail dummygap 0
Each circle is a node, and D is the dummy node. Arrows are next links. A removed node disappears from the picture.
leadthe pointer that moves first and stops on the last nodetrailthe pointer that stays n steps behind leadlead+trailboth pointers on the same node, so the gap is 0targetthe node that trail.next points at, which the code removesnew linkthe link that was just written
dummy.next = head, and lead and trail both start on the dummy node D. The gap is 0, and n is 3.
Gotcha 1 of 4
- The trap
Gotcha 1: Writing
while lead:instead ofwhile lead.next:Mistakewhile lead:Fixwhile lead.next:What happens: The gap of
nsteps is sized for a loop that stops withleadon the last node. The testwhile lead:letsleadmove one more node, toNone. Thentrailalso moves one more node and stops on the node to remove, not on the node before it. The linetrail.next = trail.next.nextthen removes the node after the right one. Whenn = 1,trailends on the last node,trail.nextisNone, andtrail.next.nextraises an AttributeError.Example: head = [1, 2, 3, 4, 5], n = 2Mistake: returns [1, 2, 3, 4]Fix: returns [1, 2, 3, 5]
- The target is the middle, a fixed fraction of the length, as in Middle of the Linked List (LC 876).
fastmoves two nodes whileslowmoves one, so the distance between them grows instead of staying atn. See Fast & Slow Pointers - You need a result about every element inside a window whose size the input fixes, as in Find All Anagrams in a String (LC 438). Here you need only the one node that sits
nsteps behindlead. See Fixed Window
Deep Dive
What this card adds to Fast & Slow Pointers
Fast & Slow Pointers moves two pointers at different speeds, so the distance between them grows. This card moves both pointers at the same speed, so the distance between them never changes.
The distance has a fixed size n. It is set once, before the main loop: lead moves n nodes while trail stays on dummy. After that, lead and trail move one node per pass, and the gap stays n.
A singly linked list has only next links and no stored length. So you cannot walk back from the end. The gap replaces the walk back: when lead reaches the end, trail is already n nodes behind it.
Tracing [1, 2, 3, 4, 5, 6, 7, 8] with n = 3
lead and trail both start on dummy. The loop for _ in range(n) runs three times, so lead ends on node 3 while trail stays on dummy. The gap is 3.
Then while lead.next runs five times. Each pass moves lead and trail by one node, and the gap stays 3. lead stops on node 8, the last node. trail stops on node 5.
trail.next is node 6, the third node from the end. The line trail.next = trail.next.next links 5 to 7, so the result is [1, 2, 3, 4, 5, 7, 8]. For Example 1 of Remove Nth Node From End of List (LC 19), [1, 2, 3, 4, 5] with n = 2, the same code returns [1, 2, 3, 5].
Why trail stops on the node before the target
Let L be the number of nodes, and count dummy as position 0. After the first loop, lead is on position n and trail is on position 0. The second loop runs until lead is on position L, which is L - n passes. So trail ends on position L - n.
Position L - n + 1 is the n-th node from the end. It is the node after trail, so trail.next = trail.next.next removes it. The code never computes L: the end of the list stops lead, and the gap places trail.
When n equals L, the second loop runs zero times, and trail stays on dummy. The target is the head, and it is removed by the same line. The head needs no special case, because dummy is a real node in front of it.
The trap, the cost and a related problem
The loop test and the gap size belong together. With while lead.next, lead stops on the last node, and the gap is n. With while lead, lead runs one node further, to None, and the gap must be n + 1. For the traced list and n = 3, while lead with the gap n removes node 7, and the result is [1, 2, 3, 4, 5, 6, 8]. The last step of the board shows this.
lead moves L nodes and trail moves L - n nodes, so the time is O(L). The extra memory is dummy, lead and trail, so the space is O(1). Counting the length first and then walking L - n nodes does the same number of moves. The fixed gap finds the node in one pass.
Rotate List (LC 61) uses the same gap. It first counts the nodes and replaces k with k % L, because k can be larger than L. Then lead moves k nodes ahead, and both pointers move until lead is on the last node. trail is then on the new tail. The code sets lead.next to the old head, the new head is trail.next, and trail.next becomes None.
60-Second Pitch
"I need the node n from the end, and the list stores no length. So I keep two pointers n nodes apart. Both start on a dummy node in front of the head. I move lead forward n times. Then I move lead and trail together while lead.next exists. The gap stays n. When lead is on the last node, trail is on the node just before the target, so trail.next = trail.next.next removes it. Because I start on the dummy node, removing the head works with the same line. I return dummy.next. The time is O(L) in one pass, and the space is O(1)."
Common Questions
- What is the time and space complexity?
The time is O(L), where
Lis the number of nodes:leadmovesLnodes andtrailmovesL - nnodes. The space isO(1):dummy,leadandtrail.- Why use a dummy node?
The target can be the head, when
nequalsL. The head has no node before it, and the code needs the node before the target.dummyis that node, so the same removal line works for every target.- Is this faster than counting the length first?
Both are O(L). Counting the length moves one pointer
Lnodes, and the second walk movesL - nnodes. The fixed gap makes the same number of moves in a single loop, so the list is read once. It matches the one-pass follow-up of Remove Nth Node From End of List (LC 19).- What changes if
ncan be larger than the list length? The first loop would move
leadpast the end, toNone, andlead.nextwould fail. Check the length first, or stop the first loop whenleadbecomesNone. Remove Nth Node From End of List (LC 19) promises thatnis at most the length.- How is this different from Fast & Slow Pointers?
In Fast & Slow Pointers,
fastmoves two nodes whileslowmoves one, so the distance grows. It finds the middle or a cycle. Here both pointers move one node, so the distance staysn. It finds a node a fixed number of nodes from the end.
SWE Reasoning
Spot it
- The problem names a node by its distance from the end of a singly linked list.
- The list stores no length, and the problem asks for one pass or
O(1)extra space. - The answer needs the node before the target, because you must change its
nextlink. - Two positions in the list must stay a known number of nodes apart.
The invariant, step by step
dummy = ListNode(0, head), andlead = trail = dummy. The gap is 0.for _ in range(n): lead = lead.nextmovesleadnnodes ahead. The gap isn.while lead.nextmovesleadandtrailone node each. The gap is stillnafter every pass.- The loop stops when
leadis on the last node.trailisnnodes behind it, on the node before the target. trail.next = trail.next.nextremoves the target. When the target is the head,trailisdummy.return dummy.nextreturns the first node of the result.
Traps
while lead:with a gap ofn.leadruns on toNone,trailends one node too far, and the node after the target is removed. Withn = 1,trail.next.nextraises an AttributeError.- Starting both pointers on
head. WhennequalsL, no node is before the head, and the pointers cannot stop in the right place. - Returning
head. If the head was removed,headstill points at the removed node. - Moving
trailduring the first loop. The gap then stays 0 and the pointers never separate.