Problem overview
The method remove receives the head of a singly linked list and a number val. It removes every node whose val field equals val and returns the head of the resulting list.
For 1 -> 2 -> 6 -> 3 -> 4 -> 5 -> 6 and val = 6 the result is 1 -> 2 -> 3 -> 4 -> 5. The statement asks for O(n) time and O(1) extra space, so the nodes must be unlinked in place rather than copied into a new list.
Approach: unlink from the node before
A singly linked node can't remove itself. To delete a node you change the next link of the node in front of it so that the link skips over it: previous.next = previous.next.next. So the loop should stand on the node before the one it examines, look at node.next, and decide.
That raises a problem for the head. The first node has no node in front of it, and if the head itself matches, the function must return a different head. You can handle it with a separate loop that first skips matching nodes at the front, but there is a simpler way: put a dummy node in front of the list. After that, every real node has a predecessor, and the head is treated like any other node. At the end, the new head is dummy.next.
The loop then follows one rule for each position of node:
- if
node.nextmatches, unlink it and stay where you are, because the new next node may match too; - otherwise move
nodeforward.
With 6 -> 6 -> 1 and val = 6, the dummy points at the first 6. It matches, so the dummy is relinked to the second 6. That one matches too, and the dummy is relinked to 1. Now 1 doesn't match, so node moves onto it, and the loop ends. The result is the list 1.
Step by step
- Create the dummy node with
new ListNode()in a variabledummy, declared withletand typedListNode. It sits in front of the list, so removing the first node works like removing any other. - Link it to the list:
dummy.next = head. - Declare
node, typedListNode, and start it atdummy. It always stands on the node before the one being examined. - Loop while
node.next !== null, that is, while there is still a node to examine. - Compare
node.next.valwithval. - On a match, unlink:
node.next = node.next.next. Do not movenode. Moving would let the second of two matching nodes in a row slip through. - Otherwise, advance:
node = node.next. - Return
dummy.next. This is the head of the cleaned list, which may differ from the originalhead, or benull.
The solution in TypeScript
class Solution {
remove(head: ListNode | null, val: number): ListNode | null {
let dummy: ListNode = new ListNode();
dummy.next = head;
let node: ListNode = dummy;
while (node.next !== null) {
if (node.next.val === val) {
node.next = node.next.next;
} else {
node = node.next;
}
}
return dummy.next;
}
}
The TypeScript details worth a closer look:
new ListNode()with no arguments. The node class used in these lessons has optional constructor parameters, so a bare node holds the value 0 and anextofnull. The dummy's value is never examined, because the loop only readsnode.next.val.- Types.
nodeis typedListNode, whilenextisListNode | null. With the compiler's strict null checks on, the testnode.next !== nullwould narrow the type so thatnode.next.valis accepted. The lessons compile withstrictoff, wherenullis allowed in every type, so the annotation documents the intent but the compiler doesn't enforce it. Anullread still fails at run time. ===compares numbers.node.next.val === valis a strict comparison. The annotations say both sides are numbers, but they are not checked at run time, so a value that arrived as a string from outside the program would never match.- Removed nodes are simply skipped. Nothing is freed by hand. Once no reference leads to a node, the garbage collector can reclaim it.
- The input list is modified. The caller's nodes are relinked. The old
headvariable may now point at a node that is no longer in the list, so use the returned value.
Complexity
Time: O(n). Every iteration either removes a node or moves forward, and each node is looked at once. The total number of iterations is the number of nodes.
Space: O(1). The dummy node and the node variable are the only additions, whatever the length of the list.
Edge cases and common mistakes
- Empty list. With
head = null, the dummy'snextisnull, the loop doesn't run, and the method returnsnull. - Every node matches. For
7 -> 7 -> 7andval = 7the dummy ends up pointing atnull, so the result is the empty list. - The head matches. For
6 -> 1 -> 2andval = 6the returned head is the node holding 1. This is the case the dummy exists for. - Consecutive matches. Moving
nodeforward after an unlink, instead of staying put, skips the check on the next node. For1 -> 6 -> 6 -> 2it would leave1 -> 6 -> 2. - No match. The list comes back unchanged, and the loop only advances.
- Returning
head. If the first node was removed,headstill points at a node that is no longer in the list. Returndummy.next. - Checking
node.valinstead ofnode.next.val. Standing on the node you test means you can't unlink it, because you no longer have its predecessor.
Testing
[1, 2, 6, 3, 4, 5, 6]withval = 6gives[1, 2, 3, 4, 5].[]withval = 1gives[].[7, 7, 7]withval = 7gives[].[6, 6, 1, 6]withval = 6gives[1].[1, 2]withval = 5gives[1, 2].