Problem
Given the head of a singly linked list and an integer n, remove the nth node counting from the tail and return the modified list. The node you're removing is exactly n positions from the last node โ so n=1 is the tail itself, n=2 is the one before it, and so on.
[1,2,3,4,5]
- Input: head = [1,2,3,4,5], n = 2
- Output: [1,2,3,5]
- Explanation: The 2nd node from the end is node 4; removing it connects node 3 directly to node 5.
Intuition
The hard part is finding a node's position from the end without knowing the list's total length. A two-pointer gap trick makes this work in a single pass: keep two pointers exactly n nodes apart, so that when the leading pointer exits the list, the trailing pointer lands on the node just before the one to delete.
Solution โ Two Pointers (One Pass)
Use a dummy head node and two pointers separated by exactly n steps. When the fast pointer reaches the last node, the slow pointer is positioned right before the target node and can unlink it in constant time.
- Create a
dummynode pointing toheadโ this handles the edge case where the head itself is removed. - Set both
slowandfasttodummy. - Advance
fastexactlynsteps forward to create the gap. - Move both pointers one step at a time until
fast.nextisnullโfastis now at the last node. - Unlink the target:
slow.next = slow.next.next. - Return
dummy.next(nothead, since head may have been removed).
1class Solution:
2 def removeNthFromEnd(self, head: Optional[ListNode], n: int) -> Optional[ListNode]:
3 dummy = ListNode(0, head) # sentinel lets us delete head without special-casing it
4 slow, fast = dummy, dummy
5
6 for _ in range(n):
7 fast = fast.next # open a gap of exactly n nodes between slow and fast
8
9 while fast.next: # stop when fast is at the last real node
10 slow = slow.next
11 fast = fast.next
12
13 slow.next = slow.next.next # bypass the nth-from-end node
14 return dummy.nextTime: O(L) โ one complete pass through a list of length L.
Space: O(1) โ two extra pointers regardless of list size.
Complexity Summary
| Approach | Time | Space | When to use |
|---|---|---|---|
| Two Pointers (One Pass) | O(L) | O(1) | Always โ canonical single-pass solution |
Common Mistakes
- Wrong stopping condition: Stopping when
fastisnull(notfast.next) putsslowone step too far โ it ends up at the node to delete rather than just before it, so the unlink breaks. - Moving fast n+1 times instead of n: Some versions check
while fast != nullinstead ofwhile fast.next != null; these require an extra initial step to stay correct. Mixing one version's gap size with the other's stopping condition deletes the wrong node. - Returning
headinstead ofdummy.next: When n equals the list's length, the head is the node to remove. Returningheadgives back the deleted node;dummy.nextalways points to the correct new head. - Omitting the dummy node for single-element lists: Without a sentinel, removing the only node requires a special
ifbranch. The dummy node makes the pointer math uniform across all inputs. - Forgetting that
slow.nextmust exist when unlinking: The problem guarantees1 โค n โค L, soslow.nextis always valid at deletion time โ but if you implement the gap incorrectly, you can reach a state whereslow.nextisnulland the line crashes.
Related Problems
middle-of-the-linked-listโ same two-pointer gap concept used to find the midpoint in one passpalindrome-linked-listโ combines two-pointer midpoint detection with in-place reversalreorder-listโ uses two-pointer midpoint then merges the two halves in interleaved orderodd-even-linked-listโ rearranges all odd-indexed nodes before even-indexed ones by rewiring next pointersreverse-linked-listโ foundational pointer manipulation that almost every linked list problem builds on