|
| 1 | +# 4.5 Linked List Cycle II |
| 2 | + |
| 3 | +> **Source:** [`src/main/kotlin/linkedlist/LinkedListCycle_II.kt`](https://github.com/arpanpathak/AdvancedAlgorithmPatterns/blob/main/src/main/kotlin/linkedlist/LinkedListCycle_II.kt) |
| 4 | +> **Pattern:** Floyd's with entry-point math · **Gym page — the "prove the math" favorite** |
| 5 | +
|
| 6 | +## The Problem |
| 7 | + |
| 8 | +Like [4.2](linked-list-cycle.md), but now also return the node where the cycle **begins**. If there is no cycle, return `null`. $O(1)$ space. |
| 9 | + |
| 10 | +## Examples |
| 11 | + |
| 12 | +``` |
| 13 | +Input: 3 -> 2 -> 0 -> -4 ─┐ |
| 14 | + ↑______________┘ (cycle starts at the node with value 2) |
| 15 | +Output: the node with value 2 |
| 16 | +``` |
| 17 | + |
| 18 | +## Intuition — one more lap reveals the entry |
| 19 | + |
| 20 | +Floyd's meeting ([4.2](linked-list-cycle.md)) finds *some* node in the cycle. The entry-point question is: **why does walking from the head and from the meeting point at equal speed meet at the cycle's start?** |
| 21 | + |
| 22 | +Set up the notation: the non-cycle prefix has length $a$; the meeting point is $b$ nodes into the cycle (so the meeting point is $b$ steps after the entry, walking forward); the cycle has length $L$. |
| 23 | + |
| 24 | +At the meeting, the tortoise has walked $a + b$ steps (it entered the cycle once and walked $b$ more). The hare walked $2(a+b)$ (twice as fast). The hare's path is also `a` (to the entry) plus some integer number $q$ of full laps plus $b$: $a + qL + b$. Equating: |
| 25 | + |
| 26 | +$$ |
| 27 | +2(a + b) = a + qL + b \quad\Longrightarrow\quad a + b = qL |
| 28 | +$$ |
| 29 | + |
| 30 | +So $a + b$ is an exact multiple of $L$. Now the key observation: |
| 31 | + |
| 32 | +- A pointer starting at the **head** needs exactly $a$ steps to reach the entry. |
| 33 | +- A pointer starting at the **meeting point** walks $a \bmod L$ steps to reach some node; but $a \equiv L - b \pmod L$ (from $a + b \equiv 0$), and walking $L - b$ steps forward from a point $b$ steps into the cycle lands **exactly on the entry**. |
| 34 | + |
| 35 | +Both pointers reach the cycle's start after exactly $a$ steps. Walk them in lockstep (1 step each); their first collision is the answer. |
| 36 | + |
| 37 | +## Approach 1 — Hash set |
| 38 | + |
| 39 | +Store every visited node; the first node seen twice is the entry. $O(n)$ time, $O(n)$ space — fails the $O(1)$-space requirement. |
| 40 | + |
| 41 | +## Approach 2 — Floyd + entry-point walk (optimal) |
| 42 | + |
| 43 | +```kotlin |
| 44 | +/** |
| 45 | + * @param head the head of the linked list |
| 46 | + * @return the node where the cycle begins, or null if there is no cycle |
| 47 | + */ |
| 48 | +fun detectCycle(head: ListNode?): ListNode? { |
| 49 | + var slow = head |
| 50 | + var fast = head |
| 51 | + |
| 52 | + // Phase 1: find a meeting point inside the cycle (standard Floyd). |
| 53 | + while (fast != null && fast.next != null) { |
| 54 | + slow = slow?.next |
| 55 | + fast = fast.next?.next |
| 56 | + if (slow == fast) break |
| 57 | + } |
| 58 | + |
| 59 | + // No cycle: the hare fell off the list. |
| 60 | + if (fast == null || fast.next == null) return null |
| 61 | + |
| 62 | + // Phase 2: head-pointer and meeting-pointer walk 1 step each. |
| 63 | + // By the congruence a == L - b (mod L), they meet at the cycle start. |
| 64 | + var entry: ListNode? = head |
| 65 | + while (entry != slow) { |
| 66 | + entry = entry?.next |
| 67 | + slow = slow?.next |
| 68 | + } |
| 69 | + return entry |
| 70 | +} |
| 71 | +``` |
| 72 | + |
| 73 | +```java |
| 74 | +public class LinkedListCycleII { |
| 75 | + /** |
| 76 | + * @param head the head of the linked list |
| 77 | + * @return the node where the cycle begins, or null if there is no cycle |
| 78 | + */ |
| 79 | + public ListNode detectCycle(ListNode head) { |
| 80 | + ListNode slow = head, fast = head; |
| 81 | + while (fast != null && fast.next != null) { // phase 1: meet inside |
| 82 | + slow = slow.next; |
| 83 | + fast = fast.next.next; |
| 84 | + if (slow == fast) break; |
| 85 | + } |
| 86 | + if (fast == null || fast.next == null) return null; // no cycle |
| 87 | + |
| 88 | + ListNode entry = head; // phase 2: walk to entry |
| 89 | + while (entry != slow) { |
| 90 | + entry = entry.next; |
| 91 | + slow = slow.next; |
| 92 | + } |
| 93 | + return entry; |
| 94 | + } |
| 95 | +} |
| 96 | +``` |
| 97 | + |
| 98 | +```cpp |
| 99 | +struct ListNode { |
| 100 | + int val; |
| 101 | + ListNode* next; |
| 102 | + ListNode(int x) : val(x), next(nullptr) {} |
| 103 | +}; |
| 104 | + |
| 105 | +class LinkedListCycleII { |
| 106 | +public: |
| 107 | + /** |
| 108 | + * @param head the head of the linked list |
| 109 | + * @return the node where the cycle begins, or null if there is no cycle |
| 110 | + */ |
| 111 | + ListNode* detectCycle(ListNode* head) { |
| 112 | + ListNode* slow = head; |
| 113 | + ListNode* fast = head; |
| 114 | + while (fast && fast->next) { |
| 115 | + slow = slow->next; |
| 116 | + fast = fast->next->next; |
| 117 | + if (slow == fast) break; |
| 118 | + } |
| 119 | + if (!fast || !fast->next) return nullptr; |
| 120 | + |
| 121 | + ListNode* entry = head; |
| 122 | + while (entry != slow) { |
| 123 | + entry = entry->next; |
| 124 | + slow = slow->next; |
| 125 | + } |
| 126 | + return entry; |
| 127 | + } |
| 128 | +}; |
| 129 | +``` |
| 130 | + |
| 131 | +```python |
| 132 | +def detect_cycle(head: ListNode | None) -> ListNode | None: |
| 133 | + """ |
| 134 | + @param head: the head of the linked list |
| 135 | + @return: the node where the cycle begins, or None if there is no cycle |
| 136 | + """ |
| 137 | + slow = fast = head |
| 138 | + while fast and fast.next: # phase 1: meet inside the cycle |
| 139 | + slow = slow.next |
| 140 | + fast = fast.next.next |
| 141 | + if slow is fast: |
| 142 | + break |
| 143 | + if fast is None or fast.next is None: |
| 144 | + return None # no cycle |
| 145 | + |
| 146 | + entry = head # phase 2: walk to the entry |
| 147 | + while entry is not slow: |
| 148 | + entry = entry.next |
| 149 | + slow = slow.next |
| 150 | + return entry |
| 151 | +``` |
| 152 | + |
| 153 | +```rust |
| 154 | +use std::collections::HashSet; |
| 155 | + |
| 156 | +impl Solution { |
| 157 | + /// @param head the head of the linked list |
| 158 | + /// @return the node where the cycle begins, or None if there is no cycle |
| 159 | + pub fn detect_cycle(head: Option<Box<ListNode>>) -> Option<Box<ListNode>> { |
| 160 | + // The O(1)-space two-pointer version needs raw-pointer gymnastics in Rust; |
| 161 | + // the readable HashSet version below is the pragmatic fallback. |
| 162 | + let mut seen: HashSet<*const ListNode> = HashSet::new(); |
| 163 | + let mut cur = head.as_ref(); |
| 164 | + while let Some(node) = cur { |
| 165 | + let ptr = node.as_ref() as *const ListNode; |
| 166 | + if !seen.insert(ptr) { |
| 167 | + return Some(node.clone()); |
| 168 | + } |
| 169 | + cur = node.next.as_ref(); |
| 170 | + } |
| 171 | + None |
| 172 | + } |
| 173 | +} |
| 174 | +``` |
| 175 | + |
| 176 | +> **Rust note:** the canonical two-pointer phase requires raw pointers (see [4.2](linked-list-cycle.md) for the detection-only version). When the entry node must be *returned*, the HashSet version shown above is the standard readable Rust tradeoff — name it honestly if asked about space. |
| 177 | +
|
| 178 | +## Dry run |
| 179 | + |
| 180 | +**Input:** `3 -> 2 -> 0 -> -4`, with `-4.next = 2`. So `a = 1` (one node before the cycle), entry = the `2`, cycle length `L = 3`. |
| 181 | + |
| 182 | +``` |
| 183 | +Phase 1 (Floyd): |
| 184 | +slow=3 fast=3 |
| 185 | +step 1: slow=2, fast=0 |
| 186 | +step 2: slow=0, fast=-4 |
| 187 | +step 3: slow=-4, fast=-4 -> meet at -4. Tortoise traveled 3 steps = a + b, so b = 2. |
| 188 | +
|
| 189 | +Phase 2 (entry walk): |
| 190 | +entry=3, slow=-4 |
| 191 | + entry=2, slow=2 -> entry == slow -> return the 2 ✓ |
| 192 | +``` |
| 193 | + |
| 194 | +Verify the math: `a + b = 1 + 2 = 3 = qL` with `q = 1` ✓. And `L − b = 1 = a`, so walking `a = 1` step from the meeting point (-4 → 2) lands on the same node as walking `a = 1` step from the head (3 → 2) — the entry. The lockstep walk found it in one step. |
| 195 | + |
| 196 | +## Complexity |
| 197 | + |
| 198 | +**Time.** Phase 1 is $O(n_0 + L)$; phase 2 walks at most $a$ steps: |
| 199 | + |
| 200 | +$$ |
| 201 | +T(n) = O(n) |
| 202 | +$$ |
| 203 | + |
| 204 | +**Space.** $O(1)$ (two pointers). |
| 205 | + |
| 206 | +## Variants & follow-ups |
| 207 | + |
| 208 | +- **[4.2](linked-list-cycle.md)** — detection only; this page adds the entry-point phase. |
| 209 | +- **Intersection of Two Linked Lists** (`src/main/kotlin/linkedlist/IntersectionOfTwoLinkedList.kt`) — the same "two walks meet at a common node" idea in a different shape. |
| 210 | +- **Interview follow-up:** "Why does the entry walk terminate?" Phase 2 pointers are both *inside-or-before* the cycle; they meet within at most $a$ steps (both reach the entry exactly then), so no infinite loop. |
| 211 | +- **Interview follow-up:** "Does the meeting point matter for phase 2?" Any meeting point works — the congruence holds for whatever `b` the race produced. That's why the algorithm is deterministic despite the "arbitrary" meeting. |
0 commit comments