diff --git a/SavelevMI/docs/data/1-st-exersize/benchmark.py b/SavelevMI/docs/data/1-st-exersize/benchmark.py new file mode 100644 index 00000000..5adcaf77 --- /dev/null +++ b/SavelevMI/docs/data/1-st-exersize/benchmark.py @@ -0,0 +1,18 @@ +# Генерация тестовых наборов данных + +import random + +def generate_records(n, seed=42): + random.seed(seed) + 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 + +def prepare_datasets(base_records): + shuffled = base_records.copy() + random.shuffle(shuffled) + sorted_records = sorted(base_records, key=lambda x: x[0]) + return shuffled, sorted_records \ No newline at end of file diff --git a/SavelevMI/docs/data/1-st-exersize/bst.py b/SavelevMI/docs/data/1-st-exersize/bst.py new file mode 100644 index 00000000..70d29529 --- /dev/null +++ b/SavelevMI/docs/data/1-st-exersize/bst.py @@ -0,0 +1,66 @@ +# Двоичное дерево поиска (не сбалансированное) + +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) + + if name == root['name']: + root['phone'] = phone + elif name < root['name']: + root['left'] = bst_insert(root['left'], name, phone) + else: + root['right'] = bst_insert(root['right'], name, phone) + return root + +def bst_find(root, name): + if root is None: + return None + if name == root['name']: + return root['phone'] + elif name < root['name']: + return bst_find(root['left'], name) + else: + return bst_find(root['right'], name) + +def _find_min(node): + while node['left'] is not None: + 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'] + + # Узел имеет двух потомков: заменяем наименьшим из правого поддерева + min_node = _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): + result = [] + + def inorder(node): + if node is None: + return + inorder(node['left']) + result.append((node['name'], node['phone'])) + inorder(node['right']) + + inorder(root) + return result \ No newline at end of file diff --git a/SavelevMI/docs/data/1-st-exersize/data_utils.py b/SavelevMI/docs/data/1-st-exersize/data_utils.py new file mode 100644 index 00000000..5adcaf77 --- /dev/null +++ b/SavelevMI/docs/data/1-st-exersize/data_utils.py @@ -0,0 +1,18 @@ +# Генерация тестовых наборов данных + +import random + +def generate_records(n, seed=42): + random.seed(seed) + 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 + +def prepare_datasets(base_records): + shuffled = base_records.copy() + random.shuffle(shuffled) + sorted_records = sorted(base_records, key=lambda x: x[0]) + return shuffled, sorted_records \ No newline at end of file diff --git a/SavelevMI/docs/data/1-st-exersize/hash_table.py b/SavelevMI/docs/data/1-st-exersize/hash_table.py new file mode 100644 index 00000000..ef3d24bc --- /dev/null +++ b/SavelevMI/docs/data/1-st-exersize/hash_table.py @@ -0,0 +1,38 @@ +# Хеш-таблица на основе списка корзин, каждая корзина – связный список + +import linked_list as ll + +def create_hash_table(size=10): + return [None] * size + +def _hash_function(key, size): + return hash(key) % size + +def ht_insert(buckets, name, phone): + idx = _hash_function(name, len(buckets)) + head = buckets[idx] + new_head = ll.ll_insert(head, name, phone) + buckets[idx] = new_head + return buckets + +def ht_find(buckets, name): + idx = _hash_function(name, len(buckets)) + head = buckets[idx] + return ll.ll_find(head, name) + +def ht_delete(buckets, name): + idx = _hash_function(name, len(buckets)) + head = buckets[idx] + new_head = ll.ll_delete(head, name) + buckets[idx] = new_head + return buckets + +def ht_list_all(buckets): + all_records = [] + for head in buckets: + current = head + while current is not None: + all_records.append((current['name'], current['phone'])) + current = current['next'] + all_records.sort(key=lambda x: x[0]) + return all_records \ No newline at end of file diff --git a/SavelevMI/docs/data/1-st-exersize/linked_list.py b/SavelevMI/docs/data/1-st-exersize/linked_list.py new file mode 100644 index 00000000..3c84f080 --- /dev/null +++ b/SavelevMI/docs/data/1-st-exersize/linked_list.py @@ -0,0 +1,58 @@ +def create_node(name, phone): + return {'name': name, 'phone': phone, 'next': None} + +def ll_insert(head, name, phone): + current = head + # Поиск существующей записи + while current is not None: + if current['name'] == name: + current['phone'] = phone + return head + current = current['next'] + + # Создание нового узла + new_node = create_node(name, phone) + + 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'] + + prev = head + current = head['next'] + while current is not None: + if current['name'] == name: + prev['next'] = current['next'] + return head + prev = current + 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 pair: pair[0]) + return records \ No newline at end of file diff --git a/SavelevMI/docs/data/1-st-exersize/main.py b/SavelevMI/docs/data/1-st-exersize/main.py new file mode 100644 index 00000000..2ba4ea1f --- /dev/null +++ b/SavelevMI/docs/data/1-st-exersize/main.py @@ -0,0 +1,73 @@ +# Запуск экспериментального сравнения трёх структур данных +# Результаты сохраняются в experiment_results.csv + +import csv +import sys +sys.setrecursionlimit(20000) + +import linked_list as ll +import hash_table as ht +import bst +import data_utils +import benchmark + +def main(): + N = 10000 # количество записей + base_records = data_utils.generate_records(N) + shuffled, sorted_records = data_utils.prepare_datasets(base_records) + + # Описания структур для бенчмарка + structures = { + 'LinkedList': { + 'name': 'LinkedList', + 'create': lambda: None, + 'insert': ll.ll_insert, + 'find': ll.ll_find, + 'delete': ll.ll_delete + }, + 'HashTable': { + 'name': 'HashTable', + 'create': lambda: ht.create_hash_table(10), # 10 корзин + 'insert': ht.ht_insert, + 'find': ht.ht_find, + 'delete': ht.ht_delete + }, + 'BST': { + 'name': 'BST', + 'create': lambda: None, + 'insert': bst.bst_insert, + 'find': bst.bst_find, + 'delete': bst.bst_delete + } + } + + all_results = [] + REPEATS = 5 # минимум 5 повторений + + for name, struct in structures.items(): + print(f"Testing {name} on random order...") + res_random = benchmark.measure_operations(struct, shuffled, 'random', REPEATS) + all_results.extend(res_random) + + print(f"Testing {name} on sorted order...") + res_sorted = benchmark.measure_operations(struct, sorted_records, 'sorted', REPEATS) + all_results.extend(res_sorted) + + # Сохранение CSV + with open('experiment_results.csv', 'w', newline='', encoding='utf-8') as f: + writer = csv.writer(f) + writer.writerow(['Structure', 'Mode', 'Repeat', 'Insert (sec)', 'Search (sec)', 'Delete (sec)']) + for row in all_results: + writer.writerow([ + row['structure'], + row['mode'], + row['repetition'], + f"{row['insert_time']:.6f}", + f"{row['find_time']:.6f}", + f"{row['delete_time']:.6f}" + ]) + + print("Experiment finished. Results saved to experiment_results.csv") + +if __name__ == '__main__': + main() \ No newline at end of file diff --git a/SavelevMI/docs/data/1-st-exersize/plot_results.py b/SavelevMI/docs/data/1-st-exersize/plot_results.py new file mode 100644 index 00000000..809a7703 --- /dev/null +++ b/SavelevMI/docs/data/1-st-exersize/plot_results.py @@ -0,0 +1,43 @@ +# Загружает CSV с результатами и строит столбчатую диаграмму сравнения + +import pandas as pd +import matplotlib.pyplot as plt +import numpy as np + +def main(): + df = pd.read_csv('experiment_results.csv') + + mean_times = df.groupby(['Structure', 'Mode'])[['Insert (sec)', 'Search (sec)', 'Delete (sec)']].mean().reset_index() + + structures = mean_times['Structure'].unique() + operations = ['Insert (sec)', 'Search (sec)', 'Delete (sec)'] + titles = ['Insertion', 'Search', 'Deletion'] + + fig, axes = plt.subplots(1, 3, figsize=(15, 5)) + + 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: + rand = mean_times[(mean_times['Structure'] == s) & (mean_times['Mode'] == 'random')] + sort = mean_times[(mean_times['Structure'] == s) & (mean_times['Mode'] == 'sorted')] + random_vals.append(rand[op].values[0] if not rand.empty else 0) + sorted_vals.append(sort[op].values[0] if not sort.empty else 0) + + ax.bar(x - width/2, random_vals, width, label='Random order') + ax.bar(x + width/2, sorted_vals, width, label='Sorted order') + ax.set_xticks(x) + ax.set_xticklabels(structures) + ax.set_ylabel('Time (seconds)') + ax.set_title(title) + ax.legend() + + plt.tight_layout() + plt.savefig('performance_comparison.png', dpi=150) + plt.show() + +if __name__ == '__main__': + main() \ No newline at end of file diff --git a/SavelevMI/docs/data/2-nd-exersize/maze_core.py b/SavelevMI/docs/data/2-nd-exersize/maze_core.py new file mode 100644 index 00000000..a605bfb0 --- /dev/null +++ b/SavelevMI/docs/data/2-nd-exersize/maze_core.py @@ -0,0 +1,146 @@ +# Модель лабиринта: клетки, карта и загрузка из файла (Builder pattern) + +class Cell: + + def __init__(self, x, y): + self._x = x + self._y = y + self._is_wall = False + self._is_start = False + self._is_exit = False + + @property + def x(self): + return self._x + + @property + def y(self): + return self._y + + @property + def is_wall(self): + return self._is_wall + + @is_wall.setter + def is_wall(self, value): + self._is_wall = value + + @property + def is_start(self): + return self._is_start + + @is_start.setter + def is_start(self, value): + self._is_start = value + + @property + def is_exit(self): + return self._is_exit + + @is_exit.setter + def is_exit(self, value): + self._is_exit = value + + def is_passable(self): + return not self._is_wall + + +class Maze: + + def __init__(self, width, height): + self._width = width + self._height = height + self._cells = [[Cell(x, y) for x in range(width)] for y in range(height)] + self._start = None + self._exit = None + + @property + def width(self): + return self._width + + @property + def height(self): + return self._height + + @property + def start(self): + return self._start + + @property + def exit(self): + return self._exit + + def get_cell(self, x, y): + if 0 <= x < self._width and 0 <= y < self._height: + return self._cells[y][x] + return None + + def set_cell(self, x, y, cell_type): + cell = self.get_cell(x, y) + if cell is None: + return + + if cell_type == 'wall': + cell.is_wall = True + elif cell_type == 'start': + if self._start: + self._start.is_start = False + cell.is_start = True + cell.is_wall = False + self._start = cell + elif cell_type == 'exit': + if self._exit: + self._exit.is_exit = False + cell.is_exit = True + cell.is_wall = False + self._exit = cell + elif cell_type == 'path': + cell.is_wall = False + + def get_neighbors(self, cell): + neighbors = [] + directions = [(0, -1), (0, 1), (-1, 0), (1, 0)] # up, down, left, right + for dx, dy in directions: + nx, ny = cell.x + dx, cell.y + dy + neighbor = self.get_cell(nx, ny) + if neighbor and neighbor.is_passable(): + neighbors.append(neighbor) + return neighbors + + +class MazeBuilder: + + def build_from_file(self, filename): + raise NotImplementedError("Must be implemented in subclass") + + +class TextFileMazeBuilder(MazeBuilder): + + def build_from_file(self, filename): + with open(filename, 'r') as f: + lines = [line.rstrip('\n') for line in f.readlines()] + + height = len(lines) + width = max(len(line) for line in lines) if height > 0 else 0 + + start_count = 0 + exit_count = 0 + maze = Maze(width, height) + + for y, line in enumerate(lines): + for x, ch in enumerate(line): + if ch == "#": + maze.set_cell(x, y, "wall") + elif ch == "S": + maze.set_cell(x, y, "start") + start_count += 1 + elif ch == "E": + maze.set_cell(x, y, "exit") + exit_count += 1 + elif ch == " ": + maze.set_cell(x, y, "path") + + if start_count != 1 or exit_count != 1: + raise ValueError(f"Лабиринт должен иметь ровно один вход S и один выход E. Найдено: S={start_count}, E={exit_count}") + + return maze \ No newline at end of file diff --git a/SavelevMI/docs/performance_comparison.png b/SavelevMI/docs/performance_comparison.png new file mode 100644 index 00000000..7bedd067 Binary files /dev/null and b/SavelevMI/docs/performance_comparison.png differ diff --git a/SavelevMI/docs/report-1.md b/SavelevMI/docs/report-1.md new file mode 100644 index 00000000..a4458f3d --- /dev/null +++ b/SavelevMI/docs/report-1.md @@ -0,0 +1,82 @@ +# Отчёт по лабораторной работе «Структуры данных» + +## Цель работы + +Реализовать три структуры данных «с нуля» (связный список, хеш‑таблицу, двоичное дерево поиска) для хранения записей телефонного справочника. Экспериментально сравнить производительность операций вставки, поиска и удаления на наборе из 10 000 записей при случайном и отсортированном порядке поступления данных. + +## Реализованные структуры + +Все структуры написаны в процедурном стиле без использования классов. + +1. **Связный список** – узлы в виде словарей `{'name': str, 'phone': str, 'next': None}`. +2. **Хеш‑таблица** – массив из 10 корзин, каждая корзина – связный список. Хеш‑функция – `hash(name) % size`. +3. **Двоичное дерево поиска** – узлы `{'name': str, 'phone': str, 'left': None, 'right': None}`. Операции реализованы рекурсивно. + +## Методика эксперимента + +- **Генерация данных**: 10 000 записей с именами `User_00001` … `User_10000`. Телефоны – случайные строки вида `XXX-XXXX`. +- **Два режима подачи данных**: + – *Случайный* – записи перемешаны. + – *Отсортированный* – записи по возрастанию имени. +- **Измеряемые операции**: + – Вставка всех 10 000 записей. + – Поиск 110 имён (100 существующих + 10 несуществующих). + – Удаление 50 случайных существующих записей. +- **Повторы**: каждый эксперимент выполнен 5 раз, зафиксировано среднее время. + +Результаты замеров сохранены в `experiment_results.csv`. Время измерялось через `time.perf_counter()`. + +## Результаты измерений + +### Связный список (LinkedList) + +При 10 000 записях связный список показал ожидаемо низкую производительность. Вставка всех элементов заняла около **4.4 секунды** в среднем, поиск – около **0.027 секунды**, удаление 50 записей – около **0.012 секунды**. Порядок входных данных практически не повлиял на результаты (случайный и отсортированный режимы показали близкие значения). Это объясняется тем, что связный список всегда работает за линейное время O(n) независимо от того, как приходят данные. + +### Хеш‑таблица (HashTable) + +Хеш‑таблица с 10 корзинами показала значительное ускорение по сравнению со связным списком. Вставка 10 000 записей заняла в среднем **0.56 секунды** (почти в 8 раз быстрее списка). Поиск выполняется за **0.004 секунды** (в 7 раз быстрее), а удаление – за **0.0016 секунды** (в 7.5 раз быстрее). Порядок данных практически не влияет на производительность – разница между случайным и отсортированным режимами не превышает 10%, что соответствует теоретической сложности O(1) в среднем. + +### Двоичное дерево поиска (BST) + +Здесь наблюдается самая интересная картина: + +**На случайных данных** BST показал выдающуюся производительность. Вставка всех 10 000 записей заняла всего **0.025 секунды**, что в 22 раза быстрее хеш‑таблицы и в 176 раз быстрее связного списка. Поиск выполняется за **0.00024 секунды** (в 16 раз быстрее хеш‑таблицы), удаление – за **0.00017 секунды** (почти в 10 раз быстрее). Это идеальный случай сбалансированного дерева. + +**На отсортированных данных** ситуация кардинально меняется. Дерево вырождается в линейный список, и производительность падает катастрофически. Вставка замедлилась до **10.15 секунды** – это в 406 раз медленнее, чем на случайных данных, и даже медленнее, чем у связного списка (в 2.3 раза). Поиск вырос до **0.091 секунды** (в 380 раз медленнее), удаление – до **0.057 секунды** (в 335 раз медленнее). Это классический пример деградации BST при упорядоченных входных данных. + +## Анализ результатов + +### Как порядок входных данных влияет на скорость вставки в BST + +Эксперимент наглядно демонстрирует проблему наивной реализации двоичного дерева поиска. На случайных данных дерево остаётся достаточно сбалансированным, и операции выполняются за логарифмическое время (O(log n)). Однако на отсортированных данных каждый новый элемент становится самым большим и добавляется только в правую ветку. В результате дерево превращается в односвязный список высотой 10 000 узлов, а сложность всех операций деградирует до линейной O(n). Это подтверждается цифрами: время вставки выросло с 0.025 до 10.15 секунд – разница в 406 раз. + +### Почему хеш‑таблица почти не чувствительна к порядку + +Хеш‑функция распределяет ключи по корзинам независимо от того, в каком порядке они поступают. «User_00001» и «User_10000» с равной вероятностью могут попасть в любую из 10 корзин. Поэтому порядок ввода не влияет на длину цепочек в каждой корзине. Результаты подтверждают это: в случайном и отсортированном режимах время выполнения операций отличается незначительно (менее 10%). + +### Почему связный список всегда медленен при поиске + +Связный список не имеет индексов или другой структуры для ускорения доступа. Чтобы найти элемент, нужно в худшем случае пройти все 10 000 узлов. Поэтому поиск занимает ~0.027 секунды независимо от того, как расположены данные. Вставка тоже требует прохода до конца списка, что даёт ~4.4 секунды на 10 000 элементов. + +### Как удаление работает в каждой структуре + +Удаление тесно связано с поиском, потому что сначала нужно найти удаляемый элемент. Поэтому время удаления коррелирует со временем поиска: + +- В связном списке удаление занимает ~0.012 секунды – примерно половину времени поиска (0.027 с), так как операция перелинковки дёшева. +- В хеш‑таблице удаление (~0.0016 с) близко ко времени поиска (~0.004 с), опять же с поправкой на перелинковку в списке корзины. +- В BST на случайных данных удаление (~0.00017 с) даже быстрее поиска (~0.00024 с) из-за особенностей рекурсивной реализации. +- В BST на отсортированных данных удаление (~0.057 с) занимает примерно половину времени поиска (~0.091 с) – та же закономерность, что и у списка, потому что вырожденное дерево ведёт себя как список. + +## Выводы + +**Какую структуру и для каких задач стоит выбирать в реальной жизни?** + +1. **Хеш‑таблица** – оптимальный выбор для подавляющего большинства сценариев, где нужен быстрый доступ по ключу (словари, кэши, индексы в базах данных). Она стабильна, предсказуема и не зависит от порядка данных. В моём эксперименте она уступила BST на случайных данных, но выиграла у BST на отсортированных и оказалась намного быстрее связного списка. Главный минус – отсутствие естественного порядка при обходе. + +2. **Сбалансированное дерево** (AVL или красно-чёрное) – выбор, когда нужны оба свойства: быстрый доступ (O(log n)) и возможность получать данные в отсортированном порядке без дополнительной сортировки. Обычный BST (как в моей реализации) **использовать не стоит**, если нельзя гарантировать случайный порядок входных данных. Деградация на упорядоченных данных делает его непригодным для реальных систем. + +3. **Связный список** – практически бесполезен для хранения больших объёмов данных. Единственное оправданное применение – очень маленькие коллекции (до сотни элементов), реализация очередей/стеков или учебные цели. + +**Рекомендация**: если нужен только быстрый доступ по ключу – берите хеш-таблицу. Если нужен отсортированный вывод и вы готовы пожертвовать небольшой долей производительности – используйте сбалансированное дерево. От наивного BST и связного списка в реальных проектах лучше отказаться. + +**Ключевой вывод эксперимента**: порядок поступления данных критически важен для производительности BST (разница в 400 раз между случайными и отсортированными данными), но почти не влияет на хеш-таблицу и связный список (хотя последний всегда медленный).