Problem overview
The method reorderList gets 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 link. 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. Only links change. The values stay where they are, no new nodes are created, and the method returns Unit: the list still starts at head.
The list 1->2->3->4 becomes 1->4->2->3, and 1->2->3->4->5 becomes 1->5->2->4->3. With an odd count, the middle node is left over and ends the list. A list of two nodes, such as 4->8, doesn't change.
Approach: three passes
The new order alternates between the front of the list and its back. A singly linked list can't be walked backwards, so reaching the last node from the front costs a full pass each time. Copying the nodes into an array would solve that, but it uses O(n) extra space. The goal here is O(n) time and O(1) space, and three simple passes get there:
- Find the middle and cut the list there. A slow pointer moves one node at a time and a fast pointer moves two. When the fast pointer reaches the end, the slow one is in the middle. Cutting after it splits
1->2->3->4->5into1->2->3and4->5. - Reverse the second half.
4->5becomes5->4. Now the back of the list can be walked from its front. - Merge the two halves, taking one node from each in turn.
1->2->3and5->4give1->5->2->4->3.
Each pass visits each node a constant number of times, so the total stays linear, and the only extra memory is a handful of pointers.
Step by step
Find the middle
- Declare
slowandfastwithvar, both starting athead, since both will move. - Loop while
fasthas two more nodes after it:fast.next != null && fast.next.next != null. The loop stops whilefaststill has nodes in front of it, soslowends on the last node of the first half: the 2nd of 4 nodes, the 3rd of 5. - Move
slowone node forward. - Move
fasttwo nodes forward. The loop condition has already checked that both exist. - Remember the second half: store
slow.nextinnode, avar, because it will move. - Cut the list: set
slow.nexttonull. Without the cut, the first half would still point into the second, and the reordered list would loop.
Reverse the second half
- Declare
prevas avarof typeListNode, set tonull. It is the head of the part already reversed, empty at the start. - Loop while
node != null. Each round turns one node around. - Save
node.nextinnextNode, because the next line overwrites the link. - Point
node.nextback atprev. - Move
prevtonode, which is now the head of the reversed part. - Move
nodetonextNode.
Merge
- Declare
firstatheadandsecondatprev, the head of the reversed half. - Loop while
second != null. The second half is never longer than the first, so when it runs out, every node is in place, and a leftover node of the first half is already at the end. - Save the next node of each half in
nextFirstandnextSecond, since both links are about to change. - Link
secondafterfirst:first.next = second. - Link the rest of the first half after
second:second.next = nextFirst. - Advance both pointers to
nextFirstandnextSecond.
The solution in Scala
class Solution {
def reorderList(head: ListNode): Unit = {
var slow = head
var fast = 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 = head
var second = prev
while (second != null) {
val nextFirst = first.next
val nextSecond = second.next
first.next = second
second.next = nextFirst
first = nextFirst
second = nextSecond
}
}
}
The Scala details worth noticing:
varfor pointers that move,valfor saved values.slow,fast,node,prev,firstandsecondare reassigned, so they arevar.nextNode,nextFirstandnextSecondare set once per round, so they areval.var prev: ListNode = nullneeds the type. Without it, Scala infers the typeNullfor the variable, and assigning a node to it later fails withFound: ListNode, Required: Null.nullis the end of the list. A node'snextisnullat the tail. In the loop condition,&&stops at the firstfalse, sofast.next.nextis read only whenfast.nextis notnull.- Assigning to a field.
slow.next = nullchanges the node itself. The nodes are objects shared by every variable pointing to them, which is why re-linking them changes the list thatheadstarts. - The method returns
Unit. Its last expression is thewhileloop, which has no value, so nothing needs to be returned, andheadkeeps its place as the first node.
Complexity
Time: O(n). Finding the middle visits about half of the nodes. Reversing the second half visits each of its nodes once, and the merge walks it once more. Every pass is linear.
Space: O(1). The method uses a fixed number of pointers and creates no nodes. It re-links the ones that exist.
Edge cases and common mistakes
- One node.
7stays7. The loop condition is false at once,slow.nextisnull, the reversal does nothing, and the merge loop never runs. - Two nodes.
4->8stays4->8: the second half is just8, and merging puts it right back after4. - Odd and even lengths. For
1->2->3->4, the slow pointer stops on 2, and for1->2->3->4->5it stops on 3. In the odd case, the first half is one node longer, and that node ends the result.1->2->3becomes1->3->2. - Forgetting to cut. Without
slow.next = null, the first half still reaches into the second, and the merged list contains a cycle. - Losing the rest while reversing. Changing
node.nextbefore saving it innextNodethrows away the unreversed part of the list. - Losing the rest while merging. The same goes for
first.nextandsecond.next: both are saved before any link changes.
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.
