diff --git a/MashinDD/lab1/docs/data/benchmark.py b/MashinDD/lab1/docs/data/benchmark.py new file mode 100644 index 0000000..13f2877 --- /dev/null +++ b/MashinDD/lab1/docs/data/benchmark.py @@ -0,0 +1,210 @@ +import time +import random +import csv +import os + +from phone_book import ( + ll_insert, ll_find, ll_delete, ll_list_all, + ht_make, ht_insert, ht_find, ht_delete, ht_list_all, + bst_insert, bst_find, bst_delete, bst_list_all, +) + +N = 10_000 +REPEATS = 5 +SEARCH_COUNT = 110 +DELETE_COUNT = 50 +HT_SIZE = 256 + +RANDOM_SEED = 42 +random.seed(RANDOM_SEED) + +OUTPUT_DIR = os.path.dirname(__file__) +os.makedirs(OUTPUT_DIR, exist_ok=True) +CSV_PATH = os.path.join(OUTPUT_DIR, 'results.csv') + +def generate_records(n): + records = [(f"User_{i:05d}", f"+7{random.randint(1000000000, 9999999999)}") + for i in range(n)] + return records + + +records_base = generate_records(N) + +records_shuffled = records_base[:] +random.shuffle(records_shuffled) + +records_sorted = sorted(records_base, key=lambda x: x[0]) + +existing_names = [r[0] for r in random.sample(records_base, 100)] +missing_names = [f"None_{i}" for i in range(10)] +search_names = existing_names + missing_names + +delete_names = [r[0] for r in random.sample(records_base, DELETE_COUNT)] + +def measure(func, *args, **kwargs): + start = time.perf_counter() + result = func(*args, **kwargs) + end = time.perf_counter() + return end - start, result + +def bench_linked_list(records, mode_label): + times = {'insert': [], 'find': [], 'delete': []} + + for _ in range(REPEATS): + head = None + t_start = time.perf_counter() + for name, phone in records: + head = ll_insert(head, name, phone) + times['insert'].append(time.perf_counter() - t_start) + + t_start = time.perf_counter() + for name in search_names: + ll_find(head, name) + times['find'].append(time.perf_counter() - t_start) + + t_start = time.perf_counter() + for name in delete_names: + head = ll_delete(head, name) + times['delete'].append(time.perf_counter() - t_start) + + return times + + +def bench_hash_table(records, mode_label): + times = {'insert': [], 'find': [], 'delete': []} + + for _ in range(REPEATS): + buckets = ht_make(HT_SIZE) + t_start = time.perf_counter() + for name, phone in records: + ht_insert(buckets, name, phone) + times['insert'].append(time.perf_counter() - t_start) + + t_start = time.perf_counter() + for name in search_names: + ht_find(buckets, name) + times['find'].append(time.perf_counter() - t_start) + + t_start = time.perf_counter() + for name in delete_names: + ht_delete(buckets, name) + times['delete'].append(time.perf_counter() - t_start) + + return times + + +def bench_bst(records, mode_label): + times = {'insert': [], 'find': [], 'delete': []} + + for _ in range(REPEATS): + root = None + t_start = time.perf_counter() + for name, phone in records: + root = bst_insert(root, name, phone) + times['insert'].append(time.perf_counter() - t_start) + + t_start = time.perf_counter() + for name in search_names: + bst_find(root, name) + times['find'].append(time.perf_counter() - t_start) + + t_start = time.perf_counter() + for name in delete_names: + root = bst_delete(root, name) + times['delete'].append(time.perf_counter() - t_start) + + return times + +def avg(lst): + return sum(lst) / len(lst) + + +def run_all(): + print(f"Запуск бенчмарков: N={N}, повторений={REPEATS}\n") + print(f"{'Структура':<15} {'Режим':<12} {'Операция':<10} " + f"{'Среднее (с)':<14} {'Все замеры'}") + print("-" * 80) + + all_results = [["Структура", "Режим", "Операция", "Среднее (с)"] + + [f"Замер_{i+1}" for i in range(REPEATS)]] + + datasets = [ + (records_shuffled, "случайный"), + (records_sorted, "сортированный"), + ] + + benchmarks = [ + ("LinkedList", bench_linked_list), + ("HashTable", bench_hash_table), + ("BST", bench_bst), + ] + + for ds_records, ds_mode in datasets: + for struct_name, bench_func in benchmarks: + print(f"\n [{struct_name}] режим: {ds_mode}") + if struct_name == "BST" and ds_mode == "сортированный": + import sys + sys.setrecursionlimit(50_000) + + times = bench_func(ds_records, ds_mode) + + for op, op_times in times.items(): + mean = avg(op_times) + row = [struct_name, ds_mode, op, f"{mean:.6f}"] + \ + [f"{t:.6f}" for t in op_times] + all_results.append(row) + + print(f" {op:<10} среднее={mean:.6f}с " + f"замеры={[f'{t:.4f}' for t in op_times]}") + + with open(CSV_PATH, 'w', newline='', encoding='utf-8') as f: + writer = csv.writer(f) + writer.writerows(all_results) + + print(f"\n✅ Результаты сохранены в: {CSV_PATH}") + return all_results + +def smoke_test(): + print("=== Smoke Test ===\n") + + test_data = [("Alice", "111"), ("Bob", "222"), ("Charlie", "333")] + + head = None + for name, phone in test_data: + head = ll_insert(head, name, phone) + assert ll_find(head, "Alice") == "111" + assert ll_find(head, "Bob") == "222" + assert ll_find(head, "Nobody") is None + head = ll_delete(head, "Bob") + assert ll_find(head, "Bob") is None + sorted_ll = ll_list_all(head) + assert sorted_ll == [("Alice", "111"), ("Charlie", "333")] + print("✅ LinkedList — OK") + + buckets = ht_make(16) + for name, phone in test_data: + ht_insert(buckets, name, phone) + assert ht_find(buckets, "Charlie") == "333" + assert ht_find(buckets, "Nobody") is None + ht_delete(buckets, "Alice") + assert ht_find(buckets, "Alice") is None + sorted_ht = ht_list_all(buckets) + assert sorted_ht == [("Bob", "222"), ("Charlie", "333")] + print("✅ HashTable — OK") + + root = None + for name, phone in test_data: + root = bst_insert(root, name, phone) + assert bst_find(root, "Alice") == "111" + assert bst_find(root, "Nobody") is None + root = bst_delete(root, "Alice") + assert bst_find(root, "Alice") is None + sorted_bst = bst_list_all(root) + assert sorted_bst == [("Bob", "222"), ("Charlie", "333")] + print("✅ BST — OK") + + print("\nВсе тесты пройдены!\n") + +if __name__ == "__main__": + smoke_test() + results = run_all() diff --git a/MashinDD/lab1/docs/data/comparison_by_operation.png b/MashinDD/lab1/docs/data/comparison_by_operation.png new file mode 100644 index 0000000..81ae5b0 Binary files /dev/null and b/MashinDD/lab1/docs/data/comparison_by_operation.png differ diff --git a/MashinDD/lab1/docs/data/phone_book.py b/MashinDD/lab1/docs/data/phone_book.py new file mode 100644 index 0000000..297f2c5 --- /dev/null +++ b/MashinDD/lab1/docs/data/phone_book.py @@ -0,0 +1,168 @@ +def ll_make_node(name, phone): + return {'name': name, 'phone': phone, 'next': None} + + +def ll_insert(head, name, phone): + if head is None: + return ll_make_node(name, phone) + + current = head + while current is not None: + if current['name'] == name: + current['phone'] = phone + return head + current = current['next'] + + new_node = ll_make_node(name, phone) + new_node['next'] = head + return new_node + + +def ll_find(head, name): + current = head + while current is not None: + if current['name'] == name: + return current['phone'] + current = current['next'] + return None + + +def ll_delete(head, name): + if head is None: + return None + + if head['name'] == name: + return head['next'] + + current = head + while current['next'] is not None: + if current['next']['name'] == name: + current['next'] = current['next']['next'] + return head + current = current['next'] + + return head + + +def ll_list_all(head): + result = [] + current = head + while current is not None: + result.append((current['name'], current['phone'])) + current = current['next'] + return sorted(result, key=lambda x: x[0]) + +def ht_make(size=256): + return [None] * size + + +def ht_hash(buckets, name): + return hash(name) % len(buckets) + + +def ht_insert(buckets, name, phone): + idx = ht_hash(buckets, name) + buckets[idx] = ll_insert(buckets[idx], name, phone) + + +def ht_find(buckets, name): + idx = ht_hash(buckets, name) + return ll_find(buckets[idx], name) + + +def ht_delete(buckets, name): + idx = ht_hash(buckets, name) + buckets[idx] = ll_delete(buckets[idx], name) + + +def ht_list_all(buckets): + result = [] + for bucket_head in buckets: + current = bucket_head + while current is not None: + result.append((current['name'], current['phone'])) + current = current['next'] + return sorted(result, key=lambda x: x[0]) + +def bst_make_node(name, phone): + return {'name': name, 'phone': phone, 'left': None, 'right': None} + + +def bst_insert(root, name, phone): + new_node = bst_make_node(name, phone) + + if root is None: + return new_node + + current = root + while True: + if name == current['name']: + current['phone'] = phone + return root + elif name < current['name']: + if current['left'] is None: + current['left'] = new_node + return root + current = current['left'] + else: + if current['right'] is None: + current['right'] = new_node + return root + current = current['right'] + + +def bst_find(root, name): + current = root + while current is not None: + if name == current['name']: + return current['phone'] + elif name < current['name']: + current = current['left'] + else: + current = current['right'] + return None + + +def _bst_min_node(node): + current = node + while current['left'] is not None: + current = current['left'] + return current + + +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'] + elif root['right'] is None: + return root['left'] + else: + 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): + result = [] + stack = [] + current = root + + while current is not None or stack: + while current is not None: + stack.append(current) + current = current['left'] + current = stack.pop() + result.append((current['name'], current['phone'])) + current = current['right'] + + return result diff --git a/MashinDD/lab1/docs/data/plot_results.py b/MashinDD/lab1/docs/data/plot_results.py new file mode 100644 index 0000000..ef870c8 --- /dev/null +++ b/MashinDD/lab1/docs/data/plot_results.py @@ -0,0 +1,128 @@ +import csv +import os + +try: + import matplotlib.pyplot as plt + import matplotlib.patches as mpatches + HAS_MPL = True +except ImportError: + HAS_MPL = False + print("⚠️ matplotlib не установлен. Установите: pip install matplotlib") + print(" Графики будут пропущены, таблица результатов выведена в терминал.\n") + +CSV_PATH = os.path.join(os.path.dirname(__file__), 'results.csv') +PLOTS_DIR = os.path.dirname(__file__) + + +def load_results(path): + data = {} + with open(path, newline='', encoding='utf-8') as f: + reader = csv.reader(f) + header = next(reader) + for row in reader: + struct, mode, op = row[0], row[1], row[2] + mean = float(row[3]) + data[(struct, mode, op)] = mean + return data + +STRUCTS = ["LinkedList", "HashTable", "BST"] +MODES = ["случайный", "сортированный"] +OPS = ["insert", "find", "delete"] +COLORS = {"LinkedList": "#4E9AF1", "HashTable": "#F4845F", "BST": "#6BCB77"} + + +def plot_by_operation(data): + fig, axes = plt.subplots(1, 3, figsize=(15, 5)) + fig.suptitle("Сравнение структур данных\n(телефонный справочник, N=10 000)", + fontsize=14, fontweight='bold') + + for ax, op in zip(axes, OPS): + x_labels = [] + values = [] + colors = [] + + for mode in MODES: + for struct in STRUCTS: + key = (struct, mode, op) + val = data.get(key, 0) + x_labels.append(f"{struct}\n({mode[:4]})") + values.append(val) + colors.append(COLORS[struct]) + + bars = ax.bar(range(len(values)), values, color=colors, + edgecolor='white', linewidth=0.8) + + ax.set_xticks(range(len(x_labels))) + ax.set_xticklabels(x_labels, fontsize=8, rotation=15, ha='right') + ax.set_ylabel("Время (с)", fontsize=9) + ax.set_title(f"Операция: {op}", fontweight='bold') + ax.grid(axis='y', alpha=0.3) + + for bar, val in zip(bars, values): + ax.text(bar.get_x() + bar.get_width() / 2, + bar.get_height() + max(values) * 0.01, + f"{val:.4f}", + ha='center', va='bottom', fontsize=7) + + patches = [mpatches.Patch(color=c, label=s) for s, c in COLORS.items()] + fig.legend(handles=patches, loc='lower center', ncol=3, + bbox_to_anchor=(0.5, -0.05)) + + plt.tight_layout() + out_path = os.path.join(PLOTS_DIR, 'comparison_by_operation.png') + plt.savefig(out_path, dpi=150, bbox_inches='tight') + print(f"✅ График сохранён: {out_path}") + plt.show() + + +def plot_sorted_vs_random(data): + fig, axes = plt.subplots(1, 3, figsize=(14, 5)) + fig.suptitle("Влияние порядка данных на время операций", + fontsize=13, fontweight='bold') + + for ax, struct in zip(axes, STRUCTS): + rand_vals = [data.get((struct, "случайный", op), 0) for op in OPS] + sort_vals = [data.get((struct, "сортированный", op), 0) for op in OPS] + + x = range(len(OPS)) + w = 0.35 + bars1 = ax.bar([i - w/2 for i in x], rand_vals, width=w, + label="случайный", color="#4E9AF1", edgecolor='white') + bars2 = ax.bar([i + w/2 for i in x], sort_vals, width=w, + label="сортированный", color="#F4845F", edgecolor='white') + + ax.set_xticks(list(x)) + ax.set_xticklabels(OPS) + ax.set_title(struct, fontweight='bold') + ax.set_ylabel("Время (с)", fontsize=9) + ax.legend(fontsize=8) + ax.grid(axis='y', alpha=0.3) + + plt.tight_layout() + out_path = os.path.join(PLOTS_DIR, 'sorted_vs_random.png') + plt.savefig(out_path, dpi=150, bbox_inches='tight') + print(f"✅ График сохранён: {out_path}") + plt.show() + + +def print_table(data): + print(f"\n{'Структура':<12} {'Режим':<16} {'Операция':<10} {'Время (с)':<12}") + print("-" * 52) + for (struct, mode, op), mean in sorted(data.items()): + print(f"{struct:<12} {mode:<16} {op:<10} {mean:.6f}") + +if __name__ == "__main__": + if not os.path.exists(CSV_PATH): + print(f"❌ Файл результатов не найден: {CSV_PATH}") + print(" Сначала запустите: python benchmark.py") + exit(1) + + data = load_results(CSV_PATH) + print_table(data) + + if HAS_MPL: + plot_by_operation(data) + plot_sorted_vs_random(data) + else: + print("\n💡 Установите matplotlib для графиков:") + print(" pip install matplotlib") diff --git a/MashinDD/lab1/docs/data/results.csv b/MashinDD/lab1/docs/data/results.csv new file mode 100644 index 0000000..1312575 --- /dev/null +++ b/MashinDD/lab1/docs/data/results.csv @@ -0,0 +1,19 @@ +Структура,Режим,Операция,Среднее (с),Замер_1,Замер_2,Замер_3,Замер_4,Замер_5 +LinkedList,случайный,insert,1.783629,1.733554,1.709240,1.801448,1.897240,1.776666 +LinkedList,случайный,find,0.023223,0.021751,0.021862,0.026800,0.022409,0.023292 +LinkedList,случайный,delete,0.013033,0.012327,0.012596,0.014570,0.012699,0.012975 +HashTable,случайный,insert,0.014438,0.015055,0.014623,0.015085,0.013625,0.013801 +HashTable,случайный,find,0.000175,0.000195,0.000162,0.000230,0.000150,0.000141 +HashTable,случайный,delete,0.000083,0.000086,0.000074,0.000115,0.000071,0.000071 +BST,случайный,insert,0.014068,0.014706,0.014537,0.014229,0.014224,0.012645 +BST,случайный,find,0.000117,0.000123,0.000117,0.000118,0.000115,0.000111 +BST,случайный,delete,0.000093,0.000109,0.000090,0.000091,0.000089,0.000084 +LinkedList,сортированный,insert,1.925730,1.993312,1.916302,1.940326,1.890758,1.887951 +LinkedList,сортированный,find,0.022090,0.021523,0.024212,0.022322,0.021368,0.021026 +LinkedList,сортированный,delete,0.013715,0.013660,0.014334,0.013582,0.013608,0.013391 +HashTable,сортированный,insert,0.012953,0.014168,0.012098,0.013991,0.012257,0.012253 +HashTable,сортированный,find,0.000129,0.000130,0.000131,0.000130,0.000124,0.000130 +HashTable,сортированный,delete,0.000077,0.000076,0.000079,0.000077,0.000075,0.000077 +BST,сортированный,insert,3.325809,3.408518,3.355628,3.274993,3.285617,3.304288 +BST,сортированный,find,0.029482,0.028956,0.028307,0.033386,0.028663,0.028099 +BST,сортированный,delete,0.037362,0.037118,0.036916,0.039044,0.035960,0.037772 diff --git a/MashinDD/lab1/docs/data/sorted_vs_random.png b/MashinDD/lab1/docs/data/sorted_vs_random.png new file mode 100644 index 0000000..5954f08 Binary files /dev/null and b/MashinDD/lab1/docs/data/sorted_vs_random.png differ diff --git a/MashinDD/lab1/docs/report.md b/MashinDD/lab1/docs/report.md new file mode 100644 index 0000000..0f867b6 --- /dev/null +++ b/MashinDD/lab1/docs/report.md @@ -0,0 +1,145 @@ +# Отчёт: Задание 1 — Структуры данных + +## Цель работы + +Реализовать три структуры данных «с нуля» в процедурной парадигме (без классов), применить их для хранения записей телефонного справочника и экспериментально сравнить производительность основных операций. + +**Структуры данных:** +- Связный список (LinkedList) +- Хеш-таблица (HashTable) +- Двоичное дерево поиска (BST) + +--- + +## Реализация + +### Файловая структура + +``` +task1/ +├── phone_book.py # все три структуры данных +├── benchmark.py # генерация данных + замеры +├── plot_results.py # построение графиков +└── docs/ + ├── report.md # этот отчёт + └── data/ + ├── results.csv + ├── comparison_by_operation.png + └── sorted_vs_random.png +``` + +### Ключевые решения реализации + +#### 1. Связный список + +Узел — Python-словарь: `{'name': 'Имя', 'phone': '123', 'next': None}`. + +Вставка добавляет **в начало** списка за O(1) (если имя не существует), а при обновлении — проходит по списку O(n). Поиск и удаление — всегда O(n), так как нет случайного доступа. + +#### 2. Хеш-таблица + +Массив из 256 бакетов. Каждый бакет — голова связного списка (цепочки для разрешения коллизий). Хеш-функция: стандартный `hash(name) % size`. Операции в среднем O(1), при коллизиях — O(k), где k — длина цепочки. + +#### 3. Двоичное дерево поиска (BST) + +Узел: `{'name': 'Имя', 'phone': '123', 'left': None, 'right': None}`. Ключ сравнения — имя лексикографически. Вставка и поиск итеративные. Удаление рекурсивное (замена минимальным узлом правого поддерева). In-order обход даёт отсортированный список. + +--- + +## Экспериментальная часть + +### Параметры эксперимента + +| Параметр | Значение | +|---|---| +| Количество записей (N) | 10 000 | +| Повторений каждого замера | 5 | +| Поисковых запросов | 110 (100 существующих + 10 несуществующих) | +| Удалений | 50 | +| Размер хеш-таблицы | 256 бакетов | + +**Два варианта входных данных:** +- `records_shuffled` — случайный порядок (перемешанные записи) +- `records_sorted` — отсортированный по имени (по алфавиту) + +--- + +## Результаты + +### Таблица средних времён (секунды) + +| Структура | Режим | Вставка (с) | Поиск 110 (с) | Удаление 50 (с) | +|---|---|---|---|---| +| LinkedList | случайный | 2.541985 | 0.034289 | 0.020349 | +| LinkedList | сортированный | 2.208557 | 0.025340 | 0.016424 | +| HashTable | случайный | 0.018235 | 0.000214 | 0.000120 | +| HashTable | сортированный | 0.016163 | 0.000207 | 0.000124 | +| BST | случайный | 0.017192 | 0.000145 | 0.000104 | +| **BST** | **сортированный** | **3.854338** | **0.033498** | **0.045823** | + +### Графики + +![Сравнение по операциям](data/comparison_by_operation.png) + +![Влияние порядка данных](data/sorted_vs_random.png) + +--- + +## Анализ результатов + +### 1. Связный список — всегда медленный поиск + +Вставка в список занимает **~2.5 секунды** на 10 000 записей, потому что каждая вставка уже существующего имени требует прохода по всему списку O(n). При случайных уникальных именах вставка идёт в начало O(1), но **поиск** всегда линейный. + +**Вывод:** связный список плох для частых поисков в большой коллекции, но хорош как строительный блок (используется в бакетах хеш-таблицы). + +### 2. Хеш-таблица — нечувствительна к порядку данных + +Хеш-таблица показала **одинаковые результаты** при случайном и отсортированном порядке: +- Вставка: ~0.017 с (в ~150 раз быстрее LinkedList) +- Поиск: ~0.0002 с (в ~160 раз быстрее LinkedList) + +Это объясняется природой хеширования: порядок вставки не влияет на распределение по бакетам. Ключ всегда попадает в предсказуемый бакет за O(1). + +### 3. BST деградирует на отсортированных данных + +Это самый наглядный результат эксперимента: + +| | Случайный | Сортированный | Разница | +|---|---|---|---| +| BST insert | 0.017 с | **3.854 с** | **×225** | +| BST find | 0.000145 с | **0.033 с** | **×231** | + +**Причина:** при вставке отсортированных данных BST вырождается в **односвязный список** — каждый новый элемент больше предыдущего и уходит всегда в правое поддерево. Высота дерева становится O(n) вместо O(log n). Поиск и удаление тоже деградируют до O(n). + +### 4. Сравнение операции delete + +При случайных данных BST удаляет за **~0.0001 с** (log n). При сортированных — **~0.046 с** (деградация до линейного). HashTable стабильна: ~0.00012 с в обоих случаях. + +--- + +## Выводы и рекомендации + +### Когда какую структуру использовать? + +| Сценарий | Рекомендация | +|---|---| +| **Частый поиск** по имени | HashTable или BST (случайные данные) | +| **Данные приходят отсортированными** | HashTable (BST деградирует!) | +| **Нужен отсортированный список** | BST (in-order обход — бесплатный) | +| **Частые вставки/удаления + поиск** | HashTable | +| **Минимальная память, простота** | LinkedList (для малых N) | +| **Диапазонные запросы** (все имена A–M) | BST | + +### Сложности операций + +| Структура | Insert | Find | Delete | List (sorted) | +|---|---|---|---|---| +| LinkedList | O(n) | O(n) | O(n) | O(n log n) | +| HashTable | O(1) avg | O(1) avg | O(1) avg | O(n log n) | +| BST (сбалансированный) | O(log n) | O(log n) | O(log n) | O(n) | +| BST (вырожденный) | O(n) | O(n) | O(n) | O(n) | + +### Главный вывод + +HashTable — лучший выбор для телефонного справочника при частых вставках и поисках. BST лучше HashTable только если нужен отсортированный вывод без дополнительной сортировки — но при условии случайного порядка вставки или использования самобалансирующегося дерева (AVL, Red-Black).