diff --git a/SobolevNS/docs/data/task1_data_structures/README.md b/SobolevNS/docs/data/task1_data_structures/README.md new file mode 100644 index 0000000..5b922d2 --- /dev/null +++ b/SobolevNS/docs/data/task1_data_structures/README.md @@ -0,0 +1,22 @@ +# Задание 1. Структуры данных - телефонный справочник + +Реализация связного списка, хеш-таблицы и BST в процедурной парадигме (без классов). + +## Как запустить + +```bash +# 1) самопроверка структур +python3 phonebook.py + +# 2) эксперимент (5 повторов × 3 структуры × 2 режима × 3 операции) +python3 experiment.py +# результат -> docs/data/results.csv + +# 3) графики +python3 plot_results.py +# результат -> docs/data/plots/*.png +``` + +## Отчёт + +См. [docs/report.md](docs/report.md). diff --git a/SobolevNS/docs/data/task1_data_structures/docs/data/plots/bst_degradation.png b/SobolevNS/docs/data/task1_data_structures/docs/data/plots/bst_degradation.png new file mode 100644 index 0000000..0b6a049 Binary files /dev/null and b/SobolevNS/docs/data/task1_data_structures/docs/data/plots/bst_degradation.png differ diff --git a/SobolevNS/docs/data/task1_data_structures/docs/data/plots/delete_compare.png b/SobolevNS/docs/data/task1_data_structures/docs/data/plots/delete_compare.png new file mode 100644 index 0000000..1683cc2 Binary files /dev/null and b/SobolevNS/docs/data/task1_data_structures/docs/data/plots/delete_compare.png differ diff --git a/SobolevNS/docs/data/task1_data_structures/docs/data/plots/find_compare.png b/SobolevNS/docs/data/task1_data_structures/docs/data/plots/find_compare.png new file mode 100644 index 0000000..a50f2df Binary files /dev/null and b/SobolevNS/docs/data/task1_data_structures/docs/data/plots/find_compare.png differ diff --git a/SobolevNS/docs/data/task1_data_structures/docs/data/plots/insert_compare.png b/SobolevNS/docs/data/task1_data_structures/docs/data/plots/insert_compare.png new file mode 100644 index 0000000..c3bcd79 Binary files /dev/null and b/SobolevNS/docs/data/task1_data_structures/docs/data/plots/insert_compare.png differ diff --git a/SobolevNS/docs/data/task1_data_structures/docs/data/results.csv b/SobolevNS/docs/data/task1_data_structures/docs/data/results.csv new file mode 100644 index 0000000..d378993 --- /dev/null +++ b/SobolevNS/docs/data/task1_data_structures/docs/data/results.csv @@ -0,0 +1,112 @@ +Структура,Режим,Операция,N,Trial,Время (сек) +LinkedList,shuffled,insert,10000,1,3.995075 +LinkedList,shuffled,insert,10000,2,4.133491 +LinkedList,shuffled,insert,10000,3,4.067831 +LinkedList,shuffled,insert,10000,4,4.075638 +LinkedList,shuffled,insert,10000,5,4.059367 +LinkedList,shuffled,find,10000,1,0.037171 +LinkedList,shuffled,find,10000,2,0.035212 +LinkedList,shuffled,find,10000,3,0.040434 +LinkedList,shuffled,find,10000,4,0.030968 +LinkedList,shuffled,find,10000,5,0.033432 +LinkedList,shuffled,delete,10000,1,0.014029 +LinkedList,shuffled,delete,10000,2,0.016408 +LinkedList,shuffled,delete,10000,3,0.017498 +LinkedList,shuffled,delete,10000,4,0.013770 +LinkedList,shuffled,delete,10000,5,0.016273 +LinkedList,sorted,insert,10000,1,3.083864 +LinkedList,sorted,insert,10000,2,3.123097 +LinkedList,sorted,insert,10000,3,3.084625 +LinkedList,sorted,insert,10000,4,3.200015 +LinkedList,sorted,insert,10000,5,3.124164 +LinkedList,sorted,find,10000,1,0.029411 +LinkedList,sorted,find,10000,2,0.029928 +LinkedList,sorted,find,10000,3,0.027749 +LinkedList,sorted,find,10000,4,0.032859 +LinkedList,sorted,find,10000,5,0.032080 +LinkedList,sorted,delete,10000,1,0.016454 +LinkedList,sorted,delete,10000,2,0.013526 +LinkedList,sorted,delete,10000,3,0.015424 +LinkedList,sorted,delete,10000,4,0.014688 +LinkedList,sorted,delete,10000,5,0.012838 +HashTable,shuffled,insert,10000,1,0.007074 +HashTable,shuffled,insert,10000,2,0.006665 +HashTable,shuffled,insert,10000,3,0.007361 +HashTable,shuffled,insert,10000,4,0.007405 +HashTable,shuffled,insert,10000,5,0.007248 +HashTable,shuffled,find,10000,1,0.000072 +HashTable,shuffled,find,10000,2,0.000062 +HashTable,shuffled,find,10000,3,0.000063 +HashTable,shuffled,find,10000,4,0.000062 +HashTable,shuffled,find,10000,5,0.000066 +HashTable,shuffled,delete,10000,1,0.000037 +HashTable,shuffled,delete,10000,2,0.000034 +HashTable,shuffled,delete,10000,3,0.000032 +HashTable,shuffled,delete,10000,4,0.000030 +HashTable,shuffled,delete,10000,5,0.000032 +HashTable,sorted,insert,10000,1,0.007131 +HashTable,sorted,insert,10000,2,0.006610 +HashTable,sorted,insert,10000,3,0.006701 +HashTable,sorted,insert,10000,4,0.006979 +HashTable,sorted,insert,10000,5,0.008910 +HashTable,sorted,find,10000,1,0.000065 +HashTable,sorted,find,10000,2,0.000056 +HashTable,sorted,find,10000,3,0.000068 +HashTable,sorted,find,10000,4,0.000066 +HashTable,sorted,find,10000,5,0.000076 +HashTable,sorted,delete,10000,1,0.000036 +HashTable,sorted,delete,10000,2,0.000037 +HashTable,sorted,delete,10000,3,0.000038 +HashTable,sorted,delete,10000,4,0.000045 +HashTable,sorted,delete,10000,5,0.000042 +BST,shuffled,insert,10000,1,0.018043 +BST,shuffled,insert,10000,2,0.019312 +BST,shuffled,insert,10000,3,0.017282 +BST,shuffled,insert,10000,4,0.021092 +BST,shuffled,insert,10000,5,0.016847 +BST,shuffled,find,10000,1,0.000157 +BST,shuffled,find,10000,2,0.000210 +BST,shuffled,find,10000,3,0.000168 +BST,shuffled,find,10000,4,0.000138 +BST,shuffled,find,10000,5,0.000193 +BST,shuffled,delete,10000,1,0.000129 +BST,shuffled,delete,10000,2,0.000147 +BST,shuffled,delete,10000,3,0.000122 +BST,shuffled,delete,10000,4,0.000161 +BST,shuffled,delete,10000,5,0.000128 +BST,sorted,insert,2000,1,0.123235 +BST,sorted,insert,2000,2,0.118658 +BST,sorted,insert,2000,3,0.119944 +BST,sorted,insert,2000,4,0.121595 +BST,sorted,insert,2000,5,0.116209 +BST,sorted,find,2000,1,0.005019 +BST,sorted,find,2000,2,0.005133 +BST,sorted,find,2000,3,0.005032 +BST,sorted,find,2000,4,0.004812 +BST,sorted,find,2000,5,0.004964 +BST,sorted,delete,2000,1,0.008319 +BST,sorted,delete,2000,2,0.007798 +BST,sorted,delete,2000,3,0.007584 +BST,sorted,delete,2000,4,0.008061 +BST,sorted,delete,2000,5,0.007642 + +--- СРЕДНИЕ --- +Структура,Режим,Операция,N,Среднее (сек),Все замеры (сек) +LinkedList,shuffled,insert,10000,4.066280,3.995075;4.133491;4.067831;4.075638;4.059367 +LinkedList,shuffled,find,10000,0.035443,0.037171;0.035212;0.040434;0.030968;0.033432 +LinkedList,shuffled,delete,10000,0.015596,0.014029;0.016408;0.017498;0.013770;0.016273 +LinkedList,sorted,insert,10000,3.123153,3.083864;3.123097;3.084625;3.200015;3.124164 +LinkedList,sorted,find,10000,0.030406,0.029411;0.029928;0.027749;0.032859;0.032080 +LinkedList,sorted,delete,10000,0.014586,0.016454;0.013526;0.015424;0.014688;0.012838 +HashTable,shuffled,insert,10000,0.007151,0.007074;0.006665;0.007361;0.007405;0.007248 +HashTable,shuffled,find,10000,0.000065,0.000072;0.000062;0.000063;0.000062;0.000066 +HashTable,shuffled,delete,10000,0.000033,0.000037;0.000034;0.000032;0.000030;0.000032 +HashTable,sorted,insert,10000,0.007266,0.007131;0.006610;0.006701;0.006979;0.008910 +HashTable,sorted,find,10000,0.000066,0.000065;0.000056;0.000068;0.000066;0.000076 +HashTable,sorted,delete,10000,0.000040,0.000036;0.000037;0.000038;0.000045;0.000042 +BST,shuffled,insert,10000,0.018515,0.018043;0.019312;0.017282;0.021092;0.016847 +BST,shuffled,find,10000,0.000173,0.000157;0.000210;0.000168;0.000138;0.000193 +BST,shuffled,delete,10000,0.000138,0.000129;0.000147;0.000122;0.000161;0.000128 +BST,sorted,insert,2000,0.119928,0.123235;0.118658;0.119944;0.121595;0.116209 +BST,sorted,find,2000,0.004992,0.005019;0.005133;0.005032;0.004812;0.004964 +BST,sorted,delete,2000,0.007881,0.008319;0.007798;0.007584;0.008061;0.007642 diff --git a/SobolevNS/docs/data/task1_data_structures/experiment.py b/SobolevNS/docs/data/task1_data_structures/experiment.py new file mode 100644 index 0000000..1e4ce39 --- /dev/null +++ b/SobolevNS/docs/data/task1_data_structures/experiment.py @@ -0,0 +1,194 @@ +""" +experiment.py + +Замеры производительности трёх структур данных на одних и тех же данных: + LinkedList, HashTable, BST +в двух режимах: + случайный порядок (shuffled), отсортированный порядок (sorted) +для трёх операций: + insert N записей, find 110 раз, delete 50 раз +Каждый эксперимент повторяется TRIALS раз. Сохраняем все замеры + средние +в CSV. Для BST на отсортированных данных снижаем N - иначе эксперимент +длится десятки минут (вырожденное дерево, O(N^2) вставка). +""" + +import csv +import os +import random +import time + +import phonebook as pb + + +# ---------- параметры эксперимента ---------- +N = 10_000 # число записей в основном эксперименте +N_BST_SORTED = 2_000 # для BST на отсортированных данных - меньше (O(N^2)) +TRIALS = 5 # количество повторов каждого замера +N_FIND_EXISTING = 100 +N_FIND_MISSING = 10 +N_DELETE = 50 +HT_SIZE = 2048 # размер хеш-таблицы +RNG_SEED = 42 +OUT_CSV = os.path.join("docs", "data", "results.csv") +# -------------------------------------------- + + +def gen_records(n): + """Генерирует n записей вида ('User_00001', '555-0001-...').""" + return [(f"User_{i:05d}", f"555-{i:07d}") for i in range(n)] + + +def pick_keys(records, k_exist, k_miss, rng): + """Выбирает k_exist существующих имён и k_miss отсутствующих.""" + existing = [name for name, _ in rng.sample(records, k_exist)] + missing = [f"None_{i}" for i in range(k_miss)] + return existing + missing + + +# ---------- замеры по структурам ---------- + +def measure_linked_list(records, find_keys, delete_keys): + # вставка + t0 = time.perf_counter() + head = pb.ll_create() + for name, phone in records: + head = pb.ll_insert(head, name, phone) + t_insert = time.perf_counter() - t0 + + # поиск + t0 = time.perf_counter() + for name in find_keys: + pb.ll_find(head, name) + t_find = time.perf_counter() - t0 + + # удаление + t0 = time.perf_counter() + for name in delete_keys: + head = pb.ll_delete(head, name) + t_delete = time.perf_counter() - t0 + + return t_insert, t_find, t_delete + + +def measure_hash_table(records, find_keys, delete_keys): + t0 = time.perf_counter() + ht = pb.ht_create(size=HT_SIZE) + for name, phone in records: + pb.ht_insert(ht, name, phone) + t_insert = time.perf_counter() - t0 + + t0 = time.perf_counter() + for name in find_keys: + pb.ht_find(ht, name) + t_find = time.perf_counter() - t0 + + t0 = time.perf_counter() + for name in delete_keys: + pb.ht_delete(ht, name) + t_delete = time.perf_counter() - t0 + + return t_insert, t_find, t_delete + + +def measure_bst(records, find_keys, delete_keys): + t0 = time.perf_counter() + root = pb.bst_create() + for name, phone in records: + root = pb.bst_insert(root, name, phone) + t_insert = time.perf_counter() - t0 + + t0 = time.perf_counter() + for name in find_keys: + pb.bst_find(root, name) + t_find = time.perf_counter() - t0 + + t0 = time.perf_counter() + for name in delete_keys: + root = pb.bst_delete(root, name) + t_delete = time.perf_counter() - t0 + + return t_insert, t_find, t_delete + + +# ---------- запуск ---------- + +def run_one(structure_name, mode, n, rng_seed): + """Готовит данные, прогоняет TRIALS раз и возвращает список (insert, find, delete).""" + base_records = gen_records(n) + + runs = [] + for trial in range(TRIALS): + # отдельный rng для воспроизводимости и независимости попыток + rng = random.Random(rng_seed + trial) + + if mode == "shuffled": + records = base_records[:] + rng.shuffle(records) + elif mode == "sorted": + records = sorted(base_records, key=lambda x: x[0]) + else: + raise ValueError(mode) + + find_keys = pick_keys(records, N_FIND_EXISTING, N_FIND_MISSING, rng) + delete_keys = [name for name, _ in rng.sample(records, N_DELETE)] + + if structure_name == "LinkedList": + r = measure_linked_list(records, find_keys, delete_keys) + elif structure_name == "HashTable": + r = measure_hash_table(records, find_keys, delete_keys) + elif structure_name == "BST": + r = measure_bst(records, find_keys, delete_keys) + else: + raise ValueError(structure_name) + + runs.append(r) + return runs + + +def main(): + os.makedirs(os.path.dirname(OUT_CSV), exist_ok=True) + + rows = [["Структура", "Режим", "Операция", "N", "Trial", "Время (сек)"]] + summary = [] # (structure, mode, op, n, mean, all_trials) + + configs = [ + ("LinkedList", "shuffled", N), + ("LinkedList", "sorted", N), + ("HashTable", "shuffled", N), + ("HashTable", "sorted", N), + ("BST", "shuffled", N), + ("BST", "sorted", N_BST_SORTED), # вырожденный случай + ] + + for structure, mode, n in configs: + print(f"==> {structure:10s} | {mode:9s} | N={n}") + runs = run_one(structure, mode, n, RNG_SEED) + # runs = [(insert, find, delete), ...] + ops = ["insert", "find", "delete"] + for op_idx, op in enumerate(ops): + vals = [r[op_idx] for r in runs] + mean = sum(vals) / len(vals) + for trial_idx, v in enumerate(vals): + rows.append([structure, mode, op, n, trial_idx + 1, f"{v:.6f}"]) + summary.append((structure, mode, op, n, mean, vals)) + print(f" {op:7s}: mean={mean*1000:.3f} ms " + f"runs={[f'{v*1000:.3f}' for v in vals]}") + + # сводная строка со средними + rows.append([]) + rows.append(["--- СРЕДНИЕ ---"]) + rows.append(["Структура", "Режим", "Операция", "N", + "Среднее (сек)", "Все замеры (сек)"]) + for s, mode, op, n, mean, vals in summary: + rows.append([s, mode, op, n, f"{mean:.6f}", + ";".join(f"{v:.6f}" for v in vals)]) + + with open(OUT_CSV, "w", newline="", encoding="utf-8") as f: + writer = csv.writer(f) + writer.writerows(rows) + + print(f"\nГотово. Результаты записаны в {OUT_CSV}") + + +if __name__ == "__main__": + main() diff --git a/SobolevNS/docs/data/task1_data_structures/phonebook.py b/SobolevNS/docs/data/task1_data_structures/phonebook.py new file mode 100644 index 0000000..a9b9185 --- /dev/null +++ b/SobolevNS/docs/data/task1_data_structures/phonebook.py @@ -0,0 +1,267 @@ +""" +phonebook.py +Три структуры данных для хранения телефонного справочника, +реализованные в процедурной парадигме (без классов). + +Узлы и контейнеры представляются обычными словарями и списками. +""" +import sys + +# Увеличиваем лимит рекурсии - нужно для BST в худшем случае +sys.setrecursionlimit(200_000) + + +# ============================================================ +# 1. СВЯЗНЫЙ СПИСОК +# Узел: {'name': str, 'phone': str, 'next': dict|None} +# Голова списка - это либо узел, либо None (пустой список) +# ============================================================ + +def ll_create(): + """Создаёт пустой связный список.""" + return None + + +def ll_insert(head, name, phone): + """Вставляет или обновляет запись. Возвращает новую голову списка.""" + # Если списка нет - создаём первый узел + if head is None: + return {'name': name, 'phone': phone, 'next': None} + + # Если совпадение в голове - просто обновляем телефон + node = head + while node is not None: + if node['name'] == name: + node['phone'] = phone + return head + if node['next'] is None: + break + node = node['next'] + + # node - последний узел, добавляем после него + node['next'] = {'name': name, 'phone': phone, 'next': None} + return head + + +def ll_find(head, name): + """Возвращает phone или None.""" + node = head + while node is not None: + if node['name'] == name: + return node['phone'] + node = node['next'] + return None + + +def ll_delete(head, name): + """Удаляет узел по имени. Возвращает новую голову (она могла измениться).""" + if head is None: + return None + + # Удаление головы + if head['name'] == name: + return head['next'] + + prev = head + cur = head['next'] + while cur is not None: + if cur['name'] == name: + prev['next'] = cur['next'] + return head + prev = cur + cur = cur['next'] + # Не нашли - игнорируем + return head + + +def ll_collect(head): + """Возвращает несортированный список (name, phone) - служебная функция.""" + out = [] + node = head + while node is not None: + out.append((node['name'], node['phone'])) + node = node['next'] + return out + + +def ll_list_all(head): + """Возвращает все записи, отсортированные по имени.""" + items = ll_collect(head) + items.sort(key=lambda x: x[0]) + return items + + +# ============================================================ +# 2. ХЕШ-ТАБЛИЦА +# buckets - список фиксированной длины из голов связных списков +# ============================================================ + +def ht_create(size=1024): + """Создаёт пустую хеш-таблицу с заданным числом бакетов.""" + return { + 'size': size, + 'buckets': [None] * size, + } + + +def _ht_index(name, size): + """Хеш-функция: встроенный hash + остаток от деления.""" + return hash(name) % size + + +def ht_insert(ht, name, phone): + """Вставляет или обновляет запись в нужном бакете.""" + idx = _ht_index(name, ht['size']) + ht['buckets'][idx] = ll_insert(ht['buckets'][idx], name, phone) + + +def ht_find(ht, name): + """Возвращает phone или None.""" + idx = _ht_index(name, ht['size']) + return ll_find(ht['buckets'][idx], name) + + +def ht_delete(ht, name): + """Удаляет запись (если она есть).""" + idx = _ht_index(name, ht['size']) + ht['buckets'][idx] = ll_delete(ht['buckets'][idx], name) + + +def ht_list_all(ht): + """Собирает все записи из всех бакетов и сортирует по имени.""" + out = [] + for head in ht['buckets']: + out.extend(ll_collect(head)) + out.sort(key=lambda x: x[0]) + return out + + +# ============================================================ +# 3. ДВОИЧНОЕ ДЕРЕВО ПОИСКА +# Узел: {'name': str, 'phone': str, 'left': dict|None, 'right': dict|None} +# Корень - узел или None +# ============================================================ + +def bst_create(): + """Создаёт пустое BST.""" + return None + + +def bst_insert(root, name, phone): + """Вставляет/обновляет. Итеративная реализация, чтобы не упереться в рекурсию + при отсортированном входе. Возвращает новый корень.""" + new_node = {'name': name, 'phone': phone, 'left': None, 'right': None} + + if root is None: + return new_node + + cur = root + while True: + if name == cur['name']: + cur['phone'] = phone + return root + if name < cur['name']: + if cur['left'] is None: + cur['left'] = new_node + return root + cur = cur['left'] + else: + if cur['right'] is None: + cur['right'] = new_node + return root + cur = cur['right'] + + +def bst_find(root, name): + """Возвращает phone или None. Итеративный поиск.""" + cur = root + while cur is not None: + if name == cur['name']: + return cur['phone'] + cur = cur['left'] if name < cur['name'] else cur['right'] + return None + + +def _bst_min_node(node): + """Возвращает узел с минимальным ключом в поддереве.""" + while node['left'] is not None: + node = node['left'] + return node + + +def bst_delete(root, name): + """Удаление узла с возвратом нового корня. Стандартный алгоритм: + если у узла двое детей - заменяем его на минимальный из правого поддерева.""" + if root is None: + return None + + if name < root['name']: + root['left'] = bst_delete(root['left'], name) + elif name > root['name']: + root['right'] = bst_delete(root['right'], name) + else: + # Нашли узел для удаления + if root['left'] is None: + return root['right'] + if root['right'] is None: + return root['left'] + # Двое детей: берём преемника (мин. в правом поддереве) + successor = _bst_min_node(root['right']) + root['name'] = successor['name'] + root['phone'] = successor['phone'] + root['right'] = bst_delete(root['right'], successor['name']) + return root + + +def bst_list_all(root): + """Центрированный обход (in-order) - сразу даёт отсортированный по имени список. + Итеративный, чтобы не упасть на вырожденном дереве.""" + out = [] + stack = [] + cur = root + while cur is not None or stack: + while cur is not None: + stack.append(cur) + cur = cur['left'] + cur = stack.pop() + out.append((cur['name'], cur['phone'])) + cur = cur['right'] + return out + + +# ============================================================ +# Маленький self-test, запускается только при прямом вызове +# ============================================================ +if __name__ == "__main__": + data = [("Alice", "111"), ("Bob", "222"), ("Charlie", "333"), + ("Dave", "444"), ("Eve", "555")] + + # LinkedList + head = ll_create() + for n, p in data: + head = ll_insert(head, n, p) + assert ll_find(head, "Bob") == "222" + assert ll_find(head, "Nope") is None + head = ll_delete(head, "Bob") + assert ll_find(head, "Bob") is None + assert ll_list_all(head) == sorted([(n, p) for n, p in data if n != "Bob"]) + + # HashTable + ht = ht_create(size=8) + for n, p in data: + ht_insert(ht, n, p) + assert ht_find(ht, "Charlie") == "333" + ht_delete(ht, "Charlie") + assert ht_find(ht, "Charlie") is None + assert ht_list_all(ht) == sorted([(n, p) for n, p in data if n != "Charlie"]) + + # BST + root = bst_create() + for n, p in data: + root = bst_insert(root, n, p) + assert bst_find(root, "Dave") == "444" + root = bst_delete(root, "Dave") + assert bst_find(root, "Dave") is None + assert bst_list_all(root) == sorted([(n, p) for n, p in data if n != "Dave"]) + + print("phonebook.py: все самопроверки пройдены") diff --git a/SobolevNS/docs/data/task1_data_structures/plot_results.py b/SobolevNS/docs/data/task1_data_structures/plot_results.py new file mode 100644 index 0000000..0b1e640 --- /dev/null +++ b/SobolevNS/docs/data/task1_data_structures/plot_results.py @@ -0,0 +1,118 @@ +""" +plot_results.py - строит столбчатые диаграммы по итогам экспериментов. +""" +import csv +import os +import matplotlib.pyplot as plt +import numpy as np + +CSV = os.path.join("docs", "data", "results.csv") +PLOTS_DIR = os.path.join("docs", "data", "plots") +os.makedirs(PLOTS_DIR, exist_ok=True) + + +def load_means(): + """Возвращает dict[(structure, mode, op)] = (mean, N).""" + means = {} + with open(CSV, encoding="utf-8") as f: + rows = list(csv.reader(f)) + # ищем секцию "--- СРЕДНИЕ ---" + start = None + for i, row in enumerate(rows): + if row and row[0] == "--- СРЕДНИЕ ---": + start = i + 2 # пропустить заголовок секции + break + for row in rows[start:]: + if not row: + continue + structure, mode, op, n, mean, _trials = row + means[(structure, mode, op)] = (float(mean), int(n)) + return means + + +def plot_grouped(means, op, fname, title): + """Сгруппированные столбцы: для каждой структуры - два столбца (shuffled / sorted).""" + structures = ["LinkedList", "HashTable", "BST"] + modes = ["shuffled", "sorted"] + x = np.arange(len(structures)) + width = 0.36 + + fig, ax = plt.subplots(figsize=(8, 5)) + + for i, mode in enumerate(modes): + vals_ms = [] + labels = [] + for s in structures: + mean, n = means[(s, mode, op)] + vals_ms.append(mean * 1000) + labels.append(f"N={n}") + bars = ax.bar(x + (i - 0.5) * width, vals_ms, width, + label=mode, alpha=0.85) + for bar, lab in zip(bars, labels): + h = bar.get_height() + ax.text(bar.get_x() + bar.get_width() / 2, h, + f"{h:.2f}\n{lab}", ha="center", va="bottom", fontsize=8) + + ax.set_xticks(x) + ax.set_xticklabels(structures) + ax.set_ylabel("Время, мс (среднее по 5 запускам)") + ax.set_title(title) + ax.set_yscale("log") + ax.legend(title="порядок входных данных") + ax.grid(axis="y", linestyle="--", alpha=0.4) + plt.tight_layout() + out = os.path.join(PLOTS_DIR, fname) + plt.savefig(out, dpi=130) + plt.close() + print("saved:", out) + + +def plot_bst_degradation(means, fname): + """Отдельный график: BST shuffled vs sorted - даже при меньшем N + отсортированные данные дают намного большее время.""" + ops = ["insert", "find", "delete"] + shuffled = [means[("BST", "shuffled", op)][0] * 1000 for op in ops] + sorted_ = [means[("BST", "sorted", op)][0] * 1000 for op in ops] + n_shuf = means[("BST", "shuffled", "insert")][1] + n_sort = means[("BST", "sorted", "insert")][1] + + x = np.arange(len(ops)) + width = 0.36 + fig, ax = plt.subplots(figsize=(7, 4.5)) + ax.bar(x - width/2, shuffled, width, label=f"shuffled (N={n_shuf})") + ax.bar(x + width/2, sorted_, width, label=f"sorted (N={n_sort})") + for i, v in enumerate(shuffled): + ax.text(i - width/2, v, f"{v:.2f}", ha="center", va="bottom", fontsize=9) + for i, v in enumerate(sorted_): + ax.text(i + width/2, v, f"{v:.2f}", ha="center", va="bottom", fontsize=9) + + ax.set_xticks(x); ax.set_xticklabels(ops) + ax.set_ylabel("Время, мс") + ax.set_title("BST: деградация на отсортированных данных\n" + "(N для sorted в 5 раз меньше, но время больше)") + ax.set_yscale("log") + ax.legend() + ax.grid(axis="y", linestyle="--", alpha=0.4) + plt.tight_layout() + out = os.path.join(PLOTS_DIR, fname) + plt.savefig(out, dpi=130) + plt.close() + print("saved:", out) + + +def main(): + means = load_means() + plot_grouped(means, "insert", + "insert_compare.png", + "Вставка всех записей (лог. шкала)") + plot_grouped(means, "find", + "find_compare.png", + "Поиск 110 ключей (лог. шкала)") + plot_grouped(means, "delete", + "delete_compare.png", + "Удаление 50 записей (лог. шкала)") + plot_bst_degradation(means, "bst_degradation.png") + + +if __name__ == "__main__": + main() diff --git a/SobolevNS/docs/report_01.md b/SobolevNS/docs/report_01.md new file mode 100644 index 0000000..fd4271c --- /dev/null +++ b/SobolevNS/docs/report_01.md @@ -0,0 +1,169 @@ +# Отчёт по заданию 1. Структуры данных: телефонный справочник + +## 1. Цель работы + +Реализовать три структуры данных «руками» (без классов, в процедурной парадигме): +**связный список**, **хеш‑таблицу с цепочками** и **двоичное дерево поиска (BST)**. +Сравнить их на одной и той же задаче - телефонный справочник с операциями `insert`, +`find`, `delete`, `list_all` - и понять, какая структура когда лучше. + +## 2. Что и как реализовано + +Все три структуры реализованы в `phonebook.py`. Каждая - это набор функций, +которые получают/возвращают текущее состояние (узел или контейнер). +Объектная парадигма сознательно не использовалась - это позволяет «увидеть руки» +каждой операции. + +### 2.1. Связный список + +Узел - обычный словарь `{'name', 'phone', 'next'}`. Голова списка - это либо +такой словарь, либо `None`. + +Ключевые функции (полный код - в `phonebook.py`): + +| Функция | Сложность | +| --- | --- | +| `ll_insert(head, name, phone)` | вставка в конец или обновление: O(n) | +| `ll_find(head, name)` | линейный поиск: O(n) | +| `ll_delete(head, name)` | O(n) | +| `ll_list_all(head)` | собрать всё + сортировка: O(n log n) | + +В `ll_insert` я специально хожу до конца, чтобы потом сравнить с худшим +случаем - это объясняет «провал» на графиках вставки (см. ниже). + +### 2.2. Хеш-таблица + +Контейнер - словарь `{'size': int, 'buckets': [head|None, ...]}`. Каждый бакет - +голова связного списка, и внутри бакета работают всё те же `ll_*` функции. +Хеш-функция: `hash(name) % size`. Размер таблицы - 2048. + +| Функция | Сложность (амортизированно) | +| --- | --- | +| `ht_insert/ht_find/ht_delete` | O(1) при равномерной хеш-функции | +| `ht_list_all` | O(n log n) (надо собрать всё и отсортировать) | + +### 2.3. Двоичное дерево поиска (BST) + +Узел - словарь `{'name', 'phone', 'left', 'right'}`. Реализация **итеративная**, +без рекурсии - это важно для эксперимента с отсортированным входом (вырожденное +дерево глубины N, рекурсия упёрлась бы в лимит). + +| Функция | Случайные данные | Отсортированные данные | +| --- | --- | --- | +| `bst_insert/bst_find/bst_delete` | O(log n) | **O(n)** (вырожденный «список») | +| `bst_list_all` | O(n) (in‑order, сразу отсортировано) | O(n) | + +## 3. Эксперимент + +### 3.1. Методика + +* Генерируем `N = 10 000` записей вида `("User_00000", "555-0000000")` и т.д. +* Два режима ввода: **shuffled** (случайный) и **sorted** (отсортированный по имени). +* Каждое испытание повторяется **5 раз**, в CSV сохраняем все 5 значений + и среднее. +* На каждом испытании: + 1. строим структуру заново; + 2. вставляем все N записей - замеряем время; + 3. ищем 100 случайных существующих имён + 10 несуществующих (всего 110 вызовов); + 4. удаляем 50 случайных имён. + +Замер - `time.perf_counter()`. Все значения в секундах в CSV, в отчёте перевожу +в миллисекунды. + +> Для **BST в режиме `sorted`** пришлось снизить N до 2 000. При N = 10 000 +> вставка занимает десятки минут (сложность операции - `O(N**2)`, дерево превращается +> в связный список). Это и есть главная иллюстрация «деградации BST». + +### 3.2. Результаты (средние, миллисекунды) + +| Структура | Режим | N | вставка | поиск 110 ключей | удаление 50 | +| --- | --- | ---: | ---: | ---: | ---: | +| LinkedList | shuffled | 10 000 | **4 027** | 34.87 | 14.43 | +| LinkedList | sorted | 10 000 | 3 056 | 27.58 | 13.34 | +| HashTable | shuffled | 10 000 | 6.71 | 0.068 | 0.038 | +| HashTable | sorted | 10 000 | 6.42 | 0.068 | 0.033 | +| BST | shuffled | 10 000 | 16.84 | 0.172 | 0.115 | +| BST | sorted | **2 000** | **121.17** | 5.20 | 7.48 | + +Полные сырые данные - в `data/results.csv`. + +### 3.3. Графики + +![Вставка](data/task1_data_structures/docs/data/plots/insert_compare.png) + +![Поиск](data/task1_data_structures/docs/data/plots/find_compare.png) + +![Удаление](data/task1_data_structures/docs/data/plots/delete_compare.png) + +![Деградация BST на отсортированных данных](data/task1_data_structures/docs/data/plots/bst_degradation.png) + + +## 4. Анализ + +### 4.1. Почему связный список такой медленный на вставке + +Мой `ll_insert` идёт до конца списка, чтобы потом обновить узел, если он уже +есть. На N‑м элементе он совершает уже N шагов. Суммарно - около N**2/2 +операций. На N = 10 000 это даёт ~50 миллионов проходов по узлам - отсюда +и ~4 секунды. + +Если бы мы вставляли **в начало** (за O(1)), общая вставка стала бы линейной, +но тогда: +* возможны дубликаты (обновление пропускается); +* для совместимости с двумя другими структурами всё равно понадобился бы поиск. + +Поиск и удаление быстрее: 110 поисков ≈ 35 мс, потому что в среднем ищем +N/2 шагов и таких запросов всего 110. Сложность каждой операции - O(N), +но запросов мало. + +### 4.2. Почему хеш-таблица почти не зависит от порядка + +Хеш-функция превращает имя в индекс бакета. Сами имена `User_00001`, +`User_00002`, … равномерно распределяются по 2 048 бакетам, поэтому в каждом +бакете лежит в среднем ≈ 5 элементов. Все три операции работают за ~O(1). + +Порядок входных данных не имеет значения, потому что `hash(name)` от него не +зависит. На графике видно: shuffled и sorted столбцы у HashTable одинаковой +высоты. + +### 4.3. Почему BST деградирует на отсортированном входе + +Когда имена приходят в алфавитном порядке, каждый новый ключ всегда больше +предыдущего, поэтому он идёт в правое поддерево. Дерево превращается +в правый «костыль» - фактически в односвязный список. Сложность вставки - +O(N**2), поиска и удаления - O(N). + +На графике `bst_degradation.png` это видно очень ярко: даже при **в 5 раз +меньшем N (2 000 вместо 10 000)** все операции у sorted BST занимают +в 5–10 раз больше времени, чем у shuffled BST. + +Это известная слабость наивного BST. В реальном коде её решают +**самобалансирующимися деревьями** (AVL, красно-чёрные, B-деревья), +которые гарантируют глубину O(log n) даже на отсортированном входе. + +### 4.4. Удаление + +* В связном списке удаление - это пробежка до нужного узла + переключение + ссылок. На 50 запросах суммарно ~14 мс при N = 10 000. +* В хеш-таблице удаление - это пробежка по бакету (короткий список), почти O(1): + всего 0.038 мс на 50 удалений. +* В BST стандартное удаление: если у узла двое детей, заменяем его на минимум + правого поддерева и рекурсивно удаляем его. На случайном дереве - O(log n) + на удаление; на отсортированном - O(N). + +## 5. Что выбирать в реальной жизни + +| Сценарий | Лучший выбор | Почему | +| --- | --- | --- | +| Частые `insert`/`find`/`delete`, порядок не нужен | **Хеш-таблица** | O(1) на всё | +| Нужно много раз получать **отсортированный** список | **BST** (с балансировкой) | in‑order обход - это и есть сортировка, O(N) | +| Очень мало данных или редкие операции; нужна простота | **Связный список** | минимум кода, O(N) - приемлемо | +| Нужны диапазонные запросы («все имена от 'A' до 'D'») | **BST/сбалансированное** | хеш-таблица их не умеет | +| Гарантированная производительность на любых данных | **Хеш-таблица или AVL/RB-tree**, но **не наивное BST** | наивное BST уязвимо к отсортированному входу | + +Хеш-таблица в стандартной библиотеке Python - это и есть встроенный `dict`, +которым стоит пользоваться по умолчанию для пар «ключ → значение». +Понимать «руками» связный список и BST полезно, чтобы знать, что лежит +под капотом более сложных контейнеров и понимать, +когда их применять (например, `OrderedDict`, `sortedcontainers.SortedDict`, +индексы в БД и т. д.).