Module 3.2

Linked List Problems

Did you know? Linked list problems appear in over 70% of technical interviews at top tech companies. Think of these problems like a detective learning to follow trails - once you master the slow/fast pointer dance, the dummy node trick, and the reverse-half technique, you'll crack almost any linked list puzzle thrown at you. Let's turn these "scary" interview classics into your secret weapons!

45 min read
Intermediate
Interview Hot Topic
What You'll Learn
  • Floyd's cycle detection
  • Merge two sorted lists
  • Remove nth node from end
  • Find intersection point
  • Palindrome linked list
Contents
00

Why Linked List Problems?

Linked list problems are the bread and butter of technical interviews. Companies like Google, Amazon, and Meta love them because they test your ability to manipulate pointers, handle edge cases, and think about space complexity. Unlike arrays, you cannot just jump to any index - you must think carefully about how to navigate and modify the structure.

Why Interviewers Love These Problems
  • Pointer Manipulation: Tests your understanding of references vs values
  • Edge Cases: Empty lists, single nodes, head changes - lots can go wrong
  • Space Optimization: Can you solve it in O(1) space?
  • Pattern Recognition: Most problems use similar techniques
  • Clean Code: Reveals coding style and attention to detail
The 5 Core Patterns You'll Learn
  1. Slow/Fast Pointers: Find middle, detect cycles
  2. Dummy Node: Simplify head modifications
  3. Prev/Curr/Next: Reverse and swap operations
  4. Two-Pointer Gap: Find nth from end
  5. Split + Reverse + Merge: Complex reordering

Master these 5 patterns and you can solve 90% of linked list problems!

Problem Difficulty Progression
EASY (Learn the basics)
├── Reverse Linked List        → prev/curr/next pattern
├── Find Middle                → slow/fast pointers
├── Merge Two Sorted Lists     → dummy node + compare
└── Detect Cycle               → Floyd's algorithm

MEDIUM (Combine patterns)
├── Remove Nth from End        → two-pointer gap
├── Palindrome Check           → find middle + reverse half
├── Reorder List               → split + reverse + merge
└── Add Two Numbers            → dummy node + carry

HARD (Multiple patterns + edge cases)
├── Reverse Nodes in k-Group   → counting + reverse + reconnect
├── Merge K Sorted Lists       → heap + dummy node
└── Copy List with Random Ptr  → interleave OR hashmap
Learning Strategy: Do not just memorize solutions! Understand WHY each pattern works. When you see a new problem, ask yourself: "Which pattern does this remind me of?" Then adapt the pattern to the specific requirements.
01

Cycle Detection - Floyd's Algorithm

Imagine you are running on a circular track with a friend. Your friend runs twice as fast as you. Even though you started together, they will eventually lap you and you will meet again! This is exactly how Floyd's Cycle Detection works. It is one of the most elegant algorithms in computer science, using only two pointers and O(1) extra space to detect if a linked list has a loop.

Floyd's Tortoise and Hare

A two-pointer algorithm where slow moves one step and fast moves two steps. If there's a cycle, they will eventually meet. Also known as the fast and slow pointer technique.

// Detect if linked list has a cycle
// Time: O(n), Space: O(1)
boolean hasCycle(ListNode head) {
    if (head == null || head.next == null) {
        return false;
    }
    
    ListNode slow = head;
    ListNode fast = head;
    
    while (fast != null && fast.next != null) {
        slow = slow.next;         // Move 1 step
        fast = fast.next.next;    // Move 2 steps
        
        if (slow == fast) {
            return true;  // Cycle detected
        }
    }
    
    return false;  // No cycle (fast reached end)
}

// Find the start of the cycle
ListNode detectCycleStart(ListNode head) {
    if (head == null || head.next == null) {
        return null;
    }
    
    ListNode slow = head;
    ListNode fast = head;
    
    // Phase 1: Detect cycle
    while (fast != null && fast.next != null) {
        slow = slow.next;
        fast = fast.next.next;
        
        if (slow == fast) {
            break;  // Cycle found
        }
    }
    
    // No cycle
    if (fast == null || fast.next == null) {
        return null;
    }
    
    // Phase 2: Find start of cycle
    // Move slow back to head, keep fast at meeting point
    // Both move 1 step at a time - they meet at cycle start!
    slow = head;
    while (slow != fast) {
        slow = slow.next;
        fast = fast.next;
    }
    
    return slow;  // Start of cycle
}
Step-by-Step Walkthrough
  1. Initialize: Both slow and fast start at the head node.
  2. Phase 1 - Detection: Move slow by 1 step, fast by 2 steps. If they meet, there's a cycle.
  3. Phase 2 - Find Start: Reset slow to head, keep fast at meeting point. Move both by 1 step until they meet again - that's the cycle start!
  4. Why it works: Math magic! Distance from head to cycle start equals distance from meeting point to cycle start (going around the cycle).
Visual: Floyd's Cycle Detection
LINKED LIST WITH CYCLE:

    1 → 2 → 3 → 4 → 5
                ↓   ↑
                6 → 7

PHASE 1: DETECTION (slow=1 step, fast=2 steps)
─────────────────────────────────────────────
Step 0: slow=1, fast=1
Step 1: slow=2, fast=3
Step 2: slow=3, fast=5
Step 3: slow=4, fast=7
Step 4: slow=5, fast=5  ← THEY MEET! Cycle exists.

PHASE 2: FIND CYCLE START
─────────────────────────────────────────────
Reset slow to head, both move 1 step:
  slow=1, fast=5 → slow=2, fast=6 → slow=3, fast=7
  → slow=4, fast=4  ← MEET AT CYCLE START (node 4)!
Why does this work? If cycle starts at distance 'a' from head, and meeting point is 'b' into the cycle, then slow traveled a+b, fast traveled a+b+c+b (c is remaining cycle). Since fast = 2×slow, a = c. So starting from head and meeting point, they meet at cycle start!
02

Merge Two Sorted Lists

Think of merging sorted lists like shuffling two sorted decks of cards into one sorted deck. You always pick the smaller card from the top of either deck and place it in your result pile. This fundamental technique appears everywhere - from merge sort to database operations. Mastering this pattern opens doors to solving many related problems!

Iterative Approach - O(n + m) Time, O(1) Space

The iterative approach uses a dummy node to simplify edge cases. We compare nodes from both lists and always pick the smaller one, building our result list one node at a time.

ListNode mergeTwoLists(ListNode l1, ListNode l2) {
    // Dummy node acts as a placeholder for the result list's head
    // This avoids special handling for the first node
    ListNode dummy = new ListNode(0);
    
    // curr pointer will build the result list
    ListNode curr = dummy;
}

Why use a dummy node? Without it, you would need special code to handle the very first node (since there is no "previous" node to attach to). The dummy node acts as a placeholder - we build the result after it, then return dummy.next to skip the placeholder. This trick eliminates messy if-else conditions and makes your code cleaner!

    // While both lists have nodes to compare
    while (l1 != null && l2 != null) {
        if (l1.val <= l2.val) {
            // l1's value is smaller, link it to result
            curr.next = l1;
            l1 = l1.next;  // Move l1 forward
        } else {
            // l2's value is smaller, link it to result
            curr.next = l2;
            l2 = l2.next;  // Move l2 forward
        }
        curr = curr.next;  // Move result pointer forward
    }

This is the heart of the algorithm. We compare the current nodes of both lists and pick the smaller one. Notice that we are not creating new nodes - we are reusing the existing nodes by changing their next pointers. This is why space complexity is O(1). Think of it like rearranging cards on a table rather than making copies of them!

    // One list is exhausted, attach the rest of the other list
    // No need to iterate - just link the remaining portion!
    curr.next = (l1 != null) ? l1 : l2;
    
    // Return the merged list (skip the dummy node)
    return dummy.next;
}

The cleanup step is simple but clever. When the while loop ends, one list is empty but the other might still have nodes. Since both lists were already sorted, the remaining nodes are guaranteed to be larger than everything we have added so far. We just attach them directly with one pointer assignment - no need to iterate through them one by one!

Recursive Approach - O(n + m) Time, O(n + m) Space

The recursive approach is more elegant but uses O(n + m) stack space due to recursion. Each call handles one node and delegates the rest to the next recursive call.

ListNode mergeTwoListsRecursive(ListNode l1, ListNode l2) {
    // Base cases: if one list is empty, return the other
    if (l1 == null) return l2;
    if (l2 == null) return l1;
    
    // Pick the smaller node and recursively merge the rest
    if (l1.val <= l2.val) {
        // l1 is smaller, so l1.next should point to the merge of (l1.next, l2)
        l1.next = mergeTwoListsRecursive(l1.next, l2);
        return l1;  // l1 becomes the head of this merged portion
    } else {
        // l2 is smaller, so l2.next should point to the merge of (l1, l2.next)
        l2.next = mergeTwoListsRecursive(l1, l2.next);
        return l2;  // l2 becomes the head of this merged portion
    }
}

The recursive approach is elegant but has a trade-off. Each recursive call adds a frame to the call stack, using O(n + m) extra memory. For a list with 10,000 nodes, that is 10,000 stack frames! If the list is very long, you might get a StackOverflowError. The iterative approach above uses only O(1) space, making it safer for large inputs. In interviews, mention this trade-off to show you understand both solutions deeply.

Advanced: Merge K Sorted Lists - O(N log k) Time

When you have K sorted lists (not just 2), comparing all K heads each time would be O(K) per node. Using a min-heap (priority queue) reduces this to O(log K) per node, giving O(N log K) total where N is the total number of nodes.

ListNode mergeKLists(ListNode[] lists) {
    // Min-heap ordered by node value
    PriorityQueue pq = new PriorityQueue<>(
        (a, b) -> a.val - b.val  // Comparator: smaller values first
    );
    
    // Add the head of each non-empty list to the heap
    for (ListNode list : lists) {
        if (list != null) {
            pq.offer(list);
        }
    }
}

Why use a heap (PriorityQueue)? If we had to compare all K list heads manually each time, that would be O(K) per node - too slow! A min-heap automatically keeps the smallest element at the top, and adding/removing takes only O(log K) time. Since the heap only holds one node from each list at a time (at most K nodes), operations stay fast even with many lists.

    ListNode dummy = new ListNode(0);
    ListNode curr = dummy;
    
    while (!pq.isEmpty()) {
        // Get the smallest node from the heap
        ListNode node = pq.poll();
        
        // Add it to our result
        curr.next = node;
        curr = curr.next;
        
        // If this node has a next, add it to the heap
        if (node.next != null) {
            pq.offer(node.next);
        }
    }
    
    return dummy.next;
}

Understanding the time complexity: Every single node across all K lists will be added to the heap once and removed once. If there are N total nodes, that is 2N heap operations. Each heap operation (offer/poll) takes O(log K) time because the heap has at most K elements. So total time = N nodes x O(log K) per operation = O(N log K). This is much better than the naive O(N x K) approach!

Step-by-Step Walkthrough
  1. Dummy Node: Create a "fake" head to simplify edge cases (empty lists, single-node lists).
  2. Compare & Link: Compare values at both lists, link the smaller one to result, advance that list's pointer.
  3. Cleanup: When one list ends, attach the remaining nodes from the other list.
  4. K Lists: Use a min-heap to efficiently find the smallest among K heads in O(log K) time.
Visual: Merging Two Sorted Lists
TWO SORTED LISTS
  List 1:  [1] ──▶ [3] ──▶ [5] ──▶ null
  List 2:  [2] ──▶ [4] ──▶ [6] ──▶ null
STEP 1-2
Compare 1 vs 2 → Take 1
  dummy → [1]
  L1 at 3, L2 at 2

Compare 3 vs 2 → Take 2
  dummy → [1][2]
  L1 at 3, L2 at 4
STEP 3-4
Compare 3 vs 4 → Take 3
  dummy → [1][2][3]
  L1 at 5, L2 at 4

Compare 5 vs 4 → Take 4
  dummy → [1][2][3][4]
  L1 at 5, L2 at 6
FINAL RESULT
  dummy ──▶ [1] ──▶ [2] ──▶ [3] ──▶ [4] ──▶ [5] ──▶ [6] ──▶ null
               
          Return dummy.next!                    ✓ Perfectly Merged!
Key Insight: Always compare current nodes from both lists. Take the smaller one, advance that pointer. The dummy node simplifies building the result!
03

Remove Nth Node from End

Here is a puzzle: How do you find something that is "5 steps from the end" when you do not know where the end is? The naive approach would traverse twice - once to find the length, once to reach the target. But there is a clever trick using two pointers with a fixed gap between them. When the leading pointer hits the end, the trailing pointer is exactly where we need it. One pass, constant space!

Remove Nth Node - O(n) Time, O(1) Space

The key insight is maintaining a fixed gap between two pointers. When the leading pointer reaches the end, the trailing pointer is exactly where we need to perform the deletion.

ListNode removeNthFromEnd(ListNode head, int n) {
    // Dummy node helps when removing head
    ListNode dummy = new ListNode(0);
    dummy.next = head;
    
    ListNode slow = dummy;
    ListNode fast = dummy;
}

Why start both at dummy? Starting at the dummy node (not head) means when we finish, slow will be at the node BEFORE the one we want to delete. This is crucial because to delete a node in a linked list, we need access to the previous node to change its next pointer.

    // Move fast n+1 steps ahead
    for (int i = 0; i <= n; i++) {
        fast = fast.next;
    }

Why n+1 steps (not n)? We move fast n+1 steps to create a gap of n+1 nodes. This way, when fast reaches null (end of list), slow will be exactly one node BEFORE the target. Remember: to delete a node, we need its predecessor!

    // Move both until fast reaches end
    while (fast != null) {
        slow = slow.next;
        fast = fast.next;
    }

The magic of synchronized movement. Both pointers move at the same speed, maintaining the n+1 gap. When fast hits null, slow has traveled (length - n - 1) steps from dummy, landing exactly at the predecessor of the nth node from end.

    // slow is now at node before the one to remove
    slow.next = slow.next.next;
    
    return dummy.next;
}

The deletion is just one line! Since slow points to the predecessor, we simply skip over the target node by pointing slow.next to slow.next.next. We return dummy.next (not head) because the original head might have been the deleted node!

Bonus: Get Nth Node (Without Deleting)

Sometimes you just need to find the nth node from end without deleting it. The approach is similar but simpler - we only need n steps gap (not n+1) since we want the actual node, not its predecessor.

ListNode getNthFromEnd(ListNode head, int n) {
    ListNode slow = head;
    ListNode fast = head;
    
    // Move fast n steps ahead (not n+1!)
    for (int i = 0; i < n; i++) {
        if (fast == null) return null;  // List too short
        fast = fast.next;
    }
}

Notice the difference: Here we move fast only n steps (not n+1), and we start both pointers at head (not dummy). This is because we want to land ON the target node, not before it. The null check handles the edge case where n is larger than the list length.

    // Move both until fast reaches end
    while (fast != null) {
        slow = slow.next;
        fast = fast.next;
    }
    
    return slow;  // nth node from end
}

Same synchronized movement, different result. With an n-step gap (instead of n+1), slow lands directly on the nth node from end. This is useful for problems like "find the middle" or "find kth from end" where you need to access the node's value.

Step-by-Step Walkthrough
  1. Two Pointers, One Gap: Create an n+1 gap between fast and slow pointers.
  2. Move Together: Advance both pointers until fast reaches the end.
  3. Delete: When fast is null, slow is right before the node to delete. Skip it!
  4. Dummy Magic: Dummy node handles the edge case when we need to remove the head itself.
Visual: Remove 2nd Node from End
INITIAL STATE
  dummy ──▶ [1] ──▶ [2] ──▶ [3] ──▶ [4] ──▶ [5] ──▶ null
                                        
                                   Target: 2nd from end
CREATE GAP
Move fast (n+1 = 3) steps ahead

dummy ──▶ [1] ──▶ [2] ──▶ [3] ──▶ [4] ──▶ [5]
                     
 slow               fast

Gap = 3 nodes (n+1)
MOVE TOGETHER
Both move until fast = null

dummy ──▶ [1] ──▶ [2] ──▶ [3] ──▶ [4] ──▶ [5] ──▶ null
                                           
                       slow                fast

slow is BEFORE target!
DELETE NODE
  slow.next = slow.next.next  // Skip node 4!

  dummy ──▶ [1] ──▶ [2] ──▶ [3] ─────────▶ [5] ──▶ null
                                  
                             [4] removed!

  ✓ Result: 1 → 2 → 3 → 5
Key Insight: The n+1 gap ensures slow stops at the predecessor of the target node, enabling easy deletion!
04

Intersection of Two Lists

Picture two roads that merge into one highway. Two cars start on different roads - how do they find where the roads meet? The beautiful solution: each car drives its road, then switches to the other road. They will meet at the intersection because they travel equal total distances! This problem teaches us that sometimes the most elegant solutions come from thinking about the problem differently.

Elegant Approach - O(n + m) Time, O(1) Space

This is one of the most beautiful algorithms in linked list problems. Instead of calculating lengths, we use a clever trick: make both pointers travel the same total distance by switching lists!

ListNode getIntersectionNode(ListNode headA, ListNode headB) {
    if (headA == null || headB == null) return null;
    
    ListNode a = headA;
    ListNode b = headB;
}

Simple setup. We create two pointers, one starting at each list's head. The null check handles the edge case where either list is empty (no intersection possible).

    // When a reaches end, redirect to headB
    // When b reaches end, redirect to headA
    // They will meet at intersection or both become null
    while (a != b) {
        a = (a == null) ? headB : a.next;
        b = (b == null) ? headA : b.next;
    }

The magic trick! When pointer 'a' finishes list A, it jumps to the head of list B. Similarly, 'b' jumps to head of A when it finishes B. This means both pointers travel exactly (lenA + lenB) nodes total. If there is an intersection, they will arrive at it simultaneously because they have traveled the same distance!

    return a;  // Intersection node or null
}

Why does this work? If lists intersect, both pointers meet at the intersection node. If they do not intersect, both pointers become null at the same time (after traveling lenA + lenB each), so we return null. Beautiful!

Alternative: Length-Based Approach

A more intuitive approach: calculate lengths first, align the starting points, then walk together until we find the intersection.

ListNode getIntersectionV2(ListNode headA, ListNode headB) {
    int lenA = getLength(headA);
    int lenB = getLength(headB);
}

First pass: count nodes. We need to know how long each list is. This requires traversing both lists once, which is O(n + m) time.

    // Advance the longer list to align starting points
    while (lenA > lenB) {
        headA = headA.next;
        lenA--;
    }
    while (lenB > lenA) {
        headB = headB.next;
        lenB--;
    }

Align the pointers. If list A has 5 nodes and list B has 3 nodes, we advance A by 2 nodes first. Now both pointers are the same distance from the end (and from the potential intersection point). Think of it like two runners starting a race at different points - we move the ahead runner back so they start together.

    // Move together until intersection (or both null)
    while (headA != headB) {
        headA = headA.next;
        headB = headB.next;
    }
    
    return headA;  // Intersection or null
}

Walk in sync. Now that both pointers are aligned (same distance from intersection), we move them together one step at a time. When they meet, we have found the intersection! If they both become null, there is no intersection.

int getLength(ListNode head) {
    int len = 0;
    while (head != null) {
        len++;
        head = head.next;
    }
    return len;
}

Helper function. A simple traversal to count nodes. Note that we use a local copy of head, so the original pointer is not modified. This is a common helper for many linked list problems.

Step-by-Step Walkthrough
  1. Elegant Approach: Pointer A traverses list A then B. Pointer B traverses list B then A.
  2. Equal Distance: Both travel (lenA + lenB) total. If there's an intersection, they'll meet there!
  3. No Intersection: Both become null at the same time if no intersection exists.
  4. Alternative: Calculate lengths first, advance the longer list, then traverse together.
Visual: Finding Intersection
TWO LISTS MERGING
  List A:  [a1] ──▶ [a2] ──╮
                              ╰──▶ [c1] ──▶ [c2] ──▶ [c3] ──▶ null
  List B:  [b1] ──▶ [b2] ──▶ [b3] ──╯
                              
                        Intersection at c1
POINTER A's JOURNEY
Traverse List A first:
  a1a2c1c2c3null

Switch to List B:
  b1b2b3c1 ✓ STOP!

Total: 2 + 3 + 3 + 1 = 9 steps
POINTER B's JOURNEY
Traverse List B first:
  b1b2b3c1c2c3null

Switch to List A:
  a1a2c1 ✓ STOP!

Total: 3 + 3 + 2 + 1 = 9 steps
THEY MEET!
  Both pointers travel lenA + lenB = 5 + 6 = 11 nodes total

  After 9 steps each:  Pointer A at [c1]  ←─┬─→  Pointer B at [c1]✓ INTERSECTION FOUND!
The Magic: By switching lists, both pointers travel the same total distance, so they arrive at the intersection simultaneously!
05

Palindrome Linked List

A palindrome reads the same forwards and backwards - like "racecar" or "12321". Checking this in a string is easy (just compare first and last characters moving inward), but linked lists only go forward! The trick is to find the middle, reverse the second half, then compare. This combines three patterns you have learned: slow/fast pointers, reversal, and two-pointer comparison.

Check Palindrome - O(n) Time, O(1) Space

This algorithm combines three patterns you have already learned: slow/fast pointers to find the middle, list reversal, and two-pointer comparison. The key insight is that we only need to reverse half the list!

boolean isPalindrome(ListNode head) {
    // Edge cases: empty list or single node is always a palindrome
    if (head == null || head.next == null) return true;
    
    // Find the middle using slow/fast pointers
    ListNode slow = head;
    ListNode fast = head;
    while (fast.next != null && fast.next.next != null) {
        slow = slow.next;
        fast = fast.next.next;
    }
}

Finding the middle is the first step. We use the slow/fast pointer technique where fast moves twice as fast as slow. When fast reaches the end, slow is at the middle. For odd-length lists (like 1-2-3-2-1), slow lands on the center node (3). For even-length lists (like 1-2-2-1), slow lands on the last node of the first half (the first 2).

    // Reverse the second half of the list
    ListNode secondHalf = reverse(slow.next);
    
    // Save reference for restoration later
    ListNode secondHalfCopy = secondHalf;

Reverse only the second half. We call our reverse helper on slow.next (the node after middle). This gives us the second half in reverse order. We save a copy of this pointer because we will move secondHalf during comparison and need the original reference to restore the list later.

    // Compare first half with reversed second half
    ListNode firstHalf = head;
    boolean isPalin = true;
    
    while (secondHalf != null) {
        if (firstHalf.val != secondHalf.val) {
            isPalin = false;
            break;
        }
        firstHalf = firstHalf.next;
        secondHalf = secondHalf.next;
    }

Compare values node by node. We walk through the first half from the beginning and the reversed second half simultaneously. If any values differ, it is not a palindrome. We use a boolean flag instead of returning immediately because we want to restore the list structure before returning (good practice to not leave data structures in a modified state).

    // Restore the list to original structure (optional but good practice)
    slow.next = reverse(secondHalfCopy);
    
    return isPalin;
}

Restore the original structure. By reversing the second half again, we put the list back to its original state. This is optional but considered good practice - the caller might not expect the list to be modified. In interviews, mention this to show you think about side effects!

Helper: Reverse a Linked List

This is the standard iterative reversal that you should memorize. It uses three pointers and runs in O(n) time with O(1) space.

private ListNode reverse(ListNode head) {
    ListNode prev = null;
    
    while (head != null) {
        ListNode next = head.next;  // Save next node
        head.next = prev;           // Reverse the link
        prev = head;                // Move prev forward
        head = next;                // Move head forward
    }
    
    return prev;  // prev is now the new head
}

The classic reversal pattern. Remember the order: save next, reverse link, advance prev, advance head. The key is saving head.next BEFORE changing it, otherwise you lose the reference to the rest of the list. When the loop ends, prev points to what was the last node - now the new head of the reversed list.

Step-by-Step Walkthrough
  1. Find Middle: Use slow/fast pointers. When fast reaches end, slow is at middle.
  2. Reverse Second Half: Reverse the list starting from middle.next.
  3. Compare: Walk through first half and reversed second half, comparing values.
  4. Restore (Optional): Reverse the second half again to restore the original list structure.
Visual: Palindrome Check (1→2→3→2→1)
ORIGINAL LIST
  [1] ──▶ [2] ──▶ [3] ──▶ [2] ──▶ [1] ──▶ null
                      
                   middle
FIND MIDDLE
slow moves 1 step, fast moves 2 steps

Start: S,F at [1]
Step 1: S→[2], F→[3]
Step 2: S→[3], F→[1] (end!)

slow is at middle node [3]
REVERSE SECOND HALF
Reverse everything after middle:

Before: [3] → [2] → [1] → null
After:  [3]   [1] → [2] → null

First half:  1 → 2 → 3
Second half: 1 → 2 (reversed!)
COMPARE VALUES
  First half:     [1] ──▶ [2] ──▶ [3]
                           
                           
  Second half:    [1] ──▶ [2]

  Compare: 1 == 1    2 == 2    IT'S A PALINDROME!
Key Insight: By reversing the second half, we can compare both ends moving inward - just like checking a string palindrome!
06

Reorder List

Imagine folding a linked list in half and interleaving the two halves like shuffling cards. This problem asks you to transform a list from its original order to an alternating pattern that picks from both ends. It combines all three core patterns: find middle, reverse, and merge.

The Transformation
BEFORE L0 L1 L2 ... Ln
AFTER L0 Ln L1 Ln-1 ...
Alternates between front and back of the original list

Reorder List - O(n) Time, O(1) Space

This problem is solved in three steps: find the middle, reverse the second half, then interleave the two halves. Each step uses a pattern you have already learned!

void reorderList(ListNode head) {
    // Edge cases: nothing to reorder
    if (head == null || head.next == null) return;
    
    // Find the middle using slow/fast pointers
    ListNode slow = head, fast = head;
    while (fast.next != null && fast.next.next != null) {
        slow = slow.next;
        fast = fast.next.next;
    }
}

Finding the middle splits the list into two halves. We use the familiar slow/fast pointer technique. When the loop ends, slow is at the middle node. For a list 1-2-3-4-5, slow will be at node 3. For a list 1-2-3-4, slow will be at node 2. This positioning ensures the first half is equal or one node longer than the second half.

    // Reverse the second half of the list
    ListNode second = reverse(slow.next);
    
    // Cut the list into two separate lists
    slow.next = null;

Reverse and disconnect. We reverse everything after the middle node, so 4-5 becomes 5-4. Then we set slow.next = null to cut the list into two separate lists. This is important because otherwise we would have a cycle when we start interleaving! Now we have: first = 1-2-3-null and second = 5-4-null.

    // Interleave: merge first and second half alternately
    ListNode first = head;
    
    while (second != null) {
        // Save next pointers before we modify them
        ListNode temp1 = first.next;
        ListNode temp2 = second.next;
        
        // Weave second node after first node
        first.next = second;
        second.next = temp1;
        
        // Move to next pair
        first = temp1;
        second = temp2;
    }
}

The interleaving is like shuffling cards. We take one node from the first half, then one from the second half, alternating until done. The key is saving both temp1 and temp2 before modifying any pointers. After each iteration: first.next points to a node from second, and that node points to what was first.next. We stop when second becomes null (the second half is always equal or shorter).

Bonus: Add Two Numbers

A classic interview problem where two numbers are represented as linked lists with digits in reverse order (123 is stored as 3-2-1). We add them digit by digit, handling carries just like grade-school addition.

ListNode addTwoNumbers(ListNode l1, ListNode l2) {
    ListNode dummy = new ListNode(0);
    ListNode curr = dummy;
    int carry = 0;
}

Setup with dummy node and carry. The dummy node simplifies building the result list. The carry variable holds any overflow from the previous digit addition (just like when 7+5=12, we write 2 and carry the 1).

    while (l1 != null || l2 != null || carry != 0) {
        int sum = carry;
        
        if (l1 != null) {
            sum += l1.val;
            l1 = l1.next;
        }
        if (l2 != null) {
            sum += l2.val;
            l2 = l2.next;
        }
        
        carry = sum / 10;
        curr.next = new ListNode(sum % 10);
        curr = curr.next;
    }
    
    return dummy.next;
}

Process digit by digit. The loop continues while there are digits in either list OR there is a remaining carry. We add the carry first, then add digits from l1 and l2 if they exist. The new digit is sum % 10 (the ones place), and the new carry is sum / 10 (the tens place). For example: 7+8+1(carry)=16, so new digit is 6, new carry is 1.

Step-by-Step Walkthrough
  1. Find Middle: Use slow/fast technique to find the midpoint of the list.
  2. Split & Reverse: Cut the list in half and reverse the second half.
  3. Interleave: Merge the two halves by alternating nodes from each list.
  4. Add Numbers: Process digit by digit with carry, similar to grade-school addition.
Visual: Reorder List
ORIGINAL LIST
  [1] ──▶ [2] ──▶ [3] ──▶ [4] ──▶ [5] ──▶ null
   L₀      L₁      L₂      L₃      L₄
FIND MIDDLE & SPLIT
First half:
  [1] ──▶ [2] ──▶ [3] ──▶ null

Second half:
  [4] ──▶ [5] ──▶ null
REVERSE SECOND HALF
Before: [4] ──▶ [5]
              ↓ reverse
After:  [5] ──▶ [4] ──▶ null
         L₄      L₃
INTERLEAVE (MERGE ALTERNATELY)
  [1] ──▶ [5] ──▶ [2] ──▶ [4] ──▶ [3] ──▶ null
   L₀      L₄      L₁      L₃      L₂

  Pattern: frontbackfrontbackfront  ✓ Perfect!
Key Insight: By reversing the second half, we can easily alternate between front and back elements in a single pass!

Common Mistakes & How to Avoid Them

Even experienced developers fall into these traps when solving linked list problems. Learn to recognize and avoid them to write bug-free code in interviews!

Bug #1: Not Saving Next

When reversing, if you change curr.next = prev before saving curr.next, you lose the rest!

Wrong
curr.next = prev;
curr = curr.next;
Correct
next = curr.next;
curr.next = prev;
curr = next;

Bug #2: Wrong Loop Condition

Using fast != null alone causes NullPointerException when checking fast.next.

Crashes!
while (fast.next != null)
Safe
while (fast != null 
    && fast.next != null)

Bug #3: Forgetting Dummy Node

When the head might be removed or changed, special handling is needed. Dummy node eliminates this!

Messy
if (head.val == target)
  return head.next;
// Different logic...
Clean
dummy.next = head;
// Same logic for all!
return dummy.next;

Bug #4: Off-by-One Error

Should fast move n steps or n+1 steps? Depends on whether you need the node or its predecessor!

To FIND nth
n steps
To REMOVE nth
n+1 steps

Bug #5: Not Handling Edge Cases

Many linked list operations fail on edge cases. Always check these first!

public ListNode solve(ListNode head) {
    if (head == null) return null;           // Edge case 1: Empty list
    if (head.next == null) return head;      // Edge case 2: Single node
    // Now safe to access head.next, use two pointers, etc.
}
Interview Tip: Always ask "What if the list is empty?" and "What if there's only one node?" before coding!

Pre-Coding Checklist

Before writing any linked list solution, ask yourself:

Empty list? head == null
Single node? head.next == null
Two nodes? Does pointer logic work?
Head changes? Need dummy node?
Saving refs? Store next before modify?
Loop condition? Check null before .next?

Pattern Recognition Guide

When you see a linked list problem, use this guide to quickly identify which pattern to apply. Most problems are variations of these core patterns!

If the Problem Says... Think of This Pattern Key Technique
"Find middle", "Split in half" Slow/Fast Pointers Fast moves 2x speed of slow
"Detect cycle", "Find cycle start" Floyd's Algorithm Slow/fast meet in cycle, then reset one to head
"Nth from end", "Remove nth" Two Pointer Gap Maintain n-node gap between pointers
"Reverse", "Swap nodes" Prev/Curr/Next Save next, reverse link, advance pointers
"Merge sorted", "Interleave" Dummy Node + Compare Dummy simplifies head handling, compare and link
"Palindrome", "Compare halves" Find Middle + Reverse Half Split at middle, reverse second half, compare
"Intersection", "Meeting point" Switch Lists Trick Each pointer traverses both lists to equalize distance
"Reorder", "Weave", "Zip" Split + Reverse + Merge Combine multiple patterns
Pro Tip: Most "Hard" linked list problems are just combinations of these patterns. For example, "Reverse Nodes in k-Group" combines counting, reversal, and pointer tracking. Master each pattern individually first!

Interactive: Floyd's Cycle Detection

Watch the tortoise (slow) and hare (fast) race through a linked list with a cycle. See how they eventually meet, proving the cycle exists!

Click "Start Demo" to watch Floyd's Algorithm in action!

List with cycle:
[1] → [2] → [3] → [4] → [5]
                  ↓       ↑
                  [6] → [7]

Slow (🐢) moves 1 step per iteration
Fast (🐇) moves 2 steps per iteration
                    
Controls
Slower ← → Faster
Current State:

Step: 0

Slow (🐢): Node 1

Fast (🐇): Node 1

Practice Problems

Master Linked List Patterns

Solidify your understanding with these carefully selected problems. Start with Easy, then progress to Medium and Hard. Each problem reinforces the patterns you've learned!

Problem: Given the head of a singly linked list, reverse the list and return the reversed list.

Key Pattern: Three-pointer technique (prev, curr, next). This is the foundation for many advanced problems!

Hint: At each step: save next, point current to prev, move prev and curr forward.

View Solution Approach
// Iterative: O(n) time, O(1) space
ListNode prev = null, curr = head;
while (curr != null) {
    ListNode next = curr.next;
    curr.next = prev;
    prev = curr;
    curr = next;
}
return prev;
LeetCode #206

Problem: Given the head of a singly linked list, return the middle node. If two middle nodes, return the second one.

Key Pattern: Slow/fast pointer - when fast reaches end, slow is at middle!

Hint: Fast moves 2x speed of slow. Works like a ruler measuring half the distance.

View Solution Approach
// Fast & Slow Pointer: O(n) time, O(1) space
ListNode slow = head, fast = head;
while (fast != null && fast.next != null) {
    slow = slow.next;
    fast = fast.next.next;
}
return slow;
LeetCode #876

Problem: Given a linked list, swap every two adjacent nodes and return its head. Must use actual nodes, not values.

Key Pattern: Dummy node + careful pointer manipulation. Draw it out!

Hint: Use a dummy node and track the node before each pair. Think: prev → first → second → rest

View Solution Approach
// Dummy Node Pattern: O(n) time, O(1) space
ListNode dummy = new ListNode(0);
dummy.next = head;
ListNode prev = dummy;

while (prev.next != null && prev.next.next != null) {
    ListNode first = prev.next;
    ListNode second = prev.next.next;
    
    first.next = second.next;
    second.next = first;
    prev.next = second;
    
    prev = first;
}
return dummy.next;
LeetCode #24

Problem: Given the head of a linked list, rotate the list to the right by k places.

Key Pattern: Connect tail to head (make circular), then cut at the right spot!

Hint: First, find length and make it circular. New head is at position (len - k % len).

View Solution Approach
// Make circular, then cut: O(n) time, O(1) space
// 1. Find tail & length
// 2. Connect tail to head (circular)
// 3. Move (len - k % len) steps from head
// 4. Cut: newTail.next = null, return newHead
LeetCode #61

Problem: Given a linked list, reverse the nodes k at a time and return the modified list. Nodes not part of a full group stay as-is.

Key Pattern: Combines reverse, counting, and dummy node patterns. The boss of linked list problems!

Hint: Count k nodes, reverse that segment, connect to previous/next segments. Recursion or iteration both work.

View Solution Approach
// Iterative: O(n) time, O(1) space
// 1. Count if k nodes exist
// 2. Reverse k nodes using standard reversal
// 3. Connect: prevGroupTail → currGroupHead
// 4. Move to next group, repeat
LeetCode #25

Problem: A linked list where each node has a next pointer and a random pointer to any node. Create a deep copy of the list.

Key Pattern: HashMap to map original nodes to copies, OR interleave copies between originals.

Hint: With HashMap: first pass creates nodes, second pass sets pointers. Without HashMap: insert copies after originals, set random, then separate lists.

View Solution Approach
// Interleaving (O(1) space):
// 1. Insert copy after each original: A→A'→B→B'→C→C'
// 2. Set random: copy.random = original.random.next
// 3. Separate lists: restore original, extract copy
LeetCode #138

Problem: Two non-negative integers represented as linked lists (digits reversed). Return their sum as a linked list.

Key Pattern: Simulate addition with carry, like grade-school math.

Hint: Use dummy node. Loop while l1, l2, or carry exists. Create node for (l1.val + l2.val + carry) % 10.

View Solution Approach
// Carry simulation: O(max(m,n)) time, O(1) space
ListNode dummy = new ListNode(0);
ListNode curr = dummy;
int carry = 0;

while (l1 != null || l2 != null || carry != 0) {
    int sum = carry;
    if (l1 != null) { sum += l1.val; l1 = l1.next; }
    if (l2 != null) { sum += l2.val; l2 = l2.next; }
    curr.next = new ListNode(sum % 10);
    carry = sum / 10;
    curr = curr.next;
}
return dummy.next;
LeetCode #2

Problem: Delete all nodes that have duplicate numbers in a sorted list, leaving only distinct numbers.

Key Pattern: Dummy node + predecessor tracking.

Hint: Keep a predecessor pointer. When duplicates found, skip all of them. Dummy node handles if head has duplicates.

View Solution Approach
// Predecessor pattern: O(n) time, O(1) space
ListNode dummy = new ListNode(0, head);
ListNode pred = dummy;

while (head != null) {
    if (head.next != null && head.val == head.next.val) {
        // Skip all duplicates
        while (head.next != null && head.val == head.next.val) {
            head = head.next;
        }
        pred.next = head.next; // Skip all duplicates
    } else {
        pred = pred.next;
    }
    head = head.next;
}
return dummy.next;
LeetCode #82

Time & Space Complexity Reference

Understanding complexity is crucial for interviews. Here is a quick reference for all the problems we covered. Notice how most achieve O(n) time and O(1) space - that is the beauty of linked list algorithms!

Problem Time Space Key Insight
Detect Cycle O(n) O(1) Fast pointer catches slow in cycle
Find Cycle Start O(n) O(1) Reset one pointer, meet at start
Merge Two Lists O(n+m) O(1) Reuse existing nodes
Merge K Lists (heap) O(N log k) O(k) Heap maintains k heads
Remove Nth from End O(n) O(1) Gap between pointers
Intersection Point O(n+m) O(1) Switch lists to equalize distance
Palindrome Check O(n) O(1) Reverse second half in-place
Reorder List O(n) O(1) Split, reverse, interleave
Add Two Numbers O(max(n,m)) O(1)* *Output not counted
Reverse in k-Groups O(n) O(1) Each node visited twice max
Time Complexity Tips
  • O(n): Traverse list once or twice (constant passes)
  • O(n log k): Using heap with k elements
  • O(n²): Nested loops - usually can be optimized!
  • Amortized: Average over many operations
Space Complexity Tips
  • O(1): Only using pointers (slow, fast, prev, etc.)
  • O(n): Using HashMap, stack, or recursion
  • Output space: Usually not counted in interviews
  • Recursion: Call stack counts as O(n) space!
Interview Gold: When the interviewer asks "Can you do better?", check if you are using extra space (HashMap, stack). Most linked list problems have O(1) space solutions using clever pointer manipulation!

Quick Code Templates

Copy these templates into your interviews as starting points. They handle common patterns and edge cases, so you can focus on the problem-specific logic.

Slow/Fast Pointer Template
ListNode slow = head, fast = head;

// For finding middle (second middle for even):
while (fast != null && fast.next != null) {
    slow = slow.next;
    fast = fast.next.next;
}
// slow is now at middle

// For cycle detection:
while (fast != null && fast.next != null) {
    slow = slow.next;
    fast = fast.next.next;
    if (slow == fast) return true;
}
return false;
Dummy Node Template
// Use when head might change
ListNode dummy = new ListNode(0);
dummy.next = head;

ListNode curr = dummy;
while (curr.next != null) {
    // Process curr.next
    // curr.next = curr.next.next to delete
    curr = curr.next;
}

return dummy.next; // New head
Reverse List Template
ListNode reverse(ListNode head) {
    ListNode prev = null;
    ListNode curr = head;
    
    while (curr != null) {
        ListNode next = curr.next; // Save
        curr.next = prev;          // Reverse
        prev = curr;               // Advance
        curr = next;
    }
    
    return prev; // New head
}
N-th From End Template
// Remove nth from end
ListNode dummy = new ListNode(0);
dummy.next = head;
ListNode fast = dummy, slow = dummy;

// Move fast n+1 ahead
for (int i = 0; i <= n; i++) {
    fast = fast.next;
}

// Move both until fast = null
while (fast != null) {
    slow = slow.next;
    fast = fast.next;
}

slow.next = slow.next.next;
return dummy.next;
Pro Tip: Practice writing these templates from memory until they become second nature. In an interview, you should be able to write the basic structure without thinking, so you can focus on the problem-specific details.

Interview Day Checklist

Use this checklist during your interview to make sure you cover all bases. Interviewers love candidates who think systematically!

Before Coding
While Coding
Common Bugs to Avoid
  • Forgetting to handle head == null
  • Not saving next before modifying pointer
  • Wrong loop condition (fast != null && fast.next != null)
  • Off-by-one error in nth from end
  • Returning head instead of dummy.next
  • Creating infinite loop by not cutting connections

Key Takeaways

Floyd's Algorithm

Slow/fast pointers for cycle detection, finding middle, and cycle start.

Dummy Node Pattern

Use dummy node to handle edge cases when head might change.

Reverse Half

Many problems (palindrome, reorder) use: find middle → reverse second half.

Two-Pointer Gap

Keep n-gap between pointers to find nth from end in one pass.

Intersection Trick

Two pointers switching lists travel equal distance to meet at intersection.

Draw It Out

Always visualize pointer movements on paper before coding linked list solutions.

Knowledge Check

Question 1 of 6

In Floyd's cycle detection, how much faster does the fast pointer move compared to slow?

Question 2 of 6

What is the purpose of using a dummy node in linked list problems?

Question 3 of 6

To check if a linked list is a palindrome in O(1) space, what's the approach?

Question 4 of 6

What's the time complexity of merging K sorted linked lists using a min-heap?

Question 5 of 6

After detecting a cycle, how do you find the cycle start node?

Question 6 of 6

What is the space complexity of reversing a linked list iteratively?