План v2 по диагностике (квиз 14/32) + 4 новые задачи (hash, heap, dijkstra, subnet) + тренажёр 60 карточек
This commit is contained in:
@@ -0,0 +1,31 @@
|
||||
// Задача 10 — хеш-таблица с открытой адресацией.
|
||||
// Реализуй методы ниже. Заглушка намеренно не проходит проверку.
|
||||
#include <cstddef>
|
||||
#include <vector>
|
||||
|
||||
class HashTable {
|
||||
public:
|
||||
explicit HashTable(std::size_t initial_capacity = 16) {
|
||||
(void)initial_capacity;
|
||||
}
|
||||
|
||||
void put(int key, int value) {
|
||||
(void)key;
|
||||
(void)value;
|
||||
}
|
||||
|
||||
bool get(int key, int& out) const {
|
||||
(void)key;
|
||||
(void)out;
|
||||
return false;
|
||||
}
|
||||
|
||||
bool erase(int key) {
|
||||
(void)key;
|
||||
return false;
|
||||
}
|
||||
|
||||
std::size_t size() const { return 0; }
|
||||
|
||||
std::size_t capacity() const { return 16; }
|
||||
};
|
||||
@@ -0,0 +1,32 @@
|
||||
# Задача 10 — хеш-таблица с открытой адресацией (C++)
|
||||
|
||||
Свой контейнер, без `std::unordered_map`. Разрешение коллизий — **линейное зондирование**
|
||||
(linear probing), ёмкость — степень двойки, при загрузке > 0.7 таблица **перехешируется**
|
||||
вдвое. Индексация — через маску `idx & (capacity - 1)`, а не `%`.
|
||||
|
||||
```c++
|
||||
class HashTable {
|
||||
public:
|
||||
explicit HashTable(std::size_t initial_capacity = 16);
|
||||
void put(int key, int value); // обновляет значение, если ключ уже есть
|
||||
bool get(int key, int& out) const; // false — ключа нет
|
||||
bool erase(int key); // false — ключа не было
|
||||
std::size_t size() const;
|
||||
std::size_t capacity() const; // степень двойки, >= 16
|
||||
};
|
||||
```
|
||||
|
||||
Требования:
|
||||
- `put` 100 000 ключей — суммарно быстрее 2 секунд (в среднем O(1));
|
||||
- ключи с совпадающими младшими битами (например, все кратные 16) обязаны находиться —
|
||||
это проверка зондирования, а не «повезло с хешем»;
|
||||
- после 100 000 вставок `capacity()` растёт (степень двойки, загрузка ≤ 0.7), а `size()`
|
||||
честно считает элементы, включая обновления существующих ключей;
|
||||
- удаление должно работать: после `erase` ключ не находится, а поиск другого ключа,
|
||||
стоявшего за ним в цепочке зондирования, по-прежнему работает.
|
||||
|
||||
Проверка: `python3 grade.py 10`. Критерий: все `ok`, сборка без предупреждений,
|
||||
ASAN/UBSAN чистые.
|
||||
|
||||
Разбор после сдачи: почему ёмкость — степень двойки; чем линейное зондирование лучше
|
||||
цепочек по кэшу и хуже по кластеризации; как tombstone-метки спасают поиск после удаления.
|
||||
@@ -0,0 +1,78 @@
|
||||
#include <cstdio>
|
||||
#include <cstdint>
|
||||
#include <random>
|
||||
#include <chrono>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
#include "solution.cpp"
|
||||
|
||||
static int failures = 0;
|
||||
#define CHECK(cond, name) do { if (cond) std::printf("ok %s\n", name); \
|
||||
else { std::printf("FAIL %s (line %d)\n", name, __LINE__); ++failures; } } while (0)
|
||||
|
||||
int main() {
|
||||
{
|
||||
HashTable t;
|
||||
CHECK(t.size() == 0, "новая таблица пуста");
|
||||
CHECK(t.capacity() >= 16 && (t.capacity() & (t.capacity() - 1)) == 0,
|
||||
"capacity — степень двойки и >= 16");
|
||||
int v = -1;
|
||||
CHECK(t.get(42, v) == false, "get по отсутствующему ключу -> false");
|
||||
CHECK(t.erase(42) == false, "erase отсутствующего -> false");
|
||||
}
|
||||
{
|
||||
HashTable t;
|
||||
for (int i = 0; i < 1000; ++i) t.put(i, i * 7);
|
||||
CHECK(t.size() == 1000, "1000 ключей: size == 1000");
|
||||
bool all = true;
|
||||
for (int i = 0; i < 1000; ++i) { int v = -1; if (!t.get(i, v) || v != i * 7) all = false; }
|
||||
CHECK(all, "1000 ключей: все значения читаются");
|
||||
t.put(500, 999);
|
||||
int v = -1;
|
||||
CHECK(t.get(500, v) && v == 999, "повторный put обновляет значение");
|
||||
CHECK(t.size() == 1000, "повторный put не увеличивает size");
|
||||
}
|
||||
{
|
||||
// ключи с одинаковыми младшими битами — проверка зондирования
|
||||
HashTable t;
|
||||
for (int i = 0; i < 512; ++i) t.put(i * 16, i);
|
||||
bool all = true;
|
||||
for (int i = 0; i < 512; ++i) { int v = -1; if (!t.get(i * 16, v) || v != i) all = false; }
|
||||
CHECK(all, "512 ключей, кратных 16: все находятся (линейное зондирование)");
|
||||
int missing = -1;
|
||||
CHECK(t.get(15, missing) == false, "нечётный отсутствующий ключ -> false");
|
||||
}
|
||||
{
|
||||
// удаление посреди цепочки зондирования
|
||||
HashTable t;
|
||||
for (int i = 0; i < 200; ++i) t.put(i * 16, i);
|
||||
CHECK(t.erase(0) == true, "erase существующего -> true");
|
||||
int v = -1;
|
||||
CHECK(t.get(0, v) == false, "удалённый ключ не найден");
|
||||
bool rest = true;
|
||||
for (int i = 1; i < 200; ++i) { int x = -1; if (!t.get(i * 16, x) || x != i) rest = false; }
|
||||
CHECK(rest, "остальные ключи из цепочки живы после удаления");
|
||||
CHECK(t.size() == 199, "size после удаления == 199");
|
||||
}
|
||||
{
|
||||
const int N = 100000;
|
||||
HashTable t;
|
||||
std::mt19937 rng(4242);
|
||||
std::vector<int> keys(N);
|
||||
for (auto& k : keys) k = int(rng() % 1000000);
|
||||
auto t0 = std::chrono::steady_clock::now();
|
||||
for (int i = 0; i < N; ++i) t.put(keys[i], i);
|
||||
double sec = std::chrono::duration<double>(std::chrono::steady_clock::now() - t0).count();
|
||||
std::printf(" (100k вставок: time=%.3fs, capacity=%zu, size=%zu)\n", sec, t.capacity(), t.size());
|
||||
CHECK(sec < 2.0, "100k вставок за < 2 c");
|
||||
CHECK(t.size() >= 95000 && t.size() <= 100000, "size в разумных пределах");
|
||||
CHECK(t.capacity() >= 131072 && t.capacity() <= 262144, "capacity выросла (загрузка <= 0.7)");
|
||||
bool all = true;
|
||||
for (int i = 0; i < N; ++i) { int v = -1; if (!t.get(keys[i], v)) all = false; }
|
||||
CHECK(all, "все 100k ключей читаются");
|
||||
int v = -1;
|
||||
CHECK(t.get(-12345, v) == false, "отсутствующий ключ -> false");
|
||||
}
|
||||
std::printf(failures ? "\nFAILURES: %d\n" : "\nALL PASS\n", failures);
|
||||
return failures ? 1 : 0;
|
||||
}
|
||||
Reference in New Issue
Block a user