Topics › Linked List › Medium Problems of LL

Delete Node in a Linked List O(1)

Medium TUF ID 258 SDE

Problem

You are given a reference to a node node inside a singly linked list. You are not given the head of the list. The node is guaranteed not to be the tail, and all values in the list are distinct. Delete the given node, meaning that after your function returns:

The function returns nothing; the judge rebuilds the list from its own reference to the head and checks the values.

Constraints: the list has 2 to 1000 nodes, values fit in an int, and node is never the last node.

static class ListNode {
    int val;
    ListNode next;
    ListNode(int val) { this.val = val; }
    ListNode(int val, ListNode next) { this.val = val; this.next = next; }
}

Example 1

list = 4 -> 5 -> 1 -> 9, node = the node holding 5
after the call: 4 -> 1 -> 9

Example 2

list = 4 -> 5 -> 1 -> 9, node = the node holding 1
after the call: 4 -> 5 -> 9

Why this is unusual. Ordinary deletion in a singly linked list needs the predecessor: you stand on the node before the target and write prev.next = target.next. Here there is no head, and singly linked nodes have no back pointers, so the predecessor is unreachable. No amount of walking forward from node will ever bring you back to it.

The way out is to notice what is actually being judged. Nobody checks which ListNode objects survive; the list is judged by the sequence of values seen from the head. So instead of removing the node object, you can make the sequence of values look exactly as if it had been removed. Both approaches below do that, and they differ in how much work they do.

Approach 1: Brute force, shift every value left

Intuition. Think of deleting from an array: you shift every later element one slot to the left and then shrink the array by one. You can do the same here. Copy each successor’s value into the current node, walking towards the end, so every value after node moves one position earlier. The last node now holds a duplicate of the value just before it, so cut it off. To cut the last node you need to stand on the second-to-last node, which is why the loop stops there.

Algorithm.

  1. Set cur = node.
  2. While cur.next.next exists (so cur is not yet second-to-last): copy cur.next.val into cur and move cur forward.
  3. Now cur is the second-to-last node: copy the tail’s value into it and set cur.next = null.
static void bruteForce(ListNode node) {
    ListNode cur = node;
    while (cur.next.next != null) {              // stop on the second-to-last node
        cur.val = cur.next.val;
        cur = cur.next;
    }
    cur.val = cur.next.val;                      // final shift
    cur.next = null;                             // the old tail is now a duplicate: drop it
}

The guarantee that node is not the tail is what makes cur.next.next safe to evaluate on the first check.

Complexity. Time O(k), where k is the number of nodes after node; deleting near the head costs a full pass. Space O(1).

This works, but it touches every later node. Looking closely at what the loop does reveals that almost all of that work is unnecessary.

Approach 2: Optimal, copy the next value and bypass it

Intuition. After the very first shift in the brute force, node holds the value of node.next, so that value appears twice in a row. The list would be correct right now if only the second copy, node.next, disappeared. And deleting node.next is an ordinary deletion, because we are standing on its predecessor: node.next = node.next.next. The shifting of all the later values was never needed.

Put another way: you cannot delete yourself, but you can become your successor and delete the successor instead.

Algorithm.

  1. Let victim = node.next (it exists because node is not the tail).
  2. Copy victim.val into node.
  3. Set node.next = victim.next, removing victim from the chain.
  4. Optionally set victim.next = null so the removed node does not keep pointing into the list.
static void optimal(ListNode node) {
    ListNode victim = node.next;
    node.val = victim.val;                       // this node takes over the next node's value
    node.next = victim.next;                     // and the next node disappears from the chain
    victim.next = null;                          // optional: detach the removed node completely
}

Dry run on 4 -> 5 -> 1 -> 9 with node holding 5 (brackets mark node):

step list as seen from the head
start 4 -> [5] -> 1 -> 9
copy next value 4 -> [1] -> 1 -> 9
bypass next node 4 -> [1] -> 9

Complexity. Time O(1): three assignments regardless of list length. Space O(1).

In Java the bypassed node becomes unreachable and the garbage collector reclaims it. In a language with manual memory management you would free victim explicitly after unlinking it.

Edge cases and pitfalls