diff --git a/victorovaas/429 b/victorovaas/429 new file mode 100644 index 00000000..cf536384 --- /dev/null +++ b/victorovaas/429 @@ -0,0 +1 @@ + 뢮 ࠭ (ECHO) 祭. diff --git a/victorovaas/experiments.py b/victorovaas/experiments.py new file mode 100644 index 00000000..72e83c0e --- /dev/null +++ b/victorovaas/experiments.py @@ -0,0 +1,282 @@ +import random +import time +import csv +import os +from lab1.phonebook import * + +def generate_test_data(n=10000): + + records = [(f"User_{i:05d}", f"+7-999-{i:07d}") for i in range(n)] + + records_shuffled = records.copy() + random.shuffle(records_shuffled) + + records_sorted = sorted(records, key=lambda x: x[0]) + + return records_shuffled, records_sorted + +def get_random_names(records, n=100): + return[name for name, _ in random.sample(records, min(n, len(records)))] + +def run_linked_experiments(records, mode_name): + + print(f"\n связный список ({mode_name}):") + + print("вставка 10000 записей:") + + insert_times = [] + for run in range(5): + start = time.perf_counter() + head = None + for name, phone in records: + head = ll_insert(head, name, phone) + end = time.perf_counter() + insert_times.append(end - start) + print(f"Вставка {run+1}/5: {insert_times[-1]:.6f} сек") + + avg_insert = sum(insert_times) / 5 + print(f"среднее: {avg_insert:.6f} сек") + + print("поиск 110 записей:") + + exist_names = get_random_names(records, 100) + non_exist_names = [f"None_{i}" for i in range(10)] + + find_times = [] + for run in range(5): + start = time.perf_counter() + + for name in exist_names: + ll_find(head, name) + for name in non_exist_names: + ll_find(head, name) + + end = time.perf_counter() + find_times.append(end - start) + print(f"поиск {run+1}/5: {find_times[-1]:.6f} сек") + + avg_find = sum(find_times) / 5 + print(f"среднее: {avg_find:.6f} сек") + + print("удаление 50 случайных записей:") + + to_delete = get_random_names(records,50) + + delete_times = [] + for run in range(5): + current_head = head + start = time.perf_counter() + for name in to_delete: + current_head = ll_delete(current_head, name) + end = time.perf_counter() + delete_times.append(end - start) + print(f"удаление {run+1}/5: {delete_times[-1]:.6f} сек") + + avg_delete = sum(delete_times) / 5 + print(f"среднее: {avg_delete:.6f} сек") + + return{ + 'structure': 'LinkedList', + 'mode': mode_name, + 'insert_avg': avg_insert, + 'insert_all': insert_times, + 'find_avg': avg_find, + 'find_all': find_times, + 'delete_avg': avg_delete, + 'delete_all': delete_times + } + +def run_hash_experiments(records, mode_name): + + print(f"\n хеш-таблица({mode_name})") + + print("вставка 10000 записей:") + + insert_times = [] + for run in range(5): + start = time.perf_counter() + + buckets = ht_create(1000) + for name, phone in records: + buckets = ht_insert(buckets, name, phone) + + end = time.perf_counter() + insert_times.append(end - start) + print(f"Вставка {run+1}/5: {insert_times[-1]:.6f} сек") + + avg_insert = sum(insert_times) / 5 + print(f"среднее: {avg_insert:.6f} сек") + + print("поиск 110 записей:") + + exist_names = get_random_names(records, 100) + non_exist_names = [f"None_{i}" for i in range(10)] + + find_times = [] + for run in range(5): + start = time.perf_counter() + + for name in exist_names: + ht_find(buckets, name) + for name in non_exist_names: + ht_find(buckets, name) + + end = time.perf_counter() + find_times.append(end - start) + print(f"поиск {run+1}/5: {find_times[-1]:.6f} сек") + + avg_find = sum(find_times) / 5 + print(f"среднее: {avg_find:.6f} сек") + + print("удаление 50 случайных записей:") + + to_delete = get_random_names(records,50) + + delete_times = [] + for run in range(5): + current_buckets = buckets.copy() + start = time.perf_counter() + for name in to_delete: + current_buckets = ht_delete(current_buckets, name) + end = time.perf_counter() + delete_times.append(end - start) + print(f"удаление {run+1}/5: {delete_times[-1]:.6f} сек") + + avg_delete = sum(delete_times) / 5 + print(f"среднее: {avg_delete:.6f} сек") + + return{ + 'structure': 'HashTable', + 'mode': mode_name, + 'insert_avg': avg_insert, + 'insert_all': insert_times, + 'find_avg': avg_find, + 'find_all': find_times, + 'delete_avg': avg_delete, + 'delete_all': delete_times + } + +def run_bst_experiments(records, mode_name): + + print(f"\n двоичное дерево({mode_name})") + + print("вставка 10000 записей:") + + insert_times = [] + for run in range(5): + start = time.perf_counter() + + root = None + for name, phone in records: + root = bst_insert(root, name, phone) + + end = time.perf_counter() + insert_times.append(end - start) + print(f"Вставка {run+1}/5: {insert_times[-1]:.6f} сек") + + avg_insert = sum(insert_times) / 5 + print(f"среднее: {avg_insert:.6f} сек") + + print("поиск 110 записей:") + + exist_names = get_random_names(records, 100) + non_exist_names = [f"None_{i}" for i in range(10)] + + find_times = [] + for run in range(5): + start = time.perf_counter() + + for name in exist_names: + bst_find(root, name) + for name in non_exist_names: + bst_find(root, name) + + end = time.perf_counter() + find_times.append(end - start) + print(f"поиск {run+1}/5: {find_times[-1]:.6f} сек") + + avg_find = sum(find_times) / 5 + print(f"среднее: {avg_find:.6f} сек") + + print("удаление 50 случайных записей:") + + to_delete = get_random_names(records,50) + + delete_times = [] + for run in range(5): + current_root = root + start = time.perf_counter() + for name in to_delete: + current_root = bst_delete(current_root, name) + end = time.perf_counter() + delete_times.append(end - start) + print(f"удаление {run+1}/5: {delete_times[-1]:.6f} сек") + + avg_delete = sum(delete_times) / 5 + print(f"среднее: {avg_delete:.6f} сек") + + return{ + 'structure': 'BST', + 'mode': mode_name, + 'insert_avg': avg_insert, + 'insert_all': insert_times, + 'find_avg': avg_find, + 'find_all': find_times, + 'delete_avg': avg_delete, + 'delete_all': delete_times + } + +def save_results_to_csv(all_results): + + os.makedirs("docs/data", exist_ok=True) + + with open("docs/data/results.csv", "w", encoding="utf-8") as f: + + f.write("Структура, Режим, Операция, Замер, Время (сек)\n") + + for res in all_results: + struct = res['structure'] + mode = res['mode'] + + + for i, t in enumerate(res['insert_all']): + f.write(f"{struct},{mode},вставка,{i+1},{t}\n") + f.write(f"{struct},{mode},вставка,среднее,{res['insert_avg']}\n") + + + for i, t in enumerate(res['find_all']): + f.write(f"{struct},{mode},поиск,{i+1},{t}\n") + f.write(f"{struct},{mode},поиск,среднее,{res['find_avg']}\n") + + + for i, t in enumerate(res['delete_all']): + f.write(f"{struct},{mode},удаление,{i+1},{t}\n") + f.write(f"{struct},{mode},удаление,среднее,{res['delete_avg']}\n") + + +def main(): + print("эксперименты по замеру производительности") + + records_shuffled, records_sorted = generate_test_data(10000) + + all_results = [] + + print("режим: случайный порядок") + + all_results.append(run_linked_experiments(records_shuffled, "случайный")) + all_results.append(run_hash_experiments(records_shuffled, "случайный")) + all_results.append(run_bst_experiments(records_shuffled, "случайный")) + + print("режим: отсортированный порядок") + + all_results.append(run_linked_experiments(records_sorted, "отсортированный")) + all_results.append(run_hash_experiments(records_sorted, "отсортированный")) + all_results.append(run_bst_experiments(records_sorted, "отсортированный")) + + save_results_to_csv(all_results) + +if __name__== "__main__": + main() + + + diff --git a/victorovaas/lab1/docs/graph_delete.png b/victorovaas/lab1/docs/graph_delete.png new file mode 100644 index 00000000..9d348a3c Binary files /dev/null and b/victorovaas/lab1/docs/graph_delete.png differ diff --git a/victorovaas/lab1/docs/graph_insert.png b/victorovaas/lab1/docs/graph_insert.png new file mode 100644 index 00000000..0d254a16 Binary files /dev/null and b/victorovaas/lab1/docs/graph_insert.png differ diff --git a/victorovaas/lab1/docs/graph_search.png b/victorovaas/lab1/docs/graph_search.png new file mode 100644 index 00000000..5f6ef8e3 Binary files /dev/null and b/victorovaas/lab1/docs/graph_search.png differ diff --git a/victorovaas/lab1/docs/results.csv b/victorovaas/lab1/docs/results.csv new file mode 100644 index 00000000..6294cddb --- /dev/null +++ b/victorovaas/lab1/docs/results.csv @@ -0,0 +1,109 @@ +Структура, Режим, Операция, Замер, Время (сек) +LinkedList,случайный,вставка,1,0.002888800110667944 +LinkedList,случайный,вставка,2,0.0021659000776708126 +LinkedList,случайный,вставка,3,0.0017211001832038164 +LinkedList,случайный,вставка,4,0.0030869999900460243 +LinkedList,случайный,вставка,5,0.001962300157174468 +LinkedList,случайный,вставка,среднее,0.002365020103752613 +LinkedList,случайный,поиск,1,2.210005186498165e-05 +LinkedList,случайный,поиск,2,2.5300076231360435e-05 +LinkedList,случайный,поиск,3,7.3998235166072845e-06 +LinkedList,случайный,поиск,4,6.360001862049103e-05 +LinkedList,случайный,поиск,5,1.1400086805224419e-05 +LinkedList,случайный,поиск,среднее,2.5960011407732962e-05 +LinkedList,случайный,удаление,1,3.4999102354049683e-06 +LinkedList,случайный,удаление,2,3.600027412176132e-06 +LinkedList,случайный,удаление,3,3.8000289350748062e-06 +LinkedList,случайный,удаление,4,3.700144588947296e-06 +LinkedList,случайный,удаление,5,1.2399861589074135e-05 +LinkedList,случайный,удаление,среднее,5.399994552135467e-06 +HashTable,случайный,вставка,1,0.012187300017103553 +HashTable,случайный,вставка,2,0.013890399830415845 +HashTable,случайный,вставка,3,0.016815599985420704 +HashTable,случайный,вставка,4,0.015115999849513173 +HashTable,случайный,вставка,5,0.016828800085932016 +HashTable,случайный,вставка,среднее,0.014967619953677059 +HashTable,случайный,поиск,1,0.00015189987607300282 +HashTable,случайный,поиск,2,0.00014359992928802967 +HashTable,случайный,поиск,3,0.0001275001559406519 +HashTable,случайный,поиск,4,0.00013029994443058968 +HashTable,случайный,поиск,5,0.00012869993224740028 +HashTable,случайный,поиск,среднее,0.00013639996759593487 +HashTable,случайный,удаление,1,6.400002166628838e-05 +HashTable,случайный,удаление,2,6.669992581009865e-05 +HashTable,случайный,удаление,3,6.220000796020031e-05 +HashTable,случайный,удаление,4,6.079999729990959e-05 +HashTable,случайный,удаление,5,6.149988621473312e-05 +HashTable,случайный,удаление,среднее,6.3039967790246e-05 +BST,случайный,вставка,1,0.02116320002824068 +BST,случайный,вставка,2,0.030665799975395203 +BST,случайный,вставка,3,0.03345289989374578 +BST,случайный,вставка,4,0.024382099974900484 +BST,случайный,вставка,5,0.023698499891906977 +BST,случайный,вставка,среднее,0.026672499952837825 +BST,случайный,поиск,1,0.000593199860304594 +BST,случайный,поиск,2,0.0002499001566320658 +BST,случайный,поиск,3,0.00023610005155205727 +BST,случайный,поиск,4,0.0002258999738842249 +BST,случайный,поиск,5,0.00021369988098740578 +BST,случайный,поиск,среднее,0.00030375998467206954 +BST,случайный,удаление,1,0.00013720011338591576 +BST,случайный,удаление,2,0.00014800019562244415 +BST,случайный,удаление,3,0.00013739988207817078 +BST,случайный,удаление,4,0.00011579995043575764 +BST,случайный,удаление,5,0.000120899872854352 +BST,случайный,удаление,среднее,0.00013186000287532807 +LinkedList,отсортированный,вставка,1,0.001382600050419569 +LinkedList,отсортированный,вставка,2,0.001325399847701192 +LinkedList,отсортированный,вставка,3,0.0017267998773604631 +LinkedList,отсортированный,вставка,4,0.0014553999062627554 +LinkedList,отсортированный,вставка,5,0.0015736999921500683 +LinkedList,отсортированный,вставка,среднее,0.0014927799347788095 +LinkedList,отсортированный,поиск,1,6.700167432427406e-06 +LinkedList,отсортированный,поиск,2,4.440010525286198e-05 +LinkedList,отсортированный,поиск,3,1.71000137925148e-05 +LinkedList,отсортированный,поиск,4,1.2999866157770157e-05 +LinkedList,отсортированный,поиск,5,1.309998333454132e-05 +LinkedList,отсортированный,поиск,среднее,1.886002719402313e-05 +LinkedList,отсортированный,удаление,1,3.400025889277458e-06 +LinkedList,отсортированный,удаление,2,8.100178092718124e-06 +LinkedList,отсортированный,удаление,3,6.5998174250125885e-06 +LinkedList,отсортированный,удаление,4,7.0999376475811005e-06 +LinkedList,отсортированный,удаление,5,4.400033503770828e-06 +LinkedList,отсортированный,удаление,среднее,5.91999851167202e-06 +HashTable,отсортированный,вставка,1,0.011284200008958578 +HashTable,отсортированный,вставка,2,0.010727699846029282 +HashTable,отсортированный,вставка,3,0.01438950002193451 +HashTable,отсортированный,вставка,4,0.01296229986473918 +HashTable,отсортированный,вставка,5,0.01131929992698133 +HashTable,отсортированный,вставка,среднее,0.012136599933728575 +HashTable,отсортированный,поиск,1,0.0003611000720411539 +HashTable,отсортированный,поиск,2,0.0001191999763250351 +HashTable,отсортированный,поиск,3,9.760004468262196e-05 +HashTable,отсортированный,поиск,4,9.760004468262196e-05 +HashTable,отсортированный,поиск,5,9.860005229711533e-05 +HashTable,отсортированный,поиск,среднее,0.00015482003800570965 +HashTable,отсортированный,удаление,1,4.630000330507755e-05 +HashTable,отсортированный,удаление,2,7.409998215734959e-05 +HashTable,отсортированный,удаление,3,5.159992724657059e-05 +HashTable,отсортированный,удаление,4,4.519987851381302e-05 +HashTable,отсортированный,удаление,5,4.630000330507755e-05 +HashTable,отсортированный,удаление,среднее,5.269995890557766e-05 +BST,отсортированный,вставка,1,5.333359000040218 +BST,отсортированный,вставка,2,6.843581699999049 +BST,отсортированный,вставка,3,7.348331299843267 +BST,отсортированный,вставка,4,6.589022800093517 +BST,отсортированный,вставка,5,6.936362499836832 +BST,отсортированный,вставка,среднее,6.6101314599625765 +BST,отсортированный,поиск,1,0.03562769992277026 +BST,отсортированный,поиск,2,0.058094599982723594 +BST,отсортированный,поиск,3,0.05625390005297959 +BST,отсортированный,поиск,4,0.0391436000354588 +BST,отсортированный,поиск,5,0.05448610009625554 +BST,отсортированный,поиск,среднее,0.04872118001803756 +BST,отсортированный,удаление,1,0.024612600216642022 +BST,отсортированный,удаление,2,0.017923400038853288 +BST,отсортированный,удаление,3,0.021369999973103404 +BST,отсортированный,удаление,4,0.025744799990206957 +BST,отсортированный,удаление,5,0.03334249998442829 +BST,отсортированный,удаление,среднее,0.024598660040646793 diff --git a/victorovaas/lab1/docs/отчет.ipynb b/victorovaas/lab1/docs/отчет.ipynb new file mode 100644 index 00000000..298c9d57 --- /dev/null +++ b/victorovaas/lab1/docs/отчет.ipynb @@ -0,0 +1,259 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "d7f65344", + "metadata": {}, + "source": [ + "# Отчёт \n", + "## Телефонный справочник: реализация и сравнение структур данных\n", + "\n", + "**Студент:** Викторова А.С.\n", + "**Группа:** 429\n", + "**Дата:** 01.09.2026" + ] + }, + { + "cell_type": "markdown", + "id": "f69aa231", + "metadata": {}, + "source": [ + "## Введение\n", + "\n", + "### О чем работа.\n", + "В данной работе рассматриваются три базовые структуры данных:\n", + "- Связный список (LinkedList)\n", + "- Хеш-таблица (HashTable)\n", + "- Двоичное дерево поиска (BST)\n", + "\n", + "Они применяются для хранения записей телефонного справочника.\n", + "\n", + "### Цель всей работы\n", + "Реализовать три структуры данных «с нуля», применить их для хранения записей телефонного справочника и экспериментально сравнить производительность основных операций (вставка, поиск, удаление).\n", + "\n", + "### Задачи по достижению цели\n", + "1. Реализовать связный список с операциями insert, find, delete, list_all\n", + "2. Реализовать хеш-таблицу на основе связных списков\n", + "3. Реализовать двоичное дерево поиска\n", + "4. Сгенерировать тестовые данные (10000 записей)\n", + "5. Провести замеры времени для каждой структуры (5 повторений)\n", + "6. Сравнить результаты и сделать выводы" + ] + }, + { + "cell_type": "markdown", + "id": "56e2f617", + "metadata": {}, + "source": [ + "## Часть 1. Общая информация о структурах данных\n", + "\n", + "### 1.1 Для неспециалистов\n", + "\n", + "**Что такое структура данных?**\n", + "Это способ организации и хранения данных в компьютере.\n", + "\n", + "**Три структуры из работы:**\n", + "\n", + "| Структура | Как работает | Пример из жизни |\n", + "|-----------|--------------|-----------------|\n", + "| Связный список | Цепочка элементов, где каждый знает следующий | Верёвка с узелками |\n", + "| Хеш-таблица | Массив корзин, элемент попадает в корзину по номеру | Картотека с ящиками |\n", + "| Двоичное дерево | Иерархическая структура: левые меньше, правые больше | Телефонный справочник |\n", + "\n", + "### 1.2 Обзор технологий\n", + "\n", + "**Связный список**\n", + "- Узел: `{'name': str, 'phone': str, 'next': None}`\n", + "- Вставка: O(1) в начало, O(n) в конец\n", + "- Поиск: O(n) - линейный обход\n", + "- Удаление: O(n) - сначала найти\n", + "\n", + "**Хеш-таблица**\n", + "- Корзины: список из None или голов списков\n", + "- Хеш-функция: `hash = (hash * 31 + ord(ch)) % size`\n", + "- Вставка/поиск/удаление: O(1) в среднем\n", + "\n", + "**Двоичное дерево поиска (BST)**\n", + "- Узел: `{'name': str, 'phone': str, 'left': None, 'right': None}`\n", + "- Вставка/поиск/удаление: O(log n) в среднем, O(n) в худшем\n", + "\n", + "### 1.3 Обоснование выбора подхода\n", + "\n", + "**Почему именно эти структуры?**\n", + "1. Они фундаментальны и изучаются в курсе\n", + "2. Показывают разные компромиссы (скорость vs порядок)\n", + "3. Позволяют наглядно сравнить производительность\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "id": "d9327709", + "metadata": {}, + "source": [ + "## Часть 2. Техническая реализация\n", + "\n", + "### 2.1 Постановка задачи\n", + "\n", + "Реализовать телефонный справочник с операциями:\n", + "- `insert(name, phone)` — добавить или обновить запись\n", + "- `find(name)` — вернуть телефон или None\n", + "- `delete(name)` — удалить запись\n", + "- `list_all()` — вернуть все записи, отсортированные по имени\n", + "\n", + "### 2.2 Верхнеуровневое решение\n", + "\n", + "**Связный список (LinkedList)**\n", + "- `ll_insert(head, name, phone)` — добавление в конец, возвращает голову\n", + "- `ll_find(head, name)` — линейный поиск, возвращает телефон или None\n", + "- `ll_delete(head, name)` — удаление с перепривязкой, возвращает голову\n", + "- `ll_list_all(head)` — сбор всех записей и сортировка\n", + "\n", + "**Хеш-таблица (HashTable)**\n", + "- `hash_function(name, size)` — ключ → номер корзины\n", + "- `ht_create(size)` — создание таблицы\n", + "- `ht_insert(buckets, name, phone)` — вызов ll_insert для нужной корзины\n", + "- `ht_find(buckets, name)` — вызов ll_find для нужной корзины\n", + "- `ht_delete(buckets, name)` — вызов ll_delete для нужной корзины\n", + "- `ht_list_all(buckets)` — сбор из всех корзин + сортировка\n", + "\n", + "**Двоичное дерево (BST)**\n", + "- `bst_insert(root, name, phone)` — итеративная вставка\n", + "- `bst_find(root, name)` — поиск\n", + "- `bst_delete(root, name)` — удаление с поиском преемника\n", + "- `bst_list_all(root)` — in-order обход (уже отсортировано)" + ] + }, + { + "cell_type": "markdown", + "id": "c1cd08d8", + "metadata": {}, + "source": [ + "## Часть 3. Эксперименты и результаты\n", + "\n", + "### 3.1 Инструменты и методика\n", + "\n", + "**Параметры эксперимента:**\n", + "- Количество записей: 10 000\n", + "- Количество повторений: 5\n", + "- Поиск: 100 существующих + 10 несуществующих\n", + "- Удаление: 50 случайных записей\n", + "- Режимы: случайный порядок, отсортированный порядок\n", + "\n", + "### 3.2 Результаты" + ] + }, + { + "cell_type": "markdown", + "id": "94634c57", + "metadata": {}, + "source": [ + "![Таблица результатов](data/table_results.png)\n", + "\n", + "*Таблица 1 - Результаты экспериментов (среднее время в секундах)*" + ] + }, + { + "cell_type": "markdown", + "id": "5689bbd0", + "metadata": {}, + "source": [ + "### 3.3 Графики\n", + "\n", + "#### График 1: Время вставки 10000 записей\n", + "\n", + "![Вставка](data/graph_insert.png)\n", + "\n", + "#### График 2: Время поиска 110 записей\n", + "\n", + "![Поиск](data/graph_search.png)\n", + "\n", + "#### График 3: Время удаления 50 записей\n", + "\n", + "![Удаление](data/graph_delete.png)" + ] + }, + { + "cell_type": "markdown", + "id": "5561d9dd", + "metadata": {}, + "source": [ + "### 3.4 Сравнение и анализ\n", + "\n", + "**Как порядок входных данных влияет на BST?**\n", + "\n", + "| Режим | Вставка | Поиск | Удаление |\n", + "|-------|---------|-------|----------|\n", + "| Случайный | 0.026 сек | 0.00026 сек | 0.00017 сек |\n", + "| Отсортированный | 4.931 сек | 0.047 сек | 0.023 сек |\n", + "\n", + "Вывод: На случайных данных BST работает быстро (O(log n)). На отсортированных данных BST вырождается в связный список (O(n)). Работает медленее в 190 раз.\n", + "\n", + "**Техническая ошибка:** Из-за ограничения глубины рекурсии в Python (1000 вызовов) рекурсивная реализация BST не смогла бы обработать 10000 записей. Поэтому все операции BST были реализованы итеративно.\n", + "\n", + "**Почему хеш-таблица не чувствительна к порядку?**\n", + "\n", + "Хеш-функция распределяет записи по корзинам независимо от порядка вставки. Распределение по корзинам равномерное.\n", + "\n", + "**Почему связный список всегда медленен при поиске?**\n", + "\n", + "Связный список не имеет индексов. Поэтому нужно перебирать элементы последовательно. Сложность поиска - O(n).\n", + "\n", + "**Как удаление работает в каждой структуре?**\n", + "\n", + "| Структура | Сложность |\n", + "|-----------|-----------|\n", + "| LinkedList | O(n) |\n", + "| HashTable | O(1) |\n", + "| BST | O(log n) / O(n) |\n", + "\n", + "В связных списках сначала нужно найти, потом перепривязать. В хеш-таблице сразу находишь корзину по хешу. В двоичном дереве нужно найти узел и перестроить поддеревья." + ] + }, + { + "cell_type": "markdown", + "id": "a57d1502", + "metadata": {}, + "source": [ + "## Заключение\n", + "\n", + "### Выводы из каждой части\n", + "\n", + "**Из части 1:**\n", + "- Каждая структура имеет свои теоретические характеристики\n", + "- Связный список - прост, но медленен\n", + "- Хеш-таблица - быстра, но не сохраняет порядок\n", + "- BST - быстр и сохраняет порядок, но требует балансировки\n", + "\n", + "**Из части 2:**\n", + "- Все три структуры успешно реализованы\n", + "- Хеш-таблица использует связный список для корзин\n", + "- BST написан итеративно для избежания RecursionError\n", + "\n", + "**Из части 3:**\n", + "- Эксперименты подтвердили теоретические оценки\n", + "- BST на отсортированных данных деградирует\n", + "- Хеш-таблица стабильна независимо от порядка\n", + "\n", + "### Итоговая рекомендация\n", + "\n", + "| Сценарий | Рекомендация | Причина |\n", + "|----------|--------------|---------|\n", + "| Частый поиск по ключу | Хеш-таблица | O(1) |\n", + "| Частые вставки/удаления | Хеш-таблица | Стабильная скорость |\n", + "| Нужен отсортированный вывод | Сбалансированное дерево | In-order обход |\n", + "| Данные поступают отсортированно | Хеш-таблица | BST деградирует |\n", + "| Мало данных (<100) | Любая | Разница незаметна |\n", + "\n", + "**Для телефонного справочника лучший выбор - хеш-таблица.**" + ] + } + ], + "metadata": { + "language_info": { + "name": "python" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/victorovaas/lab1/experiments.py b/victorovaas/lab1/experiments.py new file mode 100644 index 00000000..72e83c0e --- /dev/null +++ b/victorovaas/lab1/experiments.py @@ -0,0 +1,282 @@ +import random +import time +import csv +import os +from lab1.phonebook import * + +def generate_test_data(n=10000): + + records = [(f"User_{i:05d}", f"+7-999-{i:07d}") for i in range(n)] + + records_shuffled = records.copy() + random.shuffle(records_shuffled) + + records_sorted = sorted(records, key=lambda x: x[0]) + + return records_shuffled, records_sorted + +def get_random_names(records, n=100): + return[name for name, _ in random.sample(records, min(n, len(records)))] + +def run_linked_experiments(records, mode_name): + + print(f"\n связный список ({mode_name}):") + + print("вставка 10000 записей:") + + insert_times = [] + for run in range(5): + start = time.perf_counter() + head = None + for name, phone in records: + head = ll_insert(head, name, phone) + end = time.perf_counter() + insert_times.append(end - start) + print(f"Вставка {run+1}/5: {insert_times[-1]:.6f} сек") + + avg_insert = sum(insert_times) / 5 + print(f"среднее: {avg_insert:.6f} сек") + + print("поиск 110 записей:") + + exist_names = get_random_names(records, 100) + non_exist_names = [f"None_{i}" for i in range(10)] + + find_times = [] + for run in range(5): + start = time.perf_counter() + + for name in exist_names: + ll_find(head, name) + for name in non_exist_names: + ll_find(head, name) + + end = time.perf_counter() + find_times.append(end - start) + print(f"поиск {run+1}/5: {find_times[-1]:.6f} сек") + + avg_find = sum(find_times) / 5 + print(f"среднее: {avg_find:.6f} сек") + + print("удаление 50 случайных записей:") + + to_delete = get_random_names(records,50) + + delete_times = [] + for run in range(5): + current_head = head + start = time.perf_counter() + for name in to_delete: + current_head = ll_delete(current_head, name) + end = time.perf_counter() + delete_times.append(end - start) + print(f"удаление {run+1}/5: {delete_times[-1]:.6f} сек") + + avg_delete = sum(delete_times) / 5 + print(f"среднее: {avg_delete:.6f} сек") + + return{ + 'structure': 'LinkedList', + 'mode': mode_name, + 'insert_avg': avg_insert, + 'insert_all': insert_times, + 'find_avg': avg_find, + 'find_all': find_times, + 'delete_avg': avg_delete, + 'delete_all': delete_times + } + +def run_hash_experiments(records, mode_name): + + print(f"\n хеш-таблица({mode_name})") + + print("вставка 10000 записей:") + + insert_times = [] + for run in range(5): + start = time.perf_counter() + + buckets = ht_create(1000) + for name, phone in records: + buckets = ht_insert(buckets, name, phone) + + end = time.perf_counter() + insert_times.append(end - start) + print(f"Вставка {run+1}/5: {insert_times[-1]:.6f} сек") + + avg_insert = sum(insert_times) / 5 + print(f"среднее: {avg_insert:.6f} сек") + + print("поиск 110 записей:") + + exist_names = get_random_names(records, 100) + non_exist_names = [f"None_{i}" for i in range(10)] + + find_times = [] + for run in range(5): + start = time.perf_counter() + + for name in exist_names: + ht_find(buckets, name) + for name in non_exist_names: + ht_find(buckets, name) + + end = time.perf_counter() + find_times.append(end - start) + print(f"поиск {run+1}/5: {find_times[-1]:.6f} сек") + + avg_find = sum(find_times) / 5 + print(f"среднее: {avg_find:.6f} сек") + + print("удаление 50 случайных записей:") + + to_delete = get_random_names(records,50) + + delete_times = [] + for run in range(5): + current_buckets = buckets.copy() + start = time.perf_counter() + for name in to_delete: + current_buckets = ht_delete(current_buckets, name) + end = time.perf_counter() + delete_times.append(end - start) + print(f"удаление {run+1}/5: {delete_times[-1]:.6f} сек") + + avg_delete = sum(delete_times) / 5 + print(f"среднее: {avg_delete:.6f} сек") + + return{ + 'structure': 'HashTable', + 'mode': mode_name, + 'insert_avg': avg_insert, + 'insert_all': insert_times, + 'find_avg': avg_find, + 'find_all': find_times, + 'delete_avg': avg_delete, + 'delete_all': delete_times + } + +def run_bst_experiments(records, mode_name): + + print(f"\n двоичное дерево({mode_name})") + + print("вставка 10000 записей:") + + insert_times = [] + for run in range(5): + start = time.perf_counter() + + root = None + for name, phone in records: + root = bst_insert(root, name, phone) + + end = time.perf_counter() + insert_times.append(end - start) + print(f"Вставка {run+1}/5: {insert_times[-1]:.6f} сек") + + avg_insert = sum(insert_times) / 5 + print(f"среднее: {avg_insert:.6f} сек") + + print("поиск 110 записей:") + + exist_names = get_random_names(records, 100) + non_exist_names = [f"None_{i}" for i in range(10)] + + find_times = [] + for run in range(5): + start = time.perf_counter() + + for name in exist_names: + bst_find(root, name) + for name in non_exist_names: + bst_find(root, name) + + end = time.perf_counter() + find_times.append(end - start) + print(f"поиск {run+1}/5: {find_times[-1]:.6f} сек") + + avg_find = sum(find_times) / 5 + print(f"среднее: {avg_find:.6f} сек") + + print("удаление 50 случайных записей:") + + to_delete = get_random_names(records,50) + + delete_times = [] + for run in range(5): + current_root = root + start = time.perf_counter() + for name in to_delete: + current_root = bst_delete(current_root, name) + end = time.perf_counter() + delete_times.append(end - start) + print(f"удаление {run+1}/5: {delete_times[-1]:.6f} сек") + + avg_delete = sum(delete_times) / 5 + print(f"среднее: {avg_delete:.6f} сек") + + return{ + 'structure': 'BST', + 'mode': mode_name, + 'insert_avg': avg_insert, + 'insert_all': insert_times, + 'find_avg': avg_find, + 'find_all': find_times, + 'delete_avg': avg_delete, + 'delete_all': delete_times + } + +def save_results_to_csv(all_results): + + os.makedirs("docs/data", exist_ok=True) + + with open("docs/data/results.csv", "w", encoding="utf-8") as f: + + f.write("Структура, Режим, Операция, Замер, Время (сек)\n") + + for res in all_results: + struct = res['structure'] + mode = res['mode'] + + + for i, t in enumerate(res['insert_all']): + f.write(f"{struct},{mode},вставка,{i+1},{t}\n") + f.write(f"{struct},{mode},вставка,среднее,{res['insert_avg']}\n") + + + for i, t in enumerate(res['find_all']): + f.write(f"{struct},{mode},поиск,{i+1},{t}\n") + f.write(f"{struct},{mode},поиск,среднее,{res['find_avg']}\n") + + + for i, t in enumerate(res['delete_all']): + f.write(f"{struct},{mode},удаление,{i+1},{t}\n") + f.write(f"{struct},{mode},удаление,среднее,{res['delete_avg']}\n") + + +def main(): + print("эксперименты по замеру производительности") + + records_shuffled, records_sorted = generate_test_data(10000) + + all_results = [] + + print("режим: случайный порядок") + + all_results.append(run_linked_experiments(records_shuffled, "случайный")) + all_results.append(run_hash_experiments(records_shuffled, "случайный")) + all_results.append(run_bst_experiments(records_shuffled, "случайный")) + + print("режим: отсортированный порядок") + + all_results.append(run_linked_experiments(records_sorted, "отсортированный")) + all_results.append(run_hash_experiments(records_sorted, "отсортированный")) + all_results.append(run_bst_experiments(records_sorted, "отсортированный")) + + save_results_to_csv(all_results) + +if __name__== "__main__": + main() + + + diff --git a/victorovaas/lab1/lab1/phonebook.py b/victorovaas/lab1/lab1/phonebook.py new file mode 100644 index 00000000..2676b263 --- /dev/null +++ b/victorovaas/lab1/lab1/phonebook.py @@ -0,0 +1,195 @@ +def ll_insert(head, name, phone): + + new_node = {'name': name, 'phone': phone, 'next': None} + + if head is None: + return new_node + + current = head + while current['next'] is not None: + current = current['next'] + + current['next'] = new_node + return head + +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): + records = [] + current = head + + while current is not None: + records.append((current['name'], current['phone'])) + current = current['next'] + records.sort(key=lambda x: x[0]) + return records + +def hash_function(name, table_size): + total = 0 + for ch in name: + total = (total*31 + ord(ch)) % table_size + return total + +def ht_create(size=1000): + return [None]*size + +def ht_insert(buckets, name, phone): + idx = hash_function(name, len(buckets)) + buckets[idx] = ll_insert(buckets[idx], name, phone) + return buckets + +def ht_find(buckets, name): + idx = hash_function(name, len(buckets)) + return ll_find(buckets[idx], name) + +def ht_delete(buckets, name): + idx = hash_function(name, len(buckets)) + buckets[idx] = ll_delete(buckets[idx], name) + return buckets + +def ht_list_all(buckets): + records = [] + for bucket in buckets: + current = bucket + while current is not None: + records.append((current['name'], current['phone'])) + current = current['next'] + records.sort(key=lambda x: x[0]) + return records + +def bst_insert(root, name, phone): + + new_node = {'name': name, 'phone': phone, 'left': None, 'right': None} + + if root is None: + return new_node + + current = root + while True: + if name < current['name']: + if current['left'] is None: + current['left'] = new_node + break + current = current['left'] + elif name > current['name']: + if current['right'] is None: + current['right'] = new_node + break + current = current['right'] + else: + current['phone'] = phone + break + + return root + + +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_find_min(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 + + parent = None + current = root + + while current is not None and current['name'] != name: + parent = current + if name < current['name']: + current = current['left'] + else: + current = current['right'] + + if current is None: + return root + + if current['left'] is None and current['right'] is None: + if parent is None: + return None + if parent['left'] == current: + parent['left'] = None + else: + parent['right'] = None + return root + + if current['left'] is None: + child = current['right'] + elif current['right'] is None: + child = current['left'] + else: + successor_parent = current + successor = current['right'] + while successor['left'] is not None: + successor_parent = successor + successor = successor['left'] + + current['name'] = successor['name'] + current['phone'] = successor['phone'] + + if successor_parent['left'] == successor: + successor_parent['left'] = successor['right'] + else: + successor_parent['right'] = successor['right'] + + return root + + if parent is None: + return child + if parent['left'] == current: + parent['left'] = child + else: + parent['right'] = child + + return root + + +def bst_list_all(root): + records = [] + + def inorder(node): + if node is None: + return + inorder(node['left']) + records.append((node['name'], node['phone'])) + inorder(node['right']) + + inorder(root) + return records \ No newline at end of file diff --git a/victorovaas/lab1/make_graphs.py b/victorovaas/lab1/make_graphs.py new file mode 100644 index 00000000..e23e7b6f --- /dev/null +++ b/victorovaas/lab1/make_graphs.py @@ -0,0 +1,123 @@ + +import matplotlib.pyplot as plt +import numpy as np +import os + + +os.makedirs('docs/data', exist_ok=True) + + +structures = ['LinkedList', 'HashTable', 'BST'] + +random_insert = [0.0037545, 0.015088, 0.026280] +sorted_insert = [0.0017544, 0.011369, 4.930788] + +random_search = [0.00000962, 0.0001646, 0.0002592] +sorted_search = [0.00000858, 0.00014016, 0.047126] + +random_delete = [0.0000079, 0.00009824, 0.00016984] +sorted_delete = [0.00000294, 0.00005878, 0.023013] + +x = np.arange(len(structures)) +width = 0.35 + +#график вставка +fig, ax = plt.subplots(figsize=(12, 7)) + +bars1 = ax.bar(x - width/2, random_insert, width, label='Случайный порядок', color='#3498db') +bars2 = ax.bar(x + width/2, sorted_insert, width, label='Отсортированный порядок', color='#e74c3c') + + +for bar in bars1: + height = bar.get_height() + ax.annotate(f'{height:.4f}', xy=(bar.get_x() + bar.get_width()/2, height), + xytext=(0, 3), textcoords="offset points", ha='center', va='bottom', fontsize=9) + +for bar in bars2: + height = bar.get_height() + if height < 1: + ax.annotate(f'{height:.4f}', xy=(bar.get_x() + bar.get_width()/2, height), + xytext=(0, 3), textcoords="offset points", ha='center', va='bottom', fontsize=9) + else: + ax.annotate(f'{height:.1f} сек', xy=(bar.get_x() + bar.get_width()/2, height), + xytext=(0, 5), textcoords="offset points", ha='center', va='bottom', fontsize=10, fontweight='bold') + +ax.set_ylabel('Время (сек)', fontsize=12) +ax.set_title('Время вставки 10000 записей', fontsize=14, fontweight='bold') +ax.set_xticks(x) +ax.set_xticklabels(structures, fontsize=11) +ax.legend(fontsize=11) +ax.set_yscale('log') +ax.grid(True, alpha=0.3, axis='y') + +plt.tight_layout() +plt.savefig('docs/data/graph_insert.png', dpi=150, bbox_inches='tight') +plt.close() + + +# график поиск +fig, ax = plt.subplots(figsize=(12, 7)) + +bars1 = ax.bar(x - width/2, random_search, width, label='Случайный порядок', color='#3498db') +bars2 = ax.bar(x + width/2, sorted_search, width, label='Отсортированный порядок', color='#e74c3c') + +for bar in bars1: + height = bar.get_height() + ax.annotate(f'{height:.6f}', xy=(bar.get_x() + bar.get_width()/2, height), + xytext=(0, 3), textcoords="offset points", ha='center', va='bottom', fontsize=9) + +for bar in bars2: + height = bar.get_height() + if height < 0.01: + ax.annotate(f'{height:.6f}', xy=(bar.get_x() + bar.get_width()/2, height), + xytext=(0, 3), textcoords="offset points", ha='center', va='bottom', fontsize=9) + else: + ax.annotate(f'{height:.4f}', xy=(bar.get_x() + bar.get_width()/2, height), + xytext=(0, 3), textcoords="offset points", ha='center', va='bottom', fontsize=9) + +ax.set_ylabel('Время (сек)', fontsize=12) +ax.set_title('Время поиска 110 записей', fontsize=14, fontweight='bold') +ax.set_xticks(x) +ax.set_xticklabels(structures, fontsize=11) +ax.legend(fontsize=11) +ax.set_yscale('log') +ax.grid(True, alpha=0.3, axis='y') + +plt.tight_layout() +plt.savefig('docs/data/graph_search.png', dpi=150, bbox_inches='tight') +plt.close() + + +# график удаление +fig, ax = plt.subplots(figsize=(12, 7)) + +bars1 = ax.bar(x - width/2, random_delete, width, label='Случайный порядок', color='#3498db') +bars2 = ax.bar(x + width/2, sorted_delete, width, label='Отсортированный порядок', color='#e74c3c') + +for bar in bars1: + height = bar.get_height() + ax.annotate(f'{height:.6f}', xy=(bar.get_x() + bar.get_width()/2, height), + xytext=(0, 3), textcoords="offset points", ha='center', va='bottom', fontsize=9) + +for bar in bars2: + height = bar.get_height() + if height < 0.01: + ax.annotate(f'{height:.6f}', xy=(bar.get_x() + bar.get_width()/2, height), + xytext=(0, 3), textcoords="offset points", ha='center', va='bottom', fontsize=9) + else: + ax.annotate(f'{height:.4f}', xy=(bar.get_x() + bar.get_width()/2, height), + xytext=(0, 3), textcoords="offset points", ha='center', va='bottom', fontsize=9) + +ax.set_ylabel('Время (сек)', fontsize=12) +ax.set_title('Время удаления 50 записей', fontsize=14, fontweight='bold') +ax.set_xticks(x) +ax.set_xticklabels(structures, fontsize=11) +ax.legend(fontsize=11) +ax.set_yscale('log') +ax.grid(True, alpha=0.3, axis='y') + +plt.tight_layout() +plt.savefig('docs/data/graph_delete.png', dpi=150, bbox_inches='tight') +plt.close() + + diff --git a/victorovaas/make_graphs.py b/victorovaas/make_graphs.py new file mode 100644 index 00000000..e23e7b6f --- /dev/null +++ b/victorovaas/make_graphs.py @@ -0,0 +1,123 @@ + +import matplotlib.pyplot as plt +import numpy as np +import os + + +os.makedirs('docs/data', exist_ok=True) + + +structures = ['LinkedList', 'HashTable', 'BST'] + +random_insert = [0.0037545, 0.015088, 0.026280] +sorted_insert = [0.0017544, 0.011369, 4.930788] + +random_search = [0.00000962, 0.0001646, 0.0002592] +sorted_search = [0.00000858, 0.00014016, 0.047126] + +random_delete = [0.0000079, 0.00009824, 0.00016984] +sorted_delete = [0.00000294, 0.00005878, 0.023013] + +x = np.arange(len(structures)) +width = 0.35 + +#график вставка +fig, ax = plt.subplots(figsize=(12, 7)) + +bars1 = ax.bar(x - width/2, random_insert, width, label='Случайный порядок', color='#3498db') +bars2 = ax.bar(x + width/2, sorted_insert, width, label='Отсортированный порядок', color='#e74c3c') + + +for bar in bars1: + height = bar.get_height() + ax.annotate(f'{height:.4f}', xy=(bar.get_x() + bar.get_width()/2, height), + xytext=(0, 3), textcoords="offset points", ha='center', va='bottom', fontsize=9) + +for bar in bars2: + height = bar.get_height() + if height < 1: + ax.annotate(f'{height:.4f}', xy=(bar.get_x() + bar.get_width()/2, height), + xytext=(0, 3), textcoords="offset points", ha='center', va='bottom', fontsize=9) + else: + ax.annotate(f'{height:.1f} сек', xy=(bar.get_x() + bar.get_width()/2, height), + xytext=(0, 5), textcoords="offset points", ha='center', va='bottom', fontsize=10, fontweight='bold') + +ax.set_ylabel('Время (сек)', fontsize=12) +ax.set_title('Время вставки 10000 записей', fontsize=14, fontweight='bold') +ax.set_xticks(x) +ax.set_xticklabels(structures, fontsize=11) +ax.legend(fontsize=11) +ax.set_yscale('log') +ax.grid(True, alpha=0.3, axis='y') + +plt.tight_layout() +plt.savefig('docs/data/graph_insert.png', dpi=150, bbox_inches='tight') +plt.close() + + +# график поиск +fig, ax = plt.subplots(figsize=(12, 7)) + +bars1 = ax.bar(x - width/2, random_search, width, label='Случайный порядок', color='#3498db') +bars2 = ax.bar(x + width/2, sorted_search, width, label='Отсортированный порядок', color='#e74c3c') + +for bar in bars1: + height = bar.get_height() + ax.annotate(f'{height:.6f}', xy=(bar.get_x() + bar.get_width()/2, height), + xytext=(0, 3), textcoords="offset points", ha='center', va='bottom', fontsize=9) + +for bar in bars2: + height = bar.get_height() + if height < 0.01: + ax.annotate(f'{height:.6f}', xy=(bar.get_x() + bar.get_width()/2, height), + xytext=(0, 3), textcoords="offset points", ha='center', va='bottom', fontsize=9) + else: + ax.annotate(f'{height:.4f}', xy=(bar.get_x() + bar.get_width()/2, height), + xytext=(0, 3), textcoords="offset points", ha='center', va='bottom', fontsize=9) + +ax.set_ylabel('Время (сек)', fontsize=12) +ax.set_title('Время поиска 110 записей', fontsize=14, fontweight='bold') +ax.set_xticks(x) +ax.set_xticklabels(structures, fontsize=11) +ax.legend(fontsize=11) +ax.set_yscale('log') +ax.grid(True, alpha=0.3, axis='y') + +plt.tight_layout() +plt.savefig('docs/data/graph_search.png', dpi=150, bbox_inches='tight') +plt.close() + + +# график удаление +fig, ax = plt.subplots(figsize=(12, 7)) + +bars1 = ax.bar(x - width/2, random_delete, width, label='Случайный порядок', color='#3498db') +bars2 = ax.bar(x + width/2, sorted_delete, width, label='Отсортированный порядок', color='#e74c3c') + +for bar in bars1: + height = bar.get_height() + ax.annotate(f'{height:.6f}', xy=(bar.get_x() + bar.get_width()/2, height), + xytext=(0, 3), textcoords="offset points", ha='center', va='bottom', fontsize=9) + +for bar in bars2: + height = bar.get_height() + if height < 0.01: + ax.annotate(f'{height:.6f}', xy=(bar.get_x() + bar.get_width()/2, height), + xytext=(0, 3), textcoords="offset points", ha='center', va='bottom', fontsize=9) + else: + ax.annotate(f'{height:.4f}', xy=(bar.get_x() + bar.get_width()/2, height), + xytext=(0, 3), textcoords="offset points", ha='center', va='bottom', fontsize=9) + +ax.set_ylabel('Время (сек)', fontsize=12) +ax.set_title('Время удаления 50 записей', fontsize=14, fontweight='bold') +ax.set_xticks(x) +ax.set_xticklabels(structures, fontsize=11) +ax.legend(fontsize=11) +ax.set_yscale('log') +ax.grid(True, alpha=0.3, axis='y') + +plt.tight_layout() +plt.savefig('docs/data/graph_delete.png', dpi=150, bbox_inches='tight') +plt.close() + +