YeetCode
Data Structures & Algorithms · Linked List

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.

6 min readBy Bhavesh Singh
linked listdeletionpointer rewiringsingly linked listleetcode easy

This article has an interactive companion

Don't just read it — step through it interactively, one state change at a time.

Open the Deleting Nodes in Linked List visualizer

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.

text
Input: head = 10 -> 20 -> 30 -> 40, index = 1 Output: 10 -> 30 -> 40 // the node holding 20 is gone

Two edge cases shape the whole solution:

  • Deleting the head (index === 0) has no predecessor inside the list, so you move head itself.
  • 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.

javascript
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.

text
before: [10] -> [20] -> [30] ^target after: [10] --------> [30] // 20 has zero inbound pointers now

Once 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:

javascript
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.

Linecurrcurr.nextLoop condition i < index-1Action
curr = headNode(10)Node(20)start pointer at head, index 0
for i=0Node(10)Node(20)0 < 1 trueenter loop body
curr = curr.nextNode(20)Node(30)advance to index 1
for i=1Node(20)Node(30)1 < 1 falseexit loop
if curr.next !== nullNode(20)Node(30)target exists, proceed
curr.next = curr.next.nextNode(20)Node(40)20 now points to 40; 30 bypassed
return headreturn 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

OperationTimeSpaceWhy
Delete at index iO(i)O(1)walk i-1 steps to the predecessor, then one O(1) rewire
Delete at headO(1)O(1)no walk — head = head.next
Brute-force rebuildO(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 curr on the target instead of its predecessor. In a singly linked list you can't reach backward, so if curr is already on the node to delete, you have no way to rewire the arrow pointing into it. Walk to index - 1, not index.
  • Forgetting the head case. When index === 0 there 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 with head = head.next.
  • Dereferencing curr.next.next without a null check. If the target is the tail, curr.next.next is null — which is fine to assign — but if the index is out of range, curr.next may already be null. The if (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, and get together; 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.

Open the Deleting Nodes in Linked List visualizer