Problem overview
The function reorder_list receives head, a mutable borrow of a singly linked list: &mut Option<Box<ListNode>>. For a list L0 -> L1 -> ... -> Ln, it must rearrange the nodes 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 change. The values stay where they are, no new nodes are created, and nothing is returned, since the list still starts at head.
The lesson provides ListNode, with an i32 field val, a field next: Option<Box<ListNode>> and a constructor ListNode::new(val). The list has at least one node and at most 50,000.
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.
Approach: split, reverse the back half, interleave
A singly linked list can only be walked forward, so there is no direct way to reach the last node, then the one before it. Copying the nodes into a vector would allow it, but costs O(n) extra space. The in-place solution has three passes:
- Split the list after its middle:
1 -> 2 -> 3 -> 4 -> 5becomes1 -> 2 -> 3and4 -> 5. The first half is the same size as the second or one node longer. - Reverse the second half.
4 -> 5becomes5 -> 4. Now walking the second half from its front visits the original list from its end. - Interleave. Take one node from the first half, then one from the second, and so on:
1 -> 5 -> 2 -> 4 -> 3.
Each pass visits every node once, so the whole thing is O(n) time, and it only moves links, so the extra space is O(1).
The usual way to find the middle is two pointers, a slow one that moves one node at a time and a fast one that moves two. In Rust, a second pointer can't stay in the list while the list is being changed, because the borrow checker allows one mutable path into it at a time. The solution counts instead. One read-only pointer, fast, jumps two nodes at a time while a counter, half, goes up by one for each jump. That counter ends up where a slow pointer would be. For lists of 1 to 6 nodes, half ends as 1, 1, 2, 2, 3, 3: the size of the first half. A second walk then goes half - 1 steps from the head, with a mutable borrow, to reach the last node of the first half.
On [1, 2, 3, 4, 5], the stages are:
first [1, 2, 3] second [4, 5]
reversed [5, 4]
round 1 leftover second [4] first_has_node=true
round 2 leftover second [] first_has_node=true
[1, 5, 2, 4, 3]
Round 1 moves 5 behind 1, round 2 moves 4 behind 2, and 3 is already at the end.
Step by step
- Count the first half. Create
halfas 0 andfastashead.as_ref(), a read-only borrow of the first node.as_ref()looks inside theOptionwithout taking the node out ofhead. - Loop while
fasthas a node.while let Some(node) = fastruns as long asfaststands on a node. - Jump two nodes.
node.next.as_ref()is the next node, andand_thenruns a closure on it only when it exists, givingNoneotherwise. The closure takes that node's ownnextin the same way. Ifnodeis the last one or the one before it,fastbecomesNoneand the loop ends. - Add 1 to
half. The loop runs once for every two nodes, rounded up. - Walk to the end of the first half.
slowis a mutable borrow of the first node, fromhead.as_mut().unwrap().fastis no longer used, soheadcan be borrowed mutably. - Loop
half - 1times.for _ in 1..halfdoesn't need the loop variable.slowstarts on the first node, so it takeshalf - 1steps to reach node numberhalf. - Step forward.
slow = slow.next.as_mut().unwrap()moves to the next node. - Cut the list.
slow.next.take()moves the second half intocurrentand leavesNonebehind, so the first half now ends atslow. - Prepare the reversal.
previs anOption<Box<ListNode>>that starts asNone: the part already reversed. - Loop over the second half.
while let Some(mut node) = currenttakes the next node out ofcurrenton each round. - Keep the rest.
current = node.next.take()leavesnodealone and holds the nodes after it incurrent. - Turn the node around.
node.next = prevpoints it at the nodes reversed so far. - Extend the reversed part.
prev = Some(node)makes this node its new front. - Prepare the merge.
firstishead.as_mut(), a mutable borrow of the first node, andsecondisprev, which owns the reversed half. - Loop over the second half.
while let Some(mut node) = secondtakes its front node. The second half is never longer than the first, so when it runs out, every node is in place. - Take the front node of the first half.
first.unwrap()givesfirst_node. There is always one here wheneversecondhas a node. - Keep the rest of the second half in
secondwithnode.next.take(). - Hand over the link.
node.next = first_node.next.take()makesnodepoint at the rest of the first half. - Link it in.
first_node.next = Some(node)puts the back node right after the front one. - Advance
first. Two links afterfirst_node: its next node is the one just inserted, and the one after that is the next node of the first half, borrowed withas_mut(). It stays anOption, which isNoneonce the first half is used up.
The solution in Rust
struct ListNode {
val: i32,
next: Option<Box<ListNode>>,
}
impl ListNode {
fn new(val: i32) -> Self {
ListNode { val, next: None }
}
}
impl Solution {
fn reorder_list(head: &mut Option<Box<ListNode>>) {
let mut half = 0;
let mut fast = head.as_ref();
while let Some(node) = fast {
fast = node.next.as_ref().and_then(|next| next.next.as_ref());
half += 1;
}
let mut slow = head.as_mut().unwrap();
for _ in 1..half {
slow = slow.next.as_mut().unwrap();
}
let mut current = slow.next.take();
let mut prev: Option<Box<ListNode>> = None;
while let Some(mut node) = current {
current = node.next.take();
node.next = prev;
prev = Some(node);
}
let mut first = head.as_mut();
let mut second = prev;
while let Some(mut node) = second {
let first_node = first.unwrap();
second = node.next.take();
node.next = first_node.next.take();
first_node.next = Some(node);
first = first_node.next.as_mut().unwrap().next.as_mut();
}
}
}
Rust details worth knowing:
- Shared then mutable borrows.
fastborrowsheadread-only, and its last use is in the first loop. Rust lets the laterhead.as_mut()go ahead because the earlier borrow is no longer used. take()moves a value out of anOption. It returns the old contents and leavesNonein place. That is how the code detaches nodes from a list it only borrows: you can't move a field out of a borrowed struct, but you can swap it withNone.- The reversal moves ownership. In
while let Some(mut node) = current, each node is moved out ofcurrent. The body givescurrenta new value before the next test, so the loop can continue, and each node ends up owned byprev. - Reassigning a borrow.
slow = slow.next.as_mut().unwrap()replaces a mutable borrow by a borrow of a node further down the same list, which is how the walk moves forward. first.unwrap()moves the borrow. A&mutinsidefirstis moved out byunwrap, sofirstmust be given a new value before the next round. The last line of the loop does that.- No return value. The function changes the list through
head, so there is no final expression.
Complexity
Time: O(n). Counting walks about n / 2 steps, reaching the middle walks about n / 2 more, reversing visits each node of the second half once, and the merge visits each node of the second half once. The total is a few passes over at most n nodes.
Space: O(1). Only a few variables are used. Nodes are re-linked, never copied. The loops are iterative, so there is no recursion depth to worry about.
Edge cases and common mistakes
- One node.
[7]stays[7]. Herehalfis 1, the walk loop1..1doesn't run,slow.next.take()givesNone, and the merge loop never starts. - Two nodes.
[4, 8]stays[4, 8]: the second half is just 8, which is moved to the place it already had. - Odd and even lengths.
[1, 2, 3]becomes[1, 3, 2]and[1, 2, 3, 4, 5, 6]becomes[1, 6, 2, 5, 3, 4]. The first half is the longer one when the count is odd, which is why the middle node ends the list. - Not cutting the list. The
take()after the walk is what separates the halves. Without a cut, the first half would still be linked to the second, and the merge would have no end for the first half. - A wrong split point. If
halfwere one too small on a list with an odd count, the second half would be longer than the first, andfirst.unwrap()would run out of nodes to unwrap. - Allocating new nodes. The task is to re-link existing nodes. Creating nodes works for small lists, but uses
O(n)space. - Losing the rest of a list. Each
take()before an assignment keeps the rest of a list from being dropped. Reassigningnode.nextfirst would throw the nodes after it away.
Testing
[1, 2, 3, 4]becomes[1, 4, 2, 3].[1, 2, 3, 4, 5]becomes[1, 5, 2, 4, 3].[4, 8]stays[4, 8].[7]stays[7].[1, 2, 3]becomes[1, 3, 2].[1, 2, 3, 4, 5, 6]becomes[1, 6, 2, 5, 3, 4].
