Определить цикл в LinkedList
Условие
Напишите программу, которая определяет, содержит ли односвязный список цикл.
Цикл существует, если какой-либо узел в списке указывает на один из предыдущих узлов.
Примеры
- 1 → 2 → 3 → 4 → 2 (цикл, так как 4 указывает на 2) → true
- 1 → 2 → 3 → 4 → null → false
Подсказка
Используйте алгоритм Флойда ("черепаха и заяц"): два указателя движутся с разной скоростью. Если есть цикл, они встретятся.
Требования
- Временная сложность O(n)
- Пространственная сложность O(1)
- Не модифицируйте список
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Определить цикл в LinkedList
Это классическая задача на определение цикла в связном списке. Её часто задают на собеседованиях, так как она требует понимания работы указателей и демонстрирует элегантное решение через алгоритм Флойда (Floyd's Cycle Detection).
Алгоритм Флойда: "Черепаха и заяц"
Идея очень простая: используем два указателя, которые движутся по списку с разной скоростью:
- Черепаха (slow) — движется на 1 шаг за раз
- Заяц (fast) — движется на 2 шага за раз
Если в списке есть цикл, эти два указателя в конце концов встретятся. Если список заканчивается (fast или fast.next становится null), цикла нет.
Структура ListNode
public class ListNode {
int val;
ListNode next;
ListNode(int val) {
this.val = val;
}
}
Решение: определение наличия цикла
public class LinkedListCycle {
public static boolean hasCycle(ListNode head) {
if (head == null || head.next == null) {
return false;
}
ListNode slow = head;
ListNode fast = head;
while (fast != null && fast.next != null) {
slow = slow.next; // Черепаха: 1 шаг
fast = fast.next.next; // Заяц: 2 шага
if (slow == fast) {
return true; // Цикл найден!
}
}
return false; // Цикла нет
}
}
Пример работы
Для списка 1 → 2 → 3 → 4 → 2 → ...
| Итерация | slow | fast | Встреча? |
|---|---|---|---|
| 0 | 1 | 1 | Нет |
| 1 | 2 | 3 | Нет |
| 2 | 3 | 2 | Нет |
| 3 | 4 | 4 | ДА! |
На итерации 3 оба указателя указывают на узел 4, что означает наличие цикла.
Полная реализация с тестами
public class LinkedListCycleDetection {
public static boolean hasCycle(ListNode head) {
if (head == null || head.next == null) {
return false;
}
ListNode slow = head;
ListNode fast = head;
while (fast != null && fast.next != null) {
slow = slow.next;
fast = fast.next.next;
if (slow == fast) {
return true;
}
}
return false;
}
// Дополнительный метод: найти начало цикла
public static ListNode findCycleStart(ListNode head) {
if (head == null || head.next == null) {
return null;
}
ListNode slow = head;
ListNode fast = head;
// Находим точку пересечения
while (fast != null && fast.next != null) {
slow = slow.next;
fast = fast.next.next;
if (slow == fast) {
break;
}
}
// Если цикла нет
if (fast == null || fast.next == null) {
return null;
}
// Находим начало цикла
slow = head;
while (slow != fast) {
slow = slow.next;
fast = fast.next;
}
return slow;
}
public static void main(String[] args) {
// Тест 1: Цикл существует
ListNode node1 = new ListNode(1);
ListNode node2 = new ListNode(2);
ListNode node3 = new ListNode(3);
ListNode node4 = new ListNode(4);
node1.next = node2;
node2.next = node3;
node3.next = node4;
node4.next = node2; // Цикл: 4 → 2
System.out.println(hasCycle(node1)); // true
System.out.println(findCycleStart(node1).val); // 2
// Тест 2: Цикла нет
ListNode nodeA = new ListNode(1);
ListNode nodeB = new ListNode(2);
ListNode nodeC = new ListNode(3);
nodeA.next = nodeB;
nodeB.next = nodeC;
nodeC.next = null;
System.out.println(hasCycle(nodeA)); // false
}
}
Почему алгоритм Флойда работает?
Математическое объяснение:
Предположим цикл существует. Пусть:
a— расстояние от начала списка до начала циклаb— расстояние от начала цикла до точки встречиc— длина цикла
Когда slow делает n шагов, fast делает 2n шагов. Они встретятся, когда:
- slow находится в цикле:
n = a + b - fast в цикле дважды прошёл расстояние slow:
2n = a + b + k*c
Отсюда: n = k*c, что гарантирует встречу.
Анализ сложности
Временная сложность: O(n)
- В худшем случае оба указателя проходят весь список
- Fast указатель может сделать максимум n/2 итераций
Пространственная сложность: O(1)
- Используем только два указателя
- Не выделяем дополнительную память
- Не модифицируем список
Преимущества перед другими подходами
| Метод | Время | Память | Плюсы | Минусы |
|---|---|---|---|---|
| HashSet | O(n) | O(n) | Простой | Требует O(n) памяти |
| Флойд | O(n) | O(1) | Оптимален | Требует понимания |
| Пометка | O(n) | O(1) | Работает | Модифицирует список |
Вопросы на собеседовании
- Почему fast движется на 2 шага? — чтобы гарантировать встречу в цикле
- Найти начало цикла — используя двойной проход (см. код выше)
- Длина цикла — после встречи, считаем шаги до повторной встречи