Linked List Algorithms: Practice Problems with Solutions
Welcome to the practice problems for linked lists. If you need a refresher on the code, the code templates have the pointer techniques in all 6 languages. Each problem below includes a brief, a key insight, a visual walkthrough, and the full solution.
Recommended Study Order
The problems are ordered by difficulty, but the progression matters as much as the individual solutions.
- Reverse Linked List teaches the reversal primitive in its purest form. Master the three-pointer walk before adding any complexity.
- Middle of the Linked List adds fast and slow pointers. This is the second fundamental technique.
- Palindrome Linked List combines both: find the middle, reverse the second half, and compare.
- Remove Nth Node From End of List adds the dummy node and offset pointers. The dummy node removes the special case for the head.
- Reorder List combines all three techniques in one problem.
- Odd Even Linked List tests whether you can relink nodes without losing the reference to the rest of the list.
- Reverse Nodes in k-Group layers grouping on top of reversal. It is the hardest problem in the set and a common follow-up in onsite loops.
Two classic problems live on other pages because they belong to other patterns. Linked List Cycle and Linked List Cycle II are on the cycle detection problems page , and Merge Two Sorted Lists is on the divide and conquer problems page .
Easy Problems
1. Reverse Linked List
LeetCode 206 | Difficulty: Easy
Brief: Reverse a singly linked list and return the new head.
Why this pattern: This is the fundamental reversal technique. Three pointers walk the list and redirect each node’s pointer backward.
Key Insight: Save the next node before overwriting current.next, or the rest of the list becomes unreachable. The last node visited is the new head.
Visual:
graph LR
A["prev=null, cur=1"] --> B["save 2, 1.next=null"]
B --> C["prev=1, cur=2"]
C --> D["save 3, 2.next=1"]
D --> E["prev=2, cur=3"]
E --> F["save null, 3.next=2"]
F --> G["prev=3, cur=null"]
G --> H["return 3"]
Code:
var reverseList = function(head) {
let prev = null;
let current = head;
while (current !== null) {
// Save the node after current before its pointer is
// overwritten, or the rest of the list is unreachable.
const next = current.next;
current.next = prev;
prev = current;
current = next;
}
return prev;
};The solution uses the reversal template directly. The save step keeps the traversal alive, and the return value is the last node visited, which is the new head. The loop handles the empty list naturally because it never runs.
2. Middle of the Linked List
LeetCode 876 | Difficulty: Easy
Brief: Return the middle node of a linked list. For an even-length list, return the second middle node.
Why this pattern: Fast and slow pointers. The fast pointer moves two nodes at a time, so it reaches the end exactly when the slow pointer has covered half the list.
Key Insight: The loop condition checks both fast and fast.next, because the fast pointer’s second step needs the next node to exist.
Visual:
graph LR
A["slow=1, fast=1"] --> B["slow=2, fast=3"]
B --> C["slow=3, fast=5"]
C --> D["fast.next is null, stop"]
D --> E["return 3"]
Code:
var middleNode = function(head) {
let slow = head;
let fast = head;
// fast covers two nodes per step, so it reaches the end
// exactly when slow has covered half the list.
while (fast !== null && fast.next !== null) {
slow = slow.next;
fast = fast.next.next;
}
return slow;
};For an even-length list like 1,2,3,4, the loop stops with fast at null and slow at node 3, which is the second middle. That matches what the problem expects.
3. Palindrome Linked List
LeetCode 234 | Difficulty: Easy
Brief: Check whether a linked list reads the same forward and backward.
Why this pattern: Combines the two core techniques: find the middle with fast and slow pointers, then reverse the second half and compare both halves.
Key Insight: Reversing the second half in place avoids allocating an array, keeping the space at
Visual:
graph LR
A["1, 2, 3, 2, 1"] --> B["middle: slow at 3"]
B --> C["reverse second half: 2, 1"]
C --> D["compare 1=1, 2=2, 3=3"]
D --> E["palindrome"]
Code:
var isPalindrome = function(head) {
if (head === null || head.next === null) return true;
// Stop one node before the second half so the halves
// split cleanly for the reversal and comparison.
let slow = head;
let fast = head;
while (fast.next !== null && fast.next.next !== null) {
slow = slow.next;
fast = fast.next.next;
}
// Reverse the second half in place with the reversal
// template, then compare the two halves node by node.
let prev = null;
let current = slow.next;
while (current !== null) {
const next = current.next;
current.next = prev;
prev = current;
current = next;
}
let first = head;
let second = prev;
while (second !== null) {
if (first.val !== second.val) return false;
first = first.next;
second = second.next;
}
return true;
};The split happens one node before the actual middle, so the reversed second half is never longer than the first half. The comparison walks the reversed half to its end, which covers every node of the second half exactly once. The reversal runs in place, so no extra array is needed.
Medium Problems
4. Remove Nth Node From End of List
LeetCode 19 | Difficulty: Medium
Brief: Remove the n-th node from the end of a linked list and return the head.
Why this pattern: Two pointers offset by n steps find the target in one pass without knowing the list length. The dummy node protects the case where the head itself is removed.
Key Insight: Advance the first pointer by n+1 steps, then walk both pointers together. The second pointer lands one node before the target, exactly where the delete needs to happen.
Visual:
graph LR
A["dummy->1,2,3,4,5, n=2"] --> B["first advances 3 steps"]
B --> C["first at 3, second at dummy"]
C --> D["walk together to the end"]
D --> E["first at null, second at 3"]
E --> F["3.next=5, return 1"]
Code:
var removeNthFromEnd = function(head, n) {
// The dummy node makes head removal identical to any other
// removal, so the delete code has no special case.
const dummy = new ListNode(0, head);
let first = dummy;
let second = dummy;
// first starts n+1 nodes ahead, so when it reaches the end,
// second is the predecessor of the target node.
for (let i = 0; i <= n; i++) {
first = first.next;
}
while (first !== null) {
first = first.next;
second = second.next;
}
second.next = second.next.next;
return dummy.next;
};The n+1 offset is the detail that makes the delete safe. With n steps, the second pointer would land on the node to delete, which is not enough to remove it. One extra step puts the second pointer at the predecessor. The dummy node also means the answer is always dummy.next, even when the head was the removed node.
5. Reorder List
LeetCode 143 | Difficulty: Medium
Brief: Reorder the list so the nodes alternate between the first half and the reversed second half.
Why this pattern: Combines all three techniques: find the middle, reverse the second half, and merge the two halves.
Key Insight: The merge step saves the next pointers of both lists before relinking, because the relink overwrites them.
Visual:
graph LR
A["1, 2, 3, 4, 5"] --> B["middle: 3"]
B --> C["reverse second half: 5, 4"]
C --> D["interleave: 1, 5, 2, 4, 3"]
Code:
var reorderList = function(head) {
if (head === null || head.next === null) return;
// Stop at the end of the first half. The node after it
// is where the reversal starts.
let slow = head;
let fast = head;
while (fast.next !== null && fast.next.next !== null) {
slow = slow.next;
fast = fast.next.next;
}
// Reverse the second half and cut it off from the first.
let prev = null;
let current = slow.next;
slow.next = null;
while (current !== null) {
const next = current.next;
current.next = prev;
prev = current;
current = next;
}
// Interleave: first half node, second half node, repeat.
// Save both next pointers first, because the relink
// overwrites them.
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;
}
};The second half is never longer than the first half, so the merge loop terminates when the reversed half runs out. Each iteration saves both next pointers before relinking, which is the same save-first discipline as the reversal template. The cut at slow.next = null keeps the two halves independent during the merge.
6. Odd Even Linked List
LeetCode 328 | Difficulty: Medium
Brief: Group all odd-indexed nodes first, followed by the even-indexed nodes, preserving relative order.
Why this pattern: Relinking with two chains. Odd nodes link to odd nodes, even nodes link to even nodes, and the chains join at the end.
Key Insight: Save the even chain head before the walk. The next odd node is even.next, and the next even node is odd.next after the odd chain has moved.
Visual:
graph LR
A["odd=1, even=2"] --> B["1.next=3, odd=3"]
B --> C["2.next=4, even=4"]
C --> D["3.next=5, odd=5"]
D --> E["4.next=null, even=null"]
E --> F["5.next=2, return 1"]
Code:
var oddEvenList = function(head) {
if (head === null) return null;
let odd = head;
let even = head.next;
const evenHead = even;
// odd and even walk the list in pairs. The next odd node
// is one past even, and the next even node is one past
// the new odd node.
while (even !== null && even.next !== null) {
odd.next = even.next;
odd = odd.next;
even.next = odd.next;
even = even.next;
}
// Join the chains: odds first, then evens.
odd.next = evenHead;
return head;
};The common bug is reading the odd chain through the pointer that was just overwritten. The code reads even.next to find the next odd node, and the next even node comes from odd.next after the odd chain has moved. The saved evenHead joins the two chains at the end, and the loop condition keeps a one-node list safe.
Hard Problems
7. Reverse Nodes in k-Group
LeetCode 25 | Difficulty: Hard
Brief: Reverse the nodes of a linked list k at a time, and leave the final group as is if it has fewer than k nodes.
Why this pattern: Reversal in groups. Each group is reversed with the standard template, and the groups are linked recursively.
Key Insight: Check that the group has k nodes before reversing it. After the reversal, the original head is the last node of the reversed group, and it links to the result of the remaining list.
Visual:
graph LR
A["1, 2, 3, 4, 5, k=2"] --> B["reverse 1,2: 2,1"]
B --> C["reverse 3,4: 4,3"]
C --> D["5 has fewer than k, keep"]
D --> E["2, 1, 4, 3, 5"]
Code:
var reverseKGroup = function(head, k) {
let count = 0;
let current = head;
// If fewer than k nodes remain, this group is left as is.
while (current !== null && count < k) {
current = current.next;
count++;
}
if (count < k) return head;
// Reverse the first k nodes with the standard template.
let prev = null;
current = head;
for (let i = 0; i < k; i++) {
const next = current.next;
current.next = prev;
prev = current;
current = next;
}
// head is now the last node of the reversed group. Link
// it to the result of reversing the remaining groups.
head.next = reverseKGroup(current, k);
return prev;
};The count check happens before the reversal, so a short final group survives untouched. After the reversal, the original head is the last node of the reversed group, and linking it to the recursive result stitches the groups together. The recursion depth matches the number of groups, so the space is
These seven problems cover the full range of linked list techniques. Start with the reversal primitive in Reverse Linked List, add fast and slow pointers with Middle of the Linked List, and finish with group reversal in Reverse Nodes in k-Group. By the end, you should be able to spot which pointer technique a problem needs and reach for the right template.