Содержание Show
Задача обращения односвязного списка является классической задачей в программировании и имеет множество применений. Недавно стала популярной в кругах подготовки к техническим собеседованиям благодаря своей простоте и одновременно важности в понимании работы со связными списками.
В этой статье мы рассмотрим пошаговое решение задачи Reverse Linked List с помощью алгоритма итеративного и рекурсивного обращения.
Постановка задачи:
Дан односвязный список, требуется обратить его порядок, то есть сделать так, чтобы последний элемент стал первым, предпоследний — вторым и так далее.
Пример 1:
- Ввод: head = [1,2,3,4,5]
- Вывод: [5,4,3,2,1]
Пример 2:
- Ввод: head = [1,2]
- Вывод: [2,1]
Пример 3:
- Ввод: head = []
- Вывод: []
Ограничения:
Количество узлов в списке находится в диапазоне [0, 5000].
Значения узлов находятся в диапазоне от -5000 до 5000.
Итеративный метод
Идея заключается в том, чтобы пройти по списку, изменяя указатели на следующий элемент так, чтобы он указывал на предыдущий элемент.
class Solution:
def reverseList(self, head: ListNode) -> ListNode:
prev = None
current = head
while current:
next_node = current.next
current.next = prev
prev = current
current = next_node
return prev
Давайте теперь разберем код итеративного метода построчно:
class Solution:
def reverseList(self, head: ListNode) -> ListNode:
: Мы определяем классclass Solution
, который будет содержать решение задачи.Solution
: Мы определяем методdef reverseList(self, head: ListNode) -> ListNode:
, который принимает на вход голову (начало) односвязного списка (reverseList
head
) и возвращает новую голову списка.
здесь предполагается классом, представляющим узлы односвязного списка.ListNode
prev = None
current = head
: Мы инициализируем переменнуюprev = None
значениемprev
. Это будет указатель на предыдущий узел, который изначально равенNone
, так как у первого узла списка нет предыдущего.None
Мы инициализируем переменнуюcurrent = head
значениемcurrent
.head
будет использоваться для перемещения по списку, начиная с головы.current
while current:
next_node = current.next
current.next = prev
prev = current
current = next_node
: Мы начинаем циклwhile current
, который будет выполняться до тех пор, покаwhile
не станет равнымcurrent
, что означает достижение конца списка.None
: Мы сохраняем ссылку на следующий узел в переменнойnext_node = current.next
, чтобы не потерять доступ к следующему узлу после изменения ссылки текущего узла.next_node
: Мы изменяем ссылку текущего узла на предыдущий узел. Это осуществляет обращение связи у текущего узла, так что он указывает на предыдущий узел вместо следующего.current.next = prev
: Мы обновляем переменнуюprev = current
, чтобы она указывала на текущий узел перед переходом к следующему шагу в списке.prev
: Мы перемещаем указательcurrent = next_node
на следующий узел списка.current
return prev
: После завершения обхода списка и его обращения мы возвращаемreturn prev
, который теперь указывает на новую голову списка, после того как старая голова стала хвостом.prev
Рекурсивный метод
Рекурсивный подход основан на том, что обращение списка можно представить как обращение «остатка» списка после текущего элемента.
class Solution:
def reverseList(self, head: ListNode) -> ListNode:
if not head or not head.next:
return head
reversed_head = self.reverseList(head.next)
head.next.next = head
head.next = None
return reversed_head
Теперь построчно разберем рекурсивный метод решения этой задачи:
if not head or not head.next:
return head
: Проверка базового случая рекурсии. Если список пустой (if not head or not head.next:
) или содержит только один элемент (head is None
), то он уже обращен и мы просто возвращаем голову списка.head.next is None
reversed_head = self.reverseList(head.next)
: Рекурсивный вызов методаreversed_head = self.reverseList(head.next)
для обращения «остатка» списка, исключая текущий узелreverseList
.head
будет содержать новую голову обращенного «остатка» списка.reversed_head
head.next.next = head
: Мы обращаем связь следующего узла текущего узла (head.next.next = head
) так, чтобы он указывал на текущий узел (head.next
). Это обеспечивает обращение списка.head
head.next = None
: Мы обнуляем ссылку следующего узла текущего узла (head.next = None
). Это необходимо, чтобы предотвратить зацикливание списка после его обращения.head
return reversed_head
: Возвращаем новую голову обращенного «остатка» списка.return reversed_head
Заключение
Оба метода эффективны и могут быть использованы в зависимости от предпочтений программиста или требований к производительности. Итеративный метод основан на последовательном изменении указателей на следующий элемент списка, в то время как рекурсивный метод разбивает задачу на подзадачи и использует стек вызовов для обращения списка.
- Итеративный метод:
- Временная сложность: [math]\text{O}(\text{n})[/math]
- Где [math]\text{n}[/math]- количество узлов в списке.
Этот метод имеет линейную временную сложность, так как проходит по каждому узлу списка только один раз, меняя указатели.
- Рекурсивный метод:
- Временная сложность: [math]\text{O}(\text{n})[/math]
- Где [math]\text{n}[/math] — количество узлов в списке.
В данном методе каждый узел списка посещается один раз во время рекурсивных вызовов, поэтому он также имеет линейную временную сложность.