Files
codetutor/backend/data/questions/reorder-list.yaml

248 lines
9.8 KiB
YAML

title: Reorder List
slug: reorder-list
difficulty: medium
leetcode_id: 143
leetcode_url: https://leetcode.com/problems/reorder-list/
categories:
- linked-lists
- two-pointers
patterns:
- fast-slow-pointers
- linkedlist-reversal
function_signature: "def reorder_list(head: ListNode) -> None:"
test_cases:
visible:
- input: { head: [1, 2, 3, 4] }
expected: [1, 4, 2, 3]
- input: { head: [1, 2, 3, 4, 5] }
expected: [1, 5, 2, 4, 3]
hidden:
- input: { head: [1] }
expected: [1]
- input: { head: [1, 2] }
expected: [1, 2]
- input: { head: [1, 2, 3] }
expected: [1, 3, 2]
- input: { head: [1, 2, 3, 4, 5, 6] }
expected: [1, 6, 2, 5, 3, 4]
- input: { head: [1, 2, 3, 4, 5, 6, 7] }
expected: [1, 7, 2, 6, 3, 5, 4]
description: |
You are given the head of a singly linked-list. The list can be represented as:
```
L0 → L1 → … → Ln-1 → Ln
```
*Reorder the list to be on the following form:*
```
L0 → Ln → L1 → Ln-1 → L2 → Ln-2 → …
```
You may not modify the values in the list's nodes. Only nodes themselves may be changed.
constraints: |
- The number of nodes in the list is in the range `[1, 5 * 10^4]`
- `1 <= Node.val <= 1000`
examples:
- input: "head = [1,2,3,4]"
output: "[1,4,2,3]"
explanation: "The original list is 1→2→3→4. After reordering: take the first node (1), then the last node (4), then the second node (2), then the second-to-last node (3). Result: 1→4→2→3."
- input: "head = [1,2,3,4,5]"
output: "[1,5,2,4,3]"
explanation: "The original list is 1→2→3→4→5. After reordering: 1 (first), 5 (last), 2 (second), 4 (second-to-last), 3 (middle). Result: 1→5→2→4→3."
explanation:
intuition: |
Imagine you have a deck of cards numbered 1 through n, laid out in a row. The reordering pattern asks you to interleave from both ends: pick the first card, then the last card, then the second card, then the second-to-last card, and so on until you meet in the middle.
The challenge with a singly linked list is that you can only traverse forward — you can't easily access the last node or go backwards. So how do we efficiently get to the nodes from the end?
The key insight is to **split the list in half, reverse the second half, then merge the two halves**. After reversing the second half, both halves start from the "outside" positions we need:
- First half: `L0 → L1 → L2 → ...`
- Reversed second half: `Ln → Ln-1 → Ln-2 → ...`
Now we can simply alternate between them, picking one node from each half in turn.
approach: |
We solve this using a **three-step approach** combining classic linked list techniques:
**Step 1: Find the middle of the list**
- Use the **fast-slow pointer** technique (also called the tortoise and hare)
- `slow` moves one step at a time, `fast` moves two steps
- When `fast` reaches the end, `slow` is at the middle
- This gives us the split point between the two halves
&nbsp;
**Step 2: Reverse the second half**
- Starting from the node after `slow`, reverse the linked list in-place
- Use three pointers: `prev`, `curr`, and `next`
- After reversal, we have the second half pointing backwards from the last node
&nbsp;
**Step 3: Merge the two halves**
- Alternate nodes from the first half and the reversed second half
- `first` pointer walks through the first half
- `second` pointer walks through the reversed second half
- Interleave by adjusting `next` pointers: first→second→first→second→...
&nbsp;
**Step 4: Return**
- The modification is done in-place, so no return value is needed
- The original `head` now points to the reordered list
common_pitfalls:
- title: Using O(n) Extra Space
description: |
A common first approach is to copy all nodes into an array, then rebuild the list by picking from both ends of the array. While this works, it uses **O(n) extra space**.
The optimal solution reverses the second half in-place, achieving **O(1) space** (only a few pointers).
wrong_approach: "Store nodes in array and rebuild"
correct_approach: "Reverse second half in-place, then merge"
- title: Off-by-One Errors in Finding Middle
description: |
The middle-finding logic needs care, especially for even vs odd length lists:
- For `[1,2,3,4]`, we want `slow` at node 2, so second half is `[3,4]`
- For `[1,2,3,4,5]`, we want `slow` at node 3, so second half is `[4,5]`
Using `while fast and fast.next` with `slow` starting at `head` gives the correct split. Be careful not to lose the connection when splitting.
wrong_approach: "Inconsistent middle calculation"
correct_approach: "Use standard fast-slow with while fast.next and fast.next.next"
- title: Forgetting to Terminate the First Half
description: |
After finding the middle, you must set `slow.next = None` to terminate the first half. Otherwise, you'll have cycles or incorrect lengths when merging.
For example, with `[1,2,3,4]`, after splitting, first half should be `1→2→None` and second half `3→4→None`. Without terminating, first half would still point to 3.
wrong_approach: "Not breaking the link at the middle"
correct_approach: "Set slow.next = None before reversing"
- title: Incorrect Merge Order
description: |
When merging, remember you're alternating: first half node, then second half node. Save the next pointers before rewiring:
```python
tmp1, tmp2 = first.next, second.next
first.next = second
second.next = tmp1
first, second = tmp1, tmp2
```
Failing to save `next` pointers before modifying them leads to lost nodes.
wrong_approach: "Modifying next pointers without saving"
correct_approach: "Save both next pointers before any rewiring"
key_takeaways:
- "**Fast-slow pointer pattern**: Essential technique for finding the middle of a linked list in O(n) time and O(1) space"
- "**In-place reversal**: Reversing a linked list with three pointers (`prev`, `curr`, `next`) is a fundamental operation"
- "**Decompose complex operations**: Breaking the problem into find-middle, reverse, and merge makes it manageable"
- "**Related problems**: This pattern applies to Palindrome Linked List, Sort List, and other problems requiring middle-finding or reversal"
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 O(n), so overall O(n)."
space_complexity: "O(1). We only use a constant number of pointer variables (`slow`, `fast`, `prev`, `curr`, `first`, `second`, etc.), regardless of list size."
solutions:
- approach_name: Three-Step In-Place
is_optimal: true
code: |
class ListNode:
def __init__(self, val=0, next=None):
self.val = val
self.next = next
def reorder_list(head: ListNode) -> None:
if not head or not head.next:
return
# Step 1: Find the middle using fast-slow pointers
slow, fast = head, head
while fast.next and fast.next.next:
slow = slow.next
fast = fast.next.next
# Step 2: Reverse the second half
# slow is at the middle, second half starts at slow.next
prev, curr = None, slow.next
slow.next = None # Terminate the first half
while curr:
next_temp = curr.next # Save next node
curr.next = prev # Reverse the pointer
prev = curr # Move prev forward
curr = next_temp # Move curr forward
# prev now points to the head of reversed second half
# Step 3: Merge the two halves
first, second = head, prev
while second:
# Save next pointers
tmp1, tmp2 = first.next, second.next
# Interleave
first.next = second
second.next = tmp1
# Move to next pair
first, second = tmp1, tmp2
explanation: |
**Time Complexity:** O(n) — Three linear passes through the list.
**Space Complexity:** O(1) — Only a constant number of pointers used.
This solution combines three classic linked list operations: finding the middle with fast-slow pointers, reversing a linked list in-place, and merging two lists. Each operation is O(n) time and O(1) space.
- approach_name: Stack-Based
is_optimal: false
code: |
class ListNode:
def __init__(self, val=0, next=None):
self.val = val
self.next = next
def reorder_list(head: ListNode) -> None:
if not head or not head.next:
return
# Store all nodes in a list for random access
nodes = []
curr = head
while curr:
nodes.append(curr)
curr = curr.next
# Use two pointers on the array
left, right = 0, len(nodes) - 1
while left < right:
# Connect left to right
nodes[left].next = nodes[right]
left += 1
if left == right:
break
# Connect right to next left
nodes[right].next = nodes[left]
right -= 1
# Terminate the list
nodes[left].next = None
explanation: |
**Time Complexity:** O(n) — One pass to collect nodes, one pass to rewire.
**Space Complexity:** O(n) — Array storing all node references.
This approach trades space for simplicity. By storing nodes in an array, we gain random access and can easily pick from both ends. While correct, it uses O(n) extra space, making it suboptimal compared to the in-place solution.