You're given the head of a singly linked list — a chain of nodes where each node knows only the one after it — and a number n. Remove the node that sits n positions from the end of the list (the last node is n = 1), then return the head of the list that remains. The catch that makes this interesting: a singly linked list has no way to walk backwards, so "count from the end" is not something you can do directly. The classic solution finds the node in a single pass using two pointers spaced n nodes apart.
// A node is a plain object: { val, next }.
// - val: any value carried by the node.
// - next: the next node, or null at the end of the list.
// The list is identified by its head node; an empty list is `null`.
//
// n is 1-indexed FROM THE END: n = 1 is the last node, n = length is the head.
function linkedListDeleteNthFromEnd(head, n): Node | null;
A list like 1 -> 2 -> 3 is built as nested objects:
const head = { val: 1, next: { val: 2, next: { val: 3, next: null } } };
// Delete the 2nd node from the end of 1 -> 2 -> 3 -> 4 -> 5.
// Counting from the end: 5 is 1st, 4 is 2nd. Remove the 4.
linkedListDeleteNthFromEnd(buildList([1, 2, 3, 4, 5]), 2);
// → 1 -> 2 -> 3 -> 5
// When n equals the length, the target IS the head — return the new head.
linkedListDeleteNthFromEnd(buildList([1, 2, 3]), 3);
// → 2 -> 3
// A single node removed leaves an empty list.
linkedListDeleteNthFromEnd(buildList([42]), 1);
// → null
n is 1-indexed from the end. n = 1 removes the last node; n = length removes the head. There is no n = 0.n is always valid. You can assume 1 <= n <= length. You don't need to guard against n larger than the list or a null head with n > 0.n === length, the node to delete is the head itself, so the function must be able to return a different head than it was given.null, not a node.next, you do not construct a fresh list.You'll remove one node from a singly linked list, identified by its position counting back from the end, and return the head of what's left.
You have a chain of nodes — 1 -> 2 -> 3 -> 4 -> 5 — and someone says "delete the 2nd node from the end." Counting back from the tail, the last node (5) is the 1st, 4 is the 2nd, so you remove 4 and end up with 1 -> 2 -> 3 -> 5. The awkward part is that a singly linked list only points forward: from any node you can reach the next one, but never the previous one. So "the 2nd from the end" is a position you can't navigate to directly — you'd have to know where the end is first, and the only way to find the end is to walk the whole list.
To remove a node you also need the node before it, because deletion in a linked list is "make the previous node's next skip over the target." If you only hold the target itself, you can't unhook it — nothing points away from it.
Two ideas carry the whole solution.
The first is a dummy node: a throwaway node we place in front of the real head, with its next pointing at the head. It never holds real data. Its only job is to give the head a "previous node," so that deleting the head looks exactly like deleting any other node — we just repoint dummy.next. Without it, deleting the head is a separate code path with its own bugs.
The second is the fixed-gap two-pointer trick. Keep two pointers, fast and slow, exactly n nodes apart. Advance them in lockstep. When fast reaches the last node, slow — trailing by n — is sitting exactly one node before the node you want to delete. The gap does the "counting from the end" for you in a single forward pass, no backward navigation required.
The honest first idea: you can't count from the end without knowing the length, so compute the length, convert "n from the end" into "position from the front," then walk to that spot and delete. Two passes, but correct.
function twoPass(head, n) {
let length = 0;
for (let node = head; node !== null; node = node.next) length++;
// The node to delete is at 0-based index (length - n) from the front.
const indexFromFront = length - n;
if (indexFromFront === 0) return head.next; // deleting the head
let prev = head;
for (let i = 0; i < indexFromFront - 1; i++) prev = prev.next;
prev.next = prev.next.next; // skip the target
return head;
}
This works, and on an interview whiteboard it's a perfectly acceptable answer. Two things nag at it, though. First, it walks the list twice — once to measure, once to delete. Second, look at the if (indexFromFront === 0) line: deleting the head is a special case glued on by hand, easy to forget and easy to get wrong. Both nags have the same two cures: the dummy node kills the head special case, and the two-pointer gap collapses the two passes into one.
function linkedListDeleteNthFromEnd(head, n) {
// Dummy node before the head: gives the head a predecessor, so deleting
// the head needs no special case. dummy.next is the current head.
const dummy = { val: null, next: head };
let fast = dummy;
let slow = dummy;
// Advance fast n nodes ahead, opening a gap of exactly n links.
for (let i = 0; i < n; i++) {
fast = fast.next;
}
// Move both until fast is on the LAST node. slow now trails by n,
// so it sits one node before the target (slow.next is the target).
while (fast.next !== null) {
fast = fast.next;
slow = slow.next;
}
// Splice the target out by repointing slow.next past it.
slow.next = slow.next.next;
// dummy.next is the (possibly new) head — correct even if we deleted the head.
return dummy.next;
}
module.exports = { linkedListDeleteNthFromEnd };
The shape changed in three meaningful ways from the two-pass version. Walk through each.
Why both pointers start at dummy, not at head. Starting at the dummy is what makes the head deletable. When n === length, the target is the head; slow needs to end up at the dummy so that slow.next = slow.next.next repoints dummy.next to the second node. If slow started at head, it could never reach a position before the head, and you'd be back to the special case.
Why fast advances exactly n steps, and why the loop is fast.next !== null (not fast !== null). After the first loop, fast is n nodes ahead of slow. The second loop stops when fast is on the last node — the node whose next is null. At that moment slow is n nodes behind the last node, which is exactly one node before the target (the n-th from the end). If you instead looped while fast !== null, fast would walk one step too far (off the end), and slow would overshoot to the target itself instead of its predecessor — an off-by-one that deletes the wrong node.
Why we return dummy.next instead of head. The local variable head still points at the original first node. If we deleted that node, head is now pointing at a node that's no longer in the list. dummy.next always reflects the current first node, whether or not the head was the one removed — so it's the only safe thing to return.
Trace linkedListDeleteNthFromEnd([1, 2, 3, 4], 2) — delete the 2nd-from-end node, which is 3. After it, the list should be 1 -> 2 -> 4.
setup dummy -> 1 -> 2 -> 3 -> 4 -> null
fast = dummy, slow = dummy
advance fast n = 2 steps:
i = 0 fast = node 1
i = 1 fast = node 2 (gap is now 2: slow=dummy, fast=2)
move both while fast.next !== null:
fast.next = node 3 (not null):
fast = node 3, slow = node 1
fast.next = node 4 (not null):
fast = node 4, slow = node 2
fast.next = null -> stop (fast on last node, slow on node 2)
splice: slow.next = slow.next.next
node 2 . next = node 4 (node 3 skipped)
return dummy.next -> node 1
result: 1 -> 2 -> 4
The pivotal moment is the stop condition. fast halts on node 4 (the last node) because node4.next === null. slow is on node 2, trailing by the gap of 2. Since slow.next is node 3 — the target — one assignment unhooks it.
Now the relink itself. slow.next was node 3; slow.next.next is node 4. Assigning slow.next = node 4 makes node 2 point straight at node 4, and node 3 falls out of the chain — nothing reachable points to it anymore.
fast by n + 1 steps instead of n, or looping the second phase while fast !== null instead of fast.next !== null, both push slow one node too far — it lands on the target and you delete slow.next, which is the node after the one you wanted. Concretely, deleting the 2nd-from-end of [1,2,3,4,5] would yield 1 -> 2 -> 3 -> 4 (you removed 5) instead of 1 -> 2 -> 3 -> 5. The fix is the pairing in the working code: advance n, then stop when fast.next is null.slow at head and n === length, there is no node before the head for slow to occupy, so you can't repoint anything — you're forced into an if that special-cases returning head.next. The dummy node removes that branch entirely. Skipping the dummy is the single most common source of "works for the middle, crashes on the head" bugs here.head instead of dummy.next. When the head is the deleted node, the local head variable still references the detached original first node. Return dummy.next, which always points at the live first node.n === length (the head case) and n === 1 (the tail case). These are the two boundaries. With the dummy and the fast.next !== null stop condition, both fall out of the same code with no extra handling — but they're exactly the inputs to test first, because a wrong gap shows up there immediately. A single-node list with n = 1 walks fast to the lone node, the second loop never runs (its next is already null), slow stays on the dummy, and dummy.next becomes null — an empty list, as required.fast off the end. If the second loop used while (fast !== null), the final iteration would set fast = null, and a later fast.next (or an extra slow = slow.next) would either throw or overshoot. Stop on the last node, never past it.n - 1 steps to land on the predecessor, then prev.next = prev.next.next. The dummy still earns its keep for the n = 1 (delete-head) case.fast two nodes for every one node slow moves. When fast reaches the end, slow is at the middle. This "slow/fast" pacing is the cousin of the fixed-gap trick used here.Keep practising the same patterns with a nearby challenge.
No submissions yet
Share your approach and start the discussion.
You're given the head of a singly linked list — a chain of nodes where each node knows only the one after it — and a number n. Remove the node that sits n positions from the end of the list (the last node is n = 1), then return the head of the list that remains. The catch that makes this interesting: a singly linked list has no way to walk backwards, so "count from the end" is not something you can do directly. The classic solution finds the node in a single pass using two pointers spaced n nodes apart.
// A node is a plain object: { val, next }.
// - val: any value carried by the node.
// - next: the next node, or null at the end of the list.
// The list is identified by its head node; an empty list is `null`.
//
// n is 1-indexed FROM THE END: n = 1 is the last node, n = length is the head.
function linkedListDeleteNthFromEnd(head, n): Node | null;
A list like 1 -> 2 -> 3 is built as nested objects:
const head = { val: 1, next: { val: 2, next: { val: 3, next: null } } };
// Delete the 2nd node from the end of 1 -> 2 -> 3 -> 4 -> 5.
// Counting from the end: 5 is 1st, 4 is 2nd. Remove the 4.
linkedListDeleteNthFromEnd(buildList([1, 2, 3, 4, 5]), 2);
// → 1 -> 2 -> 3 -> 5
// When n equals the length, the target IS the head — return the new head.
linkedListDeleteNthFromEnd(buildList([1, 2, 3]), 3);
// → 2 -> 3
// A single node removed leaves an empty list.
linkedListDeleteNthFromEnd(buildList([42]), 1);
// → null
n is 1-indexed from the end. n = 1 removes the last node; n = length removes the head. There is no n = 0.n is always valid. You can assume 1 <= n <= length. You don't need to guard against n larger than the list or a null head with n > 0.n === length, the node to delete is the head itself, so the function must be able to return a different head than it was given.null, not a node.next, you do not construct a fresh list.You'll remove one node from a singly linked list, identified by its position counting back from the end, and return the head of what's left.
You have a chain of nodes — 1 -> 2 -> 3 -> 4 -> 5 — and someone says "delete the 2nd node from the end." Counting back from the tail, the last node (5) is the 1st, 4 is the 2nd, so you remove 4 and end up with 1 -> 2 -> 3 -> 5. The awkward part is that a singly linked list only points forward: from any node you can reach the next one, but never the previous one. So "the 2nd from the end" is a position you can't navigate to directly — you'd have to know where the end is first, and the only way to find the end is to walk the whole list.
To remove a node you also need the node before it, because deletion in a linked list is "make the previous node's next skip over the target." If you only hold the target itself, you can't unhook it — nothing points away from it.
Two ideas carry the whole solution.
The first is a dummy node: a throwaway node we place in front of the real head, with its next pointing at the head. It never holds real data. Its only job is to give the head a "previous node," so that deleting the head looks exactly like deleting any other node — we just repoint dummy.next. Without it, deleting the head is a separate code path with its own bugs.
The second is the fixed-gap two-pointer trick. Keep two pointers, fast and slow, exactly n nodes apart. Advance them in lockstep. When fast reaches the last node, slow — trailing by n — is sitting exactly one node before the node you want to delete. The gap does the "counting from the end" for you in a single forward pass, no backward navigation required.
The honest first idea: you can't count from the end without knowing the length, so compute the length, convert "n from the end" into "position from the front," then walk to that spot and delete. Two passes, but correct.
function twoPass(head, n) {
let length = 0;
for (let node = head; node !== null; node = node.next) length++;
// The node to delete is at 0-based index (length - n) from the front.
const indexFromFront = length - n;
if (indexFromFront === 0) return head.next; // deleting the head
let prev = head;
for (let i = 0; i < indexFromFront - 1; i++) prev = prev.next;
prev.next = prev.next.next; // skip the target
return head;
}
This works, and on an interview whiteboard it's a perfectly acceptable answer. Two things nag at it, though. First, it walks the list twice — once to measure, once to delete. Second, look at the if (indexFromFront === 0) line: deleting the head is a special case glued on by hand, easy to forget and easy to get wrong. Both nags have the same two cures: the dummy node kills the head special case, and the two-pointer gap collapses the two passes into one.
function linkedListDeleteNthFromEnd(head, n) {
// Dummy node before the head: gives the head a predecessor, so deleting
// the head needs no special case. dummy.next is the current head.
const dummy = { val: null, next: head };
let fast = dummy;
let slow = dummy;
// Advance fast n nodes ahead, opening a gap of exactly n links.
for (let i = 0; i < n; i++) {
fast = fast.next;
}
// Move both until fast is on the LAST node. slow now trails by n,
// so it sits one node before the target (slow.next is the target).
while (fast.next !== null) {
fast = fast.next;
slow = slow.next;
}
// Splice the target out by repointing slow.next past it.
slow.next = slow.next.next;
// dummy.next is the (possibly new) head — correct even if we deleted the head.
return dummy.next;
}
module.exports = { linkedListDeleteNthFromEnd };
The shape changed in three meaningful ways from the two-pass version. Walk through each.
Why both pointers start at dummy, not at head. Starting at the dummy is what makes the head deletable. When n === length, the target is the head; slow needs to end up at the dummy so that slow.next = slow.next.next repoints dummy.next to the second node. If slow started at head, it could never reach a position before the head, and you'd be back to the special case.
Why fast advances exactly n steps, and why the loop is fast.next !== null (not fast !== null). After the first loop, fast is n nodes ahead of slow. The second loop stops when fast is on the last node — the node whose next is null. At that moment slow is n nodes behind the last node, which is exactly one node before the target (the n-th from the end). If you instead looped while fast !== null, fast would walk one step too far (off the end), and slow would overshoot to the target itself instead of its predecessor — an off-by-one that deletes the wrong node.
Why we return dummy.next instead of head. The local variable head still points at the original first node. If we deleted that node, head is now pointing at a node that's no longer in the list. dummy.next always reflects the current first node, whether or not the head was the one removed — so it's the only safe thing to return.
Trace linkedListDeleteNthFromEnd([1, 2, 3, 4], 2) — delete the 2nd-from-end node, which is 3. After it, the list should be 1 -> 2 -> 4.
setup dummy -> 1 -> 2 -> 3 -> 4 -> null
fast = dummy, slow = dummy
advance fast n = 2 steps:
i = 0 fast = node 1
i = 1 fast = node 2 (gap is now 2: slow=dummy, fast=2)
move both while fast.next !== null:
fast.next = node 3 (not null):
fast = node 3, slow = node 1
fast.next = node 4 (not null):
fast = node 4, slow = node 2
fast.next = null -> stop (fast on last node, slow on node 2)
splice: slow.next = slow.next.next
node 2 . next = node 4 (node 3 skipped)
return dummy.next -> node 1
result: 1 -> 2 -> 4
The pivotal moment is the stop condition. fast halts on node 4 (the last node) because node4.next === null. slow is on node 2, trailing by the gap of 2. Since slow.next is node 3 — the target — one assignment unhooks it.
Now the relink itself. slow.next was node 3; slow.next.next is node 4. Assigning slow.next = node 4 makes node 2 point straight at node 4, and node 3 falls out of the chain — nothing reachable points to it anymore.
fast by n + 1 steps instead of n, or looping the second phase while fast !== null instead of fast.next !== null, both push slow one node too far — it lands on the target and you delete slow.next, which is the node after the one you wanted. Concretely, deleting the 2nd-from-end of [1,2,3,4,5] would yield 1 -> 2 -> 3 -> 4 (you removed 5) instead of 1 -> 2 -> 3 -> 5. The fix is the pairing in the working code: advance n, then stop when fast.next is null.slow at head and n === length, there is no node before the head for slow to occupy, so you can't repoint anything — you're forced into an if that special-cases returning head.next. The dummy node removes that branch entirely. Skipping the dummy is the single most common source of "works for the middle, crashes on the head" bugs here.head instead of dummy.next. When the head is the deleted node, the local head variable still references the detached original first node. Return dummy.next, which always points at the live first node.n === length (the head case) and n === 1 (the tail case). These are the two boundaries. With the dummy and the fast.next !== null stop condition, both fall out of the same code with no extra handling — but they're exactly the inputs to test first, because a wrong gap shows up there immediately. A single-node list with n = 1 walks fast to the lone node, the second loop never runs (its next is already null), slow stays on the dummy, and dummy.next becomes null — an empty list, as required.fast off the end. If the second loop used while (fast !== null), the final iteration would set fast = null, and a later fast.next (or an extra slow = slow.next) would either throw or overshoot. Stop on the last node, never past it.n - 1 steps to land on the predecessor, then prev.next = prev.next.next. The dummy still earns its keep for the n = 1 (delete-head) case.fast two nodes for every one node slow moves. When fast reaches the end, slow is at the middle. This "slow/fast" pacing is the cousin of the fixed-gap trick used here.Keep practising the same patterns with a nearby challenge.
No submissions yet
Share your approach and start the discussion.