Problem overview
The method reorderList(head) receives the first node of a singly linked list with at least one node. The list is L0 -> L1 -> ... -> Ln, and it must be rearranged, in place, into L0 -> Ln -> L1 -> Ln-1 -> L2 -> ...: the first node, then the last, then the second, then the second to last, and so on. Only the links may change. The values stay where they are, no new nodes are created, and the method returns nothing, because the list still starts at head. The lesson provides the ListNode type, whose nodes have a val and a next that is a node or null.
For 1->2->3->4 the result is 1->4->2->3. For 1->2->3->4->5 it is 1->5->2->4->3, where the middle node 3 is left over and ends the list. With two nodes, 4->8, nothing moves.
Approach: three passes over the list
A singly linked list can only be walked forwards, so reaching the last node, then the second to last, is not directly possible. Copying the nodes into an array would solve that, but it costs O(n) extra space. The in-place idea splits the problem into three smaller ones:
- 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 nodes from the back of the list can be walked from the front. - Merge the halves, alternating. Take a node from the first half, then one from the reversed second half:
1->5->2->4->3.
Each pass is linear and uses only a few pointers.
Step by step
Pass 1: the middle
- Two pointers. Create
slowandfastwithlet, both set tohead. - Loop while
fast.next !== null && fast.next.next !== null, that is, while fast has two more nodes ahead. - Move
slowone node andfasttwo nodes. When the loop stops,slowis on the last node of the first half. For four nodes it ends on the second node, and for five on the third. - Remember the second half:
let node = slow.next. - Cut: set
slow.next = null. Without the cut, the first half would still point into the second, and the finished list would loop back on itself.
Pass 2: reverse the second half
- Start with an empty reversed part:
let prev: ListNode | null = null. The type annotation says thatprevwill hold nodes later. - Loop while
node !== null. Each round turns one node around. - Save the next node in
nextNodebefore changingnode.next, or the rest of the half would be lost. - Point
node.nextback atprev. This is the reversal. - Advance:
prev = node, thennode = nextNode.
Pass 3: merge
- Two walkers:
firststarts athead, andsecondatprev, 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 placed. A leftover node in the first half is already at the end. - Save where each half continues:
nextFirstandnextSecond. - Link
first.next = second, thensecond.next = nextFirst: the back node now sits between two front nodes. - Move on:
first = nextFirstandsecond = nextSecond.
The solution in TypeScript
class Solution {
reorderList(head: ListNode | null): void {
let slow = head;
let fast = head;
while (fast.next !== null && fast.next.next !== null) {
slow = slow.next;
fast = fast.next.next;
}
let node = slow.next;
slow.next = null;
let prev: ListNode | null = null;
while (node !== null) {
const nextNode = node.next;
node.next = prev;
prev = node;
node = nextNode;
}
let first = head;
let second = prev;
while (second !== null) {
const nextFirst = first.next;
const nextSecond = second.next;
first.next = second;
second.next = nextFirst;
first = nextFirst;
second = nextSecond;
}
}
}
Details worth knowing:
- The return type
void. The method changes the nodes it is given, and the caller keeps usinghead, which is still the first node. There is nothing to return. ListNode | nulland the compiler. The parameter is typed as possiblynull, yet the code writesfast.nextwithout a check. The lesson's compiler settings are not strict aboutnull, so this is accepted. The lesson says the list has at least one node, which is what makes it safe. With--strict, the compiler reports "possibly null" errors on those lines.- Types are erased. Annotations such as
ListNode | nulldisappear when the code is compiled to JavaScript. They do not check anything while the program runs. - Short-circuit
&&. In the loop,fast = fast.next.nextis safe only because the loop condition has just checked both nodes. The order matters:&&stops at the first false operand, sofast.next.nextis never read whenfast.nextisnull. - Order of the two assignments in the merge.
first.next = secondoverwrites the link to the rest of the first half, which is whynextFirstwas saved first.
Complexity
Time: O(n). Finding the middle takes about n / 2 steps, since fast moves two nodes at a time. The reversal visits the second half once, and the merge visits each node once.
Space: O(1). The code uses a handful of pointer variables and creates no nodes, whatever the length of the list.
Edge cases and common mistakes
- One node.
fast.nextisnull, so the first loop doesn't run. Thenslow.nextisnull, sonodeisnull, the reversal does nothing,secondisnull, and the list is unchanged. - Two nodes.
4->8stays4->8: the middle is the first node, the second half is just8, and merging puts it right after4. - Odd and even lengths.
1->2->3becomes1->3->2, and1->2->3->4->5->6becomes1->6->2->5->3->4. With an odd length the first half is one node longer, and that extra node ends the list. - Forgetting the cut. If
slow.nextis not set tonull, the first half keeps pointing into the second, and the result contains a cycle. - Losing the rest of a list. Every time a
nextis overwritten (in the reversal and in the merge), the old value must be saved first.
Testing
[1,2,3,4]becomes1->4->2->3.[1,2,3,4,5]becomes1->5->2->4->3.[4,8]stays4->8.[7]stays7.[1,2,3]becomes1->3->2.[1,2,3,4,5,6]becomes1->6->2->5->3->4.
For [1,2,3,4,5], after the first pass slow is on 3 and the halves are 1->2->3 and 4->5. After the reversal the second half is 5->4.
