Solving Reorder List in JavaScript: Step-by-Step Guide
The Reorder List problem is a classic linked list challenge that frequently appears in coding interviews and algorithm practice. It tests your understanding of pointer manipulation, list traversal, and in-place transformations. In this tutorial, we'll break down the problem, understand the optimal approach, and implement a clean JavaScript solution step by step.
What Is the Reorder List Problem?
Given a singly linked list L: L0 โ L1 โ L2 โ โฆ โ Ln-1 โ Ln, you must reorder it in-place to:
L0 โ Ln โ L1 โ Ln-1 โ L2 โ Ln-2 โ โฆ
You may not modify the values in the list's nodes โ only the next pointers themselves can be changed. This constraint forces you to manipulate the structure rather than simply copying values into an array and rebuilding the list.
For example, given the list 1 โ 2 โ 3 โ 4 โ 5, the reordered result should be 1 โ 5 โ 2 โ 4 โ 3.
Why It Matters
This problem is valuable for several reasons:
- Pointer manipulation mastery: It forces you to carefully track multiple pointers without losing references.
- Sub-problem decomposition: The optimal solution combines three fundamental techniques โ finding the middle, reversing a list, and merging two lists.
- In-place constraints: Real-world systems often require memory-efficient transformations, and this problem trains that mindset.
- Interview relevance: It's a popular LeetCode problem (LC 143) that appears in technical interviews at major companies.
The Strategy: Three Phases
The naive approach โ storing all nodes in an array and reconnecting them โ uses O(n) extra space. The optimal approach uses O(1) space by splitting the work into three phases:
- Phase 1: Find the middle of the list using the slow/fast pointer technique.
- Phase 2: Reverse the second half of the list.
- Phase 3: Merge the first half with the reversed second half, alternating nodes.
Let's define our linked list node structure first:
function ListNode(val, next) {
this.val = (val === undefined ? 0 : val);
this.next = (next === undefined ? null : next);
}
Phase 1: Finding the Middle
We use two pointers: slow moves one step at a time, while fast moves two steps. When fast reaches the end, slow will be at the middle. To ensure a clean split for both even and odd length lists, we stop fast when fast.next or fast.next.next is null.
function findMiddle(head) {
let slow = head;
let fast = head;
while (fast.next && fast.next.next) {
slow = slow.next;
fast = fast.next.next;
}
return slow;
}
For a list 1 โ 2 โ 3 โ 4 โ 5, slow ends at node 3. The second half starts at slow.next, which is 4.
Phase 2: Reversing the Second Half
Reversing a singly linked list is a foundational skill. We iterate through the list, flipping each next pointer to point backward.
function reverseList(head) {
let prev = null;
let curr = head;
while (curr) {
const nextNode = curr.next;
curr.next = prev;
prev = curr;
curr = nextNode;
}
return prev;
}
After reversing 4 โ 5, we get 5 โ 4, with 5 as the new head of the reversed half.
Phase 3: Merging the Two Halves
Now we interleave nodes from the first half (1 โ 2 โ 3) with the reversed second half (5 โ 4). We take one node from each list alternately, reconnecting next pointers as we go.
function mergeLists(l1, l2) {
while (l2) {
const l1Next = l1.next;
const l2Next = l2.next;
l1.next = l2;
l2.next = l1Next;
l1 = l1Next;
l2 = l2Next;
}
}
We loop while l2 exists because the second half is always shorter than or equal to the first half, so it determines when merging is complete.
Putting It All Together
Here is the complete reorderList function that orchestrates all three phases:
function reorderList(head) {
if (!head || !head.next) return head;
// Phase 1: Find the middle
let slow = head;
let fast = head;
while (fast.next && fast.next.next) {
slow = slow.next;
fast = fast.next.next;
}
// Phase 2: Reverse the second half
let secondHalf = slow.next;
slow.next = null; // Cut the list in two
let prev = null;
let curr = secondHalf;
while (curr) {
const nextNode = curr.next;
curr.next = prev;
prev = curr;
curr = nextNode;
}
secondHalf = prev;
// Phase 3: Merge the two halves
let first = head;
let second = secondHalf;
while (second) {
const firstNext = first.next;
const secondNext = second.next;
first.next = second;
second.next = firstNext;
first = firstNext;
second = secondNext;
}
return head;
}
Testing the Solution
Let's build a helper to create a list from an array and another to print it, then verify our solution:
function arrayToList(arr) {
let dummy = new ListNode();
let curr = dummy;
for (const val of arr) {
curr.next = new ListNode(val);
curr = curr.next;
}
return dummy.next;
}
function listToArray(head) {
const result = [];
while (head) {
result.push(head.val);
head = head.next;
}
return result;
}
// Test cases
const list1 = arrayToList([1, 2, 3, 4, 5]);
reorderList(list1);
console.log(listToArray(list1)); // [1, 5, 2, 4, 3]
const list2 = arrayToList([1, 2, 3, 4]);
reorderList(list2);
console.log(listToArray(list2)); // [1, 4, 2, 3]
const list3 = arrayToList([1]);
reorderList(list3);
console.log(listToArray(list3)); // [1]
Complexity Analysis
- Time complexity: O(n) โ We traverse the list three times: once to find the middle, once to reverse the second half, and once to merge. Each pass is linear.
- Space complexity: O(1) โ We only use a constant number of pointers regardless of the list size. No additional data structures are allocated.
Best Practices and Common Pitfalls
- Always cut the list in two: After finding the middle, set
slow.next = null. Forgetting this creates a cycle when you reverse and merge, because the first half would still link into the second. - Handle edge cases early: Empty lists and single-node lists should return immediately. Two-node lists also work naturally with this algorithm.
- Loop on the second half during merge: The second half is always shorter than or equal to the first half, so using
while (second)prevents null dereferences. - Don't modify node values: The problem explicitly forbids changing values. Stick to pointer manipulation to honor the constraint and to preserve any data attached to nodes in real applications.
- Test even and odd lengths: Off-by-one errors in the middle-finding loop are common. The condition
fast.next && fast.next.nextensures the first half is always at least as long as the second. - Draw it out: Pointer problems are notoriously hard to reason about mentally. Sketching the list before and after each phase helps catch mistakes before writing code.
Variations to Practice
Once you're comfortable with the basic reorder, try these related challenges to deepen your understanding:
- Reorder using only a single pass with a stack (trades space for simplicity).
- Reorder a doubly linked list, where backward pointers must also be maintained.
- Generalize the reorder to interleave every
kth node instead of alternating. - Reorder based on a custom comparator rather than position from the end.
Conclusion
The Reorder List problem is a perfect exercise in decomposing a complex transformation into smaller, well-understood operations. By combining middle-finding, list reversal, and interleaved merging, you arrive at an elegant O(n) time and O(1) space solution that respects the in-place constraint. Mastering this pattern not only prepares you for interviews but also builds the pointer-manipulation intuition you'll need for more advanced data structure problems. Practice each phase in isolation until it feels automatic, then assemble them confidently whenever this problem โ or a variation of it โ appears.