77 lines
6.2 KiB
Markdown
77 lines
6.2 KiB
Markdown
# Задача 02 — связный список (C++)
|
||
|
||
## Глава I. Общая информация
|
||
|
||
- **Цель:** развернуть список, найти середину и определить цикл — без единой дополнительной
|
||
аллокации, только перелинковка указателей.
|
||
- **Почему это в Eltex:** списки соединений, очереди задач, таблицы состояний — везде связные
|
||
структуры. Приём двух указателей (slow/fast), которым решаются `find_middle` и `has_cycle`,
|
||
— это алгоритм Флойда, тот же паттерн всплывает при поиске циклов в графах зависимостей
|
||
и в обходе кольцевых структур.
|
||
- **Что сдаётся:** `solution.cpp`, проходящий `python3 grade.py 02`.
|
||
|
||
## Глава II. Что нужно знать до старта
|
||
|
||
Даны функции над односвязным списком `Node{int value; Node* next;}`:
|
||
|
||
```c
|
||
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) памяти в обоих режимах
|
||
(поиск середины и поиск цикла), и как тот же приём переносится на поиск цикла в графе.
|