diff --git a/README.md b/README.md index d0413be..2e0f43c 100644 --- a/README.md +++ b/README.md @@ -5,8 +5,10 @@ ## Что внутри - `WEEK7.md` — **рабочий документ**: интенсив на 7 дней (D0 23.09 → D7 30.09), пересобранный - по результату диагностики (квиз 14/32: алгоритмы 1/8, Linux 3/8, сети 3/8, C/C++ 7/8). - Каждый день: карточки 30 мин → алгоритмы 3 ч → системное 3 ч → разбор и код 1,5 ч. + по диагностике (квиз 14/32, карточки 11/60). День устроен так: + **урок (теория + разбор) → карточки → ступени задачи → зачёт `grade.py`**. +- `lessons/` — уроки по дням: теория по-русски, рабочие примеры кода с разбором, готовые + ответы на вопросы собеседования, ссылки на первопартийные материалы. Начинать с них. - `PLAN.md` — полный трек M1–M10 на пост-офферный период (модули, артефакты, материалы). - `TRACKER.md` — дневник: что решено, где встал, сколько минут. Это вход для калибровки. - `diag/` — пакет: @@ -15,8 +17,9 @@ epoll-сервер, bash по логу, разбор падения в gdb; - `tasks/10..13` — добор под слабые домены: хеш-таблица с открытой адресацией, куча (`heapify` O(n), top-K), Дейкстра, арифметика подсетей; - - `facts_drill.py` + `facts.json` — 60 карточек «числа и факты» (сложности, байты - заголовков, `/26 = 62`, TTL, порядок TCP-хендшейка, паддинг структур); + - `facts_drill.py` + `facts.json` — 101 карточка «числа и факты» с пояснениями и + уровнями: `base` 41 (начинать отсюда, у каждой есть объяснение), `core` 52, + `deep` 8 (нишевое, в базовый прогон не попадает); - `grade.py` — автопроверка задач (ASAN/UBSAN, таймауты, PASS/FAIL + полный лог); - `questions_export.txt` — вопросы квиза текстом. @@ -40,15 +43,22 @@ ## Как начать +Порядок первого захода — читать урок, потом карточки базы, потом задачи: + ```bash git clone https://git.kodlo.art/Kodlo/eduplan-cpp-eltex.git -cd eduplan-cpp-eltex/diag +cd eduplan-cpp-eltex +lessons/D1_algo.md # или открыть в редакторе +cd diag +python3 facts_drill.py --level base --learn # 41 карточка базы, ошибки объясняются +python3 facts_drill.py --level base,core # когда база закрыта (>=80%) python3 quiz.py # квиз (или --answers "1-3 2-4 ...") python3 grade.py # все 13 задач с санитайзерами python3 grade.py 10 11 13 # выборочно python3 grade.py --fast # без санитайзеров -python3 facts_drill.py # 20 карточек интерактивно -python3 facts_drill.py --all # все 60 +python3 facts_drill.py --level base --learn # база с пояснениями +python3 facts_drill.py --level all --all # все 101 карточка +python3 facts_drill.py --list --level base # вывести вопросы с ответами и пояснениями ``` Нужны `g++` (C++17), `gcc`, `python3`; для `07_epoll` — Linux-сокеты, для `08_bash` — diff --git a/WEEK7.md b/WEEK7.md index 4469bbb..4eb9626 100644 --- a/WEEK7.md +++ b/WEEK7.md @@ -1,6 +1,7 @@ # ИУП — интенсив 7 дней (фуллтайм, собеседование ≤ 1 неделя) -**Версия 2, 23.09** — пересобрана по результату диагностики (квиз 14/32). +**Версия 3, 23.09** — пересобрана по диагностике (квиз 14/32, карточки 11/60) и правке +формата: сначала урок с теорией, потом задачи ступенями. Срок: собеседование максимум через неделю, подготовка фуллтайм (~8 ч/день). Цель: закрыть ровно то, что спросят на входе в Eltex (C/C++, базовые алгоритмы и структуры данных, Linux, L2/L3-сети, инструменты), и не утонуть в глубине. @@ -19,10 +20,24 @@ Поэтому в плане появился ежедневный блок карточек (`facts_drill.py`), а C++ урезан: за него отвечает сильная сторона, забирать у неё часы смысла нет. -## Правила интенсива +## Как устроен день (v3, после правки 23.09) -1. Каждый день — три блока: **карточки (30 мин) → алгоритмы (3 ч) → системное (3 ч) → - разбор и код (1,5 ч)**. Блок не начинается, пока не закрыта вчерашняя точка. +Первая версия была проверкой — а проверять пока нечего. Теперь каждый день начинается +с материала: + +1. **Урок (30–40 мин чтения).** `lessons/D<день>_*.md` — теория по-русски, разобранные + примеры с кодом, готовые ответы на вопросы собеседования, ссылки на первопартийные + материалы. Ничего не решаешь, просто читаешь и запускаешь примеры. +2. **Карточки (20 мин).** `python3 facts_drill.py --level base` — начинать с базы; + в режиме `--learn` каждая ошибка объясняется. База закрыта (≥ 80%) — переходишь на + уровень `base,core` (по умолчанию), потом `all`. +3. **Ступени в задаче.** Каждая задача разбита на шаги по 10–30 минут, после каждого шага + прогон. Не «напиши хеш-таблицу», а «сначала хеш-функция, проверь, что 16/32/48 дают + разные индексы». +4. **Зачёт.** `python3 grade.py <номер>` — критерий `PASS`. Это конец дня, а не начало. + +Итого: **урок → карточки → ступени → зачёт**. Задача без урока впереди — моя ошибка, +пиши, и я допишу материал. 2. День закрыт, только если: решены задачи дня **без подсказки**, написан/починен код дня, сделана **одна задача вслух на таймер** (10 мин: разбор + код + сложность), заполнен трекер. @@ -51,36 +66,38 @@ Programming, RFC 791/793, Wireshark wiki, OSTEP (процессы/память), `priority_queue`, арифметика подсетей. - `python3 grade.py` — все 13 с автопроверкой (ASAN/UBSAN); `python3 grade.py 10 13` — выборочно; `--fast` — без санитайзеров. -- `python3 facts_drill.py` — 60 карточек «числа и факты» (algo 20, linux 14, net 14, - c_cpp 12). Маркеры: `>=80%` ок, `50–79%` добор, `<50%` дыра. +- `python3 facts_drill.py --level base` — 41 карточка базы с пояснениями (начинать отсюда); + `--level base,core` — 93; `--level all` — 101. Уровень `deep` (8 карточек, нишевое: + TID в /proc, ulimit, ICMP Time Exceeded и подобное) в базовый прогон не попадает. + Маркеры итога: `>=80%` ок, `50–79%` добор, `<50%` дыра. +- `lessons/` — уроки по дням: теория, разборы с кодом, ответы на вопросы собеседования. - Трекер дня: `TRACKER.md` в корне репозитория. --- -## D0 — 23.09, диагностика (квиз пройден, 14/32) +## D0 — 23.09, вход: база и знакомство -- Квиз: **пройден** — 14/32, слабейшие домены: алгоритмы, Linux, сети (см. таблицу выше). -- Осталось: `cd diag && python3 grade.py 01 02 03 04 05 06 07 08 09` — 9 практических задач. -- Разбор промахов квиза вместе со мной, калибровка дней (уже сделана в этой версии плана). -- **Точка:** есть `results_quiz.json` и `results_tasks.json`, названы 3 слабейших темы. +- Квиз **14/32**, карточки **11/60** — значит базы пока нет, и задачи уровня «напиши + хеш-таблицу» решать рано. Это нормальный вход, просто порядок другой: сначала уроки. +- Сегодня: `python3 facts_drill.py --level base --learn` — 41 карточка базы, каждая ошибка + объясняется. Это 20–30 минут и даёт словарь, без которого дальше нечего делать. +- Задачи 01–13 сегодня решать не нужно. Смотрим на них как на цель недели, а не как на зачёт. +- **Точка:** пройдены базовые карточки, записаны 3 темы, которые «совсем не помню» — + по ним я усилю соответствующие дни. -## D1 — 24.09: инварианты сложности + хеш-таблицы | fork/COW/сигналы - -- Карточки (30 мин) — домен `algo`: бинарный поиск в 1e6, хеш O(1)/worst O(n), heapify O(n), - устойчивость сортировок, Дейкстра и условие применимости. -- Утро, алгоритмы (3 ч) — **формальные свойства, а не «умение решать»**: O-оценки и - амортизация, хеш-таблицы (цепочки vs открытая адресация, загрузка, tombstone, worst O(n)), - сортировки и устойчивость, бинарный поиск и `lower_bound` руками. - Практика: **`tasks/10_hash`** — своя таблица на линейном зондировании; 6–8 задач - Codeforces по тегам `hashing` и `sortings`, рейтинг 800–1400. -- День, системное (3 ч) — Linux: `fork/exec/wait`, **COW-страницы**, зомби и `waitpid`, - сигналы и коды возврата (137 = 128+9 SIGKILL, 139 = 128+11 SIGSEGV), `errno`. - Практика: мини-шелл (fork + exec + wait + `exit` + пайплайн по желанию) — прямой вопрос - на собеседовании; прогнать под ASAN. -- Разбор и код (1,5 ч) — `tasks/01_bits` и `tasks/03_ring` **на чистом C** (без C++-обёрток), - сборка `-Wall -Wextra -Werror -fsanitize=address,undefined`; одна задача вслух; трекер. -- **Точка:** 10_hash PASS; мини-шелл запускает `ls`/`cat`; ASAN чистый; карточки `algo` ≥ 80%. +## D1 — 24.09: сложность и хеш-таблицы | процессы и сигналы +- **Урок:** `lessons/D1_algo.md` (сложность, таблица структур, устройство хеш-таблиц, + разбор кода) и `lessons/D1_linux.md` (fork/exec/wait, COW, сигналы, разбор мини-шелла). + Читать 40 минут, примеры из уроков запустить руками — они рабочие. +- **Карточки:** `python3 facts_drill.py --level base` (домен `algo` — обязателен, остальное + по времени). Если база даётся легко — `--level base,core`. +- **Задача дня:** `tasks/10_hash` — но по ступеням из её `task.md` (Глава IV), по 10–30 минут + на шаг, после каждого шага `python3 grade.py 10`. Ступень 1 — это хеш-функция и проверка, + что 16/32/48 дают разные индексы, и только шестая ступень — перехеширование. +- **Разминка:** `tasks/01_bits` (20 минут, четыре функции) и `tasks/03_ring` (40–60 минут, + тоже по ступеням). +- **Зачёт дня:** `PASS` по `01`, `03`, `10`; карточки `base` ≥ 80%. ## D2 — 25.09: деревья и куча | epoll и неблокирующие сокеты - Карточки (30 мин) — `algo`: heap insert O(log n), top-K, обходы деревьев, сложность DFS diff --git a/diag/README.md b/diag/README.md index 8f6841d..48478ce 100644 --- a/diag/README.md +++ b/diag/README.md @@ -54,9 +54,7 @@ python3 facts_drill.py --domain algo # только алгоритмы python3 facts_drill.py --answers "20|62|ttl|..." # проверить строку ответов разом ``` -Порядок: сначала квиз, потом задачи с 01 по 09 (диагностика), затем 10–13 как добор. -Требования: Linux/WSL (или MinGW-w64 для задач 01–06, 10–13). Задачи 07/08/09 под -Windows-native пропускаются с пометкой `ПРОПУСК` — это не провал, гоняй их в WSL. Зависание = FAIL (есть таймауты). +Порядок: сначала квиз, потом задачи с 01 по 09 (диагностика), затем 10–13 как добор. Зависание = FAIL (есть таймауты). Правила честности: без встроенных `popcount`/`reverse`-хелперов, без копирования чужих решений, `answer.txt` в задаче 09 — обязательная часть (оценивается ход разбора). diff --git a/diag/facts.json b/diag/facts.json index 078ecd0..96be7df 100644 --- a/diag/facts.json +++ b/diag/facts.json @@ -1,6 +1,6 @@ { - "version": 1, - "note": "Карточки под промахи квиза 23.09: точные числа, сложности и условия.", + "version": 2, + "note": "Уровни: base — база (начинать отсюда), core — то, что спросят, deep — нишевое. Поле ref — куда смотреть, why — пояснение.", "facts": [ { "domain": "algo", @@ -11,7 +11,10 @@ "амортизированное o(1)", "o(1) амортизированное", "амортизированно o(1)" - ] + ], + "level": "core", + "ref": "урок D1_algo §1", + "why": "" }, { "domain": "algo", @@ -19,7 +22,10 @@ "a": "O(n)", "accept": [ "o(n)" - ] + ], + "level": "core", + "ref": "урок D1_algo §1", + "why": "" }, { "domain": "algo", @@ -29,7 +35,10 @@ "20", "log2 1e6 = 20", "около 20" - ] + ], + "level": "core", + "ref": "урок D1_algo §1 (log2 1e6 ≈ 20)", + "why": "" }, { "domain": "algo", @@ -40,7 +49,10 @@ "все веса >= 0", "веса неотрицательны", "нет отрицательных рёбер" - ] + ], + "level": "core", + "ref": "урок D4 (графы)", + "why": "" }, { "domain": "algo", @@ -50,7 +62,10 @@ "беллман-форд", "беллман форд", "bellman-ford" - ] + ], + "level": "core", + "ref": "урок D4 (графы)", + "why": "" }, { "domain": "algo", @@ -59,7 +74,10 @@ "accept": [ "o(n)", "о(n)" - ] + ], + "level": "core", + "ref": "урок D1_algo §2 (heapify снизу вверх)", + "why": "" }, { "domain": "algo", @@ -67,7 +85,10 @@ "a": "O(log n)", "accept": [ "o(log n)" - ] + ], + "level": "core", + "ref": "урок D1_algo §2", + "why": "" }, { "domain": "algo", @@ -78,7 +99,10 @@ "merge sort", "слиянием", "сортировка слиянием" - ] + ], + "level": "core", + "ref": "урок D1_algo §2", + "why": "" }, { "domain": "algo", @@ -88,7 +112,10 @@ "o(n^2)", "o(n²)", "o(n2)" - ] + ], + "level": "core", + "ref": "урок D1_algo §2", + "why": "" }, { "domain": "algo", @@ -98,7 +125,10 @@ "o(n log n)", "o(n*logn)", "o(nlogn)" - ] + ], + "level": "core", + "ref": "урок D1_algo §2", + "why": "" }, { "domain": "algo", @@ -109,7 +139,10 @@ "ациклический", "без циклов", "dag (ациклический)" - ] + ], + "level": "core", + "ref": "урок D4 (графы, только DAG)", + "why": "" }, { "domain": "algo", @@ -119,7 +152,10 @@ "флойд", "черепаха и заяц", "алгоритм флойда" - ] + ], + "level": "core", + "ref": "урок D3 (списки)", + "why": "" }, { "domain": "algo", @@ -128,7 +164,10 @@ "accept": [ "bfs", "обход в ширину" - ] + ], + "level": "core", + "ref": "урок D4 (графы)", + "why": "" }, { "domain": "algo", @@ -138,7 +177,10 @@ "o((v+e) log v)", "o((v+e)logv)", "o(e log v)" - ] + ], + "level": "deep", + "ref": "урок D4 (графы)", + "why": "" }, { "domain": "algo", @@ -148,7 +190,10 @@ "o(n log n)", "o(nlogn)", "o(n*log n)" - ] + ], + "level": "deep", + "ref": "урок D5 (ДП и НВП)", + "why": "" }, { "domain": "algo", @@ -158,7 +203,10 @@ "o(n*w)", "o(nw)", "o(n*w)" - ] + ], + "level": "deep", + "ref": "урок D5 (ДП)", + "why": "" }, { "domain": "algo", @@ -169,7 +217,10 @@ "o(alpha(n))", "o(α(n))", "обратная функция аккермана" - ] + ], + "level": "deep", + "ref": "урок D4 (union-find)", + "why": "" }, { "domain": "algo", @@ -179,7 +230,10 @@ "o(v)", "o(h)", "o(v) в худшем случае" - ] + ], + "level": "deep", + "ref": "урок D4 (графы)", + "why": "" }, { "domain": "algo", @@ -188,7 +242,10 @@ "accept": [ "o(n log n)", "o(nlogn)" - ] + ], + "level": "core", + "ref": "урок D1_algo §2", + "why": "" }, { "domain": "algo", @@ -198,7 +255,10 @@ "trie", "префиксное дерево", "бор" - ] + ], + "level": "core", + "ref": "урок D3 (структуры)", + "why": "" }, { "domain": "linux", @@ -208,7 +268,10 @@ "создаёт новый процесс", "создает новый процесс", "новый процесс" - ] + ], + "level": "core", + "ref": "урок D1_linux §1", + "why": "" }, { "domain": "linux", @@ -218,7 +281,10 @@ "cow", "copy-on-write", "копирование при записи" - ] + ], + "level": "core", + "ref": "урок D1_linux §1 (COW)", + "why": "" }, { "domain": "linux", @@ -227,7 +293,10 @@ "accept": [ "epoll", "эполл" - ] + ], + "level": "core", + "ref": "урок D2 (epoll)", + "why": "" }, { "domain": "linux", @@ -237,7 +306,10 @@ "select o(n), epoll o(1)", "epoll o(1)", "o(1) на готовое" - ] + ], + "level": "core", + "ref": "урок D2 (epoll)", + "why": "" }, { "domain": "linux", @@ -247,7 +319,10 @@ "отображает файл в память", "mmap файла в память", "отображение файла в память" - ] + ], + "level": "core", + "ref": "урок D2 (mmap)", + "why": "" }, { "domain": "linux", @@ -257,7 +332,10 @@ "забрать код возврата", "убрать зомби", "чтобы не остались зомби" - ] + ], + "level": "core", + "ref": "урок D1_linux §3", + "why": "" }, { "domain": "linux", @@ -267,7 +345,10 @@ "завершившийся процесс без wait", "ждёт wait", "запись ждёт wait" - ] + ], + "level": "core", + "ref": "урок D1_linux §3", + "why": "" }, { "domain": "linux", @@ -277,7 +358,10 @@ "sigkill", "128+9", "убит sigkill" - ] + ], + "level": "core", + "ref": "урок D1_linux §3", + "why": "" }, { "domain": "linux", @@ -287,7 +371,10 @@ "sigsegv", "128+11", "сегфолт" - ] + ], + "level": "core", + "ref": "урок D1_linux §3", + "why": "" }, { "domain": "linux", @@ -297,7 +384,10 @@ "повторить позже", "нет данных, попробовать снова", "данных нет" - ] + ], + "level": "core", + "ref": "урок D1_linux §5 (errno)", + "why": "" }, { "domain": "linux", @@ -307,7 +397,10 @@ "номер задачи", "task", "в /proc//task" - ] + ], + "level": "deep", + "ref": "man 5 proc", + "why": "" }, { "domain": "linux", @@ -317,7 +410,10 @@ "лимит дескрипторов", "лимит открытых файлов", "число открытых файлов" - ] + ], + "level": "deep", + "ref": "man 2 getrlimit", + "why": "" }, { "domain": "linux", @@ -327,7 +423,10 @@ "sigterm", "sigterm (мягкое завершение)", "запрос на завершение" - ] + ], + "level": "core", + "ref": "урок D1_linux §4 (сигналы)", + "why": "" }, { "domain": "linux", @@ -336,7 +435,10 @@ "accept": [ "20", "20 байт" - ] + ], + "level": "core", + "ref": "урок D5 (TCP)", + "why": "" }, { "domain": "net", @@ -345,7 +447,10 @@ "accept": [ "14", "14 байт" - ] + ], + "level": "core", + "ref": "урок D4 (L2)", + "why": "" }, { "domain": "net", @@ -354,7 +459,10 @@ "accept": [ "4", "4 байта" - ] + ], + "level": "core", + "ref": "урок D4 (802.1Q)", + "why": "" }, { "domain": "net", @@ -363,7 +471,10 @@ "accept": [ "62", "62 узла" - ] + ], + "level": "core", + "ref": "урок D4 (подсети: /26 = 62)", + "why": "" }, { "domain": "net", @@ -371,7 +482,10 @@ "a": "254", "accept": [ "254" - ] + ], + "level": "core", + "ref": "урок D4 (подсети)", + "why": "" }, { "domain": "net", @@ -379,7 +493,10 @@ "a": "TTL", "accept": [ "ttl" - ] + ], + "level": "core", + "ref": "урок D4 (IPv4)", + "why": "" }, { "domain": "net", @@ -389,7 +506,10 @@ "syn, syn-ack, ack", "syn syn-ack ack", "syn -> syn-ack -> ack" - ] + ], + "level": "core", + "ref": "урок D4 (TCP)", + "why": "" }, { "domain": "net", @@ -399,7 +519,10 @@ "mac по ip", "ip -> mac", "находит mac по ip" - ] + ], + "level": "core", + "ref": "урок D4 (ARP)", + "why": "" }, { "domain": "net", @@ -407,7 +530,10 @@ "a": "1500 байт", "accept": [ "1500" - ] + ], + "level": "core", + "ref": "урок D4 (L2)", + "why": "" }, { "domain": "net", @@ -417,7 +543,10 @@ "l2", "канальный", "2" - ] + ], + "level": "core", + "ref": "урок D4 (OSI)", + "why": "" }, { "domain": "net", @@ -427,7 +556,10 @@ "l3", "сетевой", "3" - ] + ], + "level": "core", + "ref": "урок D4 (OSI)", + "why": "" }, { "domain": "net", @@ -435,7 +567,10 @@ "a": "20", "accept": [ "20" - ] + ], + "level": "core", + "ref": "урок D4 (IPv4)", + "why": "" }, { "domain": "net", @@ -445,7 +580,10 @@ "udp 53", "53", "udp/53" - ] + ], + "level": "core", + "ref": "урок D5 (DNS)", + "why": "" }, { "domain": "net", @@ -455,7 +593,10 @@ "4 адреса, 2 узла", "2 узла", "сеть+2 узла+broadcast" - ] + ], + "level": "core", + "ref": "урок D4 (подсети)", + "why": "" }, { "domain": "net", @@ -465,7 +606,10 @@ "ttl истёк", "истёк ttl", "сообщает об истечении ttl" - ] + ], + "level": "deep", + "ref": "RFC 792 (ICMP)", + "why": "" }, { "domain": "c_cpp", @@ -474,7 +618,10 @@ "accept": [ "12", "12 байт" - ] + ], + "level": "core", + "ref": "урок D2 (выравнивание)", + "why": "" }, { "domain": "c_cpp", @@ -484,7 +631,10 @@ "undefined behavior", "неопределённое поведение", "неопределенное поведение" - ] + ], + "level": "core", + "ref": "урок D2 (C++, UB)", + "why": "" }, { "domain": "c_cpp", @@ -494,7 +644,10 @@ "static_cast к rvalue", "ничего, это каст", "это каст к rvalue" - ] + ], + "level": "core", + "ref": "урок D2 (move)", + "why": "" }, { "domain": "c_cpp", @@ -504,7 +657,10 @@ "вызывает конструктор", "конструктор", "конструктор + типизированный указатель" - ] + ], + "level": "core", + "ref": "урок D2 (new/malloc)", + "why": "" }, { "domain": "c_cpp", @@ -514,7 +670,10 @@ "двойное освобождение", "двойное free", "поверхностное копирование" - ] + ], + "level": "core", + "ref": "урок D2 (правило 0/3/5)", + "why": "" }, { "domain": "c_cpp", @@ -524,7 +683,10 @@ "деструктор наследника не вызовется", "иначе ub", "чтобы вызвался деструктор наследника" - ] + ], + "level": "core", + "ref": "урок D2 (virtual-деструктор)", + "why": "" }, { "domain": "c_cpp", @@ -534,7 +696,10 @@ "this const", "запрещает менять поля", "метод не меняет объект" - ] + ], + "level": "core", + "ref": "урок D2 (const-метод)", + "why": "" }, { "domain": "c_cpp", @@ -545,7 +710,10 @@ "не определён", "не задан", "неопределён" - ] + ], + "level": "core", + "ref": "урок D2 (UB)", + "why": "" }, { "domain": "c_cpp", @@ -555,7 +723,10 @@ "выходы за границы", "use-after-free", "память: границы и use-after-free" - ] + ], + "level": "core", + "ref": "урок D2 (ASAN)", + "why": "" }, { "domain": "c_cpp", @@ -565,7 +736,10 @@ "undefined behavior", "неопределённое поведение", "overflow" - ] + ], + "level": "core", + "ref": "урок D2 (UBSAN)", + "why": "" }, { "domain": "c_cpp", @@ -575,7 +749,10 @@ "деструктор + копирование + перемещение", "деструктор, копирование, перемещение", "нужно определить деструктор и копирование/перемещение" - ] + ], + "level": "core", + "ref": "урок D2 (правило 0/3/5)", + "why": "" }, { "domain": "c_cpp", @@ -585,7 +762,509 @@ "единственный владелец", "нет счётчика ссылок", "счётчик ссылок у shared_ptr" - ] + ], + "level": "core", + "ref": "урок D2 (умные указатели)", + "why": "" + }, + { + "domain": "algo", + "q": "Что такое сложность O(n) словами?", + "a": "время растёт линейно с числом данных", + "accept": [ + "линейно", + "время растёт линейно", + "растёт линейно с данными" + ], + "level": "base", + "ref": "", + "why": "Правило: O(n) — во сколько раз больше данных, во столько раз больше работы." + }, + { + "domain": "algo", + "q": "Сложность доступа к элементу массива по индексу?", + "a": "O(1)", + "accept": [ + "o(1)" + ], + "level": "base", + "ref": "", + "why": "Массив лежит подряд: адрес = начало + индекс × размер, одно действие." + }, + { + "domain": "algo", + "q": "Сложность поиска значения в неотсортированном массиве?", + "a": "O(n)", + "accept": [ + "o(n)" + ], + "level": "base", + "ref": "", + "why": "Приходится смотреть каждый элемент — других зацепок нет." + }, + { + "domain": "algo", + "q": "Сложность бинарного поиска в отсортированном массиве?", + "a": "O(log n)", + "accept": [ + "o(log n)" + ], + "level": "base", + "ref": "", + "why": "Каждый шаг отбрасывает половину — это и есть log n." + }, + { + "domain": "algo", + "q": "Сколько шагов у бинарного поиска в 1000 элементах (примерно)?", + "a": "10", + "accept": [ + "10", + "около 10" + ], + "level": "base", + "ref": "", + "why": "log2(1000) ≈ 10. Помни: ×1000 данных ≈ +10 шагов." + }, + { + "domain": "algo", + "q": "Стек — какой порядок обработки?", + "a": "LIFO (последний пришёл — первый вышел)", + "accept": [ + "lifo", + "последний вошёл первый вышел", + "lifo (стек)" + ], + "level": "base", + "ref": "", + "why": "Стек: кладём и снимаем с одного конца, как стопку тарелок." + }, + { + "domain": "algo", + "q": "Очередь — какой порядок обработки?", + "a": "FIFO (первый пришёл — первый вышел)", + "accept": [ + "fifo", + "первый вошёл первый вышел" + ], + "level": "base", + "ref": "", + "why": "Очередь: добавление с одного конца, изъятие с другого." + }, + { + "domain": "algo", + "q": "Что такое амортизированная сложность?", + "a": "средняя стоимость операции с учётом редких дорогих операций", + "accept": [ + "средняя с учётом редких дорогих", + "усреднение дорогих операций", + "средняя стоимость" + ], + "level": "base", + "ref": "", + "why": "Пример: push_back у vector обычно O(1), изредка O(n) при росте — в среднем O(1)." + }, + { + "domain": "algo", + "q": "Что такое устойчивость сортировки?", + "a": "равные элементы сохраняют исходный порядок", + "accept": [ + "равные сохраняют порядок", + "порядок равных сохраняется" + ], + "level": "base", + "ref": "", + "why": "Устойчивы merge/insertion, неустойчивы quick/heap/selection." + }, + { + "domain": "algo", + "q": "Что быстрее для поиска по ключу: отсортированный массив или хеш-таблица?", + "a": "хеш-таблица (в среднем O(1) против O(log n))", + "accept": [ + "хеш таблица", + "хеш-таблица o(1)", + "хеш таблица o(1)" + ], + "level": "base", + "ref": "", + "why": "Массив даёт O(log n) и порядок, хеш — O(1) в среднем, но без порядка." + }, + { + "domain": "linux", + "q": "Что такое процесс?", + "a": "программа в исполнении со своим адресным пространством и PID", + "accept": [ + "программа в исполнении", + "исполняемая программа со своим адресным пространством" + ], + "level": "base", + "ref": "", + "why": "Процесс = код + данные + стек + открытые дескрипторы + PID." + }, + { + "domain": "linux", + "q": "Что такое PID?", + "a": "номер процесса", + "accept": [ + "номер процесса", + "идентификатор процесса" + ], + "level": "base", + "ref": "", + "why": "Ядро выдаёт каждому процессу уникальный номер." + }, + { + "domain": "linux", + "q": "Что такое файловый дескриптор?", + "a": "число, по которому программа обращается к открытому файлу или сокету", + "accept": [ + "число ссылающееся на открытый файл", + "номер открытого файла", + "дескриптор открытого файла" + ], + "level": "base", + "ref": "", + "why": "open() возвращает fd; 0/1/2 — это stdin/stdout/stderr." + }, + { + "domain": "linux", + "q": "Какие дескрипторы 0, 1, 2?", + "a": "stdin, stdout, stderr", + "accept": [ + "stdin stdout stderr", + "ввод вывод ошибки" + ], + "level": "base", + "ref": "", + "why": "0 — ввод, 1 — вывод, 2 — ошибки; перенаправление 2>&1 склеивает 2 с 1." + }, + { + "domain": "linux", + "q": "Что такое системный вызов?", + "a": "запрос программы к ядру (open, read, write, fork)", + "accept": [ + "запрос к ядру", + "обращение программы к ядру", + "запрос программе к ядру" + ], + "level": "base", + "ref": "", + "why": "Программа не работает с железом напрямую — только через ядро." + }, + { + "domain": "linux", + "q": "Разница SIGTERM и SIGKILL?", + "a": "SIGTERM можно перехватить и завершиться корректно, SIGKILL — нет", + "accept": [ + "sigterm перехватывается sigkill нет", + "sigterm можно перехватить" + ], + "level": "base", + "ref": "", + "why": "Поэтому сначала kill (SIGTERM), и только потом kill -9." + }, + { + "domain": "linux", + "q": "Что делает chmod 755 file?", + "a": "владелец rwx, остальные rx", + "accept": [ + "владелец rwx остальные rx", + "rwx для владельца rx для остальных" + ], + "level": "base", + "ref": "", + "why": "7 = rwx, 5 = r-x. Права читаются как три группы по три бита." + }, + { + "domain": "linux", + "q": "Что такое PATH?", + "a": "список каталогов, где оболочка ищет программы по имени", + "accept": [ + "список каталогов для поиска программ", + "переменная со списком каталогов программ" + ], + "level": "base", + "ref": "", + "why": "Поэтому ./prog и prog — разные вещи: второе ищется в PATH." + }, + { + "domain": "linux", + "q": "Что такое конвейер (|) в оболочке?", + "a": "вывод одной программы идёт на вход другой", + "accept": [ + "передача вывода на вход другой программы", + "соединяет вывод со входом" + ], + "level": "base", + "ref": "", + "why": "`ps aux | grep ssh` — фильтрация вывода без временных файлов." + }, + { + "domain": "linux", + "q": "Что такое демон?", + "a": "фоновый процесс без управляющего терминала", + "accept": [ + "фоновый процесс", + "процесс работающий в фоне" + ], + "level": "base", + "ref": "", + "why": "Демоны обычно запускает systemd, а не пользователь из терминала." + }, + { + "domain": "net", + "q": "Сколько бит в IPv4-адресе?", + "a": "32 (четыре октета)", + "accept": [ + "32", + "32 бита" + ], + "level": "base", + "ref": "", + "why": "Отсюда маски: /24 — 24 бита на сеть, 8 на узлы." + }, + { + "domain": "net", + "q": "Сколько бит в MAC-адресе?", + "a": "48", + "accept": [ + "48", + "48 бит" + ], + "level": "base", + "ref": "", + "why": "6 байт, обычно записывают как aa:bb:cc:dd:ee:ff." + }, + { + "domain": "net", + "q": "Что такое маска подсети?", + "a": "показывает, какая часть адреса — сеть, какая — узел", + "accept": [ + "деление адреса на сеть и узел", + "какая часть адреса сеть" + ], + "level": "base", + "ref": "", + "why": "Маска /24 = 255.255.255.0: первые 24 бита — сеть." + }, + { + "domain": "net", + "q": "Что такое порт?", + "a": "номер приложения на узле", + "accept": [ + "номер приложения", + "номер программы на узле" + ], + "level": "base", + "ref": "", + "why": "IP находит машину, порт — конкретную программу на ней." + }, + { + "domain": "net", + "q": "Чем TCP отличается от UDP?", + "a": "TCP с установкой соединения и гарантией доставки, UDP — без", + "accept": [ + "tcp надёжный udp быстрый", + "tcp с соединением udp без", + "надёжность против скорости" + ], + "level": "base", + "ref": "", + "why": "TCP: порядок и повторы. UDP: быстро, но можно потерять — как в DNS-запросах." + }, + { + "domain": "net", + "q": "Что такое шлюз (gateway)?", + "a": "узел, через который уходят пакеты за пределы подсети", + "accept": [ + "узел для выхода из подсети", + "маршрутизатор по умолчанию" + ], + "level": "base", + "ref": "", + "why": "Если адрес не в нашей подсети — отправляем на шлюз." + }, + { + "domain": "net", + "q": "Что делает DNS?", + "a": "превращает имя в IP-адрес", + "accept": [ + "имя в ip", + "преобразует имя в ip", + "разрешает имена в ip" + ], + "level": "base", + "ref": "", + "why": "Поэтому сначала DNS-запрос, потом соединение с полученным IP." + }, + { + "domain": "net", + "q": "Что делает DHCP?", + "a": "выдаёт узлу IP-адрес автоматически", + "accept": [ + "выдаёт ip автоматически", + "автоматическая выдача ip" + ], + "level": "base", + "ref": "", + "why": "Иначе адрес пришлось бы прописывать руками." + }, + { + "domain": "net", + "q": "Что такое 127.0.0.1?", + "a": "localhost — сам узел (петля)", + "accept": [ + "localhost", + "локальный адрес самого узла", + "петля" + ], + "level": "base", + "ref": "", + "why": "Пакеты не уходят в сеть, а возвращаются на ту же машину." + }, + { + "domain": "net", + "q": "Что такое NAT?", + "a": "подмена адресов при выходе в интернет", + "accept": [ + "подмена адресов", + "трансляция адресов" + ], + "level": "base", + "ref": "", + "why": "Много устройств в локальной сети выходят через один внешний IP." + }, + { + "domain": "net", + "q": "Что делает ping?", + "a": "проверяет доступность узла через ICMP echo", + "accept": [ + "проверка доступности через icmp", + "icmp эхо", + "проверяет доступность" + ], + "level": "base", + "ref": "", + "why": "ICMP — служебные сообщения; ping не использует TCP/UDP-порты." + }, + { + "domain": "c_cpp", + "q": "Что такое указатель?", + "a": "переменная, хранящая адрес в памяти", + "accept": [ + "адрес в памяти", + "хранит адрес" + ], + "level": "base", + "ref": "", + "why": "Указатель = адрес. Разыменование (*p) — доступ по этому адресу." + }, + { + "domain": "c_cpp", + "q": "Чем стек отличается от кучи?", + "a": "стек — автоматическая память (локальные переменные), куча — динамическая (new/malloc)", + "accept": [ + "стек автоматическая куча динамическая", + "автоматическая против динамической" + ], + "level": "base", + "ref": "", + "why": "Стек быстрый и ограничен, куча больше, но требует освобождения." + }, + { + "domain": "c_cpp", + "q": "Что такое утечка памяти?", + "a": "выделили и не освободили", + "accept": [ + "не освободили выделенную память", + "память не освобождена" + ], + "level": "base", + "ref": "", + "why": "ASAN с `detect_leaks=1` находит такие места с трассировкой." + }, + { + "domain": "c_cpp", + "q": "Что такое segmentation fault?", + "a": "обращение к памяти, которая программе не принадлежит", + "accept": [ + "обращение к чужой памяти", + "доступ к недопустимой памяти" + ], + "level": "base", + "ref": "", + "why": "Частые причины: нулевой указатель, выход за границы, use-after-free." + }, + { + "domain": "c_cpp", + "q": "Что такое конструктор и деструктор?", + "a": "конструктор инициализирует объект, деструктор освобождает ресурсы", + "accept": [ + "создание и уничтожение объекта", + "инициализация и освобождение" + ], + "level": "base", + "ref": "", + "why": "Деструктор вызывается автоматически при выходе из области видимости." + }, + { + "domain": "c_cpp", + "q": "Что такое RAII?", + "a": "ресурс захватывается в конструкторе и освобождается в деструкторе", + "accept": [ + "ресурс в конструкторе освобождение в деструкторе", + "захват в конструкторе освобождение в деструкторе" + ], + "level": "base", + "ref": "", + "why": "Благодаря этому не бывает утечек при исключениях." + }, + { + "domain": "c_cpp", + "q": "Что такое виртуальная функция?", + "a": "функция, которую вызывают по фактическому типу объекта (полиморфизм)", + "accept": [ + "полиморфизм", + "вызов по фактическому типу" + ], + "level": "base", + "ref": "", + "why": "Реализуется через таблицу виртуальных функций (vtable)." + }, + { + "domain": "c_cpp", + "q": "Что такое шаблон (template)?", + "a": "обобщённый код, подставляющий тип при компиляции", + "accept": [ + "обобщённый код", + "код обобщённый по типу" + ], + "level": "base", + "ref": "", + "why": "Так устроены std::vector и std::sort — код один, типы разные." + }, + { + "domain": "c_cpp", + "q": "Что такое компиляция по шагам?", + "a": "препроцессор → компиляция → сборка (линковка)", + "accept": [ + "препроцессор компиляция линковка", + "препроцессинг компиляция сборка" + ], + "level": "base", + "ref": "", + "why": "На каждом шаге свои ошибки: `#include` — препроцессор, неразрешённый символ — линковщик." + }, + { + "domain": "c_cpp", + "q": "Зачем флаг -Wall -Wextra?", + "a": "включает предупреждения компилятора о подозрительном коде", + "accept": [ + "включает предупреждения", + "предупреждения компилятора" + ], + "level": "base", + "ref": "", + "why": "С -Werror предупреждения становятся ошибками — полезная привычка." } ] } diff --git a/diag/facts_drill.py b/diag/facts_drill.py index dcaf51e..c99f353 100644 --- a/diag/facts_drill.py +++ b/diag/facts_drill.py @@ -1,14 +1,21 @@ #!/usr/bin/env python3 -"""Тренажёр «числа и факты» — добор по промахам квиза (22.09: algo 1/8, linux 3/8, net 3/8). +"""Тренажёр «числа и факты» — карточки под промахи диагностики. + +Уровни карточек: + base — база, с этого начинать (объяснение есть в поле why) + core — то, что почти наверняка спросят + deep — нишевое, в базовый прогон не попадает Использование (из каталога diag): - python3 facts_drill.py # интерактивно, 20 случайных карточек - python3 facts_drill.py --count 60 --all # все 60 - python3 facts_drill.py --domain algo # только алгоритмы - python3 facts_drill.py --list # вывести все вопросы с ответами (читать) - python3 facts_drill.py --answers "20|62|ttl|..." # проверить строку ответов - python3 facts_drill.py --answers "..." --count 60 --seed 42 # тот же порядок, что в прогоне - python3 facts_drill.py --selftest # проверить сам набор карточек + python3 facts_drill.py # 20 карточек уровня base+core, интерактивно + python3 facts_drill.py --level base # только база (первый заход — отсюда) + python3 facts_drill.py --level all --all # все 101 + python3 facts_drill.py --domain algo --level base + python3 facts_drill.py --learn # после каждого ответа показать пояснение + python3 facts_drill.py --list --level base # вывести вопросы с ответами и пояснениями + python3 facts_drill.py --answers "20|62|ttl|..." # проверить строку ответов + python3 facts_drill.py --answers "..." --level all --all --seed 42 + python3 facts_drill.py --selftest Ответы сравниваются нестрого: регистр, «ё», тире и лишние пробелы не важны. Результат пишется в results_facts.json. @@ -22,6 +29,7 @@ import sys HERE = os.path.dirname(os.path.abspath(__file__)) FACTS_PATH = os.path.join(HERE, "facts.json") +LEVELS = ("base", "core", "deep") def norm(s: str) -> str: @@ -43,20 +51,35 @@ def check(fact, answer: str) -> bool: return any(a == norm(x) for x in fact.get("accept", [])) +def hint(fact) -> str: + """Пояснение и ссылка, куда смотреть.""" + parts = [] + if fact.get("why"): + parts.append(fact["why"]) + if fact.get("ref"): + parts.append("см. " + fact["ref"]) + return " · ".join(parts) + + def selftest(facts) -> int: problems = [] for i, f in enumerate(facts, 1): - for key in ("domain", "q", "a", "accept"): + for key in ("domain", "q", "a", "accept", "level"): if not f.get(key): problems.append(f"карточка {i}: пустое поле {key}") + if f.get("level") not in LEVELS: + problems.append(f"карточка {i}: неизвестный уровень {f.get('level')!r}") if f.get("accept") and not any(norm(x) for x in f["accept"]): problems.append(f"карточка {i}: варианты ответа нормализуются в пустоту") if not check(f, f["accept"][0]): problems.append(f"карточка {i}: канонический ответ не проходит проверку") - doms = {} + if not (f.get("why") or f.get("ref")): + problems.append(f"карточка {i}: нет ни пояснения, ни ссылки — карточка не учит") + doms, lvls = {}, {} for f in facts: doms[f["domain"]] = doms.get(f["domain"], 0) + 1 - print(f"карточек: {len(facts)}; по доменам: {doms}") + lvls[f["level"]] = lvls.get(f["level"], 0) + 1 + print(f"карточек: {len(facts)}; по доменам: {doms}; по уровням: {lvls}") if problems: print("ПРОБЛЕМЫ:") for p in problems: @@ -70,9 +93,11 @@ def main(): ap = argparse.ArgumentParser() ap.add_argument("--answers", default=None, help="ответы через | (тот же порядок, что в интерактиве)") ap.add_argument("--count", type=int, default=20) - ap.add_argument("--all", action="store_true", help="взять все карточки") + ap.add_argument("--all", action="store_true", help="взять все карточки после фильтров") ap.add_argument("--domain", default=None, choices=["algo", "linux", "net", "c_cpp"]) + ap.add_argument("--level", default="base,core", help="base,core,deep или all (по умолчанию base,core)") ap.add_argument("--seed", type=int, default=42) + ap.add_argument("--learn", action="store_true", help="показывать пояснение после каждого ответа") ap.add_argument("--list", action="store_true") ap.add_argument("--selftest", action="store_true") args = ap.parse_args() @@ -80,6 +105,16 @@ def main(): facts = load() if args.selftest: sys.exit(selftest(facts)) + + if args.level.strip().lower() == "all": + want = set(LEVELS) + else: + want = {x.strip() for x in args.level.split(",") if x.strip()} + bad = want - set(LEVELS) + if bad: + print(f"неизвестный уровень: {', '.join(sorted(bad))} (есть base, core, deep, all)") + sys.exit(2) + facts = [f for f in facts if f["level"] in want] if args.domain: facts = [f for f in facts if f["domain"] == args.domain] if not facts: @@ -88,9 +123,12 @@ def main(): if args.list: for i, f in enumerate(facts, 1): - print(f"{i:2d}. [{f['domain']}] {f['q']}") - print(f" -> {f['a']}") - print(f"\nвсего: {len(facts)} (полный ответ в facts.json: {FACTS_PATH})") + print(f"{i:3d}. [{f['domain']}/{f['level']}] {f['q']}") + print(f" -> {f['a']}") + h = hint(f) + if h: + print(f" {h}") + print(f"\nвсего: {len(facts)} (полный набор: {FACTS_PATH})") sys.exit(0) rng = random.Random(args.seed) @@ -105,12 +143,22 @@ def main(): print(f"ответов меньше, чем карточек: {len(given)} < {len(picked)} (проверяю сколько есть)") for i, f in enumerate(picked): ans = given[i] if i < len(given) else "" - results.append({"n": i + 1, "domain": f["domain"], "q": f["q"], "answer": ans, - "ok": check(f, ans), "correct": f["a"]}) + results.append({"n": i + 1, "domain": f["domain"], "level": f["level"], "q": f["q"], + "answer": ans, "ok": check(f, ans), "correct": f["a"], "hint": hint(f)}) + if args.learn: + for r in results: + if not r["ok"]: + print(f"#{r['n']} [{r['domain']}] {r['q']}") + print(f" твой ответ: {r['answer'] or '(пусто)'}") + print(f" верно: {r['correct']}") + if r["hint"]: + print(f" {r['hint']}") + print() else: - print(f"Карточек в прогоне: {len(picked)}. Пустой ввод = не знаю, 'q' = выход.\n") + print(f"Карточек в прогоне: {len(picked)} (уровни: {', '.join(sorted(want))}).") + print("Пустой ввод = не знаю, 'q' = выход.\n") for i, f in enumerate(picked, 1): - print(f"[{i}/{len(picked)}] ({f['domain']}) {f['q']}") + print(f"[{i}/{len(picked)}] ({f['domain']}/{f['level']}) {f['q']}") try: ans = input(" > ").strip() except (EOFError, KeyboardInterrupt): @@ -119,26 +167,36 @@ def main(): if ans.lower() in ("q", "quit", "выход"): break ok = check(f, ans) - print(" ok" if ok else " нет. верно: " + f["a"]) - results.append({"n": len(results) + 1, "domain": f["domain"], "q": f["q"], - "answer": ans, "ok": ok, "correct": f["a"]}) + if ok: + print(" ok" + (" — " + hint(f) if args.learn and hint(f) else "")) + else: + print(" нет. верно: " + f["a"]) + h = hint(f) + if h: + print(" " + h) + results.append({"n": len(results) + 1, "domain": f["domain"], "level": f["level"], + "q": f["q"], "answer": ans, "ok": ok, "correct": f["a"], "hint": hint(f)}) ok = sum(1 for r in results if r["ok"]) total = len(results) - per = {} + per_dom, per_lvl = {}, {} for r in results: - d = per.setdefault(r["domain"], {"total": 0, "ok": 0}) - d["total"] += 1 - d["ok"] += 1 if r["ok"] else 0 + for bucket, key in ((per_dom, r["domain"]), (per_lvl, r["level"])): + v = bucket.setdefault(key, {"total": 0, "ok": 0}) + v["total"] += 1 + v["ok"] += 1 if r["ok"] else 0 with open(os.path.join(HERE, "results_facts.json"), "w", encoding="utf-8") as fh: - json.dump({"seed": args.seed, "correct": ok, "total": total, - "per_domain": per, "details": results}, fh, ensure_ascii=False, indent=2) + json.dump({"seed": args.seed, "level": args.level, "correct": ok, "total": total, + "per_domain": per_dom, "per_level": per_lvl, "details": results}, + fh, ensure_ascii=False, indent=2) print(f"\n=== ИТОГ КАРТОЧЕК: {ok}/{total} ===") - for d, v in sorted(per.items()): - share = 100 * v["ok"] / v["total"] if v["total"] else 0 - mark = "ок" if share >= 80 else ("добор" if share >= 50 else "дыра") - print(f" {d:8s} {v['ok']}/{v['total']} ({share:.0f}%) — {mark}") + for title, bucket in (("по доменам", per_dom), ("по уровням", per_lvl)): + print(f" {title}:") + for d, v in sorted(bucket.items()): + share = 100 * v["ok"] / v["total"] if v["total"] else 0 + mark = "ок" if share >= 80 else ("добор" if share >= 50 else "дыра") + print(f" {d:8s} {v['ok']}/{v['total']} ({share:.0f}%) — {mark}") print("Подробности: results_facts.json") sys.exit(0 if total and ok == total else 1) diff --git a/diag/tasks/01_bits/task.md b/diag/tasks/01_bits/task.md index f930ef7..d32a33d 100644 --- a/diag/tasks/01_bits/task.md +++ b/diag/tasks/01_bits/task.md @@ -1,8 +1,30 @@ -# Задача 01 — битовые операции (C) +# Задача 01 — битовые операции (разминка) -Реализуй в `solution.cpp` четыре функции. Запрещено пользоваться встроенными -`__builtin_popcount`, `std::popcount`, `std::byteswap` — считается любая корректная -ручная реализация. +> Первая задача дня: руки размялись, потом основная. Ориентир — 20–30 минут. + +## Глава I. Общая информация + +- **Цель:** уверенно работать с битами — это ежедневный инструмент в embedded, сетях и + протоколах. +- **Почему это в Eltex:** заголовки пакетов, маски, флаги, регистры — всё битовое. Вопросы + вида «посчитай единичные биты» и «поменяй порядок байт» на собеседовании почти гарантированы. +- **Что сдаётся:** `solution.cpp`, проходящий `python3 grade.py 01`. + +## Глава II. Что нужно знать до старта + +Четыре инструмента, которых достаточно: + +1. `x & (x - 1)` — гасит самый младший единичный бит. Отсюда классика: число единиц можно + считать циклом, пока `x` не станет нулём. +2. `x & 1` — младший бит; `x >> 1` — сдвиг вправо. +3. `x & (1u << k)` — проверка k-го бита. +4. Маски: `0x000000FF`, `0x0000FF00`, `0x00FF0000`, `0xFF000000` — это четыре байта 32-битного + числа. Комбинация «сдвинул и сложил» даёт любой порядок байт. + +## Глава III. Задание + +Реализуй в `solution.cpp` четыре функции. Встроенные `__builtin_popcount`, `std::popcount`, +`std::byteswap` использовать нельзя — нужна своя реализация (любая корректная). ```c int popcount32(uint32_t x); // число единичных бит @@ -11,8 +33,64 @@ bool is_power_of_two(uint32_t x); // x — степень двойки (0 uint32_t bswap32(uint32_t x); // поменять порядок байт местами ``` -Проверка: `python3 grade.py 01` из каталога `diag/`. -Критерий: все проверки `ok`, сборка без предупреждений, ASAN/UBSAN чистые. +Полезно сразу проверять себя руками: `bswap32(0x11223344) == 0x44332211`, +`popcount32(0b1011) == 3`, `is_power_of_two(1) == true`, `is_power_of_two(0) == false`, +`reverse_bits32(1) == 0x80000000`. -Подсказки по разбору после сдачи: `x & (x - 1)`; реверс — классический цикл на 32 итерации -или пара «маска+сдвиг»; `bswap` — четыре маски. +## Глава IV. Ступени + +**Ступень 1 (5 минут).** `is_power_of_two` — самая простая. Подумай, чем степень двойки +отличается от остальных чисел: у неё ровно один единичный бит. Значит подойдёт приём +`x & (x - 1)`. Отдельно обработай `0` (он не степень двойки) и проверь на `1` и на `0x80000000`. + +**Ступень 2 (10 минут).** `popcount32` — цикл: пока `x != 0`, гаси младший бит и считай +итерации. Альтернатива — 32 сдвига с проверкой младшего бита; оба варианта верные, первый +в среднем быстрее. + +**Ступень 3 (10 минут).** `reverse_bits32` — цикл на 32 итерации: берём младший бит `x`, +ставим его в результат на позицию `31 - i`, сдвигаем `x`. Внимание на типы: сдвиг `1u << 31` +для знакового `int` — UB, поэтому работай в `uint32_t`. + +**Ступень 4 (10 минут).** `bswap32` — четыре байта. Самый наглядный вариант: маски и сдвиги, +`((x & 0x000000FFu) << 24) | ((x & 0x0000FF00u) << 8) | ((x & 0x00FF0000u) >> 8) | ((x & 0xFF000000u) >> 24)`. + +**Ступень 5.** `python3 grade.py 01` — пока не станет `PASS`. + +## Глава V. Критерии приёмки + +`PASS` от `grade.py 01`: все проверки `ok`, сборка без предупреждений, ASAN/UBSAN чистые. + +## Глава VI. Подсказки (после первой попытки) + +
+UBSAN жалуется на сдвиг + +Сдвиг знакового числа в старший бит — неопределённое поведение. Все промежуточные значения +держи в `uint32_t` и пиши константы с суффиксом `u`: `1u << 31`. +
+ +
+is_power_of_two возвращает true на нуле + +У нуля нет единичных битов, поэтому `x & (x-1)` даёт 0 — как и у степеней двойки. Добавь +явную проверку `x != 0`. +
+ +
+reverse_bits32 работает «почти» + +Проверь порядок операций в цикле: сначала ставим бит в результат, потом сдвигаем источник. +Если перепутать — биты уедут на одну позицию. +
+ +## Глава VII. Частые ошибки + +- Сдвиги в `int` вместо `uint32_t` → UB, UBSAN ругается. +- Забыт ноль в `is_power_of_two`. +- В `bswap32` перепутаны направления сдвигов (влево/вправо) — проверь на `0x11223344`. +- Использование `__builtin_popcount` — запрещено условием: смысл в том, чтобы написать самому. + +## После сдачи + +Разбор: почему `x & (x-1)` гасит младший бит; как эти приёмы используются в реальных +протоколах (маски подсетей, флаги TCP-заголовка, разбор полей IPv4). diff --git a/diag/tasks/03_ring/task.md b/diag/tasks/03_ring/task.md index 5fd1f2d..595dac9 100644 --- a/diag/tasks/03_ring/task.md +++ b/diag/tasks/03_ring/task.md @@ -1,6 +1,26 @@ # Задача 03 — кольцевой буфер (C++) -Кольцевой буфер фиксированной ёмкости — базовая структура для embedded и сетевого кода. +> Разминка уровня «основная»: структура маленькая, но встречается везде. Ориентир — 40–60 минут. + +## Глава I. Общая информация + +- **Цель:** научиться писать структуру с фиксированной памятью и без сдвигов элементов. +- **Почему это в Eltex:** приём/передача пакетов, буферы DMA, очереди между потоками — + всё это кольцевые буферы. Понимание wrap-around и «полный/пустой» — прямой вопрос на + собеседовании. +- **Что сдаётся:** `solution.cpp`, проходящий `python3 grade.py 03`. + +## Глава II. Что нужно знать до старта + +Идея: массив фиксированного размера + два индекса. `head` — куда писать, `tail` — откуда +читать. Когда индекс доходит до конца, он возвращается в начало: `idx = (idx + 1) % capacity`. +Сдвигать элементы не нужно никогда — в этом весь смысл. + +Ловушка, из-за которой задача попадает в собеседования: **как отличить пустой буфер от +полного, если оба индекса совпали?** Варианты: хранить счётчик `size`, либо оставлять одну +ячейку свободной. В нашем интерфейсе есть `size()`, поэтому проще хранить счётчик. + +## Глава III. Задание ```c++ class RingBuffer { @@ -17,9 +37,68 @@ public: ``` Требования: -- O(1) на push/pop, никаких сдвигов элементов; -- память выделяется один раз в конструкторе, освобождается в деструкторе; -- поведение после заворачивания индексов (wrap-around) корректное; -- копирование запрещать не обязательно, но двойного освобождения быть не должно. -Проверка: `python3 grade.py 03`. Критерий: все `ok`, ASAN/UBSAN чистые. +- O(1) на `push`/`pop`, никаких сдвигов элементов; +- память выделяется один раз в конструкторе и освобождается в деструкторе; +- корректное поведение после заворачивания индексов (записали до конца, продолжили с начала); +- двойного освобождения быть не должно. + +## Глава IV. Ступени + +**Ступень 1 (10 минут).** Поля: указатель на массив, `capacity_`, `size_`, `head_`, `tail_`. +Конструктор выделяет массив и обнуляет поля. Деструктор освобождает. Если хочешь короче — +`std::vector` вместо ручного `new[]`, тогда деструктор не нужен вовсе. + +**Ступень 2 (10 минут).** `empty()` — это `size_ == 0`, `full()` — `size_ == capacity_`. +Проверь на буфере ёмкости 1: после одного `push` он полон, после одного `pop` пуст. + +**Ступень 3 (15 минут).** `push`: если полон — вернуть `false` и ничего не менять; иначе +записать в `head_`, сдвинуть `head_ = (head_ + 1) % capacity_`, увеличить `size_`, вернуть `true`. + +**Ступень 4 (15 минут).** `pop`: если пуст — `false`; иначе отдать `buffer_[tail_]`, сдвинуть +`tail_`, уменьшить `size_`, вернуть `true`. + +**Ступень 5.** Проверь руками wrap-around: ёмкость 4, положи 4 элемента, сними 2, положи +ещё 3 — все значения должны идти в правильном порядке. Затем `python3 grade.py 03`. + +## Глава V. Критерии приёмки + +`PASS` от `grade.py 03`: сборка без предупреждений, ASAN/UBSAN чистые, все проверки `ok`, +включая порядок FIFO и корректный wrap-around. + +## Глава VI. Подсказки (после первой попытки) + +
+Значения после заворота идут не в том порядке + +Проверь, что `pop` читает именно `tail_`, а `push` пишет именно в `head_`, и что оба индекса +инкрементируются **после** операции. Классическая ошибка — сдвинуть `head_` до записи. +
+ +
+Перезапись при полном буфере + +Ты не проверяешь `full()` в `push`. По условию перезапись запрещена: полный буфер возвращает +`false` и не портит старые данные. +
+ +
+ASAN: утечка или двойное освобождение + +Если память выделена через `new[]`, освобождать нужно `delete[]` (не `delete`). Проще взять +`std::vector` — тогда вопрос исчезает. +
+ +## Глава VII. Частые ошибки + +- Сдвиг элементов вместо индексов (теряется весь смысл O(1)). +- Путаница «пусто/полно» при совпавших индексах. +- `%` на каждом шаге там, где можно было обойтись условием — не ошибка, но на собеседовании + спросят «а быстрее можно?» (ответ: `if (++idx == cap) idx = 0;`). +- Копирование объекта без правила трёх → двойное освобождение. Если сдаёшь с ручным `new[]`, + запрети копирование (`= delete`). + +## После сдачи + +Разбор: как устроены буферы в драйверах (ring buffer с head/tail для DMA), почему в +многопоточном варианте нужны атомарные индексы и `memory_order`. diff --git a/diag/tasks/10_hash/task.md b/diag/tasks/10_hash/task.md index 9568a91..5b7706f 100644 --- a/diag/tasks/10_hash/task.md +++ b/diag/tasks/10_hash/task.md @@ -1,8 +1,34 @@ -# Задача 10 — хеш-таблица с открытой адресацией (C++) +# Задача 10 — хеш-таблица с открытой адресацией -Свой контейнер, без `std::unordered_map`. Разрешение коллизий — **линейное зондирование** -(linear probing), ёмкость — степень двойки, при загрузке > 0.7 таблица **перехешируется** -вдвое. Индексация — через маску `idx & (capacity - 1)`, а не `%`. +> Перед этой задачей прочитай урок `lessons/D1_algo.md` (разделы 3 и 5) — там разобран +> принцип и есть рабочий образец с цепочками. Здесь то же самое, но сложнее: коллизии +> разрешаются **внутри массива**. + +## Глава I. Общая информация + +- **Цель:** научиться писать контейнер, который используется в реальном коде (кэши, + таблицы маршрутизации, счётчики в логах) и который спрашивают на собеседовании в формате + «а сам сможешь?». +- **Почему это в Eltex:** таблицы MAC-адресов, FDB коммутатора, кэши сессий — всё это + хеш-таблицы с открытой адресацией и жёсткими требованиями к памяти. +- **Время:** 1,5–2,5 часа со ступенями. Если больше 3 часов — не долби в одиночку, скажи мне. +- **Что сдаётся:** файл `solution.cpp`, проходящий `python3 grade.py 10`. + +## Глава II. Что нужно знать до старта + +1. `idx = hash & (cap - 1)` работает как «остаток от деления», только если `cap` — степень + двойки. Пример: `cap = 8` (маска `0b111`), `hash = 19` (`0b10011`) → `19 & 7 = 3`. +2. Линейное зондирование: если ячейка занята, пробуем `(i+1) & (cap-1)`, потом `(i+2) & …` + и так по кругу, пока не найдём свободную (для вставки) или нужный ключ (для поиска). +3. Удалять «в ноль» нельзя: если между началом зондирования и элементом появится пустая + ячейка, поиск до него не дойдёт. Поэтому пустая ячейка помечается **tombstone** — + «здесь был элемент, иди дальше». +4. Фактор загрузки `load = size / capacity`. Держим ≤ 0.7: при 0.8+ пробеги становятся + длинными, и O(1) превращается в O(n). + +## Глава III. Задание + +Свой контейнер, `std::unordered_map` использовать нельзя. Интерфейс ровно такой: ```c++ class HashTable { @@ -17,16 +43,109 @@ public: ``` Требования: -- `put` 100 000 ключей — суммарно быстрее 2 секунд (в среднем O(1)); -- ключи с совпадающими младшими битами (например, все кратные 16) обязаны находиться — - это проверка зондирования, а не «повезло с хешем»; -- после 100 000 вставок `capacity()` растёт (степень двойки, загрузка ≤ 0.7), а `size()` - честно считает элементы, включая обновления существующих ключей; -- удаление должно работать: после `erase` ключ не находится, а поиск другого ключа, - стоявшего за ним в цепочке зондирования, по-прежнему работает. -Проверка: `python3 grade.py 10`. Критерий: все `ok`, сборка без предупреждений, -ASAN/UBSAN чистые. +- **ключи с совпадающими младшими битами обязаны находиться** — тест вставляет 512 ключей, + кратных 16, то есть все они попадают в одну-две стартовые ячейки. Это проверка + зондирования, а не «повезло с хешем»; +- `put` 100 000 ключей суммарно быстрее 2 секунд; +- после вставок `capacity()` растёт (степень двойки), загрузка остаётся ≤ 0.7; +- `size()` честно считает элементы: повторный `put` того же ключа **не** увеличивает `size`; +- `erase` работает: после удаления ключ не находится, но поиск другого ключа, стоявшего + за ним в цепочке зондирования, по-прежнему работает; +- удаление и последующая вставка не должны «терять» элементы, а `size()` после удаления + и вставки возвращается к правильному значению. -Разбор после сдачи: почему ёмкость — степень двойки; чем линейное зондирование лучше -цепочек по кэшу и хуже по кластеризации; как tombstone-метки спасают поиск после удаления. +## Глава IV. Ступени (делай по одной, после каждой — прогон) + +Не пытайся написать всё сразу. Каждая ступень проверяется отдельно, и это нормальный +порядок работы инженера. + +**Ступень 1. Хеш-функция и индекс (20 минут).** +Напиши `size_t hash_of(int key)` и `size_t index_of(int key) const`. Для целых ключей +хорошая хеш-функция — перемешать биты умножением на большую нечётную константу: +`h = (uint64_t)key * 2654435761u;` (это золотое сечение, классика). +Проверь себя на бумаге или в маленькой программе: для `cap = 16` ключи 16, 32, 48 должны +дать **разные** индексы. Если дают одинаковые — ты забыл перемешать биты, и все кратные 16 +свалятся в одну ячейку. + +**Ступень 2. Массивы и конструктор (15 минут).** +Нужны три вещи: массив значений, массив признаков состояния ячейки и счётчик размера. +Признаки: `EMPTY`, `OCCUPIED`, `TOMBSTONE`. Ёмкость в конструкторе приводи к степени двойки +(16 по умолчанию) — тест ожидает `capacity() >= 16` и степень двойки. +После этого прогон должен уже собираться, а часть проверок на пустой таблице — проходить. + +**Ступень 3. Вставка без перехеширования (30 минут).** +Идёшь по ячейкам от `index_of(key)`, пока не найдёшь: свой ключ (обновить значение), +`EMPTY` или `TOMBSTONE` (вставить). **Важно:** если ключ найден — не увеличивай `size()`. +Tombstone можно переиспользовать под вставку, но тогда его признак меняется на `OCCUPIED`. + +**Ступень 4. Поиск (15 минут).** +Идёшь так же, но: свой ключ — нашли; `EMPTY` — стоп, ключа нет; `TOMBSTONE` — идём дальше +(здесь легко ошибиться и вернуть `false` раньше времени). + +**Ступень 5. Удаление через tombstone (20 минут).** +Нашёл ключ → ставим `TOMBSTONE`, `size--`. Если ключа нет — `false`, ничего не меняем. + +**Ступень 6. Перехеширование (30 минут).** +Когда `size * 10 > capacity * 7`, создай массив вдвое больше и **заново вставь все занятые +ключи** (tombstone не переносим — в новой таблице пусто). Учти: после этого `size` не должен +измениться, а пробеги станут короче. Прогон: `python3 grade.py 10`. + +**Ступень 7. Прогон под санитайзерами (10 минут).** +`grade.py` уже собирает с ASAN/UBSAN — если он зелёный, память чистая. Отдельно проверь, +что деструктор освобождает ровно то, что выделил конструктор (или не выделяй вручную вовсе, +а используй `std::vector` — так короче и безопаснее). + +## Глава V. Критерии приёмки + +Задача сдана, когда `python3 grade.py 10` печатает `PASS` — это значит: сборка без +предупреждений, ASAN/UBSAN чистые, все проверки `ok`, включая 512 ключей, кратных 16, +перехеширование, удаление из середины цепочки и повторные вставки. + +## Глава VI. Подсказки (открывать после первой честной попытки) + +
+Не находит ключи, кратные 16 + +Ты используешь `key & (cap-1)` без перемешивания. Ключи 16, 32, 48, … в маске дают 0 — +все в одну ячейку, пробег растёт, а при большом числе ключей ты упираешься в «таблица +полна». Лечится хеш-функцией: умножить ключ на большую нечётную константу перед маской. +
+ +
+size() растёт от повторных put + +Сначала ищи существующий ключ. Нашёл — обнови значение и выйди, `size++` не делай. +Увеличивай счётчик только когда записал в пустую или tombstone-ячейку. +
+ +
+После erase пропадают соседние ключи + +Ты ставишь `EMPTY` вместо `TOMBSTONE`, и поиск обрывается на этом месте. `EMPTY` — только +для никогда не занятых ячеек; после удаления — `TOMBSTONE`. +
+ +
+Зацикливается при полной таблице + +Цикл зондирования обязан ограничиться `capacity` шагами. Если таблица переполнилась — +значит не сработал порог перехеширования (0.7) или ты неверно считаешь `size`. +
+ +## Глава VII. Частые ошибки и как их не допустить + +- **Не перемешать хеш** → кластеризация, тест на ключи, кратные 16, падает. Самая частая. +- **`%` вместо `&`** → работает, но теряется смысл требования «степень двойки»; и на + медленных платформах деление дороже. +- **Забыть про tombstone при rehash** → «мёртвые» ячейки переезжают и занимают место. +- **Хранить ключ отдельно от значения и перепутать порядок** → ASAN поймает, но лучше + держать их в одной структуре ячейки. +- **Проверять `size == capacity` вместо load factor** → таблица почти полна, пробеги + огромны, время уходит за лимит 2 секунды. + +## После сдачи + +Разбор со мной: почему ёмкость — степень двойки; чем открытая адресация лучше цепочек по +кэшу и хуже по кластеризации; как tombstone-метки спасают поиск после удаления; как это +устроено в реальных FDB коммутаторов (там обычно хеш с цепочками и ограничением глубины). diff --git a/lessons/D1_algo.md b/lessons/D1_algo.md new file mode 100644 index 0000000..c5d37a6 --- /dev/null +++ b/lessons/D1_algo.md @@ -0,0 +1,217 @@ +# D1, часть 1. Сложность и хеш-таблицы (урок) + +Это не проверка, а урок: сначала разбираем, потом сам решаешь. Читать сверху вниз, +код в разборах можно копировать и запускать — это образец, а не ответ на задачу. + +## 1. Что такое O-нотация (без воды) + +O-нотация отвечает на вопрос «как растёт время работы, когда данных становится в 10 раз +больше». Константы и младшие слагаемые отбрасываются: 3n + 100 → O(n). + +Три правила, которых хватает для 90% вопросов: + +1. **Последовательные блоки складываются, остаётся старший.** O(n) + O(n²) → O(n²). +2. **Вложенные циклы перемножаются.** Цикл n по циклу n → O(n²). +3. **Деление задачи вдвое даёт log n.** Бинарный поиск в 1 000 000 элементов: + 2²⁰ ≈ 1 048 576, значит 20 шагов → **O(log n), а число сравнений ≈ 20**. + +Полезно помнить наизусть: `log₂(1000) ≈ 10`, `log₂(10⁶) ≈ 20`, `log₂(10⁹) ≈ 30`. +Каждое умножение данных на 1000 добавляет примерно 10 шагов — это и есть смысл log n. + +**Амортизированная сложность** — средняя стоимость операции, если редкая дорогая операция +«размазывается» по множеству дешёвых. Классический пример: `std::vector::push_back` — +обычно O(1), но при переполнении копирует весь массив за O(n); в среднем всё равно +**амортизированное O(1)**, потому что ёмкость удваивается. + +**Худший случай ≠ средний.** Хеш-таблица: в среднем поиск O(1), но если хеш-функция плохая +и все ключи попали в одну корзину, поиск вырождается в перебор → **O(n)**. + +## 2. Таблица сложностей, которую надо знать + +| Структура | Поиск | Вставка | Удаление | Память | +|---|---|---|---|---| +| Массив (не отсортирован) | O(n) | O(1) в конец | O(n) | O(n) | +| Отсортированный массив | O(log n) | O(n) | O(n) | O(n) | +| Связный список | O(n) | O(1) по указателю | O(1) по указателю | O(n) | +| Хеш-таблица (средн.) | O(1) | O(1) | O(1) | O(n) | +| Хеш-таблица (худш.) | O(n) | O(n) | O(n) | O(n) | +| Бинарная куча | O(n) поиск | O(log n) | O(log n) удалить корень | O(n) | +| Сбалансированное BST (map) | O(log n) | O(log n) | O(log n) | O(n) | + +Кучи отдельно: **построение из произвольного массива — O(n)** (не O(n log n) — это +частый вопрос), вставка одного элемента — O(log n), взятие максимума — O(1). + +Сортировки: quicksort — в среднем O(n log n), в худшем **O(n²)** (уже отсортированный +массив при плохом выборе опорного); mergesort — всегда O(n log n) и **устойчив**; +heapsort — O(n log n), неустойчив, O(1) доп. памяти. Нижняя оценка для сортировки +сравнениями — **Ω(n log n)**, быстрее сравнениями нельзя. + +Устойчивость = равные элементы сохраняют исходный порядок. Устойчивы: merge, insertion, +bubble, counting. Неустойчивы: quick, heap, selection. + +## 3. Хеш-таблица: как устроена + +Идея: по ключу считаем число (хеш) и превращаем его в индекс массива. Хотим получить +адрес за одно действие, без перебора. + +Компоненты: **массив корзин**, **хеш-функция**, **правило разрешения коллизий**, **фактор +загрузки** (сколько занято от общего размера). + +Коллизия — два разных ключа дали один индекс. Это норма, а не ошибка. + +Два способа разрешения: + +- **Цепочки (chaining):** в каждой корзине список/вектор элементов. Просто, но много + мелких аллокаций; при плохом хеше одна цепочка растёт до O(n). +- **Открытая адресация (open addressing):** все элементы лежат в самом массиве. Занято — + ищем следующую свободную ячейку по правилу: линейное зондирование `(i+1) % cap`, + квадратичное `(i + k²) % cap`, двойное хеширование `(i + k·h2) % cap`. + Быстрее по кэшу, но есть проблема **удаления**: если просто очистить ячейку, цепочка + зондирования порвётся и поиск не найдёт элемент дальше. Решение — **tombstone** + (надгробие): помечаем ячейку «был элемент», поиск идёт дальше, вставка может её занять. + +**Фактор загрузки** `load = size / capacity`. При открытой адресации держат ≤ 0.7: +чем плотнее, тем длиннее пробеги. При превышении — **rehash**: выделяем массив вдвое +больше и переносим все элементы (это O(n), но редко, поэтому амортизированно дёшево). + +Почему ёмкость берут степенью двойки: тогда `idx = hash & (cap - 1)` вместо дорогого +деления по модулю. Отсюда же требование: хеш-функция должна хорошо перемешивать младшие +биты (для строк — FNV-1a или `std::hash`). + +## 4. Разбор примера: считаем сложности + +```cpp +// (а) сумма элементов +long long sum(const std::vector& v) { // O(n): один проход + long long s = 0; + for (int x : v) s += x; + return s; +} + +// (б) пары с суммой k +bool has_pair(const std::vector& v, int k) { // O(n^2): вложенный цикл + for (size_t i = 0; i < v.size(); ++i) + for (size_t j = i + 1; j < v.size(); ++j) + if (v[i] + v[j] == k) return true; + return false; +} + +// (в) то же, но через хеш-множество +bool has_pair_fast(const std::vector& v, int k) { // O(n) в среднем + std::unordered_set seen; + for (int x : v) { + if (seen.count(k - x)) return true; // поиск в среднем O(1) + seen.insert(x); + } + return false; +} +``` + +(в) — типовой ответ на собеседовании: «перебор O(n²), но с хеш-множеством получаем O(n) +за счёт O(n) дополнительной памяти». Уметь назвать и время, и память — половина ответа. + +## 5. Разбор примера: как руками собрать хеш-таблицу + +Учебный минимальный вариант (это разбор, не зачётная задача — запусти и поиграйся): + +```cpp +#include +#include +#include +#include + +// 1. хеш-функция: FNV-1a, хорошо перемешивает +uint64_t fnv1a(const std::string& s) { + uint64_t h = 1469598103934665603ULL; + for (unsigned char c : s) { h ^= c; h *= 1099511628211ULL; } + return h; +} + +// 2. таблица с цепочками +struct HashTable { + struct Node { std::string key; int val; Node* next; }; + std::vector buckets; + size_t sz = 0; + + explicit HashTable(size_t cap = 8) : buckets(cap, nullptr) {} + + size_t index(const std::string& k) const { return fnv1a(k) % buckets.size(); } + + void put(const std::string& k, int v) { + Node* n = buckets[index(k)]; + for (; n; n = n->next) if (n->key == k) { n->val = v; return; } // уже есть — обновляем + buckets[index(k)] = new Node{k, v, buckets[index(k)]}; // вставка в голову цепочки + ++sz; + if (sz * 10 > buckets.size() * 7) rehash(); // load > 0.7 + } + + bool get(const std::string& k, int& out) const { + for (Node* n = buckets[index(k)]; n; n = n->next) + if (n->key == k) { out = n->val; return true; } + return false; + } + + void rehash() { + std::vector old = buckets; + buckets.assign(old.size() * 2, nullptr); + for (Node* head : old) + for (Node* n = head; n; ) { + Node* next = n->next; + size_t i = index(n->key); + n->next = buckets[i]; + buckets[i] = n; + n = next; + } + } +}; +``` + +Что здесь важно понять по шагам: `index()` — где именно ищем; `put` — сначала ищем +существующий ключ (иначе будут дубли), потом вставляем; `rehash` — заново раскладываем +**все** узлы, потому что индекс зависит от размера массива. + +Открытая адресация отличается только поиском места: вместо цепочки идём вперёд по массиву +до свободной ячейки, а при удалении ставим tombstone. + +## 6. Что спросят на собеседовании (готовые ответы) + +- «Средняя и худшая сложность поиска в хеш-таблице?» — амортизированное O(1), худшая O(n) + при коллизиях. +- «Что такое load factor и зачем rehash?» — доля занятых ячеек; при превышении порога + (обычно 0.7–1.0) массив растёт, иначе пробеги/цепочки удлиняются. +- «Как удалять при открытой адресации?» — tombstone, иначе порвётся цепочка зондирования. +- «Почему ёмкость — степень двойки?» — `hash & (cap-1)` вместо `%`, дешевле. +- «Чем цепочки отличаются от открытой адресации?» — цепочки проще и терпят load > 1, + но аллокации; открытая адресация кэш-дружелюбнее, но требует load ≤ 0.7 и tombstone. + +## 7. Материалы (первопартийные) + +- cppreference: `std::unordered_map`, `std::hash` — https://en.cppreference.com/w/cpp/container/unordered_map +- OSTEP, часть «Data Structures»/«Hashing» — https://pages.cs.wisc.edu/~remzi/OSTEP/ +- Codeforces EDU, курс по структурам данных — https://codeforces.com/edu/courses +- Визуализация открытой адресации — https://www.cs.usfca.edu/~galles/visualization/OpenHash.html + +## 8. Проверь себя (ответы внизу, не подглядывай сразу) + +1. Сложность поиска в `unordered_map` в среднем и в худшем? +2. Сколько сравнений в худшем случае у бинарного поиска в массиве из 10⁶ элементов? +3. `heapify` из произвольного массива — за сколько? +4. Какая из сортировок устойчива: quick, merge, heap? +5. Зачем tombstone при открытой адресации? + +
+Ответы + +1. Амортизированное O(1); худшая O(n) — все ключи в одной корзине. +2. 20 (`log₂ 10⁶ ≈ 20`). +3. O(n), снизу вверх от середины массива к началу. +4. merge (устойчива), quick и heap — нет. +5. Чтобы удаление не разрывало цепочку зондирования: поиск должен пройти дальше удалённой + ячейки до элемента, который был вставлен за ней. +
+ +## 9. Ссылки на задачи этого дня + +- `tasks/10_hash` — своя таблица с открытой адресацией (главная задача дня, идёт ступенями). +- `tasks/01_bits` — битовые операции на C, разминка для рук. +- `tasks/03_ring` — кольцевой буфер, база для сетевого кода. diff --git a/lessons/D1_linux.md b/lessons/D1_linux.md new file mode 100644 index 0000000..b3fed71 --- /dev/null +++ b/lessons/D1_linux.md @@ -0,0 +1,208 @@ +# D1, часть 2. Процессы: fork, exec, wait, сигналы (урок) + +Тут всё держится на одной картинке: процесс = адресное пространство + поток выполнения + +открытые дескрипторы. `fork` копирует это, `exec` заменяет содержимое, `wait` собирает +результат, сигнал — асинхронное уведомление. + +## 1. fork(): что реально происходит + +`pid_t fork(void)` создаёт **новый процесс** — копию вызывающего: своё адресное +пространство, свой PID, свой поток. Родитель продолжает с того же места. + +Возвращает **дважды** — и это ключ к пониманию: +- в родителе — PID ребёнка (> 0); +- в ребёнке — 0; +- при ошибке — −1 (и `errno`), ребёнок не создан. + +Поэтому классический код всегда ветвится: + +```cpp +pid_t pid = fork(); +if (pid == 0) { + // это ребёнок + execlp("ls", "ls", "-l", nullptr); + _exit(127); // exec не вернулся — значит не смог +} else if (pid > 0) { + // это родитель: pid — номер ребёнка + int status = 0; + waitpid(pid, &status, 0); // дождаться и забрать код возврата +} else { + perror("fork"); // ошибка +} +``` + +**Copy-on-write.** Память физически не копируется: обе стороны смотрят на одни страницы, +помеченные «только чтение». При первой записи в страницу ядро делает её копию — только +тогда. Поэтому `fork` дешёвый, даже если процесс занимает гигабайты. Именно это спрашивают +в формулировке «что происходит со страницами памяти при fork?» — ответ: **COW, копируются +при первой записи**. + +Совет: `fflush(stdout)` перед `fork`, иначе буфер вывода может продублироваться в ребёнке. + +## 2. exec(): замена образа + +`exec*` **не создаёт процесс**, а заменяет содержимое текущего: код, данные, стек — +всё новое. PID и открытые дескрипторы сохраняются. Возврата при успехе нет никогда: +при успехе функция не возвращается (программа уже другая), при ошибке возвращает −1. + +Отсюда рабочий шаблон: `fork` + `exec` в ребёнке = запуск внешней программы. +Семейство: `execlp` ищет в `PATH`, `execv` — по полному пути, `execvp` — по имени в `PATH`. + +## 3. wait/waitpid и коды возврата + +Завершившийся ребёнок не исчезает: ядро держит его запись, пока родитель не заберёт код +возврата. Такой процесс называется **зомби** (состояние `Z` в `ps`). Зомби не занимает +память, но занимает слот в таблице процессов — их накопление плохо. + +- `wait(&status)` — ждёт любого ребёнка; +- `waitpid(pid, &status, 0)` — конкретного; `WNOHANG` — не блокироваться. + +Разбор `status` делается макросами, а не вручную: + +```cpp +if (WIFEXITED(status)) printf("exit code %d\n", WEXITSTATUS(status)); +else if (WIFSIGNALED(status)) printf("killed by signal %d\n", WTERMSIG(status)); +``` + +**Откуда 137 и 139.** Оболочка показывает код как 128 + номер сигнала: +- **137 = 128 + 9** → SIGKILL (убит `kill -9`, часто OOM-killer); +- **139 = 128 + 11** → SIGSEGV (падение по памяти); +- 143 = 128 + 15 → SIGTERM (корректный запрос на завершение). + +**Сирота** — процесс, чей родитель умер: его усыновляет init/systemd (PID 1), он не зомби. + +## 4. Сигналы + +Сигнал — асинхронное уведомление процессу. Основные: SIGINT (2, Ctrl+C), SIGKILL (9, +нельзя перехватить или проигнорировать), SIGTERM (15, «завершись корректно»), SIGSEGV (11), +SIGPIPE (13, запись в закрытый сокет), SIGCHLD (17, ребёнок завершился). + +Обработчик ставится `sigaction` (надёжнее устаревшего `signal`), внутри обработчика можно +менять только `volatile sig_atomic_t` — никаких `printf`/`malloc` (не async-signal-safe). + +```cpp +static volatile sig_atomic_t stop = 0; +static void on_term(int) { stop = 1; } // минимум действий + +int main() { + struct sigaction sa{}; sa.sa_handler = on_term; + sigaction(SIGTERM, &sa, nullptr); // ловим мягкое завершение + while (!stop) { /* работа */ } +} +``` + +## 5. errno и возвраты системных вызовов + +Системные вызовы сообщают об ошибке через возврат (−1 или NULL) и `errno`. Читать `errno` +можно только сразу после ошибки. EINTR — вызов прерван сигналом, надо повторить; +EAGAIN — данных сейчас нет на неблокирующем дескрипторе, повторить позже; +EINPROGRESS — неблокирующее соединение в процессе. + +```cpp +ssize_t n = read(fd, buf, sizeof buf); +if (n < 0) { + if (errno == EINTR) continue; // прервали сигналом — просто повторить + perror("read"); // иначе настоящая ошибка + break; +} +``` + +## 6. Разбор примера: мини-шелл на 40 строк + +Это образец (запусти, поиграйся), зачётная версия — отдельная задача дня. + +```cpp +#include +#include +#include +#include +#include +#include +#include + +int main() { + std::string line; + while (std::printf("sh> "), std::fflush(stdout), std::getline(std::cin, line)) { + if (line == "exit") break; + if (line.empty()) continue; + + std::istringstream is(line); + std::vector argv; std::string tok; + while (is >> tok) argv.push_back(tok); + if (argv.empty()) continue; + + std::vector cargv; + for (auto& a : argv) cargv.push_back(a.data()); + cargv.push_back(nullptr); + + pid_t pid = fork(); + if (pid == 0) { + execvp(cargv[0], cargv.data()); + std::fprintf(stderr, "не могу запустить %s\n", cargv[0]); + _exit(127); // exec не удался + } else if (pid > 0) { + int status = 0; + waitpid(pid, &status, 0); + if (WIFEXITED(status)) + std::printf("код возврата: %d\n", WEXITSTATUS(status)); + else if (WIFSIGNALED(status)) + std::printf("убит сигналом: %d (код %d)\n", + WTERMSIG(status), 128 + WTERMSIG(status)); + } else { + std::perror("fork"); + } + } +} +``` + +Что тут проверить руками: `ls -l` работает; `sleep 5` в фоне (`&` — уже доработка); +`kill -9` по своему процессу из другого терминала даёт «убит сигналом 9 (код 137)»; +несуществующая команда даёт 127. + +Проверка на утечки и падения — санитайзеры: +`g++ -g -fsanitize=address,undefined -fno-omit-frame-pointer shell.cpp -o shell` + +## 7. Что спросят на собеседовании (готовые ответы) + +- «Что делает fork и что возвращает?» — создаёт копию процесса; в родителе PID ребёнка, + в ребёнке 0, при ошибке −1. +- «Что с памятью при fork?» — copy-on-write, страницы копируются при первой записи. +- «Чем exec отличается от fork?» — fork создаёт новый процесс, exec заменяет образ + текущего, не создавая процесса. +- «Зачем waitpid?» — забрать код возврата и не оставлять зомби. +- «Как узнать, что процесс убит сигналом?» — `WIFSIGNALED`/`WTERMSIG`, код оболочки + 128 + сигнал, например 137 = SIGKILL, 139 = SIGSEGV. +- «SIGTERM против SIGKILL?» — SIGTERM можно перехватить и завершиться корректно, + SIGKILL не перехватывается и не игнорируется. + +## 8. Материалы (первопартийные) + +- `man 2 fork`, `man 2 execve`, `man 2 waitpid`, `man 7 signal` — локально, читаются за 10 минут +- OSTEP, главы 4–5 (процессы, API процессов) — https://pages.cs.wisc.edu/~remzi/OSTEP/ +- Beej, «Processes» — https://beej.us/guide/bgipc/ +- Про COW: `man 2 fork` + статья «Anatomy of a Program in Memory» — + https://manybutfinite.com/post/anatomy-of-a-program-in-memory/ + +## 9. Проверь себя + +1. Что вернёт `fork()` в ребёнке и в родителе? +2. Что происходит со страницами памяти при `fork()`? +3. Что такое зомби и кто его убирает? +4. Откуда код возврата 137 и 139? +5. Почему `exec` не возвращает управление при успехе? + +
+Ответы + +1. В ребёнке 0, в родителе — PID ребёнка, при ошибке −1. +2. Ничего сразу: copy-on-write, копия страницы создаётся при первой записи. +3. Завершившийся процесс, чья запись ждёт `wait/waitpid`; убирает родитель (или init, + если родитель умер). +4. 137 = 128+9 (SIGKILL), 139 = 128+11 (SIGSEGV) — так кодирует оболочка. +5. Потому что адресное пространство заменено новым образом: старого кода больше нет. +
+ +## 10. Задачи дня + +- `tasks/03_ring` — кольцевой буфер (база для сетевого кода). +- Мини-шелл — в зачётной части дня (полное ТЗ придёт с D1-заданиями).