Ответ
Эффективный алгоритм использует метод двух указателей (runner technique). Он решает задачу за один проход по списку с константной памятью O(1).
Алгоритм:
- Инициализируем два указателя:
fast(быстрый) иslow(медленный), оба указывают на голову списка. - Сначала перемещаем
fastна k узлов вперед. Это создает "разрыв" вkузлов между указателями. - Затем перемещаем оба указателя одновременно по одному узлу за шаг, пока
fastне достигнет конца списка (null). - В этот момент
slowбудет указывать именно на k-й элемент с конца (или наnull, если k больше длины списка).
Визуализация для k=2 и списка [1->2->3->4->5]:
Шаг 1 (fast ушел на 2 узла): slow=1, fast=3
Шаг 2: slow=2, fast=4
Шаг 3: slow=3, fast=5
Шаг 4: slow=4, fast=null -> STOP. Ответ = 4.
Реализация на C#:
public class ListNode {
public int Value { get; set; }
public ListNode Next { get; set; }
}
public ListNode FindKthFromEnd(ListNode head, int k) {
if (head == null || k <= 0) return null;
ListNode fast = head;
ListNode slow = head;
// 1. Перемещаем fast на k узлов вперед
for (int i = 0; i < k; i++) {
if (fast == null) {
// Если k больше длины списка, возвращаем null
return null;
}
fast = fast.Next;
}
// 2. Двигаем оба указателя, пока fast не достигнет конца
while (fast != null) {
slow = slow.Next;
fast = fast.Next;
}
// 3. slow теперь указывает на k-й элемент с конца
return slow;
}
Сложность:
- Время: O(n), где n — длина списка. Список проходит только один раз.
- Память: O(1), дополнительная память не зависит от размера списка.
Альтернативы и их недостатки:
- Два прохода: Сначала найти длину n, затем пройти до (n-k)-го элемента. Сложность тоже O(n), но требует двух проходов.
- Стек: Помещать все узлы в стек, затем выталкивать k элементов. Требует O(n) дополнительной памяти.
Ответ 18+ 🔞
Ну ты смотри, какой прикол придумали, чтобы не ебаться с лишними проходами по списку! Есть такая штука — метод двух указателей, её ещё "бегунками" обзывают. Суть в том, что мы за один проход находим нужный элемент, и память при этом не жрёт, как не в себя — всего O(1), то есть константа, блядь.
Как это работает, на пальцах:
- Берём два указателя:
fast(быстрый) иslow(медленный). Оба тыкаем в самый первый узел, в голову. - Сначала гоняем нашего
fastвперёд ровно на k шагов. Получается, между ними теперь дистанция вkузлов, как будто растянули резинку. - А теперь начинаем двигать их синхронно, по одному шажку за раз. И так до тех пор, пока
fastне упрётся в конец списка, в этот самыйnull. - В этот самый момент наш
slowбудет как раз на том самом месте, где сидит k-й элемент с конца! Магия, ёпта! Если, конечно,kне больше, чем длина списка — тогда вернёмnullи не будем выёбываться.
Представь на примере, ищем второй с конца (k=2) в списке [1->2->3->4->5]:
Начинаем: slow=1, fast=1
Гоняем fast на 2 шага: slow=1, fast=3
Двигаем вместе: slow=2, fast=4
Ещё шаг: slow=3, fast=5
И последний: slow=4, fast=null -> СТОП! Вот он, наш ответ — четвёрка.
А вот как это в коде на C# выглядит, если не накосячить:
public class ListNode {
public int Value { get; set; }
public ListNode Next { get; set; }
}
public ListNode FindKthFromEnd(ListNode head, int k) {
// Проверки на всякий пожарный, чтобы не получить неожиданный хуй в тапки
if (head == null || k <= 0) return null;
ListNode fast = head;
ListNode slow = head;
// 1. Загоняем fast вперёд на k узлов
for (int i = 0; i < k; i++) {
if (fast == null) {
// А вот и накосячили — k больше, чем узлов в списке
return null;
}
fast = fast.Next;
}
// 2. Топаем вместе, пока fast не кончится
while (fast != null) {
slow = slow.Next;
fast = fast.Next;
}
// 3. slow теперь и есть наш заветный k-й с конца элемент
return slow;
}
Что по сложности?
- По времени: O(n), потому что мы один раз пробегаемся по списку. Не два, не три, а один, что уже хорошо.
- По памяти: O(1), потому что мы, кроме двух указателей, нихуя не храним. Никаких массивов, стеков и прочей хуйни.
А что, разве можно по-другому? Можно, конечно, но это будет или дольше, или память жрать. Например:
- Два прохода: Сначала узнать длину списка
n, потом пройти до элементаn-k. Тоже O(n), но бегать придётся дважды — не спортивно. - Запихнуть всё в стек: Толкаешь все узлы в стек, а потом вытаскиваешь
kштук. Работает, но памяти сожрёт O(n), а это уже пиздец как расточительно для такой простой задачи.