Учебные материалы: механизм вместо тезисов, блоки «Факты для карточек», Ловушки (Claude Code) + diag/cards_src.tsv
This commit is contained in:
@@ -1,5 +1,17 @@
|
||||
# Задача 02 — связный список (C++)
|
||||
|
||||
## Глава I. Общая информация
|
||||
|
||||
- **Цель:** развернуть список, найти середину и определить цикл — без единой дополнительной
|
||||
аллокации, только перелинковка указателей.
|
||||
- **Почему это в Eltex:** списки соединений, очереди задач, таблицы состояний — везде связные
|
||||
структуры. Приём двух указателей (slow/fast), которым решаются `find_middle` и `has_cycle`,
|
||||
— это алгоритм Флойда, тот же паттерн всплывает при поиске циклов в графах зависимостей
|
||||
и в обходе кольцевых структур.
|
||||
- **Что сдаётся:** `solution.cpp`, проходящий `python3 grade.py 02`.
|
||||
|
||||
## Глава II. Что нужно знать до старта
|
||||
|
||||
Даны функции над односвязным списком `Node{int value; Node* next;}`:
|
||||
|
||||
```c
|
||||
@@ -8,10 +20,57 @@ 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) памяти в обоих режимах
|
||||
(поиск середины и поиск цикла), и как тот же приём переносится на поиск цикла в графе.
|
||||
|
||||
Reference in New Issue
Block a user