Problem overview
The method reverse receives the head of a singly linked list, a ListNode | null, and returns the head of the reversed list. Each node has a val and a next link to the following node, and the last node's next is null.
For the list 1 -> 2 -> 3 -> 4 -> 5 the result is 5 -> 4 -> 3 -> 2 -> 1. The statement asks for O(n) time and O(1) extra space, so the method can't copy the values into an array and rebuild the list. It has to rewire the existing nodes.
Approach: flip every link
Reversing a list means turning each arrow around. In 1 -> 2 -> 3 the links become 1 <- 2 <- 3, and the old tail, 3, is the new head. The old head, 1, becomes the tail, so its next must become null.
A first attempt walks forward with a variable node and sets node.next.next = node at each step. It fails, because the moment a link is flipped you lose access to the rest of the list. At node 1, flipping the link between 1 and 2 leaves 2 pointing back at 1, and the old link from 2 to 3 is gone. The walk can't continue.
The fix is to keep three references while walking:
prev, the node before the current one, which is where the flipped link will point;node, the node whose link is being flipped;nextNode, the node afternode, saved before the link changes so nothing is lost.
Each pass through the loop flips one link and moves all three references one node forward. When node runs off the end, prev is the last node of the original list, which is the head of the reversed one.
On 1 -> 2 -> 3: set prev to 1, node to 2 and cut the first node's link, so the list is 1 and 2 -> 3. First pass: save 3, point 2 back at 1, move on, so prev is 2 and node is 3. Second pass: save null, point 3 at 2, so prev is 3 and node is null. The loop ends and the method returns 3, the head of 3 -> 2 -> 1.
Step by step
- Declare
prevwithlet, typedListNode | null, and start it athead. It tracks the node before the current one. - Declare
nodethe same way and start it at the second node,head!.next. The!is a non-null assertion:headis typedListNode | null, but the lesson's lists are never empty. - Cut the head's link with
head!.next = null. The head will be the last node of the reversed list. If it kept pointing at the second node, the reversed list would end in a cycle. - Loop while
node !== null. - Save the rest of the list in
nextNode(node.next) before changing any link. - Flip the link:
node.next = prev. - Move
prevforward tonode. - Move
nodeforward tonextNode. The order of these two assignments matters: updatingnodefirst would lose the current node beforeprevhad recorded it. - Return
prev, the new head.
The solution in TypeScript
class Solution {
reverse(head: ListNode | null): ListNode | null {
let prev: ListNode | null = head;
let node: ListNode | null = head!.next;
head!.next = null;
while (node !== null) {
let nextNode: ListNode | null = node.next;
node.next = prev;
prev = node;
node = nextNode;
}
return prev;
}
}
The TypeScript details worth a closer look:
- The non-null assertion
!.head!.nexttells the compiler to treatheadas non-null. It changes nothing at run time. Ifheadis actuallynull, the line throws aTypeError("Cannot read properties of null"). The assertion is a promise by the programmer, not a check. - Union types.
ListNode | nullsays a variable holds a node ornull. With strict null checks on, the testnode !== nullin the loop would narrow the type so thatnode.nextis allowed. The lessons compile withstrictoff, wherenullis accepted everywhere, so the annotation documents the intent rather than enforcing it. - References, not copies. Variables hold references to node objects.
prev = nodemakes both names point at the same node, andnode.next = prevchanges the stored link in that object. The nodes themselves never move or get copied. - Annotations are erased. At run time only the object links exist; the type annotations are removed when TypeScript compiles to JavaScript.
Complexity
Time: O(n). The loop runs once per node after the first, doing a fixed number of assignments each time.
Space: O(1). Three variables, prev, node and nextNode, are all the extra memory, however long the list is. The links are rewired in place, and there is no recursion that would add stack depth.
Edge cases and common mistakes
- One node. For
[1],nodestarts asnull, the loop doesn't run and the method returns the same node, whosenextisnull. - Two nodes. For
[1, 2], one pass flips the single link, and the result is[2, 1]. - An empty list. This solution fails on
null, becausehead!.nextreads a property ofnull. The lesson's tests don't use an empty list. To handle one, addif (head === null) return null;at the top. - Not saving the next node. Changing
node.nextwithout keeping the old value loses the rest of the list. Walking the list afterwards ends in the wrong place. - Forgetting to cut the head. Without
head!.next = null, the old head and the second node point at each other. The returned list contains a cycle, and any code that walks it untilnullnever finishes. - Swapping the last two assignments. If
node = nextNoderan beforeprev = node,prevwould end up pointing at the next node and the links would be wrong. - Returning
head. After the reversal, the original head is the tail. The new head isprev.
Testing
[1, 2, 3, 4, 5]gives[5, 4, 3, 2, 1].[1, 2]gives[2, 1].[1]gives[1].
