Формат v3: уроки D1 (теория+разбор), задачи ступенями, карточки 101 с уровнями и пояснениями
This commit is contained in:
+1
-3
@@ -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 — обязательная часть (оценивается ход разбора).
|
||||
|
||||
|
||||
+741
-62
File diff suppressed because it is too large
Load Diff
+90
-32
@@ -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)
|
||||
|
||||
|
||||
@@ -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. Подсказки (после первой попытки)
|
||||
|
||||
<details>
|
||||
<summary>UBSAN жалуется на сдвиг</summary>
|
||||
|
||||
Сдвиг знакового числа в старший бит — неопределённое поведение. Все промежуточные значения
|
||||
держи в `uint32_t` и пиши константы с суффиксом `u`: `1u << 31`.
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>is_power_of_two возвращает true на нуле</summary>
|
||||
|
||||
У нуля нет единичных битов, поэтому `x & (x-1)` даёт 0 — как и у степеней двойки. Добавь
|
||||
явную проверку `x != 0`.
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>reverse_bits32 работает «почти»</summary>
|
||||
|
||||
Проверь порядок операций в цикле: сначала ставим бит в результат, потом сдвигаем источник.
|
||||
Если перепутать — биты уедут на одну позицию.
|
||||
</details>
|
||||
|
||||
## Глава VII. Частые ошибки
|
||||
|
||||
- Сдвиги в `int` вместо `uint32_t` → UB, UBSAN ругается.
|
||||
- Забыт ноль в `is_power_of_two`.
|
||||
- В `bswap32` перепутаны направления сдвигов (влево/вправо) — проверь на `0x11223344`.
|
||||
- Использование `__builtin_popcount` — запрещено условием: смысл в том, чтобы написать самому.
|
||||
|
||||
## После сдачи
|
||||
|
||||
Разбор: почему `x & (x-1)` гасит младший бит; как эти приёмы используются в реальных
|
||||
протоколах (маски подсетей, флаги TCP-заголовка, разбор полей IPv4).
|
||||
|
||||
@@ -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<int>` вместо ручного `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. Подсказки (после первой попытки)
|
||||
|
||||
<details>
|
||||
<summary>Значения после заворота идут не в том порядке</summary>
|
||||
|
||||
Проверь, что `pop` читает именно `tail_`, а `push` пишет именно в `head_`, и что оба индекса
|
||||
инкрементируются **после** операции. Классическая ошибка — сдвинуть `head_` до записи.
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Перезапись при полном буфере</summary>
|
||||
|
||||
Ты не проверяешь `full()` в `push`. По условию перезапись запрещена: полный буфер возвращает
|
||||
`false` и не портит старые данные.
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>ASAN: утечка или двойное освобождение</summary>
|
||||
|
||||
Если память выделена через `new[]`, освобождать нужно `delete[]` (не `delete`). Проще взять
|
||||
`std::vector<int>` — тогда вопрос исчезает.
|
||||
</details>
|
||||
|
||||
## Глава VII. Частые ошибки
|
||||
|
||||
- Сдвиг элементов вместо индексов (теряется весь смысл O(1)).
|
||||
- Путаница «пусто/полно» при совпавших индексах.
|
||||
- `%` на каждом шаге там, где можно было обойтись условием — не ошибка, но на собеседовании
|
||||
спросят «а быстрее можно?» (ответ: `if (++idx == cap) idx = 0;`).
|
||||
- Копирование объекта без правила трёх → двойное освобождение. Если сдаёшь с ручным `new[]`,
|
||||
запрети копирование (`= delete`).
|
||||
|
||||
## После сдачи
|
||||
|
||||
Разбор: как устроены буферы в драйверах (ring buffer с head/tail для DMA), почему в
|
||||
многопоточном варианте нужны атомарные индексы и `memory_order`.
|
||||
|
||||
+134
-15
@@ -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. Подсказки (открывать после первой честной попытки)
|
||||
|
||||
<details>
|
||||
<summary>Не находит ключи, кратные 16</summary>
|
||||
|
||||
Ты используешь `key & (cap-1)` без перемешивания. Ключи 16, 32, 48, … в маске дают 0 —
|
||||
все в одну ячейку, пробег растёт, а при большом числе ключей ты упираешься в «таблица
|
||||
полна». Лечится хеш-функцией: умножить ключ на большую нечётную константу перед маской.
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>size() растёт от повторных put</summary>
|
||||
|
||||
Сначала ищи существующий ключ. Нашёл — обнови значение и выйди, `size++` не делай.
|
||||
Увеличивай счётчик только когда записал в пустую или tombstone-ячейку.
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>После erase пропадают соседние ключи</summary>
|
||||
|
||||
Ты ставишь `EMPTY` вместо `TOMBSTONE`, и поиск обрывается на этом месте. `EMPTY` — только
|
||||
для никогда не занятых ячеек; после удаления — `TOMBSTONE`.
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Зацикливается при полной таблице</summary>
|
||||
|
||||
Цикл зондирования обязан ограничиться `capacity` шагами. Если таблица переполнилась —
|
||||
значит не сработал порог перехеширования (0.7) или ты неверно считаешь `size`.
|
||||
</details>
|
||||
|
||||
## Глава VII. Частые ошибки и как их не допустить
|
||||
|
||||
- **Не перемешать хеш** → кластеризация, тест на ключи, кратные 16, падает. Самая частая.
|
||||
- **`%` вместо `&`** → работает, но теряется смысл требования «степень двойки»; и на
|
||||
медленных платформах деление дороже.
|
||||
- **Забыть про tombstone при rehash** → «мёртвые» ячейки переезжают и занимают место.
|
||||
- **Хранить ключ отдельно от значения и перепутать порядок** → ASAN поймает, но лучше
|
||||
держать их в одной структуре ячейки.
|
||||
- **Проверять `size == capacity` вместо load factor** → таблица почти полна, пробеги
|
||||
огромны, время уходит за лимит 2 секунды.
|
||||
|
||||
## После сдачи
|
||||
|
||||
Разбор со мной: почему ёмкость — степень двойки; чем открытая адресация лучше цепочек по
|
||||
кэшу и хуже по кластеризации; как tombstone-метки спасают поиск после удаления; как это
|
||||
устроено в реальных FDB коммутаторов (там обычно хеш с цепочками и ограничением глубины).
|
||||
|
||||
Reference in New Issue
Block a user