← Back to DevBytes

Solving Remove Nth Node From End in JavaScript: Step-by-Step Guide

Introduction to Remove Nth Node From End

The "Remove Nth Node From End of List" problem is one of the most classic linked list challenges you'll encounter in coding interviews and algorithm practice. Given the head of a singly linked list and an integer n, your task is to remove the nth node from the end of the list and return the updated head. While the problem sounds straightforward, it tests your understanding of linked list traversal, pointer manipulation, and edge case handling.

What Is a Singly Linked List?

Before diving into the solution, let's quickly recap what a singly linked list is. A linked list is a linear data structure where elements are stored in nodes. Each node contains two things: a value and a reference (or pointer) to the next node in the sequence. Unlike arrays, linked lists do not store elements in contiguous memory locations, which makes insertion and deletion operations more efficient in certain scenarios.

Here's how a typical linked list node is defined in JavaScript:

class ListNode {
  constructor(val = 0, next = null) {
    this.val = val;
    this.next = next;
  }
}

Why This Problem Matters

You might wonder why removing a node from the end of a linked list is such an important problem. The answer lies in what it teaches you about algorithm design and optimization. This problem forces you to think about:

Mastering this problem builds a strong foundation for tackling more complex linked list problems such as detecting cycles, merging sorted lists, and reversing portions of a list.

Understanding the Problem Statement

Let's break down the problem with a concrete example. Suppose we have the following linked list:

1 -> 2 -> 3 -> 4 -> 5

If n = 2, we need to remove the 2nd node from the end, which is the node with value 4. The resulting list should be:

1 -> 2 -> 3 -> 5

The key insight here is that "from the end" means we're counting backwards. The last node is the 1st from the end, the second-to-last is the 2nd from the end, and so on.

Constraints to Consider

Approach 1: Two-Pass Solution

The most intuitive approach is to traverse the list twice. In the first pass, we calculate the total length of the list. Once we know the length, we can determine which node to remove. If the list has L nodes, the node we want to remove is at position L - n from the beginning (0-indexed). In the second pass, we traverse to that position and remove the node.

Step-by-Step Breakdown

Here's how the two-pass approach works:

Implementation

function removeNthFromEndTwoPass(head, n) {
  // Create a dummy node that points to the head
  const dummy = new ListNode(0);
  dummy.next = head;
  
  // First pass: calculate the length of the list
  let length = 0;
  let current = head;
  while (current !== null) {
    length++;
    current = current.next;
  }
  
  // Calculate the position from the start (0-indexed)
  const positionToRemove = length - n;
  
  // Second pass: traverse to the node just before the one to remove
  current = dummy;
  for (let i = 0; i < positionToRemove; i++) {
    current = current.next;
  }
  
  // Remove the target node by skipping it
  current.next = current.next.next;
  
  // Return the new head (dummy.next handles the case where head was removed)
  return dummy.next;
}

Complexity Analysis

The two-pass solution has a time complexity of O(L), where L is the length of the list, because we traverse the list twice. The space complexity is O(1) since we only use a constant amount of extra space. While this solution works, we can do better by reducing it to a single pass.

Approach 2: One-Pass Two-Pointer Solution

The optimal solution uses the two-pointer technique, also known as the fast and slow pointer approach. The idea is to maintain two pointers that are separated by exactly n nodes. When the fast pointer reaches the end of the list, the slow pointer will be positioned just before the node we need to remove.

How the Two-Pointer Technique Works

Imagine you have two runners on a track. The fast runner starts n steps ahead of the slow runner. When the fast runner reaches the finish line (the end of the list), the slow runner is exactly n steps behind, which means the slow runner is at the node just before the one we want to remove.

Here's the detailed algorithm:

Implementation

function removeNthFromEnd(head, n) {
  // Create a dummy node to handle edge cases cleanly
  const dummy = new ListNode(0);
  dummy.next = head;
  
  // Initialize both pointers at the dummy node
  let fast = dummy;
  let slow = dummy;
  
  // Move fast pointer n+1 steps ahead to create a gap of n
  for (let i = 0; i <= n; i++) {
    fast = fast.next;
  }
  
  // Move both pointers until fast reaches the end
  while (fast !== null) {
    fast = fast.next;
    slow = slow.next;
  }
  
  // Remove the target node
  slow.next = slow.next.next;
  
  // Return the new head
  return dummy.next;
}

Tracing Through an Example

Let's trace through the example with the list 1 -> 2 -> 3 -> 4 -> 5 and n = 2:

Initial state:
dummy -> 1 -> 2 -> 3 -> 4 -> 5 -> null
fast = dummy, slow = dummy

After moving fast n+1 = 3 steps:
dummy -> 1 -> 2 -> 3 -> 4 -> 5 -> null
              fast
slow

After first iteration of while loop:
dummy -> 1 -> 2 -> 3 -> 4 -> 5 -> null
                   fast
       slow

After second iteration:
dummy -> 1 -> 2 -> 3 -> 4 -> 5 -> null
                        fast
            slow

After third iteration (fast is now null):
dummy -> 1 -> 2 -> 3 -> 4 -> 5 -> null
                             fast (null)
                slow

slow.next (node 4) is removed:
slow.next = slow.next.next
Result: 1 -> 2 -> 3 -> 5 -> null

Complexity Analysis

The one-pass solution has a time complexity of O(L), where L is the length of the list. Although the asymptotic complexity is the same as the two-pass solution, this approach is more efficient in practice because it only traverses the list once. The space complexity remains O(1) since we only use a fixed number of pointers.

Handling Edge Cases

Edge cases are where many solutions fail. Let's examine the critical edge cases you need to handle:

Single Node List

When the list contains only one node and n = 1, removing that node results in an empty list. The dummy node pattern handles this gracefully because dummy.next will point to null after the removal.

// List: 1 -> null, n = 1
// After removal: null

Removing the Head Node

When n equals the length of the list, we need to remove the head node. Without the dummy node, this would require special handling because there's no previous node to update. The dummy node solves this by acting as a virtual predecessor to the head.

// List: 1 -> 2 -> 3 -> null, n = 3
// We need to remove node 1 (the head)
// Result: 2 -> 3 -> null

Removing the Last Node

When n = 1, we remove the last node in the list. This is the simplest case because the slow pointer will land on the second-to-last node, and we simply set its next pointer to null.

// List: 1 -> 2 -> 3 -> null, n = 1
// We need to remove node 3 (the tail)
// Result: 1 -> 2 -> null

Testing Your Solution

To ensure your solution is correct, you should test it against various scenarios. Here's a helper function to create a linked list from an array and another to convert it back to an array for easy verification:

// Helper function to create a linked list from an array
function createList(arr) {
  if (arr.length === 0) return null;
  const head = new ListNode(arr[0]);
  let current = head;
  for (let i = 1; i < arr.length; i++) {
    current.next = new ListNode(arr[i]);
    current = current.next;
  }
  return head;
}

// Helper function to convert a linked list to an array
function listToArray(head) {
  const result = [];
  let current = head;
  while (current !== null) {
    result.push(current.val);
    current = current.next;
  }
  return result;
}

// Test cases
function runTests() {
  // Test 1: Normal case
  let list1 = createList([1, 2, 3, 4, 5]);
  let result1 = removeNthFromEnd(list1, 2);
  console.log(listToArray(result1)); // Expected: [1, 2, 3, 5]
  
  // Test 2: Single node
  let list2 = createList([1]);
  let result2 = removeNthFromEnd(list2, 1);
  console.log(listToArray(result2)); // Expected: []
  
  // Test 3: Remove head
  let list3 = createList([1, 2, 3]);
  let result3 = removeNthFromEnd(list3, 3);
  console.log(listToArray(result3)); // Expected: [2, 3]
  
  // Test 4: Remove tail
  let list4 = createList([1, 2, 3]);
  let result4 = removeNthFromEnd(list4, 1);
  console.log(listToArray(result4)); // Expected: [1, 2]
  
  // Test 5: Two nodes, remove first
  let list5 = createList([1, 2]);
  let result5 = removeNthFromEnd(list5, 2);
  console.log(listToArray(result5)); // Expected: [2]
}

runTests();

Best Practices

When solving linked list problems like this one, following best practices will help you write cleaner, more maintainable code:

Common Mistakes to Avoid

Even experienced developers make mistakes when working with linked lists. Here are some common pitfalls:

Conclusion

The Remove Nth Node From End problem is an excellent exercise in linked list manipulation and two-pointer techniques. By using a dummy node and the fast-slow pointer approach, you can solve this problem efficiently in a single pass with O(L) time complexity and O(1) space complexity. The dummy node pattern is particularly valuable as it elegantly handles the tricky edge case of removing the head node. As you continue practicing linked list problems, you'll find that the patterns and techniques learned here—especially the two-pointer approach and dummy node strategy—will serve as building blocks for solving more complex challenges. Remember to always trace through your algorithm with examples, test edge cases thoroughly, and prioritize clean, well-commented code that clearly communicates your intent to other developers.

🛠 Tools from DevBytes

Inventory Tracker Pro — Excel inventory system, low-stock alerts · $19
AI Dev Kit for Mac — local AI dev environment templates · $9.99
KeyMapper for Mac — custom keyboard shortcut toolkit · $7.99

← Back to all articles