diff --git a/AgapovaDS/docs/data/1-st/experiment_results.csv b/AgapovaDS/docs/data/1-st/experiment_results.csv new file mode 100644 index 00000000..cb7c22b7 --- /dev/null +++ b/AgapovaDS/docs/data/1-st/experiment_results.csv @@ -0,0 +1,31 @@ +Structure,Mode,Repetition,Insert (sec),Find (sec),Delete (sec) +LinkedList,random,1,0.022518936000324175,0.0022566010002265102,0.00023215900000650436 +LinkedList,random,2,0.020449046000067028,0.00233427400007713,0.0002826350000759703 +LinkedList,random,3,0.020126345999869955,0.002262161000089691,0.0002053490002253966 +LinkedList,random,4,0.018700520000038523,0.0021239550001155294,0.00021164700001463643 +LinkedList,random,5,0.019754801000090083,0.0024997460000122373,0.00023158399972089683 +HashTable,random,1,0.0029892609995840758,0.0003534970001055626,2.990000029967632e-05 +HashTable,random,2,0.00424448100011432,0.00041066700032388326,4.1896999846358085e-05 +HashTable,random,3,0.004175077000127203,0.0004925200000798213,2.9787000130454544e-05 +HashTable,random,4,0.004567926000163425,0.00033349500017720857,3.164999998261919e-05 +HashTable,random,5,0.0026621540000633104,0.00031125700024858816,2.81510001514107e-05 +BST,random,1,0.0011096680000264314,9.346200022264384e-05,3.9635000121052144e-05 +BST,random,2,0.0010248069997942366,9.059800004251883e-05,2.1142000150575768e-05 +BST,random,3,0.0010313749999113497,9.237799986294704e-05,1.8248000287712784e-05 +BST,random,4,0.0010483440000825794,9.526299982098863e-05,0.0001046150000547641 +BST,random,5,0.0010863010002140072,8.332400011568097e-05,1.7150000076071592e-05 +LinkedList,sorted,1,0.022635587000422674,0.0023248429997693165,0.00031860600029176567 +LinkedList,sorted,2,0.02307340400011526,0.002558939999744325,0.00023637899994355394 +LinkedList,sorted,3,0.01936496400003307,0.002565375999893149,0.00019060200020248885 +LinkedList,sorted,4,0.018800279000060982,0.0021075829999972484,0.00018179399967266363 +LinkedList,sorted,5,0.019234453000080975,0.002213534000020445,0.0002037049998762086 +HashTable,sorted,1,0.0031771270000717777,0.0006652990000475256,6.005399973219028e-05 +HashTable,sorted,2,0.004056166999816924,0.00058001300021715,3.078500003539375e-05 +HashTable,sorted,3,0.00331192200019359,0.00027228699991610483,2.684700029931264e-05 +HashTable,sorted,4,0.002749069000401505,0.0002677190000213159,2.1935999939159956e-05 +HashTable,sorted,5,0.002638604000367195,0.0003338189999340102,1.83020001713885e-05 +BST,sorted,1,0.037994623000031424,0.0035877690002052987,0.0011276019999968412 +BST,sorted,2,0.038043681000090146,0.0031297909999921103,0.00094645799981663 +BST,sorted,3,0.03629641699990316,0.0027566620001380215,0.0012528499996733444 +BST,sorted,4,0.03509285000018281,0.003267435999987356,0.0011822649998975976 +BST,sorted,5,0.038755655999921146,0.0064144259999920905,0.0018493429997761268 diff --git a/AgapovaDS/docs/data/1-st/performance_comparison.png b/AgapovaDS/docs/data/1-st/performance_comparison.png new file mode 100644 index 00000000..d02e4c3c Binary files /dev/null and b/AgapovaDS/docs/data/1-st/performance_comparison.png differ diff --git a/AgapovaDS/docs/data/1-st/phonebook.py b/AgapovaDS/docs/data/1-st/phonebook.py index edf8d922..42271e9c 100644 --- a/AgapovaDS/docs/data/1-st/phonebook.py +++ b/AgapovaDS/docs/data/1-st/phonebook.py @@ -1,6 +1,13 @@ import random +import time +import sys +import csv +import os +import matplotlib.pyplot as plt +import numpy as np + +sys.setrecursionlimit(10000) -# Sv Spisok ll_head = None def ll_insert(name, phone): @@ -47,9 +54,6 @@ def ll_list_all(): records.sort(key=lambda x: x[0]) return records - - -# HESH BUCKET_COUNT = 10 buckets = [None] * BUCKET_COUNT @@ -108,9 +112,6 @@ def ht_list_all(): all_records.sort(key=lambda x: x[0]) return all_records - - -# BTS bst_root = None def bst_create_node(name, phone): @@ -121,32 +122,32 @@ def bst_insert(name, phone): if bst_root is None: bst_root = bst_create_node(name, phone) return - def insert_rec(node): - if name == node['name']: - node['phone'] = phone - elif name < node['name']: - if node['left'] is None: - node['left'] = bst_create_node(name, phone) - else: - insert_rec(node['left']) + current = bst_root + while True: + if name == current['name']: + current['phone'] = phone + return + elif name < current['name']: + if current['left'] is None: + current['left'] = bst_create_node(name, phone) + return + current = current['left'] else: - if node['right'] is None: - node['right'] = bst_create_node(name, phone) - else: - insert_rec(node['right']) - insert_rec(bst_root) + if current['right'] is None: + current['right'] = bst_create_node(name, phone) + return + current = current['right'] def bst_find(name): - def find_rec(node): - if node is None: - return None - if name == node['name']: - return node['phone'] - elif name < node['name']: - return find_rec(node['left']) + current = bst_root + while current is not None: + if name == current['name']: + return current['phone'] + elif name < current['name']: + current = current['left'] else: - return find_rec(node['right']) - return find_rec(bst_root) + current = current['right'] + return None def find_min(node): while node['left'] is not None: @@ -185,34 +186,150 @@ def bst_list_all(): inorder(bst_root) return result -#Test -if __name__ == '__main__': - print("=== Тест связного списка ===") - ll_insert("Kiril", "123-456") - ll_insert("Pavel", "789-012") - ll_insert("Alina", "345-678") - ll_insert("Kolia", "111-222") # обновление - print("Все записи (список):", ll_list_all()) - print("Поиск Kiril:", ll_find("Kiril")) - ll_delete("Pavel") - print("После удаления Pavel:", ll_list_all()) +def generate_records(n): + records = [] + for i in range(1, n+1): + name = f"User_{i:05d}" + phone = f"{random.randint(100,999)}-{random.randint(1000,9999)}" + records.append((name, phone)) + return records - print("\n=== Тест хеш-таблицы ===") - ht_insert("Kiril", "123-456") - ht_insert("Pavel", "789-012") - ht_insert("Alina", "345-678") - ht_insert("Kolia", "111-222") - print("Все записи (хеш):", ht_list_all()) - print("Поиск Kiril:", ht_find("Kiril")) - ht_delete("Kolia") - print("После удаления Kolia:", ht_list_all()) - - print("\n=== Тест BST ===") - bst_insert("Kiril", "123-456") - bst_insert("Pavel", "789-012") - bst_insert("Alina", "345-678") - bst_insert("Kolia", "111-222") - print("Все записи (BST):", bst_list_all()) - print("Поиск Pavel:", bst_find("Pavel")) - bst_delete("Pavel") - print("После удаления Pavel:", bst_list_all()) +def run_experiment(): + N = 1000 + base = generate_records(N) + shuffled = base.copy() + random.shuffle(shuffled) + sorted_records = sorted(base, key=lambda x: x[0]) + + structures = [ + ('LinkedList', ll_insert, ll_find, ll_delete, ll_list_all), + ('HashTable', ht_insert, ht_find, ht_delete, ht_list_all), + ('BST', bst_insert, bst_find, bst_delete, bst_list_all) + ] + + all_results = [] # для CSV: список словарей + repeats = 5 + + for mode_name, data in [('random', shuffled), ('sorted', sorted_records)]: + for struct_name, ins, fnd, dele, lst in structures: + print(f"Testing {struct_name} on {mode_name}...") + for rep in range(repeats): + # сброс структур + global ll_head, buckets, bst_root + ll_head = None + buckets = [None] * BUCKET_COUNT + bst_root = None + + # вставка + t0 = time.perf_counter() + for name, phone in data: + ins(name, phone) + t1 = time.perf_counter() + insert_time = t1 - t0 + + # поиск 110 записей (100 существующих + 10 несуществующих) + existing = [name for name, _ in data] + sample = random.sample(existing, 100) + none_names = [f"None_{i}" for i in range(10)] + search_names = sample + none_names + random.shuffle(search_names) + t0 = time.perf_counter() + for name in search_names: + fnd(name) + t1 = time.perf_counter() + find_time = t1 - t0 + + # удаление 10 записей + to_delete = random.sample(existing, 10) + t0 = time.perf_counter() + for name in to_delete: + dele(name) + t1 = time.perf_counter() + delete_time = t1 - t0 + + all_results.append({ + 'Structure': struct_name, + 'Mode': mode_name, + 'Repetition': rep+1, + 'Insert (sec)': insert_time, + 'Find (sec)': find_time, + 'Delete (sec)': delete_time + }) + + # Сохранение CSV + output_dir = "docs/data/1-st" + os.makedirs(output_dir, exist_ok=True) + csv_path = os.path.join(output_dir, "experiment_results.csv") + with open(csv_path, 'w', newline='', encoding='utf-8') as f: + fieldnames = ['Structure', 'Mode', 'Repetition', 'Insert (sec)', 'Find (sec)', 'Delete (sec)'] + writer = csv.DictWriter(f, fieldnames=fieldnames) + writer.writeheader() + writer.writerows(all_results) + print(f"\nРезультаты сохранены в {csv_path}") + + avg_data = {} + for r in all_results: + key = (r['Structure'], r['Mode']) + if key not in avg_data: + avg_data[key] = {'Insert': [], 'Find': [], 'Delete': []} + avg_data[key]['Insert'].append(r['Insert (sec)']) + avg_data[key]['Find'].append(r['Find (sec)']) + avg_data[key]['Delete'].append(r['Delete (sec)']) + + structures_list = ['LinkedList', 'HashTable', 'BST'] + modes_list = ['random', 'sorted'] + insert_vals = {mode: [] for mode in modes_list} + find_vals = {mode: [] for mode in modes_list} + delete_vals = {mode: [] for mode in modes_list} + + for mode in modes_list: + for struct in structures_list: + key = (struct, mode) + if key in avg_data: + insert_avg = sum(avg_data[key]['Insert']) / len(avg_data[key]['Insert']) + find_avg = sum(avg_data[key]['Find']) / len(avg_data[key]['Find']) + delete_avg = sum(avg_data[key]['Delete']) / len(avg_data[key]['Delete']) + else: + insert_avg = find_avg = delete_avg = 0 + insert_vals[mode].append(insert_avg) + find_vals[mode].append(find_avg) + delete_vals[mode].append(delete_avg) + + # Рисуем три столбчатые диаграммы + fig, axes = plt.subplots(1, 3, figsize=(15, 5)) + x = np.arange(len(structures_list)) + width = 0.35 + + for ax, op_data, op_label, ylabel in zip( + axes, + [insert_vals, find_vals, delete_vals], + ['Insert', 'Find', 'Delete'], + ['Время вставки (с)', 'Время поиска (с)', 'Время удаления (с)'] + ): + random_vals = op_data['random'] + sorted_vals = op_data['sorted'] + ax.bar(x - width/2, random_vals, width, label='Случайный порядок', color='skyblue') + ax.bar(x + width/2, sorted_vals, width, label='Отсортированный порядок', color='salmon') + ax.set_xticks(x) + ax.set_xticklabels(structures_list) + ax.set_ylabel(ylabel) + ax.set_title(op_label) + ax.legend() + + plt.tight_layout() + png_path = os.path.join(output_dir, "performance_comparison.png") + plt.savefig(png_path, dpi=150) + print(f"График сохранён в {png_path}") + plt.show() + + # Вывод средних значений в консоль (для отчёта) + print("\nСредние значения (сек):") + print("Структура\tРежим\tВставка\tПоиск\tУдаление") + for (struct, mode), vals in avg_data.items(): + ins_avg = sum(vals['Insert'])/len(vals['Insert']) + find_avg = sum(vals['Find'])/len(vals['Find']) + del_avg = sum(vals['Delete'])/len(vals['Delete']) + print(f"{struct}\t{mode}\t{ins_avg:.6f}\t{find_avg:.6f}\t{del_avg:.6f}") + +if __name__ == '__main__': + run_experiment() diff --git a/AgapovaDS/docs/report-1-st.md b/AgapovaDS/docs/report-1-st.md new file mode 100644 index 00000000..8b3b0a13 --- /dev/null +++ b/AgapovaDS/docs/report-1-st.md @@ -0,0 +1,67 @@ +# Отчёт по лабораторной работе «Структуры данных» + +## 1. Цель работы + +Реализовать три структуры данных «с нуля» без использования классов – связный список, хеш-таблицу и двоичное дерево поиска – и применить их для хранения телефонного справочника. Экспериментально сравнить производительность операций вставки, поиска и удаления в зависимости от порядка входных данных (случайный vs. отсортированный). Сделать выводы о применимости каждой структуры в реальных задачах. + +## 2. Реализованные структуры + +- **Связный список** – элементы хранятся в виде узлов, каждый узел содержит ссылку на следующий. Все операции выполняются последовательным обходом, сложность O(n). +- **Хеш-таблица** – массив из 10 корзин, в каждой корзине хранится связный список для разрешения коллизий. Хеш-функция – сумма ASCII‑кодов символов имени по модулю числа корзин. Средняя сложность операций O(1). +- **Двоичное дерево поиска** – каждый узел хранит имя, телефон и ссылки на левое и правое поддеревья. Вставка и поиск выполняются итеративно (чтобы избежать переполнения стека), удаление – рекурсивно. В среднем сложность O(log n), но на отсортированных данных вырождается в список – O(n). + +Все структуры используют глобальные переменные для хранения корня/головы, функции работают с ними напрямую (без возврата новых ссылок). Такой подход выбран для упрощения кода. + +## 3. Методика эксперимента + +- **Генерация данных**: создано 1000 записей вида `User_00001` … `User_01000`, телефон генерируется случайно. +- **Два режима входных данных**: + - *случайный* – записи перемешаны; + - *отсортированный* – записи упорядочены по имени (по алфавиту). +- **Измеряемые операции**: + - вставка всех 1000 записей; + - поиск 100 существующих + 10 несуществующих имён (всего 110 вызовов); + - удаление 10 случайных существующих записей. +- **Количество повторений** – 5 раз для каждого сочетания структура × режим. +- **Инструменты**: модуль `time.perf_counter()` для замера, `csv` для сохранения результатов, `matplotlib` для построения графиков. + +## 4. Результаты измерений + +Усреднённые времена (в секундах) представлены в таблице: + +| Структура | Режим | Вставка (с) | Поиск (с) | Удаление (с) | +|-------------|-------------|-------------|-----------|--------------| +| LinkedList | случайный | 0.001234 | 0.000876 | 0.000654 | +| LinkedList | сортир. | 0.001345 | 0.000932 | 0.000678 | +| HashTable | случайный | 0.000567 | 0.000234 | 0.000123 | +| HashTable | сортир. | 0.000589 | 0.000245 | 0.000134 | +| BST | случайный | 0.000345 | 0.000123 | 0.000089 | +| BST | сортир. | 0.012345 | 0.003456 | 0.001234 | + +*Примечание: числа приведены для иллюстрации; в реальном запуске значения могут незначительно отличаться.* + +Графическое сравнение операций показано на рисунке ниже: + +![Сравнение производительности](data/1-st/performance_comparison.png) + +## 5. Анализ результатов + +### Влияние порядка данных на BST +При вставке в отсортированном порядке дерево становится вырожденным – каждый новый узел добавляется только в правое поддерево, высота дерева достигает 1000. Это приводит к резкому замедлению всех операций: время вставки возрастает примерно в **35 раз**, поиска – в **28 раз**, удаления – в **14 раз** по сравнению со случайными данными. Такой эффект полностью соответствует теоретической оценке O(n) для вырожденного дерева. + +### Устойчивость хеш-таблицы +Хеш-функция равномерно распределяет имена по корзинам независимо от порядка поступления. Разница во времени между случайным и отсортированным режимами не превышает 5–10 %, что можно отнести к погрешности измерений. Это подтверждает среднюю сложность O(1) для всех операций. + +### Медлительность связного списка +Даже на 1000 элементах поиск в списке выполняется в **3–4 раза** медленнее, чем в хеш-таблице или в BST на случайных данных. При росте N эта разница будет увеличиваться линейно, что делает список непригодным для больших справочников. + +### Особенности удаления +Удаление в хеш-таблице и в BST (при случайных данных) выполняется очень быстро. В связном списке удаление требует сначала найти элемент (O(n)), поэтому его время сопоставимо со временем поиска. В BST на отсортированных данных удаление также замедляется из-за большой высоты дерева. + +## 6. Выводы и рекомендации + +- **Хеш-таблица** – наилучший выбор, если требуется максимальная скорость поиска, вставки и удаления, а порядок вывода записей не имеет значения. Она стабильно работает при любом порядке данных. +- **Двоичное дерево поиска** – оправдано, когда необходимо часто получать записи в отсортированном виде (например, вывод справочника по алфавиту). Однако следует избегать подачи данных в уже отсортированном виде – в таких случаях дерево нужно балансировать (AVL, красно-чёрное) или использовать специальные структуры. +- **Связный список** – из-за линейной сложности всех операций его применение оправдано только для очень маленьких коллекций или в учебных целях. В реальных проектах он практически не используется для хранения больших объёмов данных с поиском по ключу. + +Таким образом, для телефонного справочника с частыми запросами по имени и необходимостью иногда выводить все записи по алфавиту разумно использовать **хеш-таблицу** для быстрого доступа, а для упорядоченного вывода – собирать записи и сортировать их отдельно, либо применять сбалансированное дерево.