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:
- Lead Window Creation: Initialize a
fastpointer and advance it $n + 1$ steps into the list. This establishes a fixed spatial gap of exactly $n + 1$ nodes between thefastpointer and theslowpointer. - Simultaneous Advancement: Advance both
fastandslowpointers forward one step at a time untilfastreachesnullptr(the end of the list). - Target Alignment: Because the gap was fixed at $n + 1$, when
fastreaches beyond the last node,slowis positioned precisely at the node immediately preceding the $n^{\text{th}}$ node from the end. - Pointer Relinking: Update
slow->next = slow->next->nextto bypass and detach the target node.
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.nextas 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
fastpointer traverses exactly $L + 1$ nodes, guaranteeing optimal single-pass execution. - Space Complexity: $O(1)$ auxiliary space. The algorithm utilizes only two pointer variables (
fastandslow) and a stack-allocated sentinel node, requiring zero auxiliary heap memory.
