6.2 KiB
Задача 02 — связный список (C++)
Глава I. Общая информация
- Цель: развернуть список, найти середину и определить цикл — без единой дополнительной аллокации, только перелинковка указателей.
- Почему это в Eltex: списки соединений, очереди задач, таблицы состояний — везде связные
структуры. Приём двух указателей (slow/fast), которым решаются
find_middleиhas_cycle, — это алгоритм Флойда, тот же паттерн всплывает при поиске циклов в графах зависимостей и в обходе кольцевых структур. - Что сдаётся:
solution.cpp, проходящийpython3 grade.py 02.
Глава II. Что нужно знать до старта
Даны функции над односвязным списком Node{int value; Node* next;}:
Node* reverse_list(Node* head); // развернуть список на месте, вернуть новую голову
Node* find_middle(Node* head); // середина; для чётной длины — второй из двух средних
bool has_cycle(Node* head); // есть ли цикл
Механизм двух указателей: slow идёт на 1 узел за шаг, fast — на 2. Для find_middle, когда
fast доходит до конца (fast == nullptr или fast->next == nullptr), slow стоит ровно в
середине — за счёт того, что slow проходит вдвое меньше узлов, чем fast. Для чётной длины
условие остановки должно давать именно второй из двух средних узлов — это проверяется на
списке длины 2 и 4.
Для has_cycle тот же дуэт указателей ловит цикл иначе: если цикл есть, fast заходит в него
и после каждого шага сокращает расстояние до slow внутри цикла на 1 узел (потому что относительная
скорость fast к slow внутри цикла — 1 узел/шаг), значит рано или поздно slow == fast; если
цикла нет, fast первым дойдёт до nullptr.
reverse_list разворачивается за один проход тремя указателями prev/cur/next: на каждом шаге
переставляется cur->next = prev до итерации по цепочке. Рекурсивный разворот сюда не годится —
он тратит O(n) памяти стека вызовов и нарушает требование O(1) дополнительной памяти.
Почему дальше: тот же принцип «два индекса вместо лишней памяти» — в задаче 03, только на массиве, а не на указателях.
Глава III. Требования
- никаких аллокаций, O(1) дополнительной памяти, один проход там, где это возможно;
find_middleиhas_cycle— через два указателя (медленный/быстрый);- корректная работа с пустым списком и списком из одного узла.
Глава IV. Критерии приёмки
Проверка: python3 grade.py 02. Критерий: все ok, ASAN/UBSAN чистые (утечки в тесте
считаются ошибкой — тест сам освобождает память).
Глава V. Ловушки
- Забыть проверку
head == nullptrв начале любой из трёх функций → разыменование нулевого указателя → падение/ASAN SEGV на пустом списке. - В
find_middleневерное условие остановки цикла (fast->nextбез проверки самогоfastнаnullptr) → на списке чётной длины либо возвращается не тот из двух средних узлов, либо падение на последнем шаге. - В
reverse_listпереставитьcur->next = prevдо сохранения старогоcur->nextво временную переменную → потеря хвоста списка, обход обрывается раньше конца. - Рекурсивный
reverse_listвместо итеративного → лишняя память стека на каждый вызов → нарушение требования O(1), на длинном списке возможен stack overflow.
Факты для карточек
- base | Во сколько раз быстрее идёт
fastотносительноslowв паре двух указателей? — в 2 раза - core | Почему рекурсивный разворот списка не годится под требование O(1) памяти? — каждый вызов кладёт кадр в стек, суммарно O(n) памяти
- core | На чём основано доказательство, что
fastдогонитslowпри цикле? — внутри цикла расстояние между ними сокращается на 1 узел за шаг - base | Сколько дополнительных указателей нужно для итеративного разворота списка? — 3
(
prev,cur,next)
После сдачи
Разбор: почему алгоритм Флойда работает за O(n) времени и O(1) памяти в обоих режимах (поиск середины и поиск цикла), и как тот же приём переносится на поиск цикла в графе.