Учебные материалы: механизм вместо тезисов, блоки «Факты для карточек», Ловушки (Claude Code) + diag/cards_src.tsv

This commit is contained in:
Kodlo-chan
2026-09-24 13:38:58 +07:00
parent e0ad8e0fee
commit 4259fbce75
17 changed files with 1688 additions and 115 deletions
+60 -1
View File
@@ -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) памяти в обоих режимах
(поиск середины и поиск цикла), и как тот же приём переносится на поиск цикла в графе.