Week 3: Linked Lists & Fast/Slow Pointers
1. Overviewβ
Welcome to Week 3! After spending two weeks mastering arrays and strings (contiguous memory), we are now moving to Linked Lists (scattered memory). This week is fundamentally about mastering object references in Java. You will learn how to stitch data together across the heap, how to safely delete elements without memory leaks, and how to detect cycles using the famous Fast & Slow pointer technique.
Goals for this week:
- Understand how nodes are stored in the JVM Heap and how Garbage Collection works.
- Master the "Dummy Head" pattern to eliminate edge cases when modifying lists.
- Learn Floyd's Tortoise and Hare algorithm for cycle detection and finding midpoints.
Knowledge You Need Before Startingβ
- Week 1-2 comfort with pointers-as-indices and single-pass thinking.
- Java references and object mutation basics (
a = bshares the same node object). - Null-handling discipline to avoid
NullPointerException. - Confidence tracing pointer updates step-by-step on paper.
2. Theory & Fundamentalsβ
2.1 Mental Model: Arrays vs. Linked Lists in Memoryβ
This is the single most important conceptual shift of Week 3. Understanding why the memory layout differs explains every performance trade-off that follows.
Array in memory β one contiguous block:
Heap:
ββββββ¬βββββ¬βββββ¬βββββ¬βββββ
β 10 β 20 β 30 β 40 β 50 β
ββββββ΄βββββ΄βββββ΄βββββ΄βββββ
0x100 0x104 0x108 0x10C 0x110
To get element[3]: base_addr + 3 * 4 = 0x100 + 12 = 0x10C β O(1) β
Linked List in memory β scattered nodes:
Heap:
0x200 0x8A0 0x431 0x7F2
ββββββββββββ ββββββββββββ ββββββββββββ ββββββββββββ
β val: 10 β β val: 20 β β val: 30 β β val: 40 β
β next:8A0 ββββΆβ next:431 ββββΆβ next:7F2 ββββΆβ next:nullβ
ββββββββββββ ββββββββββββ ββββββββββββ ββββββββββββ
To get node[3]: must walk 0x200 β 0x8A0 β 0x431 β 0x7F2 β O(N) β
Key takeaway: A Linked List trades random access speed for flexible insertion/deletion. There is no way to jump to the 3rd node without walking through the first two β the address of node i is not mathematically predictable.
2.2 Linked Lists vs. Arrays β Full Comparisonβ
| Operation | int[] / ArrayList | Singly Linked List |
|---|---|---|
| Access element by index | O(1) | O(N) |
| Insert / Delete at head | O(N) (shift all) | O(1) β
|
| Insert / Delete at tail | O(1) amortized | O(N) (must walk to tail)* |
| Insert / Delete in middle | O(N) (shift) | O(1) if you have the prev node |
| Search (unsorted) | O(N) | O(N) |
| Memory overhead | Low (just values) | Higher (value + reference per node) |
| Cache friendliness | β Excellent | β Poor (nodes scattered) |
*O(1) tail insert if you maintain a tail reference (common in practice).
When to prefer a Linked List over an Array:
- Frequent insertions/deletions at the head (e.g., implementing a Stack or Queue).
- Size changes dramatically and unpredictably.
- You never need random access by index.
2.3 Singly vs. Doubly Linked Listsβ
Singly Linked:
head β [A|next] β [B|next] β [C|next] β null
β can only go forward
Doubly Linked:
head β [null|A|next] β [prev|B|next] β [prev|C|null] β tail
β can go forward AND backward
| Singly | Doubly | |
|---|---|---|
| Memory per node | val + 1 ref | val + 2 refs |
| Traverse backward | β | β |
| Delete a node (with reference to it) | β Need prev | β
Use node.prev |
| Use cases | Most LeetCode problems, stacks | LRU Cache, Browser history, Deque |
2.4 Java Specifics: References, Not Pointersβ
Java does not expose raw memory addresses. Instead, variables hold references (think of them as a label pointing to an object in the heap). This creates a common misconception:
ListNode a = new ListNode(1);
ListNode b = a; // b is NOT a copy of the node β both labels point to SAME object
b.val = 99;
System.out.println(a.val); // prints 99! Changing via b changes the same object.
Visualized:
Before: a βββ
βΌ
b βββββββββββββΆ [val:1 | next:null] (ONE object, TWO references)
After b.val = 99:
a βββ
βΌ
b βββΆ [val:99 | next:null]
This is why pointer manipulation works in Java: when you do curr.next = curr.next.next, you are changing which object curr.next points to β you are not copying anything.
Garbage Collection β Java's safety net:
In C/C++, you must manually free() unlinked nodes or you leak memory. In Java, once a node has zero references pointing to it, the JVM Garbage Collector (GC) automatically reclaims its memory.
// Before: dummy β [1] β [2] β [3] β null
curr.next = curr.next.next; // [2] is now unreferenced
// After: dummy β [1] β [3] β null
// [2] is floating with zero references β GC will clean it up
Interview Tip: If an interviewer asks about memory management, mention that Java's GC handles deallocation automatically via reference counting / mark-and-sweep. You don't need to worry about leaks, but you should avoid unnecessarily holding references (e.g., don't keep a reference to a detached sublist if you don't need it).
2.5 The Dummy Head Pattern β Why It Existsβ
The most common source of bugs in linked list problems is handling the head node as a special case. The Dummy Head pattern eliminates this entirely.
Without Dummy Head β messy edge cases:
// Removing all nodes with value 0 from: 0 β 0 β 1 β 0 β 2
public ListNode removeElements(ListNode head, int val) {
// Special case: skip all leading 0s at the head
while (head != null && head.val == val) {
head = head.next;
}
// Now handle the rest... (more code needed)
}
With Dummy Head β uniform treatment of all nodes:
public ListNode removeElements(ListNode head, int val) {
ListNode dummy = new ListNode(-1);
dummy.next = head; // dummy sits before the real head
ListNode curr = dummy;
while (curr.next != null) {
if (curr.next.val == val) {
curr.next = curr.next.next; // skip the target node
} else {
curr = curr.next;
}
}
return dummy.next; // real head (might have changed)
}
The dummy node acts as a stable anchor. Since curr always points to the node before the one being considered, you can remove any node (including the original head) using the same logic.
When to always use a Dummy Head:
- Merging two sorted lists.
- Removing specific elements (the head might be one of them).
- Any problem where the resulting list's head could differ from the input's head.
2.6 Floyd's Tortoise and Hare β The Math Behind Itβ
Why does fast moving at 2x speed guarantee it catches slow?
Imagine a circular running track. If you run at 2 mph and your friend runs at 1 mph starting from the same point, you will inevitably lap them. In a cyclic linked list, the "fast" pointer similarly gains 1 step on "slow" per iteration, so it will always catch up.
List with cycle: 1 β 2 β 3 β 4 β 5 β 6
β_____________|
cycle starts at node 4
Step 0: slow=1, fast=1
Step 1: slow=2, fast=3
Step 2: slow=3, fast=5
Step 3: slow=4, fast=3 (fast wrapped around via cycle)
Step 4: slow=5, fast=5 β MEET (cycle detected!)
Finding the cycle entry point (Floyd's Phase 2):
After slow and fast meet inside the cycle, reset slow to head. Then move both at 1x speed. They will meet exactly at the cycle's entry node.
The mathematical proof (simplified):
Let:
F= distance from head to cycle entryC= cycle lengthh= distance from cycle entry to meeting point
When they meet: slow has traveled F + h, fast has traveled F + h + C (one extra loop).
Since fast moves 2x: 2(F + h) = F + h + C β F + h = C β F = C - h.
C - h is the remaining distance from the meeting point back to the cycle entry. So if slow resets to head (distance F from entry) and fast stays at meeting point (distance C - h = F from entry), they walk the same number of steps to reach the entry β guaranteed to collide there.
3. Code Templates (Java)β
The Standard ListNode Classβ
public class ListNode {
int val;
ListNode next;
ListNode() {}
ListNode(int val) { this.val = val; }
ListNode(int val, ListNode next) { this.val = val; this.next = next; }
}
Template 1: Basic Traversalβ
// Always use this null-check pattern
ListNode curr = head;
while (curr != null) {
// process curr.val
curr = curr.next;
}
Template 2: The Dummy Head Patternβ
public ListNode manipulateList(ListNode head) {
ListNode dummy = new ListNode(-1);
dummy.next = head;
ListNode curr = dummy;
while (curr.next != null) {
if (shouldSkip(curr.next)) {
curr.next = curr.next.next; // detach the node, GC handles cleanup
} else {
curr = curr.next;
}
}
return dummy.next; // real head (may have changed from original)
}
Template 3: Iterative List Reversalβ
public ListNode reverseList(ListNode head) {
ListNode prev = null;
ListNode curr = head;
while (curr != null) {
ListNode nextTemp = curr.next; // 1. save what comes next
curr.next = prev; // 2. flip the pointer
prev = curr; // 3. advance prev
curr = nextTemp; // 4. advance curr
}
return prev; // prev is the new head when curr hits null
}
Pointer state walkthrough for 1 β 2 β 3 β null:
Initial: prev=null, curr=1
Step 1: nextTemp=2, 1.next=null, prev=1, curr=2
Step 2: nextTemp=3, 2.next=1, prev=2, curr=3
Step 3: nextTemp=null, 3.next=2, prev=3, curr=null
Return: prev=3 β 3 β 2 β 1 β null β
Template 4: Fast & Slow Pointers β Cycle Detectionβ
public boolean hasCycle(ListNode head) {
ListNode slow = head;
ListNode fast = head;
while (fast != null && fast.next != null) { // always check BOTH for fast
slow = slow.next;
fast = fast.next.next;
if (slow == fast) return true; // reference equality, not .equals()
}
return false;
}
Critical: The while condition must check
fast != null && fast.next != null. If you only checkfast.next != null, you will get a NullPointerException whenfastitself is null.
Template 5: Find Middle Nodeβ
// Returns the SECOND middle node when length is even (LC 876 behavior)
public ListNode findMiddle(ListNode head) {
ListNode slow = head;
ListNode fast = head;
while (fast != null && fast.next != null) {
slow = slow.next;
fast = fast.next.next;
}
return slow;
}
// Returns the FIRST middle node when length is even (useful for palindrome check)
public ListNode findMiddleFirst(ListNode head) {
ListNode slow = head;
ListNode fast = head.next; // fast starts one step ahead
while (fast != null && fast.next != null) {
slow = slow.next;
fast = fast.next.next;
}
return slow;
}
Template 6: Find Cycle Entry (Floyd's Phase 2)β
public ListNode detectCycle(ListNode head) {
ListNode slow = head, fast = head;
// Phase 1: Detect cycle
while (fast != null && fast.next != null) {
slow = slow.next;
fast = fast.next.next;
if (slow == fast) break;
}
if (fast == null || fast.next == null) return null; // no cycle
// Phase 2: Find entry
slow = head; // reset slow to head
while (slow != fast) {
slow = slow.next; // both move 1 step
fast = fast.next;
}
return slow; // cycle entry point
}
Template 7: Remove N-th Node From Endβ
// Give fast a head-start of N steps, then move both until fast hits null
public ListNode removeNthFromEnd(ListNode head, int n) {
ListNode dummy = new ListNode(-1);
dummy.next = head;
ListNode slow = dummy;
ListNode fast = dummy;
// Fast gets N+1 steps ahead (so slow stops at the node BEFORE the target)
for (int i = 0; i <= n; i++) fast = fast.next;
while (fast != null) {
slow = slow.next;
fast = fast.next;
}
slow.next = slow.next.next; // remove target
return dummy.next;
}
Template 8: Merge Two Sorted Listsβ
public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
ListNode dummy = new ListNode(-1);
ListNode curr = dummy;
while (l1 != null && l2 != null) {
if (l1.val <= l2.val) {
curr.next = l1;
l1 = l1.next;
} else {
curr.next = l2;
l2 = l2.next;
}
curr = curr.next;
}
curr.next = (l1 != null) ? l1 : l2; // attach remaining nodes
return dummy.next;
}
4. Problem-Solving Frameworkβ
Step 1 β Clarify (2 minutes)β
Before writing anything, ask:
- Is this a singly or doubly linked list?
- Can I modify the node values, or only rewire
.nextpointers? - Can I use extra memory (HashSet, Stack), or must it be
O(1)space? - What should be returned if
headisnull? - Can the list have duplicate values?
- For cycle problems: is there guaranteed to be a cycle?
Step 2 β Draw the Before/After Stateβ
Linked list problems are pointer problems. Always draw the list state before and after your operation. Trying to reason about pointer changes in your head leads to bugs.
Problem: Remove node with value 3 from 1 β 2 β 3 β 4 β 5
Before: dummy β 1 β 2 β 3 β 4 β 5 β null
β curr.next (this is what we want to remove)
After: dummy β 1 β 2 β 4 β 5 β null
β curr
Operation: curr.next = curr.next.next
Step 3 β Identify the Patternβ
| What the problem says | Pattern |
|---|---|
| Reverse the list / Reverse a sublist | Iterative reversal (prev/curr/nextTemp) |
| Find middle / N-th from end | Fast & Slow pointers |
| Detect / find entry of cycle | Floyd's algorithm (two phases) |
| Merge / Remove nodes / Head might change | Dummy Head |
| Palindrome check | Find middle + Reverse second half + Compare |
| Add numbers as lists | Dummy Head + carry variable |
| Deep copy with random pointers | HashMap (node β clone) or interleave technique |
Step 4 β Code Carefully, One Pointer at a Timeβ
Never move multiple pointers in a single line without saving next first. The most common bugs are:
- Lost reference (forgot to save
nextTempbefore overwriting.next). - NullPointerException (accessed
.nexton anullnode). - Infinite loop (forgot to advance
currin one branch of an if-else).
Step 5 β Trace With a Short Exampleβ
After coding, manually trace through a list of 3-4 nodes including edge cases:
- Empty list (
head == null) - Single node
- Two nodes
- The target is the head or tail
5. Pattern Recognition Guideβ
Signal β Pattern Mappingβ
| Problem signal | Pattern to use | Key idea |
|---|---|---|
| "Reverse the whole list" | Iterative reversal | Track prev, curr, nextTemp |
| "Reverse a sublist [left, right]" | Iterative reversal with anchor | Find the node before left, reverse right-left times |
| "Reverse in K-groups" | Iterative reversal in chunks | Reverse K nodes, attach tail to next group's head |
| "Find middle node" | Fast & Slow (2:1 speed) | When fast reaches end, slow is at middle |
| "N-th node from end" | Fast & Slow (N-step gap) | Start fast N ahead, move both till fast hits null |
| "Detect cycle" | Floyd's Phase 1 | fast gains 1 step per iteration, guaranteed to catch slow |
| "Find cycle entry" | Floyd's Phase 1 + Phase 2 | Reset slow to head, move both at 1x |
| "Head node might be removed" | Dummy Head | dummy.next = head, return dummy.next |
| "Merge two sorted lists" | Dummy Head + two pointers | Pick smaller head each step |
| "Palindrome check" | Find middle + Reverse + Compare | Reverse second half in-place, compare with first |
| "Intersection of two lists" | Two pointers with list-switching | Both pointers traverse both lists, meet at intersection |
| "Copy with random pointers" | HashMap (oldβnew) or interleave | Map each original node to its clone |
Detailed Recognition Walkthroughsβ
Recognizing Floyd's Phase 2 (LC 142 β Cycle Detection II)β
The key insight that is easy to miss: after phase 1, do not reset fast to head. Only reset slow. Then move BOTH at 1 step. The mathematical property guarantees they meet at the cycle entry.
Common mistake: students reset both pointers to head, which just re-runs phase 1 and misses the entry point.
Recognizing the "Palindrome" Pattern (LC 234)β
When you see: "check if a linked list is a palindrome":
- You cannot do random access, so comparing
list[0]withlist[n-1]directly is impossible. - Strategy: find middle β reverse second half in-place β compare node-by-node β (optionally restore the list).
- Do NOT copy all values to an array unless the interviewer says
O(N)space is allowed.
Recognizing the "Intersection" Pattern (LC 160)β
When you see: "find where two lists merge":
- The trick: both pointers travel total length
A + B. If they switch lists at their respective ends, they always meet at the intersection (or both reachnullif no intersection). - This requires zero extra memory and one pass β no length calculation needed.
public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
ListNode a = headA, b = headB;
while (a != b) {
a = (a == null) ? headB : a.next;
b = (b == null) ? headA : b.next;
}
return a; // null if no intersection, else the intersection node
}
6. Common Mistakes & How to Avoid Themβ
Mistake 1: Forgetting to Save nextTemp Before Overwritingβ
// β WRONG β you lose the rest of the list
curr.next = prev;
curr = curr.next; // curr.next is now prev, not the original next!
// β
CORRECT β save next before overwriting
ListNode nextTemp = curr.next;
curr.next = prev;
prev = curr;
curr = nextTemp;
Mistake 2: Wrong While Condition for Fast Pointerβ
// β NullPointerException when fast == null and you call fast.next
while (fast.next != null && fast.next.next != null) { ... }
// What if fast itself is null? Boom!
// β
CORRECT β check fast first, then fast.next
while (fast != null && fast.next != null) { ... }
Mistake 3: Using .equals() Instead of == for Node Comparisonβ
// β WRONG β .equals() compares object content (val, next), not identity
if (slow.equals(fast)) return true;
// β
CORRECT β use == to check if two references point to the SAME object
if (slow == fast) return true;
Mistake 4: Not Using a Dummy Head When the Head Can Changeβ
// β Fragile β requires special handling if head needs to be removed
public ListNode removeElements(ListNode head, int val) {
if (head == null) return null;
if (head.val == val) return removeElements(head.next, val); // messy recursion
// β
Clean β dummy handles all nodes uniformly
public ListNode removeElements(ListNode head, int val) {
ListNode dummy = new ListNode(-1);
dummy.next = head;
ListNode curr = dummy;
while (curr.next != null) {
if (curr.next.val == val) curr.next = curr.next.next;
else curr = curr.next;
}
return dummy.next;
}
Mistake 5: Infinite Loop β Forgetting to Advance currβ
// β Infinite loop if curr.next.val != val β curr never advances
while (curr.next != null) {
if (curr.next.val == val) {
curr.next = curr.next.next;
}
// forgot: else curr = curr.next;
}
// β
Always advance curr in the else branch
while (curr.next != null) {
if (curr.next.val == val) {
curr.next = curr.next.next;
} else {
curr = curr.next; // critical!
}
}
Mistake 6: Off-by-One in N-th From Endβ
// For "Remove N-th node from end", you need slow to stop at the node
// BEFORE the target, not AT the target.
// Give fast N+1 steps head start (not N), using dummy as starting point.
ListNode dummy = new ListNode(-1);
dummy.next = head;
ListNode slow = dummy; // β start at dummy, not head
ListNode fast = dummy;
for (int i = 0; i <= n; i++) fast = fast.next; // N+1 steps
// Now slow is one step before target when fast hits null
7. Worked Examplesβ
Example 1: LeetCode 206 β Reverse Linked Listβ
Thinking process:
- "I need to flip all
.nextpointers." - To reverse
A β B β C β nulltonull β A β B β C, each node's.nextmust point to its predecessor. - I need 3 pointers:
prev(predecessor),curr(current),nextTemp(to not lose the rest).
class Solution {
public ListNode reverseList(ListNode head) {
ListNode prev = null;
ListNode curr = head;
while (curr != null) {
ListNode nextTemp = curr.next; // save
curr.next = prev; // flip
prev = curr; // advance prev
curr = nextTemp; // advance curr
}
return prev; // prev is the new head
}
}
Time: O(N). Space: O(1).
Example 2: LeetCode 876 β Middle of the Linked Listβ
Thinking process:
- "I don't know the length, so I can't compute
length / 2." - "If fast moves 2x and slow moves 1x, when fast finishes, slow is exactly halfway."
- Even length edge case:
1 β 2 β 3 β 4: fast ends at null, slow ends at3(second middle) β .
class Solution {
public ListNode middleNode(ListNode head) {
ListNode slow = head, fast = head;
while (fast != null && fast.next != null) {
slow = slow.next;
fast = fast.next.next;
}
return slow;
}
}
Dry run for 1 β 2 β 3 β 4 β 5:
| Step | slow | fast |
|---|---|---|
| 0 | 1 | 1 |
| 1 | 2 | 3 |
| 2 | 3 | 5 |
| 3 | β | fast.next=null, stop |
Return slow = 3 β
Time: O(N). Space: O(1).
Example 3: LeetCode 234 β Palindrome Linked Listβ
Thinking process:
- "I need to compare first half with second half, but can't do random access."
- "Find middle β reverse second half β compare β done."
- Edge case: restore the list? The problem doesn't require it, but mention it for production code.
class Solution {
public boolean isPalindrome(ListNode head) {
// Step 1: Find the end of first half
ListNode slow = head, fast = head;
while (fast.next != null && fast.next.next != null) {
slow = slow.next;
fast = fast.next.next;
}
// slow is now the last node of the first half
// Step 2: Reverse the second half
ListNode secondHalfHead = reverseList(slow.next);
// Step 3: Compare both halves
ListNode p1 = head, p2 = secondHalfHead;
boolean isPalin = true;
while (p2 != null) { // second half is shorter or equal
if (p1.val != p2.val) { isPalin = false; break; }
p1 = p1.next;
p2 = p2.next;
}
// Step 4 (good practice): Restore the list
slow.next = reverseList(secondHalfHead);
return isPalin;
}
private ListNode reverseList(ListNode head) {
ListNode prev = null, curr = head;
while (curr != null) {
ListNode next = curr.next;
curr.next = prev;
prev = curr;
curr = next;
}
return prev;
}
}
Time: O(N). Space: O(1).
Example 4: LeetCode 142 β Linked List Cycle II (Floyd's Full Algorithm)β
Thinking process:
- "Detect cycle first (Phase 1). Find where they meet."
- "Reset slow to head. Move both at 1x speed (Phase 2)."
- "Math guarantees they meet at the cycle entry."
class Solution {
public ListNode detectCycle(ListNode head) {
ListNode slow = head, fast = head;
// Phase 1: Find meeting point
while (fast != null && fast.next != null) {
slow = slow.next;
fast = fast.next.next;
if (slow == fast) break;
}
if (fast == null || fast.next == null) return null;
// Phase 2: Find cycle entry
slow = head; // only reset slow
while (slow != fast) {
slow = slow.next;
fast = fast.next; // both move 1 step now
}
return slow;
}
}
Time: O(N). Space: O(1).
Example 5: LeetCode 143 β Reorder Listβ
Problem: Given 1 β 2 β 3 β 4 β 5, reorder to 1 β 5 β 2 β 4 β 3.
Thinking process:
- "The pattern is: first, last, second, second-last..." β merge first half with reversed second half.
- Three steps: Find middle β Reverse second half β Merge the two halves.
class Solution {
public void reorderList(ListNode head) {
if (head == null || head.next == null) return;
// Step 1: Find middle (use first-middle variant)
ListNode slow = head, fast = head.next;
while (fast != null && fast.next != null) {
slow = slow.next;
fast = fast.next.next;
}
// slow is the last node of the first half
// Step 2: Reverse second half
ListNode secondHalf = reverseList(slow.next);
slow.next = null; // cut the list in half!
// Step 3: Merge (interleave)
ListNode first = head, second = secondHalf;
while (second != null) {
ListNode tmp1 = first.next;
ListNode tmp2 = second.next;
first.next = second;
second.next = tmp1;
first = tmp1;
second = tmp2;
}
}
private ListNode reverseList(ListNode head) { /* same as above */ }
}
Time: O(N). Space: O(1).
8. Decision Tree: Which Linked List Pattern?β
Is the list's head likely to change (or be removed)?
βββ YES β Use a DUMMY HEAD node
Does the problem involve finding position?
βββ Middle of list β Fast (2x) & Slow (1x)
βββ N-th from end β Fast gets N-step head start, then both 1x
βββ Cycle entry point β Floyd's Phase 1 + Phase 2
Does the problem involve cycle detection?
βββ "Does a cycle exist?" β Floyd's Phase 1 only (hasCycle)
βββ "Where does cycle start?" β Floyd's Phase 1 + Phase 2 (detectCycle)
Does the problem involve reversing?
βββ Whole list β Iterative reversal (prev/curr/nextTemp)
βββ Sublist [left, right] β Find left-1 anchor, reverse N times
βββ In K-groups β Reverse K, connect tail to next group
βββ Palindrome check β Find middle + reverse second half + compare
Does the problem involve two lists?
βββ Merge sorted lists β Dummy Head + pick smaller head
βββ Intersection β Two pointers, switch lists at end
Does the problem need O(1) space?
βββ YES β Floyd's, Reversal, Two Pointers (never HashSet)
βββ NO β HashSet or HashMap acceptable (Copy with Random, Cycle detection)
9. Complexity Cheat Sheetβ
| Operation | Time | Space |
|---|---|---|
| Traverse full list | O(N) | O(1) |
| Insert at head | O(1) | O(1) |
| Insert at tail (no tail ref) | O(N) | O(1) |
Delete node (given prev) | O(1) | O(1) |
Delete node (given only node) | O(1)* | O(1) |
| Reverse full list (iterative) | O(N) | O(1) |
| Reverse full list (recursive) | O(N) | O(N) call stack |
| Find middle | O(N) | O(1) |
| Detect cycle (Floyd's) | O(N) | O(1) |
| Find cycle entry (Floyd's) | O(N) | O(1) |
| Detect cycle (HashSet) | O(N) | O(N) |
| Merge two sorted lists | O(M + N) | O(1) |
| Copy list with random pointer | O(N) | O(N) |
*Delete by copying next node's value into current node, then skip next.
10. 7-Day Practice Plan (21 Problems)β
For each problem, follow this ritual:
- Read and clarify (2 min).
- Draw the list state before and after your operation.
- State the brute force and its complexity.
- Identify the pattern from Section 5.
- Code, trace manually, and state final complexity.
Day 1: Linked List Basics
- Reverse Linked List (LC 206) β Master the 3-pointer reversal
- Linked List Cycle (LC 141) β Floyd's Phase 1
- Middle of the Linked List (LC 876) β Fast & Slow baseline
Day 2: Dummy Heads & Removals 4. Merge Two Sorted Lists (LC 21) β Dummy Head + two pointers 5. Remove Linked List Elements (LC 203) β Dummy Head removes edge case 6. Remove Nth Node From End of List (LC 19) β N-step head start
Day 3: Advanced Fast & Slow Pointers 7. Linked List Cycle II (LC 142) β Floyd's Phase 1 + Phase 2 8. Reorder List (LC 143) β Find middle + reverse + merge 9. Find the Duplicate Number (LC 287) β β Array problem solved via cycle logic!
Day 4: Reversal Variations 10. Reverse Linked List II (LC 92) β Reverse a sublist [left, right] 11. Palindrome Linked List (LC 234) β Find middle + reverse + compare 12. Reverse Nodes in k-Group (LC 25) β Hard, essential for pointer mastery
Day 5: Math & Multi-level Lists 13. Add Two Numbers (LC 2) β Dummy Head + carry 14. Copy List with Random Pointer (LC 138) β HashMap or interleave 15. Flatten a Multilevel Doubly Linked List (LC 430) β Stack or recursion
Day 6: Design & Complex Architectures 16. Design Linked List (LC 707) β Implement from scratch with dummy head 17. Design Browser History (LC 1472) β Doubly Linked List use case 18. LRU Cache (LC 146) β β Crucial: HashMap + Doubly Linked List
Day 7: Review & Re-implement 19. Intersection of Two Linked Lists (LC 160) β Two pointers with list-switching 20. Swap Nodes in Pairs (LC 24) β Pointer manipulation warm-up 21. Rotate List (LC 61) β Find length + find new tail + reconnect
11. Mock Interview Moduleβ
Problem: The Corrupt Sensor Data Streamβ
Context: You are writing backend firmware for an IoT monitoring system. Thousands of temperature sensors are linked together in a daisy-chain network. Each Sensor object has a nextSensor reference. Data flows correctly until the last sensor, which should point to null.
However, a recent firmware patch introduced a bug: the last sensor accidentally points back to a sensor somewhere in the middle of the chain, creating an infinite data loop.
Question: Write public Sensor findCorruptionPoint(Sensor head) that returns the exact Sensor where the cycle begins. Return null if there is no loop.
Step 1: Clarifying Questions & Expected Answersβ
| Candidate asks | Interviewer answers | Why it matters |
|---|---|---|
| "Can I modify the Sensor objects?" | No, read-only | Rules out marking visited nodes |
| "What are the memory constraints?" | Very limited (embedded) | O(1) space preferred |
| "Can the cycle start at the head?" | Yes | Edge case to handle |
| "Is there guaranteed to be a cycle?" | No | Must handle the null return case |
| "How large can the chain be?" | Thousands of sensors | O(N) time is acceptable |
Step 2: Brute Force Solutionβ
// Time: O(N), Space: O(N) β stores references in HashSet
public Sensor findCorruptionPoint(Sensor head) {
Set<Sensor> seen = new HashSet<>();
Sensor curr = head;
while (curr != null) {
if (seen.contains(curr)) return curr; // first duplicate = cycle entry
seen.add(curr);
curr = curr.nextSensor;
}
return null;
}
Explain: "This is O(N) time and O(N) space. On embedded hardware with thousands of sensors, a HashSet holding thousands of object references may exceed available RAM. I need an O(1) space solution."
Step 3: Optimized Solution (Floyd's Two-Phase Algorithm)β
// Time: O(N), Space: O(1)
public Sensor findCorruptionPoint(Sensor head) {
Sensor slow = head, fast = head;
// Phase 1: Detect cycle and find meeting point
while (fast != null && fast.nextSensor != null) {
slow = slow.nextSensor;
fast = fast.nextSensor.nextSensor;
if (slow == fast) break;
}
// No cycle found
if (fast == null || fast.nextSensor == null) return null;
// Phase 2: Find cycle entry
// Reset slow to head; fast stays at meeting point
slow = head;
while (slow != fast) {
slow = slow.nextSensor;
fast = fast.nextSensor; // both move 1 step
}
return slow; // this is the corruption start point
}
Walk through the math with the interviewer:
- Let
F= steps from head to cycle entry,C= cycle length,h= steps from entry to meeting point. - At meeting: slow traveled
F + h, fast traveledF + h + C. - Since fast = 2x slow:
2(F + h) = F + h + CβF = C - h. - After reset: slow needs
Fsteps to reach entry. Fast needsC - h = Fsteps from meeting point to entry. They arrive together. β
Step 4: Follow-up Questions & Expected Answersβ
Q1: "What if the sensor chain is so large it cannot fit in memory at once, and nodes are loaded from disk? How does a Linked List compare to an Array?"
| Linked List | Array / B-Tree | |
|---|---|---|
| Access pattern | Random β jumps all over disk sectors β many page faults | Sequential β OS prefetches nearby data automatically |
| Cache behavior | Poor β each .nextSensor is at a different memory address | Excellent β spatial locality, entire cache line loaded at once |
| Recommendation | β Avoid for disk-based traversal | β Prefer for large sequential data |
Expected answer: "Linked List traversal causes frequent cache misses and page faults because nodes are scattered in memory. Arrays are vastly superior for disk-backed data due to spatial locality. For very large datasets with dynamic sizes, B-Trees (used in databases) pack many keys into each node/disk block, getting the best of both worlds."
Q2: "What if we also need to count how many nodes are in the cycle?"
// After finding the meeting point in Phase 1, count the cycle length:
int cycleLength = 1;
Sensor temp = slow.nextSensor;
while (temp != slow) {
cycleLength++;
temp = temp.nextSensor;
}
// Then optionally use cycleLength to find entry via the head-start method
Q3: "How would you design the Sensor class in Java to be most memory-efficient on this embedded hardware?"
Expected answer: "Use primitive types where possible (int instead of Integer). Avoid inner classes (they hold an implicit reference to the outer class). If the value fits in a short (β32768 to 32767), use short val instead of int val to halve memory. Keep the class final to allow JVM optimizations."
12. Quick Reference: Linked List Java Idiomsβ
// ββ NODE SETUP ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
class ListNode {
int val;
ListNode next;
ListNode(int val) { this.val = val; }
}
// ββ SAFE TRAVERSAL ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
// Always null-check before accessing .next
while (curr != null) { curr = curr.next; }
// Peek two ahead safely
if (curr != null && curr.next != null) { curr.next.next; }
// ββ FAST & SLOW IDIOMS ββββββββββββββββββββββββββββββββββββββββββββββββββββ
// Both start at head β standard setup
ListNode slow = head, fast = head;
// Fast starts 1 ahead β returns FIRST middle for even-length lists
ListNode slow = head, fast = head.next;
// N-step head start for "N-th from end"
ListNode slow = dummy, fast = dummy;
for (int i = 0; i <= n; i++) fast = fast.next;
// ββ POINTER TRICKS ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
// Swap two nodes' values (easier than relinking in some problems)
int tmp = a.val; a.val = b.val; b.val = tmp;
// Delete a node when only given the node (not its prev)
// Works only if it's not the tail node
node.val = node.next.val;
node.next = node.next.next;
// Check reference equality (pointer identity) β NOT .equals()
if (nodeA == nodeB) { /* same object */ }
// ββ BUILDING A LIST ON THE FLY ββββββββββββββββββββββββββββββββββββββββββββ
ListNode dummy = new ListNode(-1);
ListNode curr = dummy;
for (int val : values) {
curr.next = new ListNode(val);
curr = curr.next;
}
ListNode result = dummy.next; // detach dummy
// ββ LENGTH CALCULATION ββββββββββββββββββββββββββββββββββββββββββββββββββββ
int length = 0;
ListNode curr = head;
while (curr != null) { length++; curr = curr.next; }