You're given the head of a singly linked list L0 -> L1 -> ... -> Ln-1. Reorder the nodes in place so they read L0 -> Ln-1 -> L1 -> Ln-2 -> L2 -> ... — first node, then last, then second, then second-to-last, and so on, zipping inward from both ends until they meet. Think of it as repeatedly drawing from the front of a queue and then the back, alternating, the way you'd interleave a deck by taking cards off the top and the bottom. You must rewire the existing nodes; you can't allocate a fresh list.
// A node in a singly linked list.
type ListNode = { val: number; next: ListNode | null };
// Reorders the list in place and returns the (unchanged) head.
function linkedListRearrange(head: ListNode | null): ListNode | null;
The tests build a list from an array of values and read it back into an array to compare. A node is a plain object { val, next }; the last node's next is null.
// Even length: 1 2 3 4 -> 1 4 2 3
// front=1, back=4, front=2, back=3
linkedListRearrange(build([1, 2, 3, 4]));
// list is now: 1 -> 4 -> 2 -> 3
// Odd length: 1 2 3 4 5 -> 1 5 2 4 3
// front=1, back=5, front=2, back=4, middle=3 stays last
linkedListRearrange(build([1, 2, 3, 4, 5]));
// list is now: 1 -> 5 -> 2 -> 4 -> 3
next pointers of the existing nodes. Don't build a new list and copy values across — the target solution uses O(1) extra space.first, last, second, second-to-last, third, .... The two ends meet in the middle.[1,2,3,4,5] -> [1,5,2,4,3] example.null) stays null; a single node stays itself; two nodes a -> b stay a -> b. There's nothing to interleave until there are at least three nodes.You'll reorder a singly linked list so it zips inward from both ends — first node, last node, second node, second-to-last, and so on — rewiring the existing nodes without allocating a new list.
You have a row of nodes 1 -> 2 -> 3 -> 4 -> 5 -> 6 and you want 1 -> 6 -> 2 -> 5 -> 3 -> 4. Read that target out loud: front, back, front, back. You keep pulling from the two ends of the list and laying them down alternately until the ends meet in the middle. It's the same move as dealing a deck by drawing one card off the top and one off the bottom, over and over.
The catch with a singly linked list is that each node only knows its next — there are no backward pointers. So "take from the back" isn't a cheap operation: you can't step backward from the tail. The whole solution is about getting around that limitation in O(1) extra space.
The target order is two sequences interleaved: the front half walking forward, and the back half walking backward. If you had both of those as ordinary forward-walking lists, the reorder would just be "take one from each, alternating." So the plan is to manufacture exactly that: split the list in half, reverse the second half so it walks backward-as-forward, then merge the two halves one node at a time.
That gives the three-step plan we'll build toward: (1) find the middle, (2) reverse the second half, (3) merge the two halves alternately. Each step is O(n) time and O(1) space, so the whole thing is O(n) / O(1).
Before the three-step dance, here's the version almost everyone writes first. The problem was "take from the front, then the back." A linked list makes the back hard to reach — so dump every node into an array, where indexing from both ends is cheap, and rewire from there.
function linkedListRearrangeArray(head) {
if (head === null) return head;
// Collect every node into an array — now we can index from both ends.
const nodes = [];
for (let node = head; node !== null; node = node.next) {
nodes.push(node);
}
let left = 0;
let right = nodes.length - 1;
while (left < right) {
nodes[left].next = nodes[right]; // front node points to back node
left++;
if (left === right) break; // they met — don't create a self-loop
nodes[right].next = nodes[left]; // back node points to the next front node
right--;
}
nodes[left].next = null; // the meeting node is the new tail
return head;
}
This is correct — it produces exactly the right order, and the two-index walk is easy to reason about. The problem is the nodes array: it holds a reference to every node, so it costs O(n) extra space. For a linked-list question, that defeats the point. The reason linked lists exist is to rearrange data by moving pointers, not by copying everything into a contiguous buffer first. The interviewer who asks this wants the O(1)-space rewiring — the array version is the answer they're hoping you'll improve on.
It's also fiddly: the if (left === right) break guard and the final nodes[left].next = null are easy to get wrong, and a single off-by-one there creates a cycle. We can do better on both space and clarity.
The three steps each map to a well-known linked-list technique. Find the middle with slow/fast pointers; reverse the second half with the in-place reversal loop; merge with a two-pointer splice.
function linkedListRearrange(head) {
// Fewer than 3 nodes: nothing to interleave. [], [a], [a,b] are unchanged.
if (head === null || head.next === null || head.next.next === null) {
return head;
}
// STEP 1 — find the middle with slow/fast pointers.
// fast moves twice as fast, so when it can't step again, slow is the last
// node of the first half. For odd lengths this leaves the first half one
// longer than the second, which is exactly what the interleave wants.
let slow = head;
let fast = head;
while (fast.next !== null && fast.next.next !== null) {
slow = slow.next;
fast = fast.next.next;
}
// STEP 2 — reverse the second half, starting just past slow.
// Cut the first half loose by nulling slow.next, then reverse what follows.
let second = slow.next;
slow.next = null; // terminate the first half so the merge has a clean end
let prev = null;
while (second !== null) {
const nextNode = second.next; // remember where we were going
second.next = prev; // flip the pointer to face backward
prev = second; // advance the reversed-list head
second = nextNode; // advance through the original second half
}
// prev is now the head of the reversed second half.
// STEP 3 — merge the two halves, alternating one node from each.
// The first half is always >= the second half in length, so we drive the
// loop off `second` and stop when it runs out.
let first = head;
second = prev;
while (second !== null) {
const firstNext = first.next; // save both nexts before we overwrite them
const secondNext = second.next;
first.next = second; // splice the back node in after the front node
second.next = firstNext; // then reconnect to the rest of the first half
first = firstNext; // advance both pointers into their own halves
second = secondNext;
}
return head;
}
module.exports = { linkedListRearrange, build, toArray };
// --- helpers used by the tests ---
function build(values) {
let head = null;
for (let i = values.length - 1; i >= 0; i--) {
head = { val: values[i], next: head };
}
return head;
}
function toArray(head) {
const out = [];
for (let node = head; node !== null; node = node.next) {
out.push(node.val);
}
return out;
}
The shift from the naive version is that we never store all the nodes at once. Step 1 uses two scalar pointers to locate the split. Step 2 reverses the back half in place, reusing the same nodes with flipped pointers. Step 3 walks both halves with two pointers, splicing them together. At no point do we hold more than a handful of pointer variables — that's the O(1) space we were after.
Trace 1 -> 2 -> 3 -> 4 -> 5 (the odd-length case, where the middle node has to land last).
Step 1 — find the middle. Both pointers start at node 1.
start: slow=1, fast=1
fast.next=2, .next.next=3 ok → slow=2, fast=3
fast.next=4, .next.next=5 ok → slow=3, fast=5
fast.next=null → loop stops. slow=3
slow lands on node 3 — the last node of the first half 1 -> 2 -> 3.
Step 2 — split and reverse. second = slow.next is node 4. We set slow.next = null, cutting the list into 1 -> 2 -> 3 -> null and 4 -> 5 -> null. Now reverse the second piece:
second=4, prev=null → 4.next=null, prev=4, second=5
second=5, prev=4 → 5.next=4, prev=5, second=null
loop stops. reversed head prev = 5 -> 4 -> null
Step 3 — merge alternately. first = 1 (head), second = 5 (reversed head).
iter 1: firstNext=2, secondNext=4
1.next=5, 5.next=2 → list so far: 1 -> 5 -> 2 -> 3
first=2, second=4
iter 2: firstNext=3, secondNext=null
2.next=4, 4.next=3 → list so far: 1 -> 5 -> 2 -> 4 -> 3
first=3, second=null
loop stops (second === null).
The middle node 3 was already the tail of the first half (its next is null from the split), so it stays last untouched. Final list: 1 -> 5 -> 2 -> 4 -> 3 — exactly the expected output.
while (fast.next && fast.next.next) makes slow the last node of the first half, which leaves the first half one longer than (odd) or equal to (even) the second half. If you instead use while (fast && fast.next), slow advances one node too far and the second half becomes the longer one — then the merge, which is driven by second, walks off the end of first. Stick with the fast.next && fast.next.next form for this problem.slow.next = null, the first half still points into the original second half. After the merge you get a tangled list — often a cycle, because a node ends up reachable from two directions. The slow.next = null line is what makes the two halves truly independent.first.next and second.next into temporaries before overwriting them. If you write first.next = second first and then read first.next expecting the old value, you've already lost it — you'll splice a node back onto itself and create a self-loop. Save both nexts, then rewire, then advance.while (second !== null), not while (first !== null) — first may still have one node left (the middle, on odd lengths) when second is exhausted, and that node already terminates correctly. Looping on first would dereference second.next when second is null and throw.slow.next, not nulling the original second-half head during reversal (the very first second.next = prev with prev = null handles this), or losing a next in the merge. The test that walks the list counting steps and asserts it reaches null exists specifically to catch this — a cycle never terminates.fast.next.next when fast.next is null). The early head === null || head.next === null || head.next.next === null guard returns immediately for these, leaving them unchanged.k nodes (1 2 3 4 5, k=2 → 2 1 4 3 5). It reuses the same in-place reversal loop from step 2, applied repeatedly to fixed-size windows, with careful pointer surgery to stitch the reversed groups back together. Good practice for the reversal mechanics in isolation.Keep practising the same patterns with a nearby challenge.
No submissions yet
Share your approach and start the discussion.
You're given the head of a singly linked list L0 -> L1 -> ... -> Ln-1. Reorder the nodes in place so they read L0 -> Ln-1 -> L1 -> Ln-2 -> L2 -> ... — first node, then last, then second, then second-to-last, and so on, zipping inward from both ends until they meet. Think of it as repeatedly drawing from the front of a queue and then the back, alternating, the way you'd interleave a deck by taking cards off the top and the bottom. You must rewire the existing nodes; you can't allocate a fresh list.
// A node in a singly linked list.
type ListNode = { val: number; next: ListNode | null };
// Reorders the list in place and returns the (unchanged) head.
function linkedListRearrange(head: ListNode | null): ListNode | null;
The tests build a list from an array of values and read it back into an array to compare. A node is a plain object { val, next }; the last node's next is null.
// Even length: 1 2 3 4 -> 1 4 2 3
// front=1, back=4, front=2, back=3
linkedListRearrange(build([1, 2, 3, 4]));
// list is now: 1 -> 4 -> 2 -> 3
// Odd length: 1 2 3 4 5 -> 1 5 2 4 3
// front=1, back=5, front=2, back=4, middle=3 stays last
linkedListRearrange(build([1, 2, 3, 4, 5]));
// list is now: 1 -> 5 -> 2 -> 4 -> 3
next pointers of the existing nodes. Don't build a new list and copy values across — the target solution uses O(1) extra space.first, last, second, second-to-last, third, .... The two ends meet in the middle.[1,2,3,4,5] -> [1,5,2,4,3] example.null) stays null; a single node stays itself; two nodes a -> b stay a -> b. There's nothing to interleave until there are at least three nodes.You'll reorder a singly linked list so it zips inward from both ends — first node, last node, second node, second-to-last, and so on — rewiring the existing nodes without allocating a new list.
You have a row of nodes 1 -> 2 -> 3 -> 4 -> 5 -> 6 and you want 1 -> 6 -> 2 -> 5 -> 3 -> 4. Read that target out loud: front, back, front, back. You keep pulling from the two ends of the list and laying them down alternately until the ends meet in the middle. It's the same move as dealing a deck by drawing one card off the top and one off the bottom, over and over.
The catch with a singly linked list is that each node only knows its next — there are no backward pointers. So "take from the back" isn't a cheap operation: you can't step backward from the tail. The whole solution is about getting around that limitation in O(1) extra space.
The target order is two sequences interleaved: the front half walking forward, and the back half walking backward. If you had both of those as ordinary forward-walking lists, the reorder would just be "take one from each, alternating." So the plan is to manufacture exactly that: split the list in half, reverse the second half so it walks backward-as-forward, then merge the two halves one node at a time.
That gives the three-step plan we'll build toward: (1) find the middle, (2) reverse the second half, (3) merge the two halves alternately. Each step is O(n) time and O(1) space, so the whole thing is O(n) / O(1).
Before the three-step dance, here's the version almost everyone writes first. The problem was "take from the front, then the back." A linked list makes the back hard to reach — so dump every node into an array, where indexing from both ends is cheap, and rewire from there.
function linkedListRearrangeArray(head) {
if (head === null) return head;
// Collect every node into an array — now we can index from both ends.
const nodes = [];
for (let node = head; node !== null; node = node.next) {
nodes.push(node);
}
let left = 0;
let right = nodes.length - 1;
while (left < right) {
nodes[left].next = nodes[right]; // front node points to back node
left++;
if (left === right) break; // they met — don't create a self-loop
nodes[right].next = nodes[left]; // back node points to the next front node
right--;
}
nodes[left].next = null; // the meeting node is the new tail
return head;
}
This is correct — it produces exactly the right order, and the two-index walk is easy to reason about. The problem is the nodes array: it holds a reference to every node, so it costs O(n) extra space. For a linked-list question, that defeats the point. The reason linked lists exist is to rearrange data by moving pointers, not by copying everything into a contiguous buffer first. The interviewer who asks this wants the O(1)-space rewiring — the array version is the answer they're hoping you'll improve on.
It's also fiddly: the if (left === right) break guard and the final nodes[left].next = null are easy to get wrong, and a single off-by-one there creates a cycle. We can do better on both space and clarity.
The three steps each map to a well-known linked-list technique. Find the middle with slow/fast pointers; reverse the second half with the in-place reversal loop; merge with a two-pointer splice.
function linkedListRearrange(head) {
// Fewer than 3 nodes: nothing to interleave. [], [a], [a,b] are unchanged.
if (head === null || head.next === null || head.next.next === null) {
return head;
}
// STEP 1 — find the middle with slow/fast pointers.
// fast moves twice as fast, so when it can't step again, slow is the last
// node of the first half. For odd lengths this leaves the first half one
// longer than the second, which is exactly what the interleave wants.
let slow = head;
let fast = head;
while (fast.next !== null && fast.next.next !== null) {
slow = slow.next;
fast = fast.next.next;
}
// STEP 2 — reverse the second half, starting just past slow.
// Cut the first half loose by nulling slow.next, then reverse what follows.
let second = slow.next;
slow.next = null; // terminate the first half so the merge has a clean end
let prev = null;
while (second !== null) {
const nextNode = second.next; // remember where we were going
second.next = prev; // flip the pointer to face backward
prev = second; // advance the reversed-list head
second = nextNode; // advance through the original second half
}
// prev is now the head of the reversed second half.
// STEP 3 — merge the two halves, alternating one node from each.
// The first half is always >= the second half in length, so we drive the
// loop off `second` and stop when it runs out.
let first = head;
second = prev;
while (second !== null) {
const firstNext = first.next; // save both nexts before we overwrite them
const secondNext = second.next;
first.next = second; // splice the back node in after the front node
second.next = firstNext; // then reconnect to the rest of the first half
first = firstNext; // advance both pointers into their own halves
second = secondNext;
}
return head;
}
module.exports = { linkedListRearrange, build, toArray };
// --- helpers used by the tests ---
function build(values) {
let head = null;
for (let i = values.length - 1; i >= 0; i--) {
head = { val: values[i], next: head };
}
return head;
}
function toArray(head) {
const out = [];
for (let node = head; node !== null; node = node.next) {
out.push(node.val);
}
return out;
}
The shift from the naive version is that we never store all the nodes at once. Step 1 uses two scalar pointers to locate the split. Step 2 reverses the back half in place, reusing the same nodes with flipped pointers. Step 3 walks both halves with two pointers, splicing them together. At no point do we hold more than a handful of pointer variables — that's the O(1) space we were after.
Trace 1 -> 2 -> 3 -> 4 -> 5 (the odd-length case, where the middle node has to land last).
Step 1 — find the middle. Both pointers start at node 1.
start: slow=1, fast=1
fast.next=2, .next.next=3 ok → slow=2, fast=3
fast.next=4, .next.next=5 ok → slow=3, fast=5
fast.next=null → loop stops. slow=3
slow lands on node 3 — the last node of the first half 1 -> 2 -> 3.
Step 2 — split and reverse. second = slow.next is node 4. We set slow.next = null, cutting the list into 1 -> 2 -> 3 -> null and 4 -> 5 -> null. Now reverse the second piece:
second=4, prev=null → 4.next=null, prev=4, second=5
second=5, prev=4 → 5.next=4, prev=5, second=null
loop stops. reversed head prev = 5 -> 4 -> null
Step 3 — merge alternately. first = 1 (head), second = 5 (reversed head).
iter 1: firstNext=2, secondNext=4
1.next=5, 5.next=2 → list so far: 1 -> 5 -> 2 -> 3
first=2, second=4
iter 2: firstNext=3, secondNext=null
2.next=4, 4.next=3 → list so far: 1 -> 5 -> 2 -> 4 -> 3
first=3, second=null
loop stops (second === null).
The middle node 3 was already the tail of the first half (its next is null from the split), so it stays last untouched. Final list: 1 -> 5 -> 2 -> 4 -> 3 — exactly the expected output.
while (fast.next && fast.next.next) makes slow the last node of the first half, which leaves the first half one longer than (odd) or equal to (even) the second half. If you instead use while (fast && fast.next), slow advances one node too far and the second half becomes the longer one — then the merge, which is driven by second, walks off the end of first. Stick with the fast.next && fast.next.next form for this problem.slow.next = null, the first half still points into the original second half. After the merge you get a tangled list — often a cycle, because a node ends up reachable from two directions. The slow.next = null line is what makes the two halves truly independent.first.next and second.next into temporaries before overwriting them. If you write first.next = second first and then read first.next expecting the old value, you've already lost it — you'll splice a node back onto itself and create a self-loop. Save both nexts, then rewire, then advance.while (second !== null), not while (first !== null) — first may still have one node left (the middle, on odd lengths) when second is exhausted, and that node already terminates correctly. Looping on first would dereference second.next when second is null and throw.slow.next, not nulling the original second-half head during reversal (the very first second.next = prev with prev = null handles this), or losing a next in the merge. The test that walks the list counting steps and asserts it reaches null exists specifically to catch this — a cycle never terminates.fast.next.next when fast.next is null). The early head === null || head.next === null || head.next.next === null guard returns immediately for these, leaving them unchanged.k nodes (1 2 3 4 5, k=2 → 2 1 4 3 5). It reuses the same in-place reversal loop from step 2, applied repeatedly to fixed-size windows, with careful pointer surgery to stitch the reversed groups back together. Good practice for the reversal mechanics in isolation.Keep practising the same patterns with a nearby challenge.
No submissions yet
Share your approach and start the discussion.