Быстрый и медленный указатели
Середина связного списка
Найти средний узел связного списка за один проход
Постановка задачи
Описание (LeetCode - 876. Middle of the Linked List)
Дана голова односвязного списка head. Нужно вернуть средний узел списка.
Если средних узлов два, нужно вернуть второй из них.
Пример 1:
head = [1, 2, 3, 4, 5]
Результат:
[3, 4, 5]
Пояснение:
Средний узел имеет значение 3, поэтому возвращаем список начиная с этого узла:
[3, 4, 5].
Пример 2:
head = [1, 2, 3, 4, 5, 6]
Результат:
[4, 5, 6]
Пояснение:
В списке два средних узла со значениями 3 и 4. По условию нужно вернуть
второй из них, то есть узел со значением 4.
Ограничения:
- Количество узлов в списке от
1до100 1 <= Node.val <= 100
Решение
Прямое решение - сначала посчитать длину списка за один проход, затем пройти еще
раз до узла с индексом length / 2. Это работает, но требует двух проходов по
списку.
Паттерн быстрого и медленного указателей позволяет найти середину за один проход.
Идея решения:
Запустим два указателя из головы списка:
slowдвигается на один узел за шаг.fastдвигается на два узла за шаг.
Когда быстрый указатель дойдет до конца списка, медленный пройдет ровно половину пути и окажется на среднем узле.
Разберем на примере списка нечетной длины head = [1, 2, 3, 4, 5].
- Инициализация:
slowиfastстоят на голове списка.
- Двигаем
slowна один узел,fastна два (slowна2,fastна3).
- Двигаем еще раз:
slowна3,fastна5. Уfast.nextзначениеnull, цикл останавливается.slowуказывает на середину списка.
Итог: узел со значением 3.
Для списка четной длины head = [1, 2, 3, 4, 5, 6]:
- Инициализация: оба указателя на голове списка.
- Первый шаг:
slowна2,fastна3.
- Второй шаг:
slowна3,fastна5.
- Третий шаг:
fastпереходит вnull, аslowперемещается на4. Цикл завершается,slowуказывает на второй средний узел.
Итог: узел со значением 4.
Основные шаги:
- Инициализируем
slow = head,fast = head. - Пока
fast != nullиfast.next != null:- двигаем
slowна один шаг (slow = slow.next); - двигаем
fastна два шага (fast = fast.next.next).
- двигаем
- Возвращаем
slow.
Условие fast != null && fast.next != null гарантирует, что мы не обратимся к
fast.next.next, когда следующего узла уже нет (как в списках четной, так и
нечетной длины).
Оценка сложности
Временная сложность
Мы проходим по списку за один цикл. Быстрый указатель доходит до конца списка, а медленный проходит половину.
Итоговая временная сложность: O(n).
Пространственная сложность
Мы используем только два указателя.
Пространственная сложность: O(1).
Код решения
Приведем код решения.
func middleNode(head *ListNode) *ListNode {
slow := head
fast := head
for fast != nil && fast.Next != nil {
slow = slow.Next
fast = fast.Next.Next
}
return slow
}Итоги
Задача Middle of the Linked List - классический пример использования паттерна
быстрого и медленного указателей.
- Суть алгоритма:
fastидет в два раза быстрееslow, поэтому к моменту достижения конца спискаslowоказывается ровно в середине. - Эффективность:
- Временная сложность:
O(n), так как список просматривается за один проход. - Пространственная сложность:
O(1), используются только два указателя.
- Временная сложность:
- Граничные случаи:
- Один узел: он же является серединой.
- Два узла: алгоритм корректно возвращает второй узел.
- Нечетная длина: возвращается единственный средний узел.
- Четная длина: условие остановки цикла приводит к возврату второго среднего узла.
- Оптимальное решение: мы не считаем длину списка отдельным проходом, а находим середину на лету.