Facebook Pixel

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.

Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

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 Evaluator

Example 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 exactly n times (not n+1)
  • Use while fast.next: to ensure slow 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.

Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

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

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More