Связные списки
Удалить N-й узел с конца
Удалить узел из односвязного списка за один проход
Постановка задачи
Описание (LeetCode - 19. Remove Nth Node From End of List)
Дана голова односвязного списка head и число n.
Нужно удалить n-й узел с конца списка и вернуть голову измененного списка.
Пример 1:
head = [1, 2, 3, 4, 5]
n = 2
Результат:
[1, 2, 3, 5]
Пример 2:
head = [1]
n = 1
Результат:
[]
Пример 3:
head = [1, 2]
n = 1
Результат:
[1]
Ограничения:
- Количество узлов в списке равно
sz 1 <= sz <= 300 <= Node.val <= 1001 <= n <= sz
Решение
Удалить узел из односвязного списка легко, если у нас есть указатель на узел
перед ним. Значит, задача сводится к тому, чтобы найти узел, который стоит перед
n-м узлом с конца.
Можно сначала посчитать длину списка, затем удалить узел с позиции length - n.
Это решение корректно, но делает два прохода. На интервью обычно хотят
увидеть решение за один проход с двумя указателями.
Идея решения:
Создадим фиктивный узел dummy, который указывает на head. Это упростит
удаление головы списка.
Дальше используем два указателя: fast и slow. Сначала сдвинем fast на
n + 1 шагов от dummy. После этого между fast и slow будет расстояние
n + 1 узлов.
Теперь двигаем оба указателя одновременно, пока fast не дойдет до null. В
этот момент slow будет стоять прямо перед узлом, который нужно удалить.
Основные шаги:
- Создаем
dummy, гдеdummy.next = head. - Ставим
fastиslowнаdummy. - Сдвигаем
fastнаn + 1шагов. - Двигаем
fastиslowвместе, покаfastне станетnull. - Удаляем
slow.next. - Возвращаем
dummy.next.
Если не использовать dummy, удаление первого узла придется обрабатывать
отдельно. Например, для head = [1] и n = 1 ответ должен быть пустым списком.
Разберем на примере head = [1, 2, 3, 4, 5], n = 2.
- Инициализация:
slowиfastстоят наdummy.
- Сдвигаем
fastнаn + 1 = 3шага. Теперь между указателями расстояниеn + 1.
- Двигаем
slowиfastвместе.
- Когда
fastдоходит доnull,slowстоит перед удаляемым узлом4.
- Удаляем
slow.next:
slow.next = slow.next.nextИтог: [1, 2, 3, 5]. Возвращаем dummy.next.
Оценка сложности
Временная сложность
Указатели проходят по списку линейно. Каждый узел посещается не более одного раза,
поэтому временная сложность равна O(n).
Пространственная сложность
Мы используем только несколько указателей. Дополнительная память равна O(1).
Код решения
Приведем код решения.
func removeNthFromEnd(head *ListNode, n int) *ListNode {
dummy := &ListNode{Next: head}
fast := dummy
slow := dummy
for i := 0; i <= n; i++ {
fast = fast.Next
}
for fast != nil {
fast = fast.Next
slow = slow.Next
}
slow.Next = slow.Next.Next
return dummy.Next
}Итоги
Задача Remove Nth Node From End of List показывает, как расстояние между двумя указателями заменяет предварительный подсчет длины списка.
- Суть алгоритма: мы держим между
fastиslowрасстояниеn + 1, чтобы к моменту завершения проходаslowстоял перед удаляемым узлом. - Эффективность:
- Временная сложность:
O(n), так как мы проходим список один раз. - Пространственная сложность:
O(1), так как мы используем фиксированное количество указателей.
- Временная сложность:
- Граничные случаи:
- Нужно удалить голову:
dummyпозволяет сделать это без отдельной обработки. - В списке один узел: после удаления
dummy.nextстанетnull. - Нужно удалить последний узел:
slowостановится на предпоследнем узле.
- Нужно удалить голову:
- Оптимальное решение: выполняет один проход и меняет только одну ссылку
next, не создавая новый список.