Алгоритмы и структуры данных

Fast and Slow Pointers

Быстрый и медленный указатель

актуальноТекущий рабочий стандарт

Часть темы: Two Pointers and Sliding WindowДва указателя: семейство приёмов

Направления: Основы · Алгоритмы и структуры данных

идут в одну сторону с разной скоростью. Если структура зациклена, быстрый догоняет медленного — цикл находится без журнала посещённых узлов.

Ключевые тезисы

  • Память O(1) против O(n) у решения с множеством посещённых — на длинных списках это решающее различие.
  • Алгоритм Флойда находит не только факт цикла, но и его начало: второй этап опирается на арифметику длин.
  • Тот же приём даёт середину списка, k-й элемент с конца и дубликат в массиве, прочитанном как функция.

Какую задачу решает

может замыкаться на себя, и обычный обход по нему не закончится. Очевидная защита — запоминать посещённые узлы, но это памяти, а на потоке состояний такой журнал вести попросту негде.

Приём заменяет память арифметикой: идут в одну сторону с разной скоростью, и внутри цикла расстояние между ними сокращается на единицу за шаг. Встреча неизбежна — а из арифметики длин выводится и точка входа в цикл.

Подробный разбор

10 подтем — объяснения, формулы, примеры и интерактивные графики.

может замыкаться сам на себя, и тогда обычный обход не закончится никогда. Очевидное решение — складывать посещённые узлы в множество и проверять каждый следующий: работает, но стоит памяти. На списке из миллионов узлов или на последовательности состояний, которую нельзя сохранить целиком, это неприемлемо.

Та же проблема шире, чем списки: повтор состояния в детерминированной последовательности — это цикл, и искать его перебором с запоминанием так же дорого.

На практике

Пустить по структуре с разной скоростью: если цикл есть, быстрый неизбежно догонит медленного изнутри цикла.

Память при этом — , то есть константа. Ничего не запоминается вообще: факт цикла выводится из встречи, а не из журнала посещений.

1 -> 2 -> 3 -> 4
          ^    |
          +----+

шаг 0:   S = 1    F = 1
шаг 1:   S = 2    F = 3
шаг 2:   S = 3    F = 3   встретились -- цикл есть
Медленный делает шаг, быстрый два
ШагМедленныйБыстрыйРасстояние внутри цикла
011оба ещё вне цикла
123
2330 — встреча

Если бы цикла не было, быстрый просто упёрся бы в конец списка: условие 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
Алгоритм Флойда: обнаружение цикла и его начало
Обозначения
  • среднее значение
  • сила регуляризации: штраф за сложность модели
  • номер или количество: индекс шага, число соседей, кластеров или позиций
μ — длина хвоста, λ — длина цикла, k — сдвиг точки встречи от входа в цикл
  • Время, то есть линейное по числу узлов.
  • Память: и ничего больше.
  • Предусловие — переход из узла детерминирован: у каждого состояния ровно один следующий.

Как только оба указателя оказались внутри цикла, рассмотрим расстояние от быстрого до медленного по направлению движения. За один шаг медленный продвигается на 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. Почему указатели со скоростями 1 и 2 обязаны встретиться, а не перепрыгнуть друг друга?
  2. Список из одного узла, ссылающегося на себя: отработает ли ваш код?
  3. Найдите середину списка за один проход — и скажите, какую из двух середин вернёт ваш код при чётной длине.
  4. Почему поиск входа в цикл начинается именно из головы списка?
  5. Массив [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%

Монотонный стек и дек · Алгоритмы и структуры данных

Приём поверх стека: хранить только тех, для кого ответ ещё не найден. Даёт ближайший больший элемент для всех позиций сразу и максимум в скользящем окне — за один проход.

Следующий шагДальше по связи: Two Pointers and Sliding WindowЗонтичная тема: что общего у четырёх приёмов с двумя индексами и как выбрать нужный. Сами приёмы разобраны каждый на своей странице.Перейти →

Глава «Алгоритмы и структуры данных» последний раз правилась . Нашли ошибку — напишите.