DSA DSA In Depth
0%
lesson cycle-detectionfloydbrent

Pointer Chasing: Floyd's Tortoise-and-Hare vs Brent's Cycle Detection

7 min read Module 1.3: Linked Structures, Monotonic Stacks/Queues & Cache-Conscious Lists Updated Oct 23, 2026
📄 Cheat Sheet
Floyd's Cycle Detection Algorithm (Tortoise and Hare) — Visual Proof
Inside code Floyd's Cycle Detection Algorithm (Tortoise and Hare) — Visual Proof Watch on YouTube ↗
0:00 / --:--

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.
Algorithmic Invariant: Brent's Power-of-Two Teleporting Turtle (1980)

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

Rho Topology: Tail Length mu = 3, Cycle Length lambda = 5. Meeting at k = m * lambda H0 N1 N2 E (mu) C1 Meet C3 C4 <--- Tail mu = 3 ---> At Meeting Point (k = m * lambda): 1. Offset from E is (k - mu) mod lambda 2. Remaining arc back to E is mu mod lambda! 3. Start P1 at H0, P2 at Meet; step 1x each --> P1 and P2 collide at E after mu steps! Array Index Encoding (Find Duplicate in [1..N]): Treat i -> nums[i] as a functional graph; index 0 has in-degree 0!

Functional Graph Reduction: Finding Duplicates in $O(1)$ Space Without Mutating Array

flowchart LR A["Array nums[0..N]
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"]
Hardware Reality: Why Brent's Algorithm Dominates Pollard's Rho & Pointer Chasing

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.

Floyd Tortoise-and-Hare Cycle Detection & Pointer Reversal
O(N) Time · O(1) Space

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 λ)).

Step 1 / 8
Custom Input:
Timeline

Invariant & State

Phase 1 (Cycle Detection): dist(fast) = 2·dist(slow) = 2 vs 1
🔗 Live Pointer & State Transition Graph — Floyd Tortoise-and-Hare Cycle Detection & Pointer Reversal Active Pointer Jump/Transition Matched/Optimal
11 [0] 🐢 slow 22 [1] 🐇 fast 33 [2] μ=3 (en 44 [3] 55 [4] 66 [5] 77 [6] 88 [7]
[0]11
[1]22🐢 slow
[2]33🐇 fast
[3]44μ=3 (entry)
[4]55
[5]66
[6]77
[7]88
Tail: [0]→[1]→[2]→[3] | Cycle (λ=5): [3]→[4]→[5]→[6]→[7]↩[3]
Phase
Phase 1 Chase
Tortoise (slow)
[1] (val 22)
Hare (fast)
[2] (val 33)
Steps
1
[Step 1] Tortoise moved to [1], Hare jumped 2 steps to [2].

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};
}
Production Pitfall: Starting 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).

Check yourself Question 1 of 4
Concept Check Floyd's Cycle Entry Mathematical Identity recall

In Floyd's Tortoise and Hare algorithm, let $\mu$ be the distance from the list head to the start of the cycle, $\lambda$ be the cycle length, and $k$ be the distance from the cycle start to the meeting point of `slow` and `fast`. Why does resetting one pointer to `head` and advancing both by 1 step per iteration guarantee they meet at the cycle entry?

⚡ Interactive Challenge · Staff Level

Algorithmic Design & Complexity Check

Analyze the invariant, asymptotic bound, and hardware behavior before selecting.

Why must Floyd's duplicate-finding algorithm start at index `0` when values are in `[1, n]`?

Key Takeaways

In Floyd's Tortoise and Hare, the pointers meet at step k = m * lambda inside the cycle, leaving the slow pointer exactly (-mu mod lambda) steps past the cycle entrance E.
Resetting one pointer to head and stepping both pointers by 1 causes them to collide at the exact cycle entrance E after mu steps.
Brent's cycle detection keeps the Tortoise stationary and teleports it to the Hare at powers of two (2^i), performing only 1 pointer chase per step and yielding the cycle length lambda directly.
Any read-only array of N + 1 integers with values in [1..N] forms an implicit functional graph i -> nums[i] rooted at 0, where the duplicate value is the cycle entrance.
Was this lesson helpful? Spot an issue or have a suggestion? Let us know on GitHub.
Report an issue