Linked List Exercises: Answer Sheet & Worked Solutions
This is the answer sheet for the linked list exercises. Each solution gives the key idea, the optimal complexity, the code in all four languages (use the switcher), and common pitfalls. All examples assume a singly linked node with data and next fields. Try the problems first — then check your work here.
Q1. Reverse a singly linked list
Idea: Walk once with three pointers — prev, cur, and a saved next — flipping each link to point backward. When cur runs off the end, prev is the new head.
Complexity: O(n) time, O(1) space.
Node* reverse(Node* head) {
Node* prev = nullptr;
Node* cur = head;
while (cur != nullptr) {
Node* next = cur->next;
cur->next = prev;
prev = cur;
cur = next;
}
return prev;
}
Node reverse(Node head) {
Node prev = null, cur = head;
while (cur != null) {
Node next = cur.next;
cur.next = prev;
prev = cur;
cur = next;
}
return prev;
}
function reverse(head) {
let prev = null, cur = head;
while (cur !== null) {
const next = cur.next;
cur.next = prev;
prev = cur;
cur = next;
}
return prev;
}
def reverse(head):
prev, cur = None, head
while cur is not None:
nxt = cur.next
cur.next = prev
prev = cur
cur = nxt
return prev
Pitfall: Forgetting to save
cur.nextbefore overwriting it strands the rest of the list. The saved pointer is the whole reason you need three variables. See reversing a linked list for the recursive version.
Q2. Find the middle node
Idea: Two fast/slow pointers — slow moves one step, fast two. When fast falls off the end, slow is at the middle. Starting both at the head returns the second middle for even lengths.
Complexity: O(n) time, O(1) space.
Node* middle(Node* head) {
Node* slow = head;
Node* fast = head;
while (fast != nullptr && fast->next != nullptr) {
slow = slow->next;
fast = fast->next->next;
}
return slow;
}
Node middle(Node head) {
Node slow = head, fast = head;
while (fast != null && fast.next != null) {
slow = slow.next;
fast = fast.next.next;
}
return slow;
}
function middle(head) {
let slow = head, fast = head;
while (fast !== null && fast.next !== null) {
slow = slow.next;
fast = fast.next.next;
}
return slow;
}
def middle(head):
slow = fast = head
while fast is not None and fast.next is not None:
slow = slow.next
fast = fast.next.next
return slow
Pitfall: Check
fastbeforefast.nextin the condition, or an even-length list dereferences null and crashes.
Q3. Detect a cycle
Idea: Floyd’s tortoise and hare. If a cycle exists, the fast pointer laps the slow one and they meet; if not, fast reaches null first.
Complexity: O(n) time, O(1) space.
bool hasCycle(Node* head) {
Node* slow = head;
Node* fast = head;
while (fast != nullptr && fast->next != nullptr) {
slow = slow->next;
fast = fast->next->next;
if (slow == fast) return true;
}
return false;
}
boolean hasCycle(Node head) {
Node slow = head, fast = head;
while (fast != null && fast.next != null) {
slow = slow.next;
fast = fast.next.next;
if (slow == fast) return true;
}
return false;
}
function hasCycle(head) {
let slow = head, fast = head;
while (fast !== null && fast.next !== null) {
slow = slow.next;
fast = fast.next.next;
if (slow === fast) return true;
}
return false;
}
def has_cycle(head):
slow = fast = head
while fast is not None and fast.next is not None:
slow = slow.next
fast = fast.next.next
if slow is fast:
return True
return False
Pitfall: A hash set of visited nodes also works in O(n) time but costs O(n) space. The two-pointer method is the O(1)-space answer interviewers want.
Q4. Merge two sorted lists
Idea: Use a dummy head and a tail pointer. Repeatedly attach the smaller of the two front nodes and advance that list. When one list runs out, attach the other wholesale.
Complexity: O(m + n) time, O(1) space.
Node* mergeTwo(Node* a, Node* b) {
Node dummy(0);
Node* tail = &dummy;
while (a != nullptr && b != nullptr) {
if (a->data <= b->data) { tail->next = a; a = a->next; }
else { tail->next = b; b = b->next; }
tail = tail->next;
}
tail->next = (a != nullptr) ? a : b;
return dummy.next;
}
Node mergeTwo(Node a, Node b) {
Node dummy = new Node(0);
Node tail = dummy;
while (a != null && b != null) {
if (a.data <= b.data) { tail.next = a; a = a.next; }
else { tail.next = b; b = b.next; }
tail = tail.next;
}
tail.next = (a != null) ? a : b;
return dummy.next;
}
function mergeTwo(a, b) {
const dummy = new Node(0);
let tail = dummy;
while (a !== null && b !== null) {
if (a.data <= b.data) { tail.next = a; a = a.next; }
else { tail.next = b; b = b.next; }
tail = tail.next;
}
tail.next = (a !== null) ? a : b;
return dummy.next;
}
def merge_two(a, b):
dummy = Node(0)
tail = dummy
while a is not None and b is not None:
if a.data <= b.data:
tail.next = a
a = a.next
else:
tail.next = b
b = b.next
tail = tail.next
tail.next = a if a is not None else b
return dummy.next
Pitfall: Use
<=(not<) when comparing to keep the merge stable for equal values, and don’t forget to attach the leftover tail — dropping it loses half the result.
Q5. Remove the Nth node from the end
Idea: Start a fast pointer from a dummy head and advance it n + 1 steps. Then move fast and slow together until fast is null — now slow sits just before the target, ready to unlink it. One pass.
Complexity: O(n) time, O(1) space.
Node* removeNthFromEnd(Node* head, int n) {
Node dummy(0);
dummy.next = head;
Node* fast = &dummy;
Node* slow = &dummy;
for (int i = 0; i <= n; i++) fast = fast->next; // n+1 ahead
while (fast != nullptr) { fast = fast->next; slow = slow->next; }
Node* doomed = slow->next;
slow->next = doomed->next;
delete doomed;
return dummy.next;
}
Node removeNthFromEnd(Node head, int n) {
Node dummy = new Node(0);
dummy.next = head;
Node fast = dummy, slow = dummy;
for (int i = 0; i <= n; i++) fast = fast.next; // n+1 ahead
while (fast != null) { fast = fast.next; slow = slow.next; }
slow.next = slow.next.next;
return dummy.next;
}
function removeNthFromEnd(head, n) {
const dummy = new Node(0);
dummy.next = head;
let fast = dummy, slow = dummy;
for (let i = 0; i <= n; i++) fast = fast.next; // n+1 ahead
while (fast !== null) { fast = fast.next; slow = slow.next; }
slow.next = slow.next.next;
return dummy.next;
}
def remove_nth_from_end(head, n):
dummy = Node(0)
dummy.next = head
fast = slow = dummy
for _ in range(n + 1): # n+1 ahead
fast = fast.next
while fast is not None:
fast = fast.next
slow = slow.next
slow.next = slow.next.next
return dummy.next
Pitfall: The dummy head is what makes removing the first node (when
nequals the length) work without a special case. Advancingfastbyn + 1, notn, is what leavesslowon the predecessor.
Q6. Palindrome linked list
Idea: Find the middle with fast/slow, reverse the second half, then walk both halves comparing values. O(1) space because we reuse the nodes instead of copying to an array.
Complexity: O(n) time, O(1) space.
bool isPalindrome(Node* head) {
if (head == nullptr || head->next == nullptr) return true;
// find middle
Node* slow = head; Node* fast = head;
while (fast->next != nullptr && fast->next->next != nullptr) {
slow = slow->next; fast = fast->next->next;
}
// reverse second half (after slow)
Node* prev = nullptr; Node* cur = slow->next;
while (cur != nullptr) {
Node* nx = cur->next; cur->next = prev; prev = cur; cur = nx;
}
// compare
Node* a = head; Node* b = prev;
while (b != nullptr) {
if (a->data != b->data) return false;
a = a->next; b = b->next;
}
return true;
}
boolean isPalindrome(Node head) {
if (head == null || head.next == null) return true;
Node slow = head, fast = head;
while (fast.next != null && fast.next.next != null) {
slow = slow.next; fast = fast.next.next;
}
Node prev = null, cur = slow.next;
while (cur != null) {
Node nx = cur.next; cur.next = prev; prev = cur; cur = nx;
}
Node a = head, b = prev;
while (b != null) {
if (a.data != b.data) return false;
a = a.next; b = b.next;
}
return true;
}
function isPalindrome(head) {
if (head === null || head.next === null) return true;
let slow = head, fast = head;
while (fast.next !== null && fast.next.next !== null) {
slow = slow.next; fast = fast.next.next;
}
let prev = null, cur = slow.next;
while (cur !== null) {
const nx = cur.next; cur.next = prev; prev = cur; cur = nx;
}
let a = head, b = prev;
while (b !== null) {
if (a.data !== b.data) return false;
a = a.next; b = b.next;
}
return true;
}
def is_palindrome(head):
if head is None or head.next is None:
return True
slow = fast = head
while fast.next is not None and fast.next.next is not None:
slow = slow.next
fast = fast.next.next
prev, cur = None, slow.next
while cur is not None:
nx = cur.next
cur.next = prev
prev = cur
cur = nx
a, b = head, prev
while b is not None:
if a.data != b.data:
return False
a = a.next
b = b.next
return True
Pitfall: Comparing only until
b(the reversed half) is null avoids the middle-node mismatch on odd lengths. If you must not mutate the input, reverse the second half back after comparing.
Q7. Detect the cycle’s start node
Idea: Run Floyd’s detection until the pointers collide. Then reset one pointer to the head and advance both one step at a time — they meet exactly at the cycle’s entry. This follows from the distance equation: the gap from the head to the entry equals the gap from the meeting point to the entry.
Complexity: O(n) time, O(1) space.
Node* detectCycleStart(Node* head) {
Node* slow = head; Node* fast = head;
while (fast != nullptr && fast->next != nullptr) {
slow = slow->next;
fast = fast->next->next;
if (slow == fast) {
Node* p = head;
while (p != slow) { p = p->next; slow = slow->next; }
return p;
}
}
return nullptr;
}
Node detectCycleStart(Node head) {
Node slow = head, fast = head;
while (fast != null && fast.next != null) {
slow = slow.next;
fast = fast.next.next;
if (slow == fast) {
Node p = head;
while (p != slow) { p = p.next; slow = slow.next; }
return p;
}
}
return null;
}
function detectCycleStart(head) {
let slow = head, fast = head;
while (fast !== null && fast.next !== null) {
slow = slow.next;
fast = fast.next.next;
if (slow === fast) {
let p = head;
while (p !== slow) { p = p.next; slow = slow.next; }
return p;
}
}
return null;
}
def detect_cycle_start(head):
slow = fast = head
while fast is not None and fast.next is not None:
slow = slow.next
fast = fast.next.next
if slow is fast:
p = head
while p is not slow:
p = p.next
slow = slow.next
return p
return None
Pitfall: The reset-and-walk step only works after a genuine collision inside the loop — don’t start it from the initial
head == headposition. Return null whenfastreaches the end so acyclic lists are handled.
That’s the full answer sheet. Back to the linked list exercises or continue to stacks.