Problem overview
The function reverse receives the head of a singly linked list as a ListNode? and returns the head of the reversed list. 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 function must not copy the values into another structure. It has to rewire the existing nodes.
Approach: flip every link, but save the next node first
With two pointers you can reverse an array by swapping from both ends, but that needs a right pointer that moves left, and a singly linked list only has links that go forward. The idea here is different: reverse each link. Where node 1 points to node 2, make node 2 point to node 1. The old tail then becomes the head.
The naive version walks the list and sets node.next.next = node. It fails because the first rewrite destroys the way forward: once node 2 points back at node 1, nothing leads from node 2 to node 3. The fix is to remember where you are going before you change anything.
The algorithm uses three references:
prev, the node before the current one, which becomes the newnextof the current node;node, the node whose link is being flipped;nextNode, the node afternode, saved before the flip.
Here is the run on 1 -> 2 -> 3. At the start, prev is node 1, node is node 2, and node 1's link has been cut. In the first pass, nextNode is node 3, node 2 now points to node 1, then prev becomes node 2 and node becomes node 3. In the second pass, nextNode is null, node 3 points to node 2, prev becomes node 3 and node becomes null. The loop stops, and prev is the head of 3 -> 2 -> 1.
Step by step
- Declare
prevas avarset tohead. It follows the current node and starts on the first node. - Declare
nodeas avarset tohead!!.next, the second node. The first node is handled before the loop. The!!asserts thathead, whose type isListNode?, is not null; the lesson never passes an empty list. - Cut the first node's link with
head.next = null. This node ends up as the last node of the reversed list. If it kept pointing at the second node, the result would end in a cycle. No!!is needed here, because after the one on the previous line Kotlin knowsheadis not null. - Loop while
node != null. Inside the loop, Kotlin treatsnodeas a non-nullListNode. - Save the rest of the list in
nextNode, aval, set tonode.next. Without this, rewritingnode.nextwould lose every node afternode. - Flip the link: set
node.nexttoprev. - Advance
prev: set it tonode. This must come before movingnode, or the current node would be lost. - Advance
node: set it to the savednextNode. - Return
prev. When the loop ends,nodeisnullandprevholds the last node processed, the new head.
The solution in Kotlin
class Solution {
fun reverse(head: ListNode?): ListNode? {
var prev = head
var node = head!!.next
head.next = null
while (node != null) {
val nextNode = node.next
node.next = prev
prev = node
node = nextNode
}
return prev
}
}
The Kotlin details worth a closer look:
- Nullable types. A parameter of type
ListNode?may be null, and Kotlin refuses to readhead.nextuntil it knows otherwise.head!!is that promise; it throws aNullPointerExceptionat run time if the promise is false. - Smart casts. After
head!!, the compiler remembers thatheadis not null, because it is a function parameter and cannot be reassigned. In the loop, the testnode != nulldoes the same for the local variablenode. A property such asnode.nextis mutable, so the compiler does not smart-cast it; only local variables and parameters get that treatment. - Inferred types.
var prev = headhas typeListNode?, so it accepts both nodes andnull.var node = head!!.nextis alsoListNode?, becausenextis nullable. - The list is changed in place. The nodes are objects, and the function rewrites their links. After the call, the original
headvariable still refers to the first node, which is now the last: itsnextisnull.
Complexity
Time: O(n). The loop runs once per node after the first, and each pass does a constant amount of work.
Space: O(1). Only the references prev, node and nextNode are created, no matter how long the list is. A recursive solution would use O(n) stack instead.
Edge cases and common mistakes
- A single node. The list
[7]givesnode = nullstraight away, the loop is skipped, andprev, the same node, is returned. - Two nodes.
[1, 2]becomes[2, 1]after one pass of the loop. - An empty list. The reference solution calls
head!!, so anullhead throws aNullPointerException. The lesson's lists are never empty. To support an empty list, start withvar prev: ListNode? = nullandvar node = headand drop the special handling of the first node. - Forgetting
head.next = null. The old first node would still point at the second node, and the reversed list would end in a cycle between those two nodes. - Not saving
nextNode. Flipping the link first loses the rest of the list. - Updating in the wrong order. If
nodemoves beforeprevdoes,prevnever catches up with the node just processed. - Returning
head. The old head is now the tail. Returnprev.
Testing
[1, 2, 3, 4, 5]gives[5, 4, 3, 2, 1].[1, 2]gives[2, 1].[7]gives[7].
