Формат v3: уроки D1 (теория+разбор), задачи ступенями, карточки 101 с уровнями и пояснениями
This commit is contained in:
+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