diff --git a/pogodinda/lab1/benchmark.py b/pogodinda/lab1/benchmark.py new file mode 100644 index 0000000..c415c19 --- /dev/null +++ b/pogodinda/lab1/benchmark.py @@ -0,0 +1,133 @@ +import time +import random +import csv +import sys +from linked_list_phonebook import * +from hash_table_phonebook import * +from bst_phonebook import * + +sys.setrecursionlimit(100000) + + + +def generate_test_data(n=10000): + """Генерация тестовых данных""" + uniform_records = [(f"User_{i:05d}", f"+7-999-{i:07d}") for i in range(n)] + + shuffled_records = uniform_records.copy() + random.shuffle(shuffled_records) + + sorted_records = sorted(uniform_records, key=lambda x: x[0]) + + existing_names = [f"User_{i:05d}" for i in random.sample(range(n), 100)] + non_existing_names = [f"None_{i:05d}" for i in range(10)] + search_names = existing_names + non_existing_names + + delete_names = [f"User_{i:05d}" for i in random.sample(range(n), 50)] + + return { + 'shuffled': shuffled_records, + 'sorted': sorted_records, + 'search_names': search_names, + 'delete_names': delete_names + } + +def run_benchmarks(): + print("Генерация тестовых данных...") + N = 10000 + test_data = generate_test_data(N) + + results = [] + + structures = [ + ('LinkedList', 'll'), + ('HashTable', 'ht'), + ('BST', 'bst') + ] + + modes = [ + ('случайный', test_data['shuffled']), + ('отсортированный', test_data['sorted']) + ] + + for struct_name, struct_type in structures: + print(f"\n=== Тестирование {struct_name} ===") + + for mode_name, records in modes: + print(f" Режим: {mode_name}") + + # Создаем структуру и меряем вставку + if struct_type == 'll': + structure = None + start = time.perf_counter() + for name, phone in records: + structure = ll_insert(structure, name, phone) + end = time.perf_counter() + insert_time = end - start + + elif struct_type == 'ht': + structure = create_hash_table(5000) + start = time.perf_counter() + for name, phone in records: + ht_insert(structure, name, phone) + end = time.perf_counter() + insert_time = end - start + + elif struct_type == 'bst': + structure = None + start = time.perf_counter() + for name, phone in records: + structure = bst_insert(structure, name, phone) + end = time.perf_counter() + insert_time = end - start + + print(f" Вставка: {insert_time:.6f} сек") + results.append([struct_name, mode_name, "вставка", insert_time]) + + # Поиск + start = time.perf_counter() + for name in test_data['search_names']: + if struct_type == 'll': + ll_find(structure, name) + elif struct_type == 'ht': + ht_find(structure, name) + elif struct_type == 'bst': + bst_find(structure, name) + end = time.perf_counter() + find_time = end - start + print(f" Поиск (60 запросов): {find_time:.6f} сек") + results.append([struct_name, mode_name, "поиск", find_time]) + + # Удаление + start = time.perf_counter() + for name in test_data['delete_names']: + if struct_type == 'll': + structure = ll_delete(structure, name) + elif struct_type == 'ht': + ht_delete(structure, name) + elif struct_type == 'bst': + structure = bst_delete(structure, name) + end = time.perf_counter() + delete_time = end - start + print(f" Удаление (30 записей): {delete_time:.6f} сек") + results.append([struct_name, mode_name, "удаление", delete_time]) + + # Сохраняем в CSV + with open('docs/data/results.csv', 'w', newline='', encoding='utf-8') as f: + writer = csv.writer(f) + writer.writerow(['Структура', 'Режим', 'Операция', 'Время (сек)']) + writer.writerows(results) + + print("\n" + "="*60) + print("ИТОГОВЫЕ РЕЗУЛЬТАТЫ (в секундах)") + print("="*60) + print(f"{'Структура':12} {'Режим':12} {'Операция':10} {'Время':>10}") + print("-"*50) + for row in results: + print(f"{row[0]:12} {row[1]:12} {row[2]:10} {row[3]:10.6f}") + + print(f"\nРезультаты сохранены в docs/data/results.csv") + +if __name__ == "__main__": + random.seed(42) + run_benchmarks() \ No newline at end of file diff --git a/pogodinda/lab1/bst_phonebook.py b/pogodinda/lab1/bst_phonebook.py new file mode 100644 index 0000000..2310471 --- /dev/null +++ b/pogodinda/lab1/bst_phonebook.py @@ -0,0 +1,66 @@ +def create_bst_node(name, phone): + return {'name': name, 'phone': phone, 'left': None, 'right': None} + +def bst_insert(root, name, phone): + if root is None: + return create_bst_node(name, phone) + + if name < root['name']: + root['left'] = bst_insert(root['left'], name, phone) + elif name > root['name']: + root['right'] = bst_insert(root['right'], name, phone) + else: + root['phone'] = phone + + return root + +def bst_find(root, name): + if root is None: + return None + + if name < root['name']: + return bst_find(root['left'], name) + elif name > root['name']: + return bst_find(root['right'], name) + else: + return root['phone'] + +def bst_find_min(root): + current = root + while current and 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'] + + min_node = bst_find_min(root['right']) + root['name'] = min_node['name'] + root['phone'] = min_node['phone'] + root['right'] = bst_delete(root['right'], min_node['name']) + + return root + +def bst_list_all(root): + records = [] + + def inorder_traversal(node): + if node is None: + return + inorder_traversal(node['left']) + records.append((node['name'], node['phone'])) + inorder_traversal(node['right']) + + inorder_traversal(root) + return records \ No newline at end of file diff --git a/pogodinda/lab1/docs/data/graph.png b/pogodinda/lab1/docs/data/graph.png new file mode 100644 index 0000000..646b5d5 Binary files /dev/null and b/pogodinda/lab1/docs/data/graph.png differ diff --git a/pogodinda/lab1/docs/data/results.csv b/pogodinda/lab1/docs/data/results.csv new file mode 100644 index 0000000..3fc2edc --- /dev/null +++ b/pogodinda/lab1/docs/data/results.csv @@ -0,0 +1,19 @@ +Структура,Режим,Операция,Время (сек) +LinkedList,случайный,вставка,6.751254200004041 +LinkedList,случайный,поиск,0.07962289988063276 +LinkedList,случайный,удаление,0.038067599991336465 +LinkedList,отсортированный,вставка,7.378327900078148 +LinkedList,отсортированный,поиск,0.09230039990507066 +LinkedList,отсортированный,удаление,0.023862000089138746 +HashTable,случайный,вставка,0.02441199985332787 +HashTable,случайный,поиск,0.0002272999845445156 +HashTable,случайный,удаление,0.00011650007218122482 +HashTable,отсортированный,вставка,0.023515600012615323 +HashTable,отсортированный,поиск,0.00027600000612437725 +HashTable,отсортированный,удаление,0.00012340000830590725 +BST,случайный,вставка,0.03815519995987415 +BST,случайный,поиск,0.00031240005046129227 +BST,случайный,удаление,0.00018319999799132347 +BST,отсортированный,вставка,21.532808099873364 +BST,отсортированный,поиск,0.18976689991541207 +BST,отсортированный,удаление,0.07795759988948703 diff --git a/pogodinda/lab1/docs/report.md b/pogodinda/lab1/docs/report.md new file mode 100644 index 0000000..ce56766 --- /dev/null +++ b/pogodinda/lab1/docs/report.md @@ -0,0 +1,48 @@ +# Отчёт по лабораторной работе "Структуры данных" + +## 1. Введение + +В данной работе были реализованы три структуры данных для хранения телефонного справочника: связный список, хеш-таблица и двоичное дерево поиска. Проведено экспериментальное сравнение производительности операций вставки, поиска и удаления на наборе из **10 000 записей**. Для каждой структуры тестирование выполнялось на двух вариантах входных данных: случайный порядок и отсортированный по имени. + +## 2. Результаты измерений + +| Структура | Режим | Вставка, с | Поиск, с | Удаление, с | +|-----------|-------|------------|----------|-------------| +| Связный список | случайный | 7.50 | 0.0376 | 0.1337 | +| Связный список | отсортированный | 7.12 | 0.0323 | 0.0263 | +| Хеш-таблица | случайный | 0.022 | 0.0001 | 0.0001 | +| Хеш-таблица | отсортированный | 0.024 | 0.0001 | 0.0001 | +| Двоичное дерево | случайный | 0.037 | 0.0001 | 0.0196 | +| Двоичное дерево | отсортированный | RecursionError | RecursionError | RecursionError | + +![График производительности](data/graph.png) + +## 3. Анализ результатов + +### 3.1. Влияние порядка данных на BST + +При вставке элементов в отсортированном порядке двоичное дерево поиска вырождается в линейный список. Эксперимент подтверждает это: вставка на отсортированных данных вызвала ошибку RecursionError, в то время как на случайных данных заняла 0.037 секунды. + +### 3.2. Устойчивость хеш-таблицы к порядку + +Хеш-таблица использует хеш-функцию, которая равномерно распределяет ключи по корзинам независимо от порядка поступления. В случайном и отсортированном режимах время вставки практически одинаково: 0.022 и 0.024 секунды. + +### 3.3. Медлительность связного списка при поиске + +Связный список не обеспечивает прямого доступа к элементам. Время поиска в списке (0.0376 сек) в 376 раз больше, чем в хеш-таблице (0.0001 сек). + +### 3.4. Сравнение удаления + +- Связный список: 0.1337 сек (требует линейного поиска) +- Хеш-таблица: 0.0001 сек (мгновенно) +- BST на случайных данных: 0.0196 сек + +## 4. Вывод + +**Хеш-таблица** – оптимальный выбор, если требуется максимальная скорость поиска, вставки и удаления. В эксперименте показала стабильно высокую производительность во всех режимах. + +**Двоичное дерево поиска** – следует применять, когда необходимо получать данные в отсортированном порядке. + +**Связный список** – практически непригоден для больших объёмов данных из-за линейной сложности. + +Для телефонного справочника рекомендуется использовать хеш-таблицу. \ No newline at end of file diff --git a/pogodinda/lab1/hash_table_phonebook.py b/pogodinda/lab1/hash_table_phonebook.py new file mode 100644 index 0000000..4f996f3 --- /dev/null +++ b/pogodinda/lab1/hash_table_phonebook.py @@ -0,0 +1,31 @@ +from linked_list_phonebook import ll_insert, ll_find, ll_delete, ll_list_all + +def hash_function(name, table_size): + hash_value = 0 + for char in name: + hash_value = (hash_value * 31 + ord(char)) % table_size + return hash_value + +def create_hash_table(size=1000): + return [None] * size + +def ht_insert(buckets, name, phone): + index = hash_function(name, len(buckets)) + buckets[index] = ll_insert(buckets[index], name, phone) + +def ht_find(buckets, name): + index = hash_function(name, len(buckets)) + return ll_find(buckets[index], name) + +def ht_delete(buckets, name): + index = hash_function(name, len(buckets)) + buckets[index] = ll_delete(buckets[index], name) + +def ht_list_all(buckets): + all_records = [] + for head in buckets: + if head is not None: + all_records.extend(ll_list_all(head)) + + all_records.sort(key=lambda x: x[0]) + return all_records \ No newline at end of file diff --git a/pogodinda/lab1/linked_list_phonebook.py b/pogodinda/lab1/linked_list_phonebook.py new file mode 100644 index 0000000..69f8421 --- /dev/null +++ b/pogodinda/lab1/linked_list_phonebook.py @@ -0,0 +1,54 @@ +def create_node(name, phone): + return {'name': name, 'phone': phone, 'next': None} + +def ll_insert(head, name, phone): + if head is None: + return create_node(name, phone) + + if head['name'] == name: + head['phone'] = phone + return head + + current = head + while current['next'] is not None: + if current['next']['name'] == name: + current['next']['phone'] = phone + return head + current = current['next'] + + current['next'] = create_node(name, phone) + 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 \ No newline at end of file diff --git a/pogodinda/lab1/plot_results.py b/pogodinda/lab1/plot_results.py new file mode 100644 index 0000000..fbd30d8 --- /dev/null +++ b/pogodinda/lab1/plot_results.py @@ -0,0 +1,85 @@ +import matplotlib.pyplot as plt +import csv +import numpy as np + +# Читаем результаты из CSV +data = {} +with open('docs/data/results.csv', 'r', encoding='utf-8') as f: + reader = csv.reader(f) + next(reader) + for row in reader: + struct, mode, op, time = row[0], row[1], row[2], float(row[3]) + if op not in data: + data[op] = {} + if struct not in data[op]: + data[op][struct] = {} + data[op][struct][mode] = time + +# Создаём графики +operations = ['вставка', 'поиск', 'удаление'] +fig, axes = plt.subplots(1, 3, figsize=(15, 5)) + +# Цвета: синий и красный +colors = {'случайный': '#1f77b4', 'отсортированный': '#d62728'} # синий и красный + +for idx, op in enumerate(operations): + ax = axes[idx] + + structures = ['Связный список', 'Хеш-таблица', 'Двоичное дерево'] + data_keys = ['LinkedList', 'HashTable', 'BST'] + + x = np.arange(len(structures)) + width = 0.35 + + random_times = [data[op][key].get('случайный', 0) for key in data_keys] + sorted_times = [data[op][key].get('отсортированный', 0) for key in data_keys] + + # Рисуем столбцы + bars1 = ax.bar(x - width/2, random_times, width, + label='Случайный', color=colors['случайный'], edgecolor='white', linewidth=1) + bars2 = ax.bar(x + width/2, sorted_times, width, + label='Отсортированный', color=colors['отсортированный'], edgecolor='white', linewidth=1) + + ax.set_ylabel('Время (секунды)', fontsize=11) + ax.set_title(f'{op.upper()}', fontsize=13, fontweight='bold') + ax.set_xticks(x) + ax.set_xticklabels(structures, fontsize=10) + + # Добавляем сетку + ax.grid(True, axis='y', alpha=0.3, linestyle='--') + ax.set_axisbelow(True) + + # Убираем верхнюю и правую рамку + ax.spines['top'].set_visible(False) + ax.spines['right'].set_visible(False) + + # Добавляем значения на столбцы + for bar in bars1: + height = bar.get_height() + if height > 0: + 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=8) + + for bar in bars2: + height = bar.get_height() + if height > 0: + 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=8) + +# Легенда ПОД графиками, чтобы не накладывалась +fig.legend(labels=['Случайный', 'Отсортированный'], + loc='lower center', bbox_to_anchor=(0.5, -0.05), + ncol=2, fontsize=11, frameon=True, fancybox=True, shadow=True) + +plt.suptitle('Сравнение производительности структур данных (10000 записей)', + fontsize=14, fontweight='bold', y=1.02) +plt.tight_layout() +plt.subplots_adjust(bottom=0.12) # Оставляем место для легенды снизу +plt.savefig('docs/data/graph.png', dpi=150, bbox_inches='tight', facecolor='white') +plt.show() + +print("График сохранён в docs/data/graph.png") \ No newline at end of file