Problem overview
The method ReorderList receives head, the first node of a singly linked list with at least one node. The lesson provides the ListNode class: the code uses its val and next fields. The list L0 -> L1 -> ... -> Ln must become L0 -> Ln -> L1 -> Ln-1 -> L2 -> ...: the first node, the last, the second, the second to last, and so on.
Only the links change. The values stay as they are and no new nodes are created. The method returns nothing: the list still starts at head, and the caller prints it from there.
[1, 2, 3, 4]becomes[1, 4, 2, 3].[1, 2, 3, 4, 5]becomes[1, 5, 2, 4, 3]. The middle node, 3, is left over and ends the list.[4, 8]stays[4, 8], because the last node already follows the first.
Approach: three passes
The difficulty is reaching the last node. In a singly linked list there is no way to step backwards, so "take a node from the end" isn't directly possible. Copying the nodes into an array would solve that but costs O(n) extra space. The in-place solution uses three simple passes instead:
- Find the middle and cut the list there.
1->2->3->4->5becomes1->2->3and4->5. - Reverse the second half.
4->5becomes5->4. Now the back of the list can be walked from its front. - Merge the halves, alternating. Take one node from the first half, then one from the reversed second half, and repeat:
1->5->2->4->3.
The middle is found with two pointers. slow moves one node per step and fast moves two, so when fast runs out of list, slow is halfway. The loop stops while fast still has nodes after it, which leaves slow on the last node of the first half: the 2nd node of 4, and the 3rd node of 5. With an odd count, the middle node therefore stays in the first half, and it is exactly the node that ends the reordered list.
On [1, 2, 3, 4, 5], the code stops with slow on 3 and fast on 5. After the cut, the first half is 1->2->3 and the second is 4->5. After the reversal, the second half is 5->4. The merge then runs twice: after the first round the list reads 1->5->2->3, and after the second it reads 1->5->2->4->3.
Step by step
Find the middle and cut
- Declare
slowandfast, twoListNodevariables, both set tohead. - Loop while
fast.next != null && fast.next.next != null. That meansfasthas two more nodes ahead, so it can safely jump two. The first test also protects the second one from a nullfast.next. - Move
slowone node, toslow.next. - Move
fasttwo nodes, tofast.next.next. - Remember the second half:
ListNode node = slow.next. - Cut with
slow.next = null. Without the cut, the first half would still flow into the second, and after the merge the list would loop back on itself.
Reverse the second half
- Declare
prevasnull. It is the head of the part already reversed, which is empty at first. - Loop while
node != null. Each round turns one node around. - Save
node.nextinnextNode. The next line overwrites that link, and without the copy the rest of the half would be lost. - Point
node.nextatprev, which is the reversal itself. - Move
prevtonode, the new head of the reversed part. - Move
nodetonextNode.
Merge the halves
- Declare
firstasheadandsecondasprev, the heads of the two halves. - Loop while
second != null. The second half is never longer than the first. When it runs out, every node is placed, and a leftover node of the first half is already at the end. - Save the next nodes of both halves in
nextFirstandnextSecond, because the next two lines change both links. - Link
first.next = second, so a node from the back follows a node from the front. - Link
second.next = nextFirst, so the rest of the first half follows. - Advance
firsttonextFirstandsecondtonextSecond.
The solution in C#
class Solution {
public void ReorderList(ListNode head) {
ListNode slow = head;
ListNode fast = head;
while (fast.next != null && fast.next.next != null) {
slow = slow.next;
fast = fast.next.next;
}
ListNode node = slow.next;
slow.next = null;
ListNode prev = null;
while (node != null) {
ListNode nextNode = node.next;
node.next = prev;
prev = node;
node = nextNode;
}
ListNode first = head;
ListNode second = prev;
while (second != null) {
ListNode nextFirst = first.next;
ListNode nextSecond = second.next;
first.next = second;
second.next = nextFirst;
first = nextFirst;
second = nextSecond;
}
}
}
The C# details worth knowing:
- Nodes are references.
ListNodeis a class, so a variable such asslowholds a reference to a node, not a copy. Assigningslow = slow.nextmoves the reference, while assigningslow.next = nullchanges the node itself, and everything holding that node sees the change. voidis enough. The method changes the nodes in place, and the caller'sheadstill points to the first node, which doesn't move. Reassigning the parameterheadinside the method would not affect the caller's variable.- Short-circuit
&&. Infast.next != null && fast.next.next != null, the right side is evaluated only when the left side is true. Swapping the two tests would throw aNullReferenceExceptionon lists with an odd number of nodes, wherefastends on the last node. nullfor "no node". The reversal starts withprev = null, so the old first node of the second half ends the reversed list.
Complexity
Time: O(n). Finding the middle takes about n/2 steps. The reversal and the merge each visit every node of the second half once. Each pass is linear, and there are three of them.
Space: O(1). The method only re-links existing nodes and uses a fixed number of reference variables. The loops are iterative, so there is no recursion depth to worry about on lists of up to 50,000 nodes.
Edge cases and common mistakes
- One node. The loop doesn't run,
slow.nextis alreadynull, the second half is empty, and the merge loop doesn't run. The list is unchanged. - Two nodes. The second half is the last node, reversing one node changes nothing, and the merge makes
first.nextpoint to the node it already pointed to.[4, 8]stays[4, 8]. - Odd and even lengths. With 3 nodes the result is
1->3->2, and with 6 nodes it is1->6->2->5->3->4. - Forgetting the cut. The first half keeps pointing into the second, and the final list forms a cycle.
- Losing the rest of a half. In the reversal and in the merge, the next node must be saved before a link is overwritten.
- Cutting too early. If the first half were shorter than the second, the merge would run out of first-half nodes while second-half nodes remain, and
first.nextwould throw aNullReferenceException. - Creating new nodes or changing values. The task forbids both. Only
nextlinks change.
Testing
[1, 2, 3, 4]gives1->4->2->3.[1, 2, 3, 4, 5]gives1->5->2->4->3.[4, 8]gives4->8.[7]gives7.[1, 2, 3]gives1->3->2.[1, 2, 3, 4, 5, 6]gives1->6->2->5->3->4.
