算法知识图谱总览 / 离线与杂项技巧族 / Floyd环检测

Floyd环检测

P2intermediatestandardG-E-7

What · 是什么

龟兔赛跑算法:快慢指针检测链表/序列环,O(n)O(1)找环起点与周期

Floyd 环检测(龟兔赛跑算法)用快慢两个指针检测链表或函数式序列是否有环:慢指针每步走 1,快指针每步走 2,若相遇则有环。相遇后慢指针重置到起点,两指针各走 1 步,再次相遇处即环起点。全程 O(n) 时间 O(1) 空间。也可推广到检测序列进入循环的周期。

Why · 为什么

直觉 · Intuition

快指针速度是慢指针 2 倍,若存在环,两者必定在环上相遇(快指针追上慢指针)。因为每步距离差增 1,总会在某圈赶上。找到环后,设链长(到环入口)=a、环长=L,相遇点距入口 b,则有快指针路程=2×慢路程,解出慢指针再走 (L-b) 步到入口;重置慢指针到头部后两指针等速,必在入口相遇。

原理 · Principle

①存在性:慢走 x 步,快走 2x 步,两者在环上时相距随步数线性缩小,直到相遇。②起点:设相遇点慢指针已走 a+b(a=头部到入口, b=入口到相遇点),快指针走 2(a+b)=a+b+kL(绕 k 圈),得 a = kL - b,即从头部再走 a 步(与从相遇点继续走)同时到达入口。③周期:检测到环后继续以步长 1 计数一圈即得环长。

历史 · History

算法 1960 年代由 Robert W. Floyd 提出,因其'兔子与乌龟'的速度比喻得名。CP-Algorithms 的 tortoise_and_hare 页详述存在性与环起点两阶段的数学证明。在链表面试题(LeetCode 141/142)、伪随机序列周期检测(Brent 算法为其变体)、函数迭代找循环等场景广泛使用。

How · 实现

伪代码

# 阶段1: 检测环
def has_cycle(head):
  slow = fast = head
  while fast and fast.next:
    slow = slow.next
    fast = fast.next.next
    if slow == fast:
      return True
  return False

# 阶段2: 找环起点
def cycle_start(head):
  slow = fast = head
  while fast and fast.next:
    slow = slow.next; fast = fast.next.next
    if slow == fast:
      break
  if not fast or not fast.next:
    return None
  slow = head
  while slow != fast:
    slow = slow.next; fast = fast.next
  return slow

Python 实现

class ListNode:
    def __init__(self, val=0):
        self.val = val
        self.next = None


def build_list(values, cycle_to=None):
    """建链表。cycle_to 为环入口结点下标(负索引从尾起), None 则无环。"""
    head = None
    nodes = []
    for v in values:
        nd = ListNode(v)
        nodes.append(nd)
    for i in range(len(nodes) - 1):
        nodes[i].next = nodes[i + 1]
    if cycle_to is not None:
        nodes[-1].next = nodes[cycle_to]
    return nodes[0] if nodes else None


def has_cycle(head):
    """检测链表是否有环。"""
    slow = fast = head
    while fast is not None and fast.next is not None:
        slow = slow.next
        fast = fast.next.next
        if slow is fast:
            return True
    return False


def cycle_start(head):
    """返回环入口结点, 无环返回 None。"""
    slow = fast = head
    while fast is not None and fast.next is not None:
        slow = slow.next
        fast = fast.next.next
        if slow is fast:
            break
    if fast is None or fast.next is None:
        return None
    slow = head
    while slow is not fast:
        slow = slow.next
        fast = fast.next
    return slow


def cycle_length(head):
    """环长(在环内数一圈)。无环返回0。"""
    entry = cycle_start(head)
    if entry is None:
        return 0
    cur = entry.next
    length = 1
    while cur is not entry:
        cur = cur.next
        length += 1
    return length


if __name__ == '__main__':
    # 无环
    h = build_list([1, 2, 3, 4])
    assert has_cycle(h) is False
    assert cycle_start(h) is None
    assert cycle_length(h) == 0

    # 有环: 1->2->3->4->2
    h2 = build_list([1, 2, 3, 4], cycle_to=1)
    assert has_cycle(h2) is True
    assert cycle_start(h2).val == 2
    assert cycle_length(h2) == 3

    # 环在头部: 1->1
    h3 = build_list([1], cycle_to=0)
    assert has_cycle(h3) is True
    assert cycle_start(h3).val == 1
    assert cycle_length(h3) == 1

    # 较长的链+环
    h4 = build_list(list(range(10)), cycle_to=4)  # 0..9, 9->4
    assert has_cycle(h4) is True
    assert cycle_start(h4).val == 4
    assert cycle_length(h4) == 6

    # 函数迭代序列环检测: x_{n+1} = (x*2+1) % 13 检测进入循环
    seen = {}
    x = 1
    seq = []
    while x not in seen:
        seen[x] = len(seq)
        seq.append(x)
        x = (x * 2 + 1) % 13
    # 用 Floyd 求序列环(对照)
    def f(v):
        return (v * 2 + 1) % 13
    slow = fast = 1
    # 找相遇
    for _ in range(30):
        slow = f(slow)
        fast = f(f(fast))
        if slow == fast:
            break
    print('seq head->cycle:', seq)
    print('meeting value  :', slow, ' cycle entry from dict:', x)

    print('cycle tests: h2 entry=2 len=3; h4 entry=4 len=6; all OK')
    print('All tortoise-hare tests passed!')
PyPy 提示链表指针在 Python 中是对象引用, 节点操作慢; 大数据量建议数组模拟链表(next 数组)提升性能; 循环迭代序列检测用函数式 f(x) 时注意快指针走两步要 f(f(x))。
时间复杂度O(N) — 快指针至多 2N 步; 找起点 O(N)
空间复杂度O(1) — 仅两个指针变量
注意 · 局限链表环检测无法知道环的'结构', 仅返回入口/长度; 需要统计环内节点数时需另数一圈; 快指针每步2次可能跳过单个节点但数学上仍能追上; 对函数式序列(无链表)适用但需映射可计算。

易错点

例题

OJ题号难度考点链接
LeetCode1411环形链表: 检测是否有环, Floyd 基础题https://leetcode.cn/problems/linked-list-cycle/
LeetCode1422环形链表II: 找环入口, Floyd 两阶段完整版https://leetcode.cn/problems/linked-list-cycle-ii/
Codeforces2008G3Sakurako's Chefir: 函数迭代周期检测场景(数组模拟链环)https://codeforces.com/problemset/problem/2008/G
步骤迷你图(节点 = 算法步骤 · 边 = 流程 · 绿色 = 起始步骤)

信源

✓ = 已核验 · △ = 未核验