идут в одну сторону с разной скоростью. Если структура зациклена, быстрый догоняет медленного — цикл находится без журнала посещённых узлов.
Ключевые тезисы
- Память O(1) против O(n) у решения с множеством посещённых — на длинных списках это решающее различие.
- Алгоритм Флойда находит не только факт цикла, но и его начало: второй этап опирается на арифметику длин.
- Тот же приём даёт середину списка, k-й элемент с конца и дубликат в массиве, прочитанном как функция.
Какую задачу решает
может замыкаться на себя, и обычный обход по нему не закончится. Очевидная защита — запоминать посещённые узлы, но это памяти, а на потоке состояний такой журнал вести попросту негде.
Приём заменяет память арифметикой: идут в одну сторону с разной скоростью, и внутри цикла расстояние между ними сокращается на единицу за шаг. Встреча неизбежна — а из арифметики длин выводится и точка входа в цикл.
Подробный разбор
может замыкаться сам на себя, и тогда обычный обход не закончится никогда. Очевидное решение — складывать посещённые узлы в множество и проверять каждый следующий: работает, но стоит памяти. На списке из миллионов узлов или на последовательности состояний, которую нельзя сохранить целиком, это неприемлемо.
Та же проблема шире, чем списки: повтор состояния в детерминированной последовательности — это цикл, и искать его перебором с запоминанием так же дорого.
Пустить по структуре с разной скоростью: если цикл есть, быстрый неизбежно догонит медленного изнутри цикла.
Память при этом — , то есть константа. Ничего не запоминается вообще: факт цикла выводится из встречи, а не из журнала посещений.
1 -> 2 -> 3 -> 4
^ |
+----+
шаг 0: S = 1 F = 1
шаг 1: S = 2 F = 3
шаг 2: S = 3 F = 3 встретились -- цикл есть| Шаг | Медленный | Быстрый | Расстояние внутри цикла |
|---|---|---|---|
| 0 | 1 | 1 | оба ещё вне цикла |
| 1 | 2 | 3 | — |
| 2 | 3 | 3 | 0 — встреча |
Если бы цикла не было, быстрый просто упёрся бы в конец списка: условие fast and fast.next перестало бы выполняться, и цикл завершился бы без встречи.
slow = fast = head
while fast and fast.next:
slow = slow.next
fast = fast.next.next
if slow is fast:
break
else:
return None # цикла нет
slow = head # второй этап: ищем вход в цикл
while slow is not fast:
slow = slow.next
fast = fast.next
return slow- среднее значение
- сила регуляризации: штраф за сложность модели
- номер или количество: индекс шага, число соседей, кластеров или позиций
- Время — , то есть линейное по числу узлов.
- Память — : и ничего больше.
- Предусловие — переход из узла детерминирован: у каждого состояния ровно один следующий.
Как только оба указателя оказались внутри цикла, рассмотрим расстояние от быстрого до медленного по направлению движения. За один шаг медленный продвигается на 1, быстрый на 2, значит расстояние сокращается ровно на 1 — и, будучи целым неотрицательным, рано или поздно обнуляется. Перепрыгнуть друг друга они не могут именно потому, что разница скоростей равна единице.
Второй этап опирается на арифметику: к моменту встречи медленный прошёл шагов, быстрый — вдвое больше, а разница между ними кратна длине цикла. Отсюда следует, что расстояние от головы до входа в цикл и расстояние от точки встречи до входа сравнимы по модулю — поэтому , идущие по шагу из головы и из точки встречи, сходятся именно во входе.
| Сигнал в условии | Что применять |
|---|---|
| «Есть ли в списке цикл», «где начинается цикл» | алгоритм Флойда целиком |
| «Середина списка» за один проход | быстрый и медленный без второго этапа |
| «k-й элемент с конца» односвязного списка | указатели, разнесённые на k |
| Массив длины n + 1 со значениями от 1 до n, ищем дубликат | массив как функциональный |
| Последовательность состояний, вопрос о зацикливании | цикл по состояниям, а не по узлам |
| Явное требование дополнительной памяти | почти всегда именно этот приём |
| Вариант | Что меняется | Результат |
|---|---|---|
| Обнаружение цикла | базовый вариант | есть цикл или нет |
| Вход в цикл | второй этап Флойда | узел, с которого цикл начинается |
| Длина цикла | пройти круг от точки встречи | λ |
| Середина списка | остановиться, когда быстрый дошёл до конца | узел посередине |
| k-й с конца | разнести указатели на k и идти вместе | узел за один проход |
| Алгоритм Брента | быстрый стоит, медленный идёт, шаг удваивается | то же, но обычно меньше переходов |
Параметр здесь ровно один — соотношение скоростей. Классическое 1 и 2 удобно тем, что встреча гарантирована и доказывается в одну строку; при скоростях 1 и 3 указатели могут перепрыгивать друг друга, и условие встречи усложняется.
- Порядок проверок.
while fast and fast.next— иначе обращение кfast.next.nextпадает на списке чётной длины. - Какая середина. При чётном числе узлов «середин» две; какая нужна, определяет начальное положение указателей.
- Сравнение узлов, а не значений. Проверять нужно тождество узлов (
is), иначе два одинаковых значения выглядят как встреча. - Изменение списка во время обхода. Развернуть половину списка для проверки палиндрома удобно, но список остаётся изменённым — если это API, его надо восстановить.
- Недетерминированный переход. Если у состояния несколько следующих, это уже , и цикл ищется обходом в глубину.
| Приём | Когда он лучше | Цена |
|---|---|---|
| Множество посещённых | нужен не только факт цикла, но и весь путь | памяти |
| Алгоритм Брента | переходы дорогие, важна скорость | чуть сложнее в реализации |
| Обход в глубину | , а не последовательность | цикл ищется раскраской вершин |
| Хеш-таблица | состояния произвольные и их надо различать | памяти |
- Почему указатели со скоростями 1 и 2 обязаны встретиться, а не перепрыгнуть друг друга?
- Список из одного узла, ссылающегося на себя: отработает ли ваш код?
- Найдите середину списка за один проход — и скажите, какую из двух середин вернёт ваш код при чётной длине.
- Почему поиск входа в цикл начинается именно из головы списка?
- Массив [1, 3, 4, 2, 2]: как здесь возникает цикл и почему дубликат — это вход в него?
Чем заменяют
Видеолекции
Тайм-кода именно на эту тему в записях нет. Но глава «Алгоритмы и структуры данных» разобрана в курсе целиком — с той оговоркой, что место в записи придётся искать самому.
Связанные темы
Два указателя и окно
Two Pointers and Sliding Window90%
Два указателя: семейство приёмов · Алгоритмы и структуры данныхЗонтичная тема: что общего у четырёх приёмов с двумя индексами и как выбрать нужный. Сами приёмы разобраны каждый на своей странице.
Opposite-Direction Pointers90%
Встречные указатели · Алгоритмы и структуры данныхДва индекса сходятся с концов отсортированного массива. На каждом шаге отбрасывается конец, который заведомо не даст лучшего ответа, — и вместе с ним целая полоса пар.
Sliding Window90%
Скользящее окно · Алгоритмы и структуры данныхОкно между двумя границами: правая расширяет, левая чинит. Состояние не пересчитывается, а обновляется инкрементально — отсюда линейное время на задачах о подотрезках.
Same-Direction Pointers90%
Два указателя в одну сторону · Алгоритмы и структуры данныхСлияние, пересечение и запись на месте: каждому источнику свой указатель, ни один не откатывается назад. Основа сортировки слиянием и merge join в базах данных.
Prefix Sums90%
Префиксные суммы · Алгоритмы и структуры данныхОдин предварительный проход превращает сумму на любом отрезке в одно вычитание. Тот же приём вместе с хеш-таблицей находит подотрезок с заданной суммой за линейное время.
Monotonic Stack and Deque90%
Монотонный стек и дек · Алгоритмы и структуры данныхПриём поверх стека: хранить только тех, для кого ответ ещё не найден. Даёт ближайший больший элемент для всех позиций сразу и максимум в скользящем окне — за один проход.
Всё, что названо в описании, разобрано здесь же или в соседней теме — переходы под каждым определением.
Сокращение расстояния
Разность скоростей равна единице, поэтому перепрыгнуть друг друга нельзя.
Разбор ниже: Почему это работаетФункциональный граф
Массив, прочитанный как функция «индекс → значение»: дубликат становится входом в цикл.
Разбор ниже: Вариации и параметрыАлгоритм Брента
Та же задача с меньшим числом переходов: шаг удваивается.
Разбор ниже: Вариации и параметрыГлава «Алгоритмы и структуры данных» последний раз правилась . Нашли ошибку — напишите.