Deleting Nodes in a Linked List: The Bypass Pointer Trick
Delete a node from a linked list in O(1) once you find it by rewiring curr.next = curr.next.next — with a worked walkthrough, JavaScript, and complexity.
This article has an interactive companion
Don't just read it — step through it interactively, one state change at a time.
Deleting from an array means shifting every element after the gap — O(n) of memory churn. A linked list makes the same operation almost free: you never move data, you just change one arrow. That single reassignment, curr.next = curr.next.next, is the whole trick, and it's the reason linked lists exist.
The catch is that a singly linked list only lets you walk forward. To unlink a node you need the node before it, so the real work is the walk, not the delete. Get the predecessor right and the deletion is a one-liner.
The problem
Given the head of a singly linked list and an integer index, remove the node at that position (0-based) and return the head of the modified list.
Input: head = 10 -> 20 -> 30 -> 40, index = 1
Output: 10 -> 30 -> 40 // the node holding 20 is goneTwo edge cases shape the whole solution:
- Deleting the head (
index === 0) has no predecessor inside the list, so you moveheaditself. - An out-of-range index must not crash — the walk has to stop safely if it runs off the end before reaching the target.
The brute force baseline
You might reach for a fresh list: walk the original, copy every node except the target into a brand-new list, and return that.
function deleteAtIndex(head, index) {
const kept = [];
let node = head;
let i = 0;
while (node) {
if (i !== index) kept.push(node.val);
node = node.next;
i++;
}
// rebuild a whole new linked list from kept[]
let dummy = { next: null }, tail = dummy;
for (const val of kept) {
tail.next = { val, next: null };
tail = tail.next;
}
return dummy.next;
}It returns the right answer, but it throws away the entire point of a linked list. You allocate n new nodes and copy n values to remove exactly one. That's O(n) time and O(n) extra memory for an operation the data structure was designed to do in place.
The key insight: rewire, don't rebuild
A node's identity in a linked list is entirely defined by who points at it. Nothing "contains" the node holding 20; it exists only because the node holding 10 has a .next arrow aimed at it. Redirect that arrow past it and the node becomes unreachable — no traversal can ever visit it again.
So deletion is not about erasing memory. It's about skipping: point the predecessor's next at the target's successor.
before: [10] -> [20] -> [30]
^target
after: [10] --------> [30] // 20 has zero inbound pointers nowOnce the [20] node has no references, a garbage-collected runtime (JavaScript, Java, Go) reclaims it automatically. The only thing you had to compute was where the predecessor is — everything after that is a single pointer write.
The optimal solution
Walk a curr pointer to the node one before the target (index - 1), then bypass:
function deleteAtIndex(head, index) {
if (!head) return null;
// Head deletion: no predecessor, so move head itself.
if (index === 0) {
head = head.next;
return head;
}
let curr = head;
for (let i = 0; i < index - 1 && curr.next; i++) {
curr = curr.next;
}
// curr now sits at index-1 (or the last reachable node).
if (curr.next !== null) {
curr.next = curr.next.next; // bypass the target node
}
return head;
}Three guards make this robust. The !head check handles the empty list. The index === 0 branch handles head deletion, where there is no in-list predecessor. And the && curr.next condition inside the loop stops the walk early if the index points past the end — followed by the curr.next !== null check so we never dereference null. For an interior deletion the head reference never changes, which is why we return the original head.
Walkthrough
Trace deleteAtIndex(10 -> 20 -> 30 -> 40, index = 2). The target is the node holding 30, so we need curr to land on 20 (index 1) before rewiring.
| Line | curr | curr.next | Loop condition i < index-1 | Action |
|---|---|---|---|---|
curr = head | Node(10) | Node(20) | — | start pointer at head, index 0 |
for i=0 | Node(10) | Node(20) | 0 < 1 true | enter loop body |
curr = curr.next | Node(20) | Node(30) | — | advance to index 1 |
for i=1 | Node(20) | Node(30) | 1 < 1 false | exit loop |
if curr.next !== null | Node(20) | Node(30) | — | target exists, proceed |
curr.next = curr.next.next | Node(20) | Node(40) | — | 20 now points to 40; 30 bypassed |
return head | — | — | — | return 10 -> 20 -> 40 |
The loop runs exactly index - 1 = 1 time. Notice the default case index = 1 from the visualizer skips the loop entirely (0 < 0 is false immediately): curr stays on the head and 10.next jumps straight from 20 to 30. The number of iterations is the cost of the whole operation.
Complexity
| Operation | Time | Space | Why |
|---|---|---|---|
Delete at index i | O(i) | O(1) | walk i-1 steps to the predecessor, then one O(1) rewire |
| Delete at head | O(1) | O(1) | no walk — head = head.next |
| Brute-force rebuild | O(n) | O(n) | copies every node into a fresh list |
The rewire itself is always O(1); the only variable cost is finding the predecessor. Deleting near the front is cheap, deleting near the tail approaches O(n) — but you never allocate or copy, so extra space stays constant.
Common mistakes
- Stopping
curron the target instead of its predecessor. In a singly linked list you can't reach backward, so ifcurris already on the node to delete, you have no way to rewire the arrow pointing into it. Walk toindex - 1, notindex. - Forgetting the head case. When
index === 0there is no in-list predecessor. Trying to run the loop leaves the original head still pointing into the list, so it never actually detaches. Handle it withhead = head.next. - Dereferencing
curr.next.nextwithout a null check. If the target is the tail,curr.next.nextisnull— which is fine to assign — but if the index is out of range,curr.nextmay already benull. Theif (curr.next !== null)guard is what keeps a bad index from throwing. - Assuming the head reference is stable. For an interior delete it is, so you return the same
head. But head deletion changes it — always return the (possibly new)head, never assume the caller's reference is still valid.
Where this pattern shows up next
The predecessor-then-rewire move is the backbone of almost every linked-list edit:
- Introduction to Linked List — the node/pointer model that makes O(1) rewiring possible in the first place.
- Design Linked List — implement
addAtIndex,deleteAtIndex, andgettogether; the same predecessor walk drives all of them. - Adding Nodes to Linked List — insertion is the mirror image: find the predecessor, then splice a new node in instead of skipping one.
- Middle of the Linked List — a different traversal (fast/slow pointers) for when you need a position without knowing the length.
Watch the arrow redirect and the freed node fall away in the Deleting Nodes in Linked List visualizer, one step at a time.
FAQ
Why is deleting from a linked list O(1) but O(n) from an array?
An array stores elements in contiguous memory, so removing one leaves a hole that every later element must shift left to fill — that's up to n moves. A linked list stores nodes anywhere in memory, connected by pointers, so removing a node means changing exactly one pointer (curr.next = curr.next.next). No data shifts. The one caveat is that you still spend O(i) walking to the node's predecessor first; the deletion is O(1), the search is not.
How do I delete the head node of a linked list?
The head has no predecessor inside the list, so the general prev.next = prev.next.next rewire doesn't apply. Instead move the head pointer forward: head = head.next. The old first node now has no inbound references and gets garbage collected. Because this changes which node is the head, you must return the new head to the caller — a function that assumes the head never moves will silently keep the deleted node.
What happens if the index is out of range?
The loop condition i < index - 1 && curr.next stops the walk the moment curr.next becomes null, so curr never runs off the end. After the loop, the if (curr.next !== null) guard means that if there's no node at the target position, the function simply returns the list unchanged instead of throwing a null-pointer error. Handling this explicitly is what separates a robust solution from one that crashes on edge inputs.
Can I delete a node if I only have a pointer to it, not the head?
Only if it isn't the tail. Since you can't reach the predecessor, you use the copy-forward trick: overwrite the current node's value with its successor's value, then bypass the successor (node.val = node.next.val; node.next = node.next.next). You've effectively "become" the next node and deleted it instead. This fails on the tail because there's no successor to copy from — which is exactly why some problem variants promise the target is never the last node.
Make it stick: run this one yourself
Don't just read it — step through it interactively, one state change at a time.