Быстрый и медленный указатели
Палиндромный связный список
Проверить, читается ли связный список одинаково в обе стороны
Постановка задачи
Описание (LeetCode - 234. Palindrome Linked List)
Дана голова односвязного списка head. Нужно вернуть true, если значения в
списке образуют палиндром. Иначе нужно вернуть false.
Палиндром - это последовательность, которая читается одинаково слева направо и справа налево.
Пример 1:
head = [1, 2, 2, 1]
Результат:
true
Пример 2:
head = [1, 2]
Результат:
false
Ограничения:
- Количество узлов в списке от
1до10⁵ 0 <= Node.val <= 9
Решение
Для массива решение было бы простым: ставим один указатель в начало, второй в конец массива и сравниваем элементы, двигаясь навстречу. В односвязном списке нельзя перемещаться назад, поэтому классический подход с двумя указателями с концов не подходит напрямую.
Можно скопировать все значения в массив и проверить палиндром на массиве. Это
работает за O(n), но требует O(n) дополнительной памяти.
Оптимальный способ - решить задачу на месте за O(1) памяти.
Идея решения:
Нам нужно сравнить первую половину списка со второй половиной в обратном порядке:
- Находим середину списка быстрым и медленным указателями (Середина связного списка).
- Разворачиваем вторую половину списка (Разворот списка).
- Сравниваем значения в первой и развернутой второй половине.
Разберем на примере списка четной длины head = [1, 2, 2, 1].
- Инициализация:
slowиfastстоят на голове списка.
- Двигаем
slowна один шаг,fastна два.
fastдоходит до конца.slowуказывает на начало второй половины (на узел2).
- Разворачиваем вторую половину и сравниваем значения первой и развернутой второй части.
- Все значения совпадают, список является палиндромом.
Для списка нечетной длины head = [1, 2, 3, 2, 1]:
- Инициализация: оба указателя на голове.
slowостанавливается на среднем узле3.
- Разворачиваем вторую половину от узла
3. Сравнение идет, пока не закончится развернутая часть. Средний элемент сравнивается сам с собой и не мешает проверке.
Основные шаги:
- Инициализируем
slow = head,fast = head. - Двигаем
slowна один шаг, аfastна два шага, покаfast != nullиfast.next != null. - Разворачиваем вторую половину списка, начиная с
slow. - Сравниваем значения от
headи от головы развернутой части. - Если значения в узлах не совпадают, возвращаем
false. - Если развернутая часть закончилась, возвращаем
true.
В этой задаче важно сравнивать значения узлов (val), а не сами ссылки на
объекты. Два разных узла с одинаковым значением считаются равными.
Оценка сложности
Временная сложность
Мы один раз ищем середину списка, один раз разворачиваем вторую половину и один раз сравниваем половины. Каждая операция линейна.
Итоговая временная сложность: O(n).
Пространственная сложность
Мы используем только несколько указателей и разворачиваем список на месте.
Пространственная сложность: O(1).
Код решения
Приведем код решения.
func isPalindrome(head *ListNode) bool {
slow := head
fast := head
for fast != nil && fast.Next != nil {
slow = slow.Next
fast = fast.Next.Next
}
second := reverseList(slow)
first := head
for second != nil {
if first.Val != second.Val {
return false
}
first = first.Next
second = second.Next
}
return true
}
func reverseList(head *ListNode) *ListNode {
var previous *ListNode
current := head
for current != nil {
next := current.Next
current.Next = previous
previous = current
current = next
}
return previous
}Итоги
Задача Palindrome Linked List показывает, как использовать паттерн быстрого и
медленного указателей вместе с разворотом списка, чтобы избежать выделения
дополнительного массива.
- Суть алгоритма: быстрым и медленным указателями находим середину списка, разворачиваем вторую половину и поэлементно сравниваем ее с первой.
- Эффективность:
- Временная сложность:
O(n), так как поиск середины, разворот и сравнение занимают линейное время. - Пространственная сложность:
O(1), так как список разворачивается на месте.
- Временная сложность:
- Граничные случаи:
- Один узел: список всегда является палиндромом.
- Четная длина: сравниваются две половины одинакового размера.
- Нечетная длина: центральный узел корректно сравнивается сам с собой.
- Оптимальное решение: мы не копируем значения в массив, а работаем со структурой самого списка.