19. Remove Nth Node From End of List
Problem Description
You are given the head of a singly linked list and an integer n
. Your task is to remove the n
-th node from the end of the list and return the modified list's head.
For example, if you have a linked list 1 -> 2 -> 3 -> 4 -> 5
and n = 2
, you need to remove the 2nd node from the end (which is node with value 4), resulting in 1 -> 2 -> 3 -> 5
.
The solution uses a two-pointer technique with fast and slow pointers. First, a dummy node is created pointing to the head to handle edge cases where the head itself needs to be removed. Both fast
and slow
pointers start at the dummy node.
The fast
pointer advances n
steps ahead first. This creates a gap of n
nodes between the fast and slow pointers. Then both pointers move forward together, one step at a time, maintaining this gap. When fast.next
becomes None
(meaning fast
is at the last node), the slow
pointer will be positioned right before the node that needs to be removed.
At this point, slow.next
is the node to be deleted. The removal is done by updating slow.next = slow.next.next
, effectively bypassing the target node in the linked list. Finally, dummy.next
is returned as the new head of the modified list.
This approach works in a single pass through the linked list with O(n)
time complexity and O(1)
space complexity.
Intuition
The key insight is recognizing that finding the n-th node from the end requires us to somehow relate it to a position from the beginning of the list. Since we don't know the total length of the linked list initially, we need a clever way to identify this position without counting all nodes first.
Think about it this way: if we have two runners on a track, and one runner starts n
steps ahead of the other, when the first runner reaches the finish line, where would the second runner be? The second runner would be exactly n
steps behind the finish line.
We can apply this same concept to our linked list problem. By maintaining two pointers with a gap of n
nodes between them, when the leading pointer reaches the end of the list, the trailing pointer will naturally be positioned at the node just before our target node.
Why do we need to stop at the node before our target? Because in a singly linked list, to remove a node, we need access to its predecessor to update the next
reference and bypass the node we want to delete.
The dummy node technique emerges from considering the edge case where we need to remove the head itself (when the list has exactly n
nodes). Without a dummy node, we'd need special handling for this case. By adding a dummy node before the head, we ensure there's always a predecessor node, making our logic uniform for all cases.
This approach elegantly solves the problem in one pass - we don't need to traverse the list twice (once to count nodes and once to remove), making it an optimal solution.
Learn more about Linked List and Two Pointers patterns.
Solution Approach
The implementation uses the fast and slow pointer technique with a dummy node to handle edge cases elegantly.
Step 1: Create a dummy node
dummy = ListNode(next=head)
We create a dummy node that points to the head of the linked list. This ensures we always have a predecessor node, even when removing the head itself.
Step 2: Initialize two pointers
fast = slow = dummy
Both fast
and slow
pointers start at the dummy node. These will maintain a fixed gap to help us locate the node to remove.
Step 3: Advance the fast pointer by n steps
for _ in range(n):
fast = fast.next
We move the fast
pointer forward by exactly n
nodes. This creates a gap of n
nodes between fast
and slow
. For example, if n = 2
, after this step, fast
is 2 nodes ahead of slow
.
Step 4: Move both pointers until fast reaches the end
while fast.next:
slow, fast = slow.next, fast.next
Both pointers now advance together, maintaining their gap of n
nodes. The loop continues while fast.next
exists (meaning fast
hasn't reached the last node yet). When this loop ends, fast
is at the last node, and slow
is positioned right before the node we need to remove.
Step 5: Remove the target node
slow.next = slow.next.next
Since slow
is positioned before the node to remove, we bypass the target node by updating slow.next
to point to slow.next.next
. This effectively removes the n-th node from the end.
Step 6: Return the new head
return dummy.next
We return dummy.next
which is the head of the modified linked list. If the original head was removed, dummy.next
correctly points to the new head.
This approach achieves O(n)
time complexity with a single pass through the list and O(1)
space complexity as we only use two extra pointers regardless of the list size.
Ready to land your dream job?
Unlock your dream job with a 5-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through a concrete example with the linked list 1 -> 2 -> 3 -> 4 -> 5
and n = 2
(removing the 2nd node from the end, which is node 4).
Initial Setup:
dummy -> 1 -> 2 -> 3 -> 4 -> 5 -> None ↑ fast slow
We create a dummy node pointing to head (1), and both fast
and slow
start at dummy.
Step 1: Advance fast pointer by n=2 steps
dummy -> 1 -> 2 -> 3 -> 4 -> 5 -> None ↑ ↑ slow fast
After moving fast
forward 2 times: fast
moves from dummy to 1, then from 1 to 2.
Step 2: Move both pointers together until fast.next is None
First move:
dummy -> 1 -> 2 -> 3 -> 4 -> 5 -> None ↑ ↑ slow fast
Second move:
dummy -> 1 -> 2 -> 3 -> 4 -> 5 -> None ↑ ↑ slow fast
Third move:
dummy -> 1 -> 2 -> 3 -> 4 -> 5 -> None ↑ ↑ slow fast
Now fast.next
is None (fast is at the last node), so we stop.
Step 3: Remove the target node
slow
is at node 3, and slow.next
is node 4 (our target). We execute:
slow.next = slow.next.next
This makes node 3 point directly to node 5, bypassing node 4:
dummy -> 1 -> 2 -> 3 -> 5 -> None ↑ ↑ slow (4 is removed)
Step 4: Return the result
Return dummy.next
, which gives us the modified list: 1 -> 2 -> 3 -> 5
The node with value 4 has been successfully removed from the list.
Solution Implementation
1# Definition for singly-linked list.
2# class ListNode:
3# def __init__(self, val=0, next=None):
4# self.val = val
5# self.next = next
6
7from typing import Optional
8
9class Solution:
10 def removeNthFromEnd(self, head: Optional[ListNode], n: int) -> Optional[ListNode]:
11 """
12 Remove the nth node from the end of a linked list and return the head.
13 Uses two-pointer technique with a gap of n nodes between them.
14
15 Args:
16 head: The head of the linked list
17 n: The position from the end (1-indexed)
18
19 Returns:
20 The head of the modified linked list
21 """
22 # Create a dummy node pointing to head to handle edge cases
23 # (e.g., removing the first node)
24 dummy_node = ListNode(next=head)
25
26 # Initialize two pointers at the dummy node
27 fast_pointer = dummy_node
28 slow_pointer = dummy_node
29
30 # Move fast pointer n steps ahead to create a gap of n nodes
31 for _ in range(n):
32 fast_pointer = fast_pointer.next
33
34 # Move both pointers until fast pointer reaches the last node
35 # This positions slow pointer right before the node to be removed
36 while fast_pointer.next is not None:
37 slow_pointer = slow_pointer.next
38 fast_pointer = fast_pointer.next
39
40 # Remove the nth node from the end by skipping it
41 slow_pointer.next = slow_pointer.next.next
42
43 # Return the actual head (dummy.next handles case where head was removed)
44 return dummy_node.next
45
1/**
2 * Definition for singly-linked list.
3 * public class ListNode {
4 * int val;
5 * ListNode next;
6 * ListNode() {}
7 * ListNode(int val) { this.val = val; }
8 * ListNode(int val, ListNode next) { this.val = val; this.next = next; }
9 * }
10 */
11class Solution {
12 public ListNode removeNthFromEnd(ListNode head, int n) {
13 // Create a dummy node pointing to head to handle edge case when removing the first node
14 ListNode dummyNode = new ListNode(0, head);
15
16 // Initialize two pointers both starting at dummy node
17 ListNode fastPointer = dummyNode;
18 ListNode slowPointer = dummyNode;
19
20 // Move fast pointer n steps ahead to create a gap of n nodes between fast and slow
21 while (n > 0) {
22 fastPointer = fastPointer.next;
23 n--;
24 }
25
26 // Move both pointers simultaneously until fast pointer reaches the last node
27 // This ensures slow pointer stops at the node just before the target node
28 while (fastPointer.next != null) {
29 slowPointer = slowPointer.next;
30 fastPointer = fastPointer.next;
31 }
32
33 // Remove the nth node from end by skipping it in the linked list
34 slowPointer.next = slowPointer.next.next;
35
36 // Return the head of the modified list (dummy.next handles case when original head was removed)
37 return dummyNode.next;
38 }
39}
40
1/**
2 * Definition for singly-linked list.
3 * struct ListNode {
4 * int val;
5 * ListNode *next;
6 * ListNode() : val(0), next(nullptr) {}
7 * ListNode(int x) : val(x), next(nullptr) {}
8 * ListNode(int x, ListNode *next) : val(x), next(next) {}
9 * };
10 */
11class Solution {
12public:
13 /**
14 * Remove the nth node from the end of a linked list
15 * @param head: pointer to the head of the linked list
16 * @param n: position from the end (1-indexed)
17 * @return: pointer to the head of the modified linked list
18 */
19 ListNode* removeNthFromEnd(ListNode* head, int n) {
20 // Create a dummy node pointing to head to handle edge cases
21 // (e.g., removing the head node)
22 ListNode* dummyNode = new ListNode(0, head);
23
24 // Initialize two pointers for the two-pointer technique
25 ListNode* fastPointer = dummyNode;
26 ListNode* slowPointer = dummyNode;
27
28 // Move fast pointer n steps ahead
29 // This creates a gap of n nodes between fast and slow pointers
30 while (n--) {
31 fastPointer = fastPointer->next;
32 }
33
34 // Move both pointers simultaneously until fast reaches the last node
35 // When fast reaches the end, slow will be at the node before the target
36 while (fastPointer->next != nullptr) {
37 slowPointer = slowPointer->next;
38 fastPointer = fastPointer->next;
39 }
40
41 // Remove the nth node from the end by skipping it
42 // slowPointer is now at the node before the one to be removed
43 ListNode* nodeToDelete = slowPointer->next;
44 slowPointer->next = slowPointer->next->next;
45
46 // Clean up memory (optional but good practice)
47 delete nodeToDelete;
48
49 // Store the actual head before deleting dummy
50 ListNode* newHead = dummyNode->next;
51 delete dummyNode;
52
53 return newHead;
54 }
55};
56
1/**
2 * Definition for singly-linked list node
3 */
4class ListNode {
5 val: number;
6 next: ListNode | null;
7
8 constructor(val?: number, next?: ListNode | null) {
9 this.val = (val === undefined ? 0 : val);
10 this.next = (next === undefined ? null : next);
11 }
12}
13
14/**
15 * Removes the nth node from the end of a linked list
16 * @param head - The head of the linked list
17 * @param n - The position from the end (1-indexed)
18 * @returns The head of the modified linked list
19 */
20function removeNthFromEnd(head: ListNode | null, n: number): ListNode | null {
21 // Create a dummy node pointing to head to handle edge cases
22 // (e.g., removing the first node)
23 const dummyNode: ListNode = new ListNode(0, head);
24
25 // Initialize two pointers at the dummy node
26 let fastPointer: ListNode = dummyNode;
27 let slowPointer: ListNode = dummyNode;
28
29 // Move fast pointer n steps ahead to create a gap of n nodes
30 while (n > 0) {
31 fastPointer = fastPointer.next!;
32 n--;
33 }
34
35 // Move both pointers simultaneously until fast reaches the last node
36 // This ensures slow pointer is just before the node to be removed
37 while (fastPointer.next !== null) {
38 slowPointer = slowPointer.next!;
39 fastPointer = fastPointer.next;
40 }
41
42 // Skip the nth node from the end by updating the next pointer
43 slowPointer.next = slowPointer.next!.next;
44
45 // Return the actual head (skipping the dummy node)
46 return dummyNode.next;
47}
48
Time and Space Complexity
The time complexity is O(n)
, where n
is the length of the linked list. The algorithm makes two passes through portions of the list: first, the fast pointer advances n
nodes ahead, then both pointers traverse together until the fast pointer reaches the end. In the worst case, this requires traversing approximately 2n
nodes, which simplifies to O(n)
.
The space complexity is O(1)
. The algorithm only uses a constant amount of extra space for the dummy node and two pointers (fast and slow), regardless of the input size. No additional data structures that scale with input size are created.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Not Handling Edge Case When Removing the Head Node
One of the most common mistakes is forgetting to handle the case where we need to remove the first node of the linked list (when the list has exactly n
nodes).
Incorrect Implementation:
def removeNthFromEnd(self, head: Optional[ListNode], n: int) -> Optional[ListNode]:
fast = slow = head # Starting directly from head instead of dummy
for _ in range(n):
fast = fast.next
# This will fail when fast is None (removing the head)
while fast.next: # NoneType has no attribute 'next'
slow = slow.next
fast = fast.next
slow.next = slow.next.next
return head
Why it fails: When the list has exactly n
nodes and we need to remove the head, after advancing fast
by n
steps, fast
becomes None
. Trying to access fast.next
raises an AttributeError.
Solution: Always use a dummy node that points to the head. This ensures there's always a predecessor node, even when removing the head itself.
2. Off-by-One Error in Pointer Positioning
Another common mistake is confusion about where the pointers should end up, leading to removing the wrong node.
Incorrect Implementation:
def removeNthFromEnd(self, head: Optional[ListNode], n: int) -> Optional[ListNode]:
dummy = ListNode(next=head)
fast = slow = dummy
# Advancing n+1 times instead of n
for _ in range(n + 1): # Wrong!
fast = fast.next
while fast: # Should be fast.next
slow = slow.next
fast = fast.next
slow.next = slow.next.next
return dummy.next
Why it fails: This creates a gap of n+1
nodes instead of n
, causing the wrong node to be removed. Additionally, using while fast:
instead of while fast.next:
positions slow
one node too far.
Solution:
- Advance
fast
exactlyn
times (notn+1
) - Use
while fast.next:
to ensureslow
stops right before the target node
3. Not Validating Input Assumptions
While the problem typically guarantees valid input, in production code, not checking if n
is valid can cause issues.
Risky Implementation:
def removeNthFromEnd(self, head: Optional[ListNode], n: int) -> Optional[ListNode]:
dummy = ListNode(next=head)
fast = slow = dummy
# What if n is larger than the list length?
for _ in range(n):
fast = fast.next # Could become None, then crash on next iteration
# Rest of the code...
Better Practice (for production code):
def removeNthFromEnd(self, head: Optional[ListNode], n: int) -> Optional[ListNode]:
dummy = ListNode(next=head)
fast = slow = dummy
# Validate that n doesn't exceed list length
for i in range(n):
if fast.next is None:
# Handle invalid n (larger than list length)
return head # or raise an exception
fast = fast.next
while fast.next:
slow = slow.next
fast = fast.next
slow.next = slow.next.next
return dummy.next
4. Memory Leak in Languages with Manual Memory Management
In languages like C or C++, forgetting to free the removed node can cause memory leaks.
C++ Example:
ListNode* removeNthFromEnd(ListNode* head, int n) {
ListNode* dummy = new ListNode(0, head);
ListNode* fast = dummy;
ListNode* slow = dummy;
for (int i = 0; i < n; i++) {
fast = fast->next;
}
while (fast->next) {
slow = slow->next;
fast = fast->next;
}
ListNode* toDelete = slow->next; // Save reference to node being removed
slow->next = slow->next->next;
delete toDelete; // Free the memory
ListNode* newHead = dummy->next;
delete dummy; // Also free the dummy node
return newHead;
}
Solution: Always remember to deallocate nodes that are removed from the list in languages without garbage collection.
What is the best way of checking if an element exists in an unsorted array once in terms of time complexity? Select the best that applies.
Recommended Readings
Linked List Cycle Given a linked list with potentially a loop determine whether the linked list from the first node contains a cycle in it For bonus points do this with constant space Parameters nodes The first node of a linked list with potentially a loop Result Whether there is a loop contained
Tech Interview Pattern Two Pointers Introduction If you prefer videos here's a super quick introduction to Two Pointers div class responsive iframe iframe src https www youtube com embed xZ4AfXHQ1VQ title YouTube video player frameborder 0 allow accelerometer autoplay clipboard write encrypted media gyroscope picture in picture allowfullscreen iframe div Two pointers is a common interview
Coding Interview Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way
Want a Structured Path to Master System Design Too? Don’t Miss This!