The $\rho$ (Rho) Topology & Floyd's Modular Congruence Proof
Consider a singly linked list (or iterated function $x_{i+1} = f(x_i)$ in Pollard's rho integer factorization and PRNG period testing) that has a non-cyclic prefix (tail) of $\mu$ edges leading to a cycle entrance node $E$, followed by a directed cycle of $\lambda$ edges. In Floyd's Cycle-Finding Algorithm (attributed to Robert W. Floyd, 1967, in Knuth TAOCP Vol. 2), a slow pointer $T$ (Tortoise) advances 1 step per iteration ($t_k = k$) and a fast pointer $H$ (Hare) advances 2 steps per iteration ($h_k = 2k$):
- Phase 1 (Cycle Detection & Meeting Point): Once $k \ge \mu$, both pointers are inside the cycle of length $\lambda$. Since $H$ closes the gap to $T$ by exactly $2 - 1 = 1$ edge per iteration, $H$ cannot jump over $T$ without landing on it. They meet at the smallest step $k \ge \mu$ where the distance between them $2k - k = k$ is an exact multiple of the cycle length $\lambda$: $$k = m \cdot \lambda \quad \text{for some integer } m \ge 1$$
- Phase 2 (Finding the Exact Cycle Entrance $\mu$): At the meeting step $k = m \lambda$, the Tortoise is at position $\mu + (k - \mu) \bmod \lambda$—meaning it has traveled $(k - \mu) \equiv -\mu \pmod \lambda$ steps past the cycle entrance $E$. Therefore, advancing a further $\mu$ steps along the cycle reaches offset $(k - \mu) + \mu = k = m \lambda \equiv 0 \pmod \lambda$, which is precisely the cycle entrance $E$! Resetting one pointer to
head(distance $\mu$ from $E$) and advancing both pointers at speed 1 causes them to collide at $E$ after exactly $\mu$ steps.
In Floyd's algorithm, every iteration advances $T$ by 1 step and $H$ by 2 steps—costing 3 pointer dereferences (or $f(x)$ evaluations) per step, and requiring a post-processing loop to count the cycle length $\lambda$. Richard P. Brent (1980) asked: why move both pointers continuously when we can keep the Tortoise stationary and teleport it to the Hare's position at exponentially growing powers of two ($2^0, 2^1, 2^2, \dots, 2^i$)?
In Brent's algorithm, the Hare advances 1 step at a time while incrementing the current segment length $\lambda$. Whenever $H == T$, the exact cycle length $\lambda$ is already known! Whenever $\lambda$ reaches the current power-of-two limit $P = 2^i$, we teleport $T \leftarrow H$, reset $\lambda \leftarrow 0$, and double $P \leftarrow 2P$. Once the first power of two $2^i \ge \max(\mu, \lambda)$ is reached, $T$ is parked inside the cycle and $H$ is guaranteed to collide with $T$ in exactly $\lambda$ steps—performing only 1 function evaluation per step and directly yielding $\lambda$!
Comparative Analysis of Cycle Detection Algorithms
| Algorithm | Time Complexity | Auxiliary Space | Pointer Chases per Step | Directly Yields Cycle Length $\lambda$? |
|---|---|---|---|---|
| Hash Set (Visited Addresses) | $O(\mu + \lambda)$ | $\Theta(\mu + \lambda)$ | 1 chase + 1 hash insert | Yes (if storing step index in map) |
| Floyd's Tortoise & Hare | $O(\mu + \lambda)$ | $O(1)$ (2 pointers) | 3 chases (1 slow + 2 fast) | No (requires extra $\lambda$-step loop) |
| Brent's Teleporting Turtle | $O(\mu + \lambda)$ | $O(1)$ (2 pointers + 2 scalars) | 1 chase ($\approx 36\%$ faster than Floyd) | Yes! Counter equals $\lambda$ upon collision |
Visualizing the $\rho$ (Rho) Cycle Geometry & Phase 2 Entrance Alignment
Functional Graph Reduction: Finding Duplicates in $O(1)$ Space Without Mutating Array
Values in [1..N]"] --> B["Implicit Directed Graph:
Edge i -> nums[i]"] B --> C["Index 0 has in-degree 0
(Since all values >= 1)"] C --> D["Duplicate value v has
in-degree >= 2 (Cycle Entrance E!)"] D --> E["Run Floyd or Brent from 0
Return E in O(N) time, O(1) space"]
In pointer-chasing workloads where nodes do not fit in L1/L2 cache, every pointer dereference stalls the CPU for ~60–100 ns waiting on DRAM. In Floyd's algorithm, the Tortoise lags behind the Hare by $k / 2$ nodes; if the cycle exceeds L2 cache capacity, the Hare's cache lines are evicted before the Tortoise reaches them, so all 3 pointer dereferences per iteration miss cache! In Brent's algorithm, the Tortoise sits completely stationary in a CPU register while only the Hare advances 1 step per iteration—cutting DRAM traffic and ALU evaluations by up to $3\times$ per step.
Visualize Phase 1 (Tortoise 1× vs Hare 2× meeting inside cycle of length λ) and Phase 2 (resetting one pointer to head to locate cycle entry μ via x ≡ −c (mod λ)).
Invariant & State
Production Implementation: Floyd's Entrance Finder & Brent's Cycle Detector
#include <cstddef>
#include <utility>
#include <vector>
struct ListNode {
int val;
ListNode* next;
};
// Floyd's Tortoise and Hare: Returns cycle entrance node (or nullptr if acyclic)
ListNode* floyd_find_cycle_entrance(ListNode* head) noexcept {
ListNode* slow = head;
ListNode* fast = head;
// Phase 1: Find meeting point inside cycle
while (fast != nullptr && fast->next != nullptr) {
slow = slow->next;
fast = fast->next->next;
if (slow == fast) {
// Phase 2: Reset one pointer to head; advance both at speed 1
ListNode* finder = head;
while (finder != slow) {
finder = finder->next;
slow = slow->next;
}
return finder; // Exact cycle entrance!
}
}
return nullptr;
}
// Brent's Teleporting Turtle: Returns {cycle_entrance, cycle_length_lambda}
std::pair<ListNode*, std::size_t> brent_find_cycle(ListNode* head) noexcept {
if (head == nullptr) return {nullptr, 0};
std::size_t power = 1;
std::size_t lambda = 1;
ListNode* tortoise = head;
ListNode* hare = head->next;
// Phase 1: Find cycle length lambda with 1 pointer chase per iteration!
while (hare != nullptr && tortoise != hare) {
if (power == lambda) {
tortoise = hare; // Teleport tortoise to hare
power *= 2;
lambda = 0;
}
hare = hare->next;
++lambda;
}
if (hare == nullptr) return {nullptr, 0};
// Phase 2: Advance hare lambda steps ahead of head, then move both at speed 1
tortoise = head;
hare = head;
for (std::size_t i = 0; i < lambda; ++i) {
hare = hare->next;
}
while (tortoise != hare) {
tortoise = tortoise->next;
hare = hare->next;
}
return {tortoise, lambda};
}
slow = head and fast = head->next Breaks Phase 2 Entrance Math!A frequent bug in Floyd's cycle entrance implementation is initializing slow = head; fast = head->next; in Phase 1 (to make a while (slow != fast) loop condition work) and then running standard Phase 2 (finder = head; while (finder != slow) ...). If fast starts at head->next (1 step ahead at $k = 0$), then after $k$ steps fast is at $2k + 1$ while slow is at $k$, so their meeting satisfies $k + 1 = m \lambda$, shifting the meeting point backward by 1 node and causing Phase 2 to spin forever or return the wrong node! Always initialize both slow = head and fast = head at step $0$ and step them before testing if (slow == fast).