Remove Nth Node From End of List: Two-Pointer Fast & Slow Runner Mechanics

In singly linked list data structures, removing a specific element from the end of the sequence—formally popularized as Remove Nth Node From End of List (LeetCode 19)—is a classic algorithmic problem that tests an engineer's mastery of pointer manipulation, boundary invariants, and traversal complexity. Because singly linked lists provide only unidirectional forward pointers (next) without backward traversal or random access ($O(1)$ indexing), identifying an element relative to the unknown tail requires either two complete sequential passes or an elegant Two-Pointer Fast and Slow Runner strategy. Below is a comprehensive architectural analysis of single-pass pointer mechanics, the dummy sentinel node invariant, and systems memory management.

1. The Two-Pointer Runner Technique (Fast & Slow Invariants)

The standard naive approach to removing the $n^{\text{th}}$ node from the end involves two passes: first, traverse the list completely to calculate its total length $L$; second, traverse $L - n$ nodes from the head to reach the target node's predecessor and update its next pointer. While this operates in $O(L)$ time, it requires scanning the list twice.

The Fast and Slow Runner Technique accomplishes this in a single contiguous pass by maintaining a constant geometric window between two pointers:

  1. Lead Window Creation: Initialize a fast pointer and advance it $n + 1$ steps into the list. This establishes a fixed spatial gap of exactly $n + 1$ nodes between the fast pointer and the slow pointer.
  2. Simultaneous Advancement: Advance both fast and slow pointers forward one step at a time until fast reaches nullptr (the end of the list).
  3. Target Alignment: Because the gap was fixed at $n + 1$, when fast reaches beyond the last node, slow is positioned precisely at the node immediately preceding the $n^{\text{th}}$ node from the end.
  4. Pointer Relinking: Update slow->next = slow->next->next to bypass and detach the target node.
Two-Pointer Runner Invariant with Dummy Sentinel Node (n = 2) Step 1: Advance Fast Pointer by (n + 1) Steps [Dummy:0] -> [1] -> [2] -> [3] -> [4] -> [5] -> NULL ^slow ^fast (advanced 3 steps) Gap maintained = 3 nodes (n + 1) Step 2: Advance Concurrently Until Fast is NULL [Dummy] -> [1] -> [2] -> [3] -> [4] -> [5] -> NULL ^slow ^fast (NULL) slow->next is node [4] (the 2nd from end!) Step 3: Relink Pointer & Reclaim Memory Operation: slow->next = slow->next->next (relinks node [3] directly to node [5]). Memory Safety (C++): Isolate pointer target = slow->next, update link, then call delete target. Resulting List: [1] -> [2] -> [3] -> [5] -> NULL (Clean single-pass O(L) time, O(1) space).

2. The Dummy Sentinel Node Invariant

In pointer algorithms, boundary conditions are the primary source of memory segmentation faults and null pointer exceptions. Consider what happens when removing the head node of a list (e.g., removing the $5^{\text{th}}$ node from end of a 5-node list, or removing the only node from a 1-node list):

  • Without a dummy node, the node to be removed has no predecessor within the list. The algorithm must introduce complex conditional branches: if (n == length) return head->next;.
  • By allocating a Dummy Sentinel Node that precedes the actual head (dummy.next = head), every node in the original list—including the original head—is guaranteed to have a valid preceding node.
  • The algorithm can treat every removal identically without special-case branches, returning dummy.next as the sanitized new head.

3. Production Implementations: Modern C++20 & TypeScript

Below are production-ready implementations demonstrating proper pointer ownership and memory management in both compiled and managed runtimes.

C++20 Implementation with Explicit Memory Reclaim

#include <iostream>

struct ListNode {
    int val;
    ListNode* next;
    ListNode(int x, ListNode* n = nullptr) : val(x), next(n) {}
};

class Solution {
public:
    ListNode* removeNthFromEnd(ListNode* head, int n) {
        // Allocate dummy sentinel node on the stack (zero heap allocation)
        ListNode dummy(0, head);
        ListNode* fast = &dummy;
        ListNode* slow = &dummy;

        // Advance fast pointer n + 1 steps
        for (int i = 0; i <= n; ++i) {
            if (!fast) return head; // Guard against n exceeding list length
            fast = fast->next;
        }

        // Slide the window until fast drops off the tail
        while (fast != nullptr) {
            fast = fast->next;
            slow = slow->next;
        }

        // Isolate target node and relink
        ListNode* toDelete = slow->next;
        slow->next = slow->next->next;

        // Explicitly free memory in C++ to prevent heap leak
        delete toDelete;

        return dummy.next;
    }
};

TypeScript Implementation for Managed Runtimes

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

export function removeNthFromEnd(head: ListNode | null, n: number): ListNode | null {
  const dummy = new ListNode(0, head);
  let fast: ListNode | null = dummy;
  let slow: ListNode | null = dummy;

  // Move fast n + 1 steps ahead
  for (let i = 0; i <= n; i++) {
    if (!fast) return head;
    fast = fast.next;
  }

  // Move fast to the end, maintaining the gap
  while (fast !== null) {
    fast = fast.next;
    slow = slow!.next;
  }

  // Remove the node (garbage collector reclaims unreferenced memory)
  if (slow && slow.next) {
    slow.next = slow.next.next;
  }

  return dummy.next;
}

4. Memory Management & Garbage Collection Pitfalls

In systems programming languages like C, C++, and Rust, simply updating slow->next = slow->next->next without explicitly calling delete or free() on the removed node creates an insidious heap memory leak. In high-frequency services processing millions of linked lists (e.g., event queues, hash table buckets with collision chaining), leaked nodes eventually trigger out-of-memory (OOM) kernel kills.

In garbage-collected environments (JavaScript, Java, Go), detaching the pointer allows the mark-and-sweep collector to reclaim the object. However, if external event listeners or debug registries retain a secondary reference to the detached node, the node and any attached payload remain permanently pinned in the heap.

5. Complexity Proof & Invariants

  • Time Complexity: $O(L)$ where $L$ is the number of nodes in the linked list. The fast pointer traverses exactly $L + 1$ nodes, guaranteeing optimal single-pass execution.
  • Space Complexity: $O(1)$ auxiliary space. The algorithm utilizes only two pointer variables (fast and slow) and a stack-allocated sentinel node, requiring zero auxiliary heap memory.