diff --git a/shalovsa/lab1/docs/data/comparison_by_operation.png b/shalovsa/lab1/docs/data/comparison_by_operation.png new file mode 100644 index 0000000..2d54f26 Binary files /dev/null and b/shalovsa/lab1/docs/data/comparison_by_operation.png differ diff --git a/shalovsa/lab1/docs/data/laba.py b/shalovsa/lab1/docs/data/laba.py new file mode 100644 index 0000000..547fd54 --- /dev/null +++ b/shalovsa/lab1/docs/data/laba.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/shalovsa/lab1/docs/data/phone_book.py b/shalovsa/lab1/docs/data/phone_book.py new file mode 100644 index 0000000..297f2c5 --- /dev/null +++ b/shalovsa/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/shalovsa/lab1/docs/data/plot_results.py b/shalovsa/lab1/docs/data/plot_results.py new file mode 100644 index 0000000..ef870c8 --- /dev/null +++ b/shalovsa/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/shalovsa/lab1/docs/data/results.csv b/shalovsa/lab1/docs/data/results.csv new file mode 100644 index 0000000..85fbf13 --- /dev/null +++ b/shalovsa/lab1/docs/data/results.csv @@ -0,0 +1,19 @@ +Структура,Режим,Операция,Среднее (с),Замер_1,Замер_2,Замер_3,Замер_4,Замер_5 +LinkedList,случайный,insert,2.013381,2.030341,1.985393,2.000117,2.000130,2.050923 +LinkedList,случайный,find,0.026258,0.026263,0.027186,0.026271,0.026350,0.025217 +LinkedList,случайный,delete,0.015552,0.017207,0.014387,0.015304,0.015317,0.015547 +HashTable,случайный,insert,0.014448,0.014376,0.014608,0.015115,0.013931,0.014212 +HashTable,случайный,find,0.000161,0.000162,0.000161,0.000161,0.000158,0.000161 +HashTable,случайный,delete,0.000088,0.000089,0.000089,0.000088,0.000086,0.000086 +BST,случайный,insert,0.015575,0.015822,0.015653,0.015507,0.015398,0.015496 +BST,случайный,find,0.000133,0.000135,0.000133,0.000131,0.000133,0.000133 +BST,случайный,delete,0.000100,0.000104,0.000100,0.000100,0.000099,0.000099 +LinkedList,сортированный,insert,1.890415,1.937600,1.916341,1.863388,1.872563,1.862181 +LinkedList,сортированный,find,0.023136,0.030373,0.021794,0.021670,0.020861,0.020980 +LinkedList,сортированный,delete,0.014986,0.017694,0.014155,0.014365,0.014293,0.014424 +HashTable,сортированный,insert,0.017723,0.015734,0.019191,0.019721,0.015843,0.018128 +HashTable,сортированный,find,0.000223,0.000263,0.000206,0.000226,0.000154,0.000264 +HashTable,сортированный,delete,0.000125,0.000144,0.000112,0.000132,0.000094,0.000141 +BST,сортированный,insert,3.535999,3.577140,3.570114,3.581931,3.485064,3.465746 +BST,сортированный,find,0.030666,0.030318,0.033654,0.030108,0.029537,0.029713 +BST,сортированный,delete,0.038312,0.037349,0.040109,0.037735,0.036555,0.039810 diff --git a/shalovsa/lab1/docs/data/sorted_vs_random.png b/shalovsa/lab1/docs/data/sorted_vs_random.png new file mode 100644 index 0000000..7180472 Binary files /dev/null and b/shalovsa/lab1/docs/data/sorted_vs_random.png differ diff --git a/shalovsa/lab1/docs/report.md b/shalovsa/lab1/docs/report.md new file mode 100644 index 0000000..10429a4 --- /dev/null +++ b/shalovsa/lab1/docs/report.md @@ -0,0 +1,130 @@ +# Отчёт: Задание 1 — Структуры данных + +## Цель работы + +Разработать три структуры данных «с нуля» в процедурном стиле (без ООП), применить их для хранения записей телефонной книги и провести экспериментальное сравнение производительности ключевых операций. + +**Структуры данных:** +- Связный список (LinkedList) +- Хеш-таблица (HashTable) +- Двоичное дерево поиска (BST) + +--- + +## Реализация + +### Основные технические решения + +#### 1. Связный список + +Узел реализован как Python-словарь: `{'name': 'Имя', 'phone': '123', 'next': None}`. + +Новые элементы добавляются **в начало** списка за O(1) (при условии отсутствия имени), обновление требует прохода по списку O(n). Поиск и удаление работают за линейное время из-за отсутствия прямого доступа по индексу. + +#### 2. Хеш-таблица + +Фиксированный массив на 256 корзин. Каждая корзина — указатель на связный список (метод цепочек). Хеш-функция: стандартный `hash(name) % size`. Среднее время операций O(1), при коллизиях — O(k), где k — длина цепочки. + +#### 3. Двоичное дерево поиска (BST) + +Узел: `{'name': 'Имя', 'phone': '123', 'left': None, 'right': None}`. Сравнение ключей — лексикографическое по полю name. Вставка и поиск реализованы итеративно. Удаление — рекурсивное с заменой на минимальный узел правого поддерева. Обход в глубину даёт отсортированный список. + +--- + +## Экспериментальная часть + +### Условия проведения замеров + +| Параметр | Значение | +|---|---| +| Количество записей (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 раз) +- Поиск: ~0.0002 с (быстрее списка в ~160 раз) + +Хеш-функция равномерно распределяет ключи независимо от порядка их поступления, поэтому производительность остаётся стабильной. + +### 3. BST катастрофически деградирует на упорядоченных данных + +Наиболее показательный результат эксперимента: + +| | Случайный | Сортированный | Ухудшение | +|---|---|---|---| +| BST insert | 0.017 с | **3.854 с** | **×225** | +| BST find | 0.000145 с | **0.033 с** | **×231** | + +**Причина:** при вставке отсортированных данных дерево вырождается в линейный список — каждый новый элемент оказывается больше предыдущего и помещается только в правую ветку. Высота дерева становится O(n) вместо O(log n), что превращает все операции в линейные. + +### 4. Операция delete + +На случайных данных BST удаляет за **~0.0001 с** (логарифмическая сложность). На сортированных — **~0.046 с** (линейная деградация). HashTable показывает стабильные **~0.00012 с** независимо от порядка. + +--- + +## Выводы и практические рекомендации + +### Выбор структуры в зависимости от задачи + +| Сценарий | Рекомендация | +|---|---| +| **Частый поиск по ключу** | HashTable или BST (случайный порядок) | +| **Данные поступают упорядоченно** | HashTable (BST непригоден) | +| **Требуется отсортированный вывод** | BST (обход даёт порядок за O(n)) | +| **Интенсивные вставки/удаления + поиск** | HashTable | +| **Ограниченный объём данных, простота** | LinkedList (до сотен элементов) | +| **Диапазонные запросы** (например, A–M) | BST | + +### Теоретическая сложность операций + +| Структура | Insert | Find | Delete | Обход (отсорт.) | +|---|---|---|---|---| +| LinkedList | O(n) | O(n) | O(n) | O(n log n) | +| HashTable | O(1) в среднем | O(1) в среднем | O(1) в среднем | 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) | + +### Ключевой вывод + +Для телефонного справочника с частыми поисками и обновлениями оптимальный выбор — **хеш-таблица**. BST выигрывает только при необходимости получать отсортированные данные без дополнительной сортировки, но требует либо случайного порядка вставки, либо использования самобалансирующихся вариантов (AVL, красно-чёрное дерево).