Skip to content

Floyd's Cycle Detection - Tortoise and Hare

"Hai người chạy cùng vòng tròn, sớm muộn cũng gặp nhau." - HPN

Định nghĩa

Floyd's Cycle Detection (còn gọi là Tortoise and Hare) là thuật toán phát hiện cycle trong linked list hoặc sequence:

  • Slow pointer (Tortoise): Di chuyển 1 bước mỗi lần
  • Fast pointer (Hare): Di chuyển 2 bước mỗi lần
  • Nếu có cycle → Hai pointer sẽ gặp nhau

📘 Key Property

Space: O(1) - Không cần HashSet để track visited nodes!

Tại sao cần Floyd's Cycle?

Bài toán: Detect Cycle in Linked List

ApproachTimeSpace
HashSetO(N)O(N)
Floyd'sO(N)O(1)

🎯 MEMORY MATTERS

Với 1 triệu nodes:

  • HashSet: ~8 MB (1M × 8 bytes pointer)
  • Floyd's: ~16 bytes (2 pointers)

Mô hình hóa

Linked List với Cycle

    1 → 2 → 3 → 4 → 5
              ↑     ↓
              8 ← 7 ← 6
              
Cycle: 3 → 4 → 5 → 6 → 7 → 8 → 3 → ...

Tortoise and Hare Movement

Step 0: T=1, H=1
        T
        1 → 2 → 3 → 4 → 5 → 6 → 7 → 8 → (back to 3)
        H

Step 1: T moves 1, H moves 2
            T
        1 → 2 → 3 → 4 → 5 → 6 → 7 → 8
                H

Step 2: T moves 1, H moves 2
                T
        1 → 2 → 3 → 4 → 5 → 6 → 7 → 8
                        H

Step 3:
                    T
        1 → 2 → 3 → 4 → 5 → 6 → 7 → 8
                                H

Step 4:
                        T
        1 → 2 → 3 → 4 → 5 → 6 → 7 → 8
                    H (H đã loop back!)

... Eventually they MEET inside the cycle!

The Math: Tại sao Guaranteed Meet?

Proof

Gọi:

  • μ = Khoảng cách từ head đến cycle start
  • λ = Độ dài cycle
  • k = Số bước để gặp nhau

Khi slow pointer vào cycle (tại step μ):

  • Slow đã đi: μ bước
  • Fast đã đi: 2μ bước → Fast đang ở vị trí (μmodλ) trong cycle

Relative speed: Fast "đuổi" slow với tốc độ 1 bước/iteration. → Fast sẽ đuổi kịp sau tối đa λ iterations.

Guaranteed: Sau O(μ+λ) = O(N) bước, hai pointer gặp nhau.

Production Implementation

python
# HPN Engineering Standard
# Implementation: Floyd's Cycle Detection

from typing import Optional, TypeVar, Generic, Tuple
from dataclasses import dataclass


T = TypeVar('T')


@dataclass
class ListNode(Generic[T]):
    """Singly linked list node."""
    val: T
    next: Optional['ListNode[T]'] = None


class FloydCycleDetection:
    """
    Floyd's Cycle Detection Algorithm.
    
    Features:
        - O(N) time
        - O(1) space
        - Can find cycle start point
        - Can find cycle length
    
    Use Cases:
        - Linked list cycle detection
        - Find duplicate in array (as linked list)
        - Deadlock detection in resource graphs
    """
    
    @staticmethod
    def has_cycle(head: Optional[ListNode]) -> bool:
        """
        Detect if linked list has a cycle.
        
        Time: O(N)
        Space: O(1)
        
        Returns:
            True if cycle exists
        """
        if not head or not head.next:
            return False
        
        slow = head
        fast = head
        
        while fast and fast.next:
            slow = slow.next
            fast = fast.next.next
            
            if slow == fast:
                return True
        
        return False
    
    @staticmethod
    def find_cycle_start(head: Optional[ListNode]) -> Optional[ListNode]:
        """
        Find the node where the cycle begins.
        
        Algorithm:
        1. Detect meeting point using fast/slow
        2. Reset one pointer to head
        3. Move both at speed 1 → they meet at cycle start
        
        Math: Distance from head to cycle start = 
              Distance from meeting point to cycle start
              
        Time: O(N)
        Space: O(1)
        
        Returns:
            Node where cycle starts, or None if no cycle
        """
        if not head or not head.next:
            return None
        
        # Phase 1: Detect cycle
        slow = head
        fast = head
        
        while fast and fast.next:
            slow = slow.next
            fast = fast.next.next
            
            if slow == fast:
                break
        else:
            return None  # No cycle
        
        # Phase 2: Find cycle start
        # Reset one pointer to head
        slow = head
        
        while slow != fast:
            slow = slow.next
            fast = fast.next
        
        return slow
    
    @staticmethod
    def find_cycle_length(head: Optional[ListNode]) -> int:
        """
        Find the length of the cycle.
        
        Returns:
            Length of cycle, or 0 if no cycle
        """
        if not head or not head.next:
            return 0
        
        # Find meeting point
        slow = head
        fast = head
        
        while fast and fast.next:
            slow = slow.next
            fast = fast.next.next
            
            if slow == fast:
                break
        else:
            return 0  # No cycle
        
        # Count cycle length
        length = 1
        current = slow.next
        
        while current != slow:
            length += 1
            current = current.next
        
        return length


def find_duplicate_number(nums: list[int]) -> int:
    """
    Find the duplicate number in array [1, n] with n+1 elements.
    
    This is a classic application of Floyd's algorithm!
    Treat array as linked list where nums[i] points to nums[nums[i]].
    
    Time: O(N)
    Space: O(1)
    
    Example:
        nums = [1, 3, 4, 2, 2]
        → Duplicate is 2
        
        As linked list:
        0 → 1 → 3 → 2 → 4
                    ↑   ↓
                    └───┘
    """
    # Phase 1: Find intersection point
    slow = nums[0]
    fast = nums[0]
    
    while True:
        slow = nums[slow]
        fast = nums[nums[fast]]
        
        if slow == fast:
            break
    
    # Phase 2: Find cycle entrance (= duplicate)
    slow = nums[0]
    
    while slow != fast:
        slow = nums[slow]
        fast = nums[fast]
    
    return slow


# ============================================
# USAGE EXAMPLE
# ============================================
if __name__ == "__main__":
    # Create linked list with cycle
    # 1 → 2 → 3 → 4 → 5 → 6
    #         ↑           ↓
    #         └───────────┘
    
    nodes = [ListNode(i) for i in range(1, 7)]
    for i in range(len(nodes) - 1):
        nodes[i].next = nodes[i + 1]
    
    # Create cycle: 6 → 3
    nodes[5].next = nodes[2]
    
    detector = FloydCycleDetection()
    
    print("=== Cycle Detection ===")
    print(f"Has cycle: {detector.has_cycle(nodes[0])}")
    
    cycle_start = detector.find_cycle_start(nodes[0])
    print(f"Cycle starts at node with value: {cycle_start.val}")
    
    cycle_length = detector.find_cycle_length(nodes[0])
    print(f"Cycle length: {cycle_length}")
    
    # Find duplicate example
    print("\n=== Find Duplicate Number ===")
    nums = [1, 3, 4, 2, 2]
    duplicate = find_duplicate_number(nums)
    print(f"Array: {nums}")
    print(f"Duplicate: {duplicate}")
    
    # List without cycle
    print("\n=== No Cycle Case ===")
    simple_list = ListNode(1, ListNode(2, ListNode(3)))
    print(f"Has cycle: {detector.has_cycle(simple_list)}")

Ứng dụng Thực tế

Use CaseDescription
Linked ListDetect memory leak (circular reference)
Find DuplicateLeetCode 287 - O(1) space solution
Deadlock DetectionResource allocation graph cycle
State MachineDetect infinite loops
BlockchainValidate chain integrity

Brent's Algorithm (Variant)

Brent's algorithm là variant nhanh hơn Floyd's trong practice:

python
def brent_cycle_detection(head: ListNode) -> bool:
    """
    Brent's Algorithm - Faster cycle detection.
    
    Difference: Instead of slow moving every step,
    slow stays put until fast has moved 2^k steps.
    
    ~36% faster than Floyd's in practice.
    """
    if not head:
        return False
    
    slow = head
    fast = head.next
    power = 1
    length = 1
    
    while fast and fast != slow:
        if length == power:
            slow = fast
            power *= 2
            length = 0
        
        fast = fast.next
        length += 1
    
    return fast is not None

Anti-patterns

❌ TRÁNH

1. Dùng HashSet khi O(1) space required

python
# ❌ WRONG: O(N) space
def has_cycle(head):
    visited = set()
    while head:
        if head in visited:
            return True
        visited.add(head)
        head = head.next
    return False

# ✅ RIGHT: O(1) space với Floyd's

2. Quên check null pointers

python
# ❌ WRONG: Crash on empty list
fast = head.next  # NullPointerException!

# ✅ RIGHT: Always check
if not head or not head.next:
    return False

3. Nhầm meeting point với cycle start

python
# ❌ WRONG: Meeting point KHÔNG phải cycle start
if slow == fast:
    return slow  # Sai! Đây chỉ là meeting point

# ✅ RIGHT: Cần phase 2 để tìm cycle start
# (Reset one pointer, move both at speed 1)

So sánh Cycle Detection Algorithms

AlgorithmTimeSpaceNotes
HashSetO(N)O(N)Simple, more memory
Floyd'sO(N)O(1)Classic, interview favorite
Brent'sO(N)O(1)~36% faster in practice

💡 HPN's Rule

"Floyd's cho interview (dễ nhớ). Brent's cho production (nhanh hơn)."