Быстрый и медленный указатели
Цикл в связном списке
Проверить, есть ли цикл с помощью быстрого и медленного указателей
Постановка задачи
Описание (LeetCode - 141. Linked List Cycle)
Дана голова связного списка head. Нужно определить, есть ли в списке цикл.
Цикл есть, если какой-то узел снова ссылается на один из предыдущих узлов списка.
Пример 1:
head -> 3 -> 2 -> 0 -> -4
^ |
| |
+----------+
head = [3, 2, 0, -4], pos = 1
Результат:
true
Пояснение:
Последний узел указывает на узел с индексом 1.
Пример 2:
head = [1, 2], pos = 0
Результат:
true
Пример 3:
head = [1], pos = -1
Результат:
false
Ограничения:
- Количество узлов в списке от
0до10⁴ -10⁵ <= Node.val <= 10⁵posне передается в функцию, он используется только для описания цикла
Решение
Простое решение - хранить все посещенные узлы в хеш-множестве. Если мы снова
увидели тот же узел, значит цикл есть. Это работает за O(n), но требует O(n)
дополнительной памяти.
Паттерн быстрого и медленного указателей позволяет решить задачу без дополнительной структуры данных.
Идея решения:
Запустим два указателя из головы списка:
slowдвигается на один узел за шаг.fastдвигается на два узла за шаг.
Если цикла нет, fast дойдет до конца списка. Если цикл есть, оба указателя
окажутся внутри кольца, и быстрый указатель догонит медленный.
Основные шаги:
- Инициализируем
slow = head,fast = head. - Пока
fastиfast.nextне равныnull:- двигаем
slowна один шаг; - двигаем
fastна два шага; - если
slow == fast, возвращаемtrue.
- двигаем
- Если цикл завершился, возвращаем
false.
Почему нужно проверять fast.next? Потому что быстрый указатель делает два
шага, и перед обращением к fast.next.next нужно убедиться, что следующий узел
существует.
Этот алгоритм часто называют алгоритмом Флойда или "черепаха и заяц". Медленный указатель - черепаха, быстрый - заяц.
Оценка сложности
Временная сложность
В списке без цикла быстрый указатель пройдет список до конца. В списке с циклом встреча тоже произойдет за линейное число шагов.
Итоговая временная сложность: O(n).
Пространственная сложность
Мы используем только два указателя, поэтому дополнительная память равна O(1).
Код решения
Приведем код решения.
func hasCycle(head *ListNode) bool {
slow := head
fast := head
for fast != nil && fast.Next != nil {
slow = slow.Next
fast = fast.Next.Next
if slow == fast {
return true
}
}
return false
}Итоги
Разная скорость движения позволяет обнаружить цикл без памяти на посещенные
узлы. Если в списке есть цикл, быстрый указатель обязательно встретится с
медленным. Если цикла нет, быстрый указатель дойдет до null.