Problem overview
The function reorderList receives head, the first node of a singly linked list with at least one node. The lesson provides the ListNode class, whose nodes have a next field. The list L0 -> L1 -> ... -> Ln must become L0 -> Ln -> L1 -> Ln-1 -> L2 -> ...: the first node, then the last, then the second, then the second to last, and so on. The function returns nothing. It only changes links, the list still starts at head, and no nodes are created.
The list 1->2->3->4 becomes 1->4->2->3. The list 1->2->3->4->5 becomes 1->5->2->4->3, where the middle node, 3, ends the list. A list of two nodes, such as 4->8, stays as it is.
Approach: split, reverse, merge
A singly linked list can only be walked forward, so reaching the last node and then the second to last is awkward. Copying the nodes into an array would allow indexing from both ends, but it costs O(n) extra space. The lesson asks for O(1) extra space, so the work is done on the links in three passes:
- Find the middle and cut the list there. A slow pointer moves one node at a time and a fast pointer two. When the fast one is near the end, the slow one is in the middle. For
1->2->3->4->5the halves are1->2->3and4->5. - Reverse the second half.
4->5becomes5->4, so it can be walked from the node that must come first. - Merge the halves alternately. Take one node from the first half, then one from the reversed second half:
1->5->2->4->3.
Each pass visits every node at most once, so the total time is O(n). The pointers are a fixed handful of variables, so the extra space is O(1).
Step by step
Pass 1: find the middle.
- Declare
slowandfastasvars of typeListNode, both starting at the head. The parameter isListNode?, sohead!!asserts it isn'tnull. After that Kotlin knowsheadis notnull, sofastcan take it directly. - Loop while
fasthas two more nodes after it:fast.next != null && fast.next!!.next != null. The!!is needed becausenextis avarproperty, so Kotlin doesn't smart-cast it. The first half of the condition has just checked it. - Move
slowone node andfasttwo nodes in the loop. The condition guarantees both nodes exist.
The loop ends with slow on the last node of the first half: the second of 4 nodes, the third of 5. For lists of length 1 to 5 that is nodes at positions 1, 1, 2, 2 and 3.
Cut. Store slow.next in node, which is where the second half begins, then set slow.next = null. Without the cut, the first half would still lead into the second half, and the merged list would loop back on itself.
Pass 2: reverse the second half.
- Start
prevasnull, with the typeListNode?. It is the head of the part already reversed, which is empty at first. - Loop while
node != null. Savenode.nextinnextNode, because the next line overwrites it. - Point
node.nextatprev, moveprevtonode, and movenodetonextNode.
Pass 3: merge.
- Set
firsttoheadandsecondtoprev, both typedListNode?because they becomenullat the end. - Loop while
second != null. The second half is never longer than the first, so when it runs out every node is placed. A leftover node in the first half is already at the end. - Save what comes next in each half:
nextFirstandnextSecond. The!!infirst!!.nextis safe, sincefirsthas a node wheneverseconddoes. - Re-link:
first.next = second, thensecond.next = nextFirst. - Advance
firsttonextFirstandsecondtonextSecond.
The solution in Kotlin
class Solution {
fun reorderList(head: ListNode?) {
var slow: ListNode = head!!
var fast: ListNode = head
while (fast.next != null && fast.next!!.next != null) {
slow = slow.next!!
fast = fast.next!!.next!!
}
var node = slow.next
slow.next = null
var prev: ListNode? = null
while (node != null) {
val nextNode = node.next
node.next = prev
prev = node
node = nextNode
}
var first: ListNode? = head
var second: ListNode? = prev
while (second != null) {
val nextFirst = first!!.next
val nextSecond = second.next
first.next = second
second.next = nextFirst
first = nextFirst
second = nextSecond
}
}
}
The Kotlin details worth a closer look:
- Nullable types.
ListNode?may holdnull, andListNodemay not.head!!turns the nullable value into a non-null one, and throws aNullPointerExceptionif it wasnull. The lesson guarantees at least one node. - Smart casts. After
head!!, Kotlin knowsheadis notnull, sovar fast: ListNode = headcompiles. Insidewhile (second != null),secondis smart-cast to non-null because it is a local variable. Afterfirst!!.next,firstis smart-cast too, sofirst.next = secondcompiles. A property such asfast.nextcan't be smart-cast, since it might change between the check and the use, which is whyfast.next!!.nextappears. - The type of
node.var node = slow.nextinfersListNode?, which is what the reversal loop needs, becausenextNodeisnullafter the last node. - The order of assignments in the reversal and the merge matters: always save the node you are about to overwrite a link of.
Complexity
Time: O(n). Finding the middle takes about n / 2 iterations, the reversal handles about n / 2 nodes, and the merge handles about n / 2 pairs. Each iteration does a constant amount of work.
Space: O(1). Only a fixed number of pointers are used, and the recursion-free loops allocate nothing. Nodes are re-linked, not copied, so even a list of 50,000 nodes needs no extra memory proportional to its size.
Edge cases and common mistakes
- One node.
7stays7: the fast pointer can't move, the second half is empty, and the merge loop never runs. - Two nodes.
4->8stays as it is, because the last node already follows the first. - Odd and even lengths.
1->2->3becomes1->3->2, and1->2->3->4->5->6becomes1->6->2->5->3->4. - Repeated values.
5->5->5->5comes out as four 5s. The algorithm moves nodes, not values, so equal values cause no trouble. - Forgetting the cut. If
slow.nextstays linked to the second half, the first half's last node still points into it, and the merged list can contain a cycle. - Losing the rest of a list. Changing
node.nextbefore saving it innextNodedrops the remaining nodes. The same goes forfirst.nextandsecond.nextin the merge.
Testing
1->2->3->4becomes1->4->2->3.1->2->3->4->5becomes1->5->2->4->3.4->8stays4->8.7stays7.1->2->3becomes1->3->2.1->2->3->4->5->6becomes1->6->2->5->3->4.
For 1->2->3->4, after the first pass slow is on 2, the halves are 1->2 and 3->4, and after the reversal the second half is 4->3. For 1->2->3->4->5, slow ends on 3 and fast on 5.
