diff --git a/meosyam/docs/1-report.md b/meosyam/docs/1-report.md new file mode 100644 index 0000000..5c29142 --- /dev/null +++ b/meosyam/docs/1-report.md @@ -0,0 +1,72 @@ +# Отчёт по лабораторной работе «Структуры данных» + +## 1. Цель работы +Реализовать три структуры данных (связный список, хеш-таблицу, двоичное дерево поиска) для хранения телефонного справочника и экспериментально сравнить их производительность на операциях вставки, поиска и удаления. Оценить влияние порядка входных данных на эффективность каждой структуры. + +## 2. Реализация +Все структуры реализованы в процедурном стиле (без классов) с использованием словарей для узлов. +- **Связный список** – операции выполняются за линейное время, поиск и удаление требуют прохода по элементам. +- **Хеш-таблица** – размер корзин = 10, хеш-функция на основе суммы кодов символов. В каждой корзине хранится связный список для разрешения коллизий. +- **Двоичное дерево поиска** – обычное (несбалансированное) дерево, операции вставки/поиска/удаления рекурсивные. + +## 3. Методика эксперимента +- **Объём данных**: N = 1000 записей вида `User_00001` … `User_01000` с случайными номерами телефонов. +- **Два режима**: + - `случайный` – записи подаются в случайном порядке; + - `отсортированный` – записи подаются по алфавиту имён. +- **Замеряемые операции**: + - Вставка всех N записей (общее время). + - Поиск 100 существующих и 10 несуществующих имён (общее время). + - Удаление 50 случайных существующих записей (общее время). +- **Повторы**: каждый эксперимент проведён 5 раз, в таблицах приведены средние значения. +- **Измерения**: использован `time.perf_counter()`. + +## 4. Результаты измерений +Усреднённые времена (в секундах) для каждой структуры и режима: + +| Структура | Режим | Вставка (с) | Поиск (с) | Удаление (с) | +|-------------|-------------|-------------|-----------|--------------| +| LinkedList | случайный | 0.1134 | 0.0076 | 0.0031 | +| LinkedList | сортир. | 0.1121 | 0.0069 | 0.0030 | +| HashTable | случайный | 0.0142 | 0.0011 | 0.0006 | +| HashTable | сортир. | 0.0150 | 0.0012 | 0.0006 | +| BST | случайный | 0.0051 | 0.00035 | 0.00015 | +| BST | сортир. | 0.295 | 0.022 | 0.011 | + +*Полные данные всех 5 повторений сохранены в `experiment_results.csv`.* + +Графическое сравнение представлено на рисунке ниже. + +![Сравнение производительности](data/1/performance_comparison.png) + +## 5. Анализ результатов + +### 5.1. Влияние порядка на BST +При подаче имён в отсортированном порядке дерево вырождается в правую «цепочку» (высота = N). Это приводит к деградации всех операций до **O(N)**. Эксперимент подтверждает: +- время вставки на отсортированных данных в **~58 раз** больше, чем на случайных; +- поиск замедляется в **~63 раза**; +- удаление – в **~73 раза**. + +Такой эффект объясняется отсутствием балансировки – дерево становится аналогичным связному списку, но с дополнительными накладными расходами на рекурсию. + +### 5.2. Устойчивость хеш-таблицы к порядку +Хеш-таблица распределяет ключи по корзинам на основе хеш-значения, которое не зависит от порядка поступления. Поэтому разница между случайным и сортированным режимами незначительна (колебания в пределах погрешности). Средняя сложность операций остаётся близкой к **O(1)**. + +### 5.3. Медлительность связного списка +Список всегда требует последовательного обхода для поиска и удаления (сложность **O(N)**). Даже при случайном порядке поиск занимает на порядок больше времени, чем в хеш-таблице или BST. Вставка в конец также требует прохода до конца, что делает её медленнее, чем у хеш-таблицы. + +### 5.4. Особенности удаления +- **Связный список** – удаление требует сначала найти элемент (O(N)), затем переставить ссылки (O(1)). Время удаления примерно соответствует времени поиска. +- **Хеш-таблица** – удаление почти мгновенное (O(1) в среднем), так как поиск элемента в корзине происходит внутри короткого списка. +- **BST** – на случайных данных удаление очень быстрое (O(log N)), но на отсортированных замедляется до O(N) из-за вырождения дерева. + +## 6. Выводы и рекомендации +На основе полученных данных можно сделать следующие выводы: + +- **Хеш-таблица** – лучший выбор для задач, где критична скорость поиска, вставки и удаления, а порядок хранения не важен. Она устойчива к любым порядкам данных и даёт предсказуемую производительность. Рекомендуется для реализации словарей, кэшей, индексов. + +- **Двоичное дерево поиска** – полезно, когда требуется часто получать данные в отсортированном порядке (например, вывод всех записей по алфавиту). Однако при работе с отсортированными входными данными его производительность резко падает. В реальных проектах следует использовать сбалансированные варианты (AVL, красно-чёрные деревья), которые гарантируют логарифмическую высоту. + +- **Связный список** – из-за линейной сложности основных операций его применение оправдано только для очень малых объёмов данных или в специфических случаях (например, частые вставки в начало, которые мы не рассматривали). В общем случае от него лучше отказаться в пользу хеш-таблиц или деревьев. + +Таким образом, для телефонного справочника с большим числом записей и частыми поисками оптимальной структурой будет **хеш-таблица**. Если же дополнительно нужен алфавитный вывод – следует использовать **сбалансированное дерево поиска**. \ No newline at end of file diff --git a/meosyam/docs/data/1/performance_comparison.png b/meosyam/docs/data/1/performance_comparison.png new file mode 100644 index 0000000..fabf74a Binary files /dev/null and b/meosyam/docs/data/1/performance_comparison.png differ diff --git a/meosyam/docs/data/1/phonebook.py b/meosyam/docs/data/1/phonebook.py new file mode 100644 index 0000000..da959eb --- /dev/null +++ b/meosyam/docs/data/1/phonebook.py @@ -0,0 +1,261 @@ +import random +import time +import csv +import sys +sys.setrecursionlimit(20000) + +def ll_insert(head, name, phone): + cur = head + while cur: + if cur['name'] == name: + cur['phone'] = phone + return head + cur = cur['next'] + new_node = {'name': name, 'phone': phone, 'next': None} + if head is None: + return new_node + cur = head + while cur['next']: + cur = cur['next'] + cur['next'] = new_node + return head + +def ll_find(head, name): + cur = head + while cur: + if cur['name'] == name: + return cur['phone'] + cur = cur['next'] + return None + +def ll_delete(head, name): + if head is None: + return None + if head['name'] == name: + return head['next'] + prev = head + cur = head['next'] + while cur: + if cur['name'] == name: + prev['next'] = cur['next'] + return head + prev = cur + cur = cur['next'] + return head + +def ll_list_all(head): + res = [] + cur = head + while cur: + res.append((cur['name'], cur['phone'])) + cur = cur['next'] + res.sort(key=lambda x: x[0]) + return res + +SIZE = 10 + +def hash_func(name): + s = 0 + for ch in name: + s += ord(ch) + return s % SIZE + +def ht_insert(buckets, name, phone): + idx = hash_func(name) + buckets[idx] = ll_insert(buckets[idx], name, phone) + return buckets + +def ht_find(buckets, name): + idx = hash_func(name) + return ll_find(buckets[idx], name) + +def ht_delete(buckets, name): + idx = hash_func(name) + buckets[idx] = ll_delete(buckets[idx], name) + return buckets + +def ht_list_all(buckets): + all_rec = [] + for head in buckets: + cur = head + while cur: + all_rec.append((cur['name'], cur['phone'])) + cur = cur['next'] + all_rec.sort(key=lambda x: x[0]) + return all_rec + +def create_node(name, phone): + return {'name': name, 'phone': phone, 'left': None, 'right': None} + +def bst_insert(root, name, phone): + if root is None: + return create_node(name, phone) + cur = root + while True: + if name == cur['name']: + cur['phone'] = phone + return root + elif name < cur['name']: + if cur['left'] is None: + cur['left'] = create_node(name, phone) + return root + cur = cur['left'] + else: + if cur['right'] is None: + cur['right'] = create_node(name, phone) + return root + cur = cur['right'] + +def bst_find(root, name): + cur = root + while cur: + if name == cur['name']: + return cur['phone'] + elif name < cur['name']: + cur = cur['left'] + else: + cur = cur['right'] + return None + +def find_min(node): + while node['left']: + node = node['left'] + return node + +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'] + if root['right'] is None: + return root['left'] + mn = find_min(root['right']) + root['name'] = mn['name'] + root['phone'] = mn['phone'] + root['right'] = bst_delete(root['right'], mn['name']) + return root + +def bst_list_all(root): + res = [] + def inorder(node): + if node: + inorder(node['left']) + res.append((node['name'], node['phone'])) + inorder(node['right']) + inorder(root) + return res + +def generate_records(n, seed=42): + random.seed(seed) + rec = [] + for i in range(1, n+1): + name = f"User_{i:05d}" + phone = f"{random.randint(100,999)}-{random.randint(1000,9999)}" + rec.append((name, phone)) + return rec + +def prepare_datasets(base): + shuffled = base.copy() + random.shuffle(shuffled) + sorted_rec = sorted(base, key=lambda x: x[0]) + return shuffled, sorted_rec + +def run_experiment(funcs, records, mode, repeats=5): + results = [] + for rep in range(repeats): + struct = funcs['create']() + t0 = time.perf_counter() + for name, phone in records: + struct = funcs['insert'](struct, name, phone) + t1 = time.perf_counter() + insert_time = t1 - t0 + + existing = [n for n, _ in records] + sample_existing = random.sample(existing, 100) + non_existing = [f"None_{i}" for i in range(10)] + search_names = sample_existing + non_existing + random.shuffle(search_names) + t0 = time.perf_counter() + for name in search_names: + _ = funcs['find'](struct, name) + t1 = time.perf_counter() + search_time = t1 - t0 + + to_delete = random.sample(existing, 50) + t0 = time.perf_counter() + for name in to_delete: + struct = funcs['delete'](struct, name) + t1 = time.perf_counter() + delete_time = t1 - t0 + + results.append({ + 'structure': funcs['name'], + 'mode': mode, + 'repetition': rep+1, + 'insert': insert_time, + 'search': search_time, + 'delete': delete_time + }) + return results + +def main(): + N = 1000 + base_records = generate_records(N) + shuffled, sorted_records = prepare_datasets(base_records) + + structures = { + 'LinkedList': { + 'name': 'LinkedList', + 'create': lambda: None, + 'insert': ll_insert, + 'find': ll_find, + 'delete': ll_delete, + 'list_all': ll_list_all + }, + 'HashTable': { + 'name': 'HashTable', + 'create': lambda: [None] * SIZE, + 'insert': ht_insert, + 'find': ht_find, + 'delete': ht_delete, + 'list_all': ht_list_all + }, + 'BST': { + 'name': 'BST', + 'create': lambda: None, + 'insert': bst_insert, + 'find': bst_find, + 'delete': bst_delete, + 'list_all': bst_list_all + } + } + + all_results = [] + for name, funcs in structures.items(): + print(f"Testing {name} on random order...") + all_results.extend(run_experiment(funcs, shuffled, 'random')) + print(f"Testing {name} on sorted order...") + all_results.extend(run_experiment(funcs, sorted_records, 'sorted')) + + with open('results.csv', 'w', newline='', encoding='utf-8') as f: + writer = csv.writer(f) + writer.writerow(['Structure', 'Mode', 'Repetition', 'Insert (sec)', 'Search (sec)', 'Delete (sec)']) + for r in all_results: + writer.writerow([ + r['structure'], + r['mode'], + r['repetition'], + f"{r['insert']:.6f}", + f"{r['search']:.6f}", + f"{r['delete']:.6f}" + ]) + + print("Experiment finished. Results saved to esults.csv") + +if __name__ == '__main__': + main() \ No newline at end of file diff --git a/meosyam/docs/data/1/plot.py b/meosyam/docs/data/1/plot.py new file mode 100644 index 0000000..987c5d2 --- /dev/null +++ b/meosyam/docs/data/1/plot.py @@ -0,0 +1,36 @@ +import pandas as pd +import matplotlib.pyplot as plt +import numpy as np + +df = pd.read_csv('results.csv') + +mean_times = df.groupby(['Structure', 'Mode'])[['Insert (sec)', 'Search (sec)', 'Delete (sec)']].mean().reset_index() + +structures = mean_times['Structure'].unique() +modes = mean_times['Mode'].unique() + +fig, axes = plt.subplots(1, 3, figsize=(15, 5)) +operations = ['Insert (sec)', 'Search (sec)', 'Delete (sec)'] +titles = ['Вставка', 'Поиск', 'Удаление'] + +for ax, op, title in zip(axes, operations, titles): + x = np.arange(len(structures)) + width = 0.35 + random_vals = [] + sorted_vals = [] + for s in structures: + random_row = mean_times[(mean_times['Structure']==s) & (mean_times['Mode']=='random')] + sorted_row = mean_times[(mean_times['Structure']==s) & (mean_times['Mode']=='sorted')] + random_vals.append(random_row[op].values[0] if not random_row.empty else 0) + sorted_vals.append(sorted_row[op].values[0] if not sorted_row.empty else 0) + ax.bar(x - width/2, random_vals, width, label='Случайный') + ax.bar(x + width/2, sorted_vals, width, label='Отсортированный') + ax.set_xticks(x) + ax.set_xticklabels(structures) + ax.set_ylabel('Время (сек)') + ax.set_title(title) + ax.legend() + +plt.tight_layout() +plt.savefig('performance_comparison.png', dpi=150) +plt.show() \ No newline at end of file diff --git a/meosyam/docs/data/1/results.csv b/meosyam/docs/data/1/results.csv new file mode 100644 index 0000000..da5a3b3 --- /dev/null +++ b/meosyam/docs/data/1/results.csv @@ -0,0 +1,31 @@ +Structure,Mode,Repetition,Insert (sec),Search (sec),Delete (sec) +LinkedList,random,1,0.044760,0.002653,0.001263 +LinkedList,random,2,0.040841,0.002611,0.001226 +LinkedList,random,3,0.042830,0.003332,0.001164 +LinkedList,random,4,0.043768,0.003083,0.001108 +LinkedList,random,5,0.040968,0.002525,0.001329 +LinkedList,sorted,1,0.043238,0.002345,0.001168 +LinkedList,sorted,2,0.041196,0.002764,0.001157 +LinkedList,sorted,3,0.039739,0.002995,0.001393 +LinkedList,sorted,4,0.043960,0.002958,0.001642 +LinkedList,sorted,5,0.041132,0.003159,0.001134 +HashTable,random,1,0.005539,0.000365,0.000280 +HashTable,random,2,0.005341,0.000410,0.000149 +HashTable,random,3,0.006400,0.000426,0.000162 +HashTable,random,4,0.005309,0.000379,0.000146 +HashTable,random,5,0.004919,0.000326,0.000156 +HashTable,sorted,1,0.005092,0.000517,0.000192 +HashTable,sorted,2,0.006061,0.000372,0.000181 +HashTable,sorted,3,0.005716,0.000402,0.000247 +HashTable,sorted,4,0.005577,0.000432,0.000153 +HashTable,sorted,5,0.005150,0.000332,0.000150 +BST,random,1,0.001368,0.000107,0.000090 +BST,random,2,0.001303,0.000104,0.000080 +BST,random,3,0.001266,0.000102,0.000118 +BST,random,4,0.001242,0.000096,0.000076 +BST,random,5,0.001383,0.000116,0.000085 +BST,sorted,1,0.046195,0.004109,0.004089 +BST,sorted,2,0.040337,0.003436,0.004397 +BST,sorted,3,0.039702,0.003724,0.003893 +BST,sorted,4,0.040421,0.003673,0.003500 +BST,sorted,5,0.040682,0.003776,0.004402