Запускаем два указателя: slow_ptr идёт на один узел вперёд, fast_ptr - на два.
Если цикла в списке нет, то fast_ptr дойдёт до конца списка и вернёт nullptr.
Пусть в списке есть цикл. Пусть n - количество узлов до цикла, m - количество узлов в цикле.
Рассмотрим момент, когда slow_ptr дойдёт до начала цикла.
В этот момент fast_ptr находится на расстоянии n % m (остаток от деления).
В какой-то момент fast_ptr догонит slow_ptr. Они будут находится на расстоянии (m - n % m) % m.
Теперь запускаем slow_ptr со скоростью 1 узел из head и продолжаем движение fast_ptr из точки встречи со скоростью 1 узел.
Осталось рассмотреть момент, когда slow_ptr дойдёт до начала списка. В этот момент fast_ptr будет находится на расстоянии (m + n - n % m) % m = 0. Т.е. fast_ptr и slow_ptr встретятся в начале цикла.
Алгоритм использует линейные проходы по списку, поэтому временная сложность - O(n)
Доп. память - O(1)