Palindrome Linked List
Дан односвязный список, хранящийся в двух массивах одинаковой длины. Узел i содержит значение values[i] и связан с узлом next[i]; -1 обозначает конец списка, а головой списка является узел 0. Узлы хранятся не в порядке следования в списке, поэтому переходите по ссылкам.
Верните true, если значения, прочитанные от головы до конца, совпадают со значениями, прочитанными от конца к голове, и false в противном случае.
Функция
- nextinteger-array
- индекс узла, на который ссылается каждый узел, или -1 для последнего узла
- valuesinteger-array
- значение, хранящееся в каждом узле
- Возвращаетboolean
- true, если список читается одинаково в обоих направлениях
Ограничения
1 ≤ L ≤ 5000, гдеL— длинаnextиvalues.-100 ≤ values[i] ≤ 100- Каждый
next[i]— это-1или индекс узла от0доL-1. - Начиная с узла
0, список проходит через каждый узел ровно один раз, а затем достигает-1. Цикла нет.
Примеры
- Ввод
- next = [4, 3, -1, 2, 1]values = [3, 4, 3, 1, 1]
- Вывод
- true
- Пояснение
- Связи посещают узлы
0, 4, 1, 3, 2, поэтому список выглядит так:3, 1, 4, 1, 3. Две тройки совпадают, две единицы совпадают, а у4посередине нет пары для сравнения.
- Ввод
- next = [2, -1, 3, 1]values = [4, 4, 9, 9]
- Вывод
- true
- Пояснение
- Список посещает узлы
0, 2, 3, 1и считывает4, 9, 9, 4— одинаково в обоих направлениях.
- Ввод
- next = [2, -1, 1]values = [1, 2, 1]
- Вывод
- false
- Пояснение
- Массив
values— это[1, 2, 1], но список посещает узлы0, 2, 1и считывает значения1, 1, 2. В обратном порядке это2, 1, 1, поэтому ответ —false.
Дополнительный вопрос
Сможешь сделать это за время O(L) и используя O(1) дополнительной памяти?
Подсказки
Открывайте по одной. Каждая подсказывает чуть больше.
При проверке палиндрома сравнивают первое значение с последним, второе — с предпоследним и так далее. В односвязном списке можно двигаться только вперёд. Что позволило бы добраться до конца списка?
Копирование значений в массив в порядке списка работает, но требует
O(L)памяти. Без копирования по второй половине нужно пройти в обратном направлении. Изменение её ссылок может сделать это возможным.Найди середину с помощью двух указателей:
fastпроходит по две ссылки за раз, покаslowпроходит по одной. Когдаfastбольше не может двигаться,slowоказывается в конце первой половины. Разверни ссылки послеslow, затем одновременно двигайся от начала и от прежнего последнего узла и сравнивай.
Решение
Проверка на палиндром сопоставляет каждое значение с его зеркальным отражением с другого конца. Односвязный список можно обходить только вперёд, поэтому сложность заключается в том, чтобы прочитать вторую половину в обратном порядке. Копия массива позволяет сделать это, используя память объёмом O(L), где L — количество узлов; разворот связей во второй половине позволяет сделать это с использованием памяти объёмом O(1).
Скопируйте значения в массив
Идея
Здесь указатель — это индекс узла, а переход вперёд выполняется так: node = next[node]. Один раз пройдите по списку и скопируйте каждое значение в массив, который можно читать с обоих концов. Сравнивайте первое и последнее значения, перемещайте оба индекса к середине и возвращайте false при любом несовпадении. При нечётной длине среднее значение является парой самому себе.
Нужно прочитать каждое значение, поэтому время O(L) — наилучшее из возможных. Цена — L дополнительных целых чисел.
Алгоритм
- Иди от узла
0до-1и добавляй каждыйvalues[node]в массив. - Задай
i = 0, аj— последний индекс. - Пока
i < j, возвращайfalse, если значения различаются, иначе увеличивайiи уменьшайj. - Верни
true.
def isPalindrome(next, values):
# Write the values out in list order.
seq = []
node = 0
while node != -1:
seq.append(values[node])
node = next[node]
# Compare from both ends toward the middle.
i = 0
j = len(seq) - 1
while i < j:
if seq[i] != seq[j]:
return False
i += 1
j -= 1
return TrueРазверните вторую половину на месте
Идея
Разверни ссылки второй половины вместо её копирования: проход от старого последнего узла теперь ведёт к середине. Чтобы найти середину, перемещай fast на два узла за каждый узел, на который перемещается slow, пока впереди у fast есть два узла. slow остановится на последнем узле первой половины, а если L нечётно — на среднем узле. В первом примере slow проходит 0 → 4 → 1, а fast — 0 → 1 → 2, поэтому slow останавливается на узле 4.
Разверни узлы после slow, затем одновременно проходи по left от начала списка и по right от развёрнутой половины: сначала 3 с 3, затем 1 с 1. Развёрнутая половина никогда не длиннее первой, поэтому, остановившись, когда закончится right, ты сравнишь именно нужные пары. Каждый этап проходит не более L узлов и хранит несколько целых чисел. В R изменение next_ копирует вектор один раз, поэтому R использует O(L) памяти.
Алгоритм
- Установите
slow = fast = 0. Пока иnext[fast], иnext[next[fast]]не равны-1, перемещайтеslowна один узел, аfast— на два. - Разверните список, начинающийся с
next[slow]: для каждого узла сохраните его ссылку, направьте её наprev, затем переместитеprevи узел вперёд. - Установите
left = 0иright = prev, то есть голову развёрнутой половины. - Пока
right != -1, при несовпадении вернитеfalse, иначе переместите оба указателя на один узел. - Верните
true.
def isPalindrome(next, values):
# Find the middle: fast moves two links for every one link slow moves.
slow = 0
fast = 0
while next[fast] != -1 and next[next[fast]] != -1:
slow = next[slow]
fast = next[next[fast]]
# slow is the last node of the first half (the middle node when the length is odd).
# Reverse the links of the second half.
prev = -1
node = next[slow]
while node != -1:
after = next[node]
next[node] = prev
prev = node
node = after
# prev is the old last node, now the head of the reversed half.
left = 0
right = prev
while right != -1:
if values[left] != values[right]:
return False
left = next[left]
right = next[right]
return True
Ловушки и крайние случаи
- Проверка массивов вместо списка. В третьем примере
values— это[1, 2, 1], но список имеет вид1, 1, 2. - Проверка только
next[fast] != -1. Затемfastможет перейти к-1и прочитатьnext[-1], что в большинстве языков выходит за допустимый диапазон, а в Python и Ruby без предупреждения означает последний элемент. - Сравнение до конца
leftвместоright. Средний узел по-прежнему связан со старой второй половиной, поэтому проход от головы списка получается длиннее. - Перезапись
next[node]во время разворота до сохранения его значения, из-за чего остальная часть списка теряется. - В Lua и R, где нумерация массивов начинается с 1, чтение
next[node]для идентификатора узла с нумерацией от 0. Читайтеnext[node + 1].
Частые вопросы3
Какова временная сложность проверки палиндрома в связанном списке?
Каждый подход имеет сложность O(L) для списка из L узлов, поскольку нужно сравнить каждое значение. Копирование значений в массив или стек требует дополнительной памяти O(L); разворот второй половины на месте требует O(1).
Как быстрый и медленный указатели находят середину связного списка?
Оба начинают с головы списка, и быстрый указатель делает два шага на каждый шаг медленного. Когда быстрый указатель достигает конца, он проходит весь список, а медленный — половину.
Разрушает ли обращение половины списка входные данные?
Да, это меняет связи во второй половине, и в большинстве языков этот массив принадлежит самому вызывающему коду. Если список всё ещё нужен, завершите сравнение, не возвращаясь раньше времени, а затем снова разверните эту половину. Средний узел по-прежнему связан с первым узлом этой половины, поэтому список снова целиком восстановлен с использованием дополнительной памяти O(1).
Похожие задачи
Задачи на те же идеи. Решите две или три, и приём запомнится.
def isPalindrome(next, values):
# Напишите код здесьТест #1
Тест #2
Тест #3
Аргументы
next = [4, 3, -1, 2, 1] values = [3, 4, 3, 1, 1]
Ожидаемый вывод
true