From 6716bd1d0bd8854441a2a51e871cf8f6d07640ef Mon Sep 17 00:00:00 2001 From: semyanovra Date: Wed, 8 Apr 2026 22:01:28 +0300 Subject: [PATCH 01/36] Add data generation --- semyanovra/docs/experiment_setup.md | 14 ++++ semyanovra/scr/generate_data.py | 109 ++++++++++++++++++++++++++++ 2 files changed, 123 insertions(+) create mode 100644 semyanovra/docs/experiment_setup.md create mode 100644 semyanovra/scr/generate_data.py diff --git a/semyanovra/docs/experiment_setup.md b/semyanovra/docs/experiment_setup.md new file mode 100644 index 0000000..c99b66f --- /dev/null +++ b/semyanovra/docs/experiment_setup.md @@ -0,0 +1,14 @@ +# Эксперимент 1: Генерация данных + +**Студент:** semyanovra +**Ветка:** structura-dannuh + +## Результаты +- Создано 10000 записей +- Имена: User_00000 до User_09999 +- Телефоны: случайные 10 цифр + +## Файлы +- `src/generate_data.py` - скрипт генерации +- `docs/data/records_shuffled.csv` - случайный порядок +- `docs/data/records_sorted.csv` - сортированный порядок \ No newline at end of file diff --git a/semyanovra/scr/generate_data.py b/semyanovra/scr/generate_data.py new file mode 100644 index 0000000..b239ec1 --- /dev/null +++ b/semyanovra/scr/generate_data.py @@ -0,0 +1,109 @@ +""" +Экспериментальная часть. Пункт 1: Генерация тестовых данных. +Цель: создать список записей (имя, телефон) для дальнейшего тестирования +структур данных (связный список, хеш-таблица, BST). + +Особенности реализации: +- N = 10000 записей. +- Два режима: случайный порядок и отсортированный по имени. +- Имена генерируются равномерно (User_00000 ... User_09999) + небольшой + набор повторяющихся имен для создания коллизий в хеш-таблице. +- Телефоны — случайные строки из 10 цифр. +""" + +import random + +def generate_test_data(n=10000, duplicate_names_ratio=0.1): + """ + Генерирует два набора записей: в случайном порядке и отсортированном. + + Параметры: + - n: общее количество записей (по умолчанию 10000). + - duplicate_names_ratio: доля имен, которые будут повторяться (коллизии). + Например, 0.1 означает, что 10% записей будут использовать + имена из небольшого пула, остальные 90% — уникальные User_XXXXX. + + Возвращает: + - records_shuffled: список кортежей (name, phone) в случайном порядке. + - records_sorted: тот же список, но отсортированный по имени. + """ + + # 1. Создаем пул уникальных имен (равномерное распределение) + # Формат: User_00000, User_00001, ..., User_09999 + unique_names = [f"User_{i:05d}" for i in range(n)] + + # 2. Создаем небольшой пул имен для повторений (коллизий) + # Например, 20 разных имен, которые будут многократно встречаться. + collision_pool_size = max(1, int(n * duplicate_names_ratio)) # ~1000 имен для 10000 записей (10%) + collision_names = [f"Common_{j:03d}" for j in range(collision_pool_size)] + + # 3. Формируем итоговый список имен с повторениями + # - Первые (n - collision_pool_size) записей — уникальные. + # - Оставшиеся collision_pool_size записей — случайные из пула коллизий. + names = [] + # Уникальная часть + names.extend(unique_names[:n - collision_pool_size]) + # Часть с повторениями (для проверки коллизий в хеш-таблице) + for _ in range(collision_pool_size): + names.append(random.choice(collision_names)) + + # Перемешиваем имена, чтобы повторяющиеся имена не шли подряд + random.shuffle(names) + + # 4. Генерируем случайные телефоны (10 цифр) + phones = [] + for _ in range(n): + phone = ''.join(random.choices('0123456789', k=10)) + phones.append(phone) + + # 5. Собираем записи в список кортежей + records = list(zip(names, phones)) + + # 6. Создаем две версии: случайную и отсортированную + records_shuffled = records.copy() # уже случайный порядок после shuffle + records_sorted = sorted(records, key=lambda x: x[0]) # сортировка по имени + + return records_shuffled, records_sorted + +def print_sample(records, title, count=10): + """Вспомогательная функция: печатает первые count записей.""" + print(f"\n{title} (первые {count} записей):") + for name, phone in records[:count]: + print(f" {name}: {phone}") + +# ========== Демонстрация работы ========== +if __name__ == "__main__": + # Фиксируем seed для воспроизводимости результатов + random.seed(42) + + # Генерируем данные: N = 10000, доля коллизий 10% + N = 10000 + shuffled, sorted_data = generate_test_data(N, duplicate_names_ratio=0.1) + + # Выводим статистику и примеры + print(f"Сгенерировано {N} записей.") + print(f"Доля имен с повторениями (коллизиями): ~10%") + + # Показываем несколько примеров из каждого набора + print_sample(shuffled, "Случайный порядок") + print_sample(sorted_data, "Отсортированный порядок") + + # Дополнительно: проверка, что в отсортированном порядке имена действительно упорядочены + first_five_sorted = [name for name, _ in sorted_data[:5]] + print(f"\nПервые 5 имен в отсортированном наборе: {first_five_sorted}") + # Ожидается: ['Common_000', 'Common_001', ...] или 'User_...' — лексикографически + + # Проверка наличия коллизий (повторяющихся имен) + unique_names_in_shuffled = set(name for name, _ in shuffled) + print(f"\nУникальных имен в случайном наборе: {len(unique_names_in_shuffled)}") + print(f"(меньше {N} из-за повторений для коллизий)") + + # Сохраняем в файлы (опционально, для отладки) + # import csv + # with open("docs/data/records_shuffled.csv", "w") as f: + # writer = csv.writer(f) + # writer.writerows(shuffled) + # with open("docs/data/records_sorted.csv", "w") as f: + # writer = csv.writer(f) + # writer.writerows(sorted_data) + -- 2.43.0 From f42be3b51aa734017154b67a724c06c025fe0ef8 Mon Sep 17 00:00:00 2001 From: semyanovra Date: Tue, 12 May 2026 21:17:40 +0300 Subject: [PATCH 02/36] =?UTF-8?q?=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=20=D0=BF=D1=83=D0=BD=D0=BA=D1=82=202:=20=D0=B8?= =?UTF-8?q?=D0=BD=D1=82=D1=80=D1=83=D0=BC=D0=B5=D0=BD=D1=82=D1=8B=20=D0=B7?= =?UTF-8?q?=D0=B0=D0=BC=D0=B5=D1=80=D0=B0=20=D0=B2=D1=80=D0=B5=D0=BC=D0=B5?= =?UTF-8?q?=D0=BD=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/measure_time.py | 129 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 129 insertions(+) create mode 100644 src/measure_time.py diff --git a/src/measure_time.py b/src/measure_time.py new file mode 100644 index 0000000..f84fe2a --- /dev/null +++ b/src/measure_time.py @@ -0,0 +1,129 @@ +""" +Экспериментальная часть. Пункт 2: Инструменты замера времени. +Цель: предоставить функции для многократного измерения времени выполнения +операций со структурами данных (LinkedList, HashTable, BST). + +Особенности: +- Используется time.perf_counter() для высокой точности. +- Каждый эксперимент повторяется min_runs раз (по умолчанию 5), результаты сохраняются. +- Вычисляется среднее арифметическое и список всех замеров. +- Результаты можно напрямую сохранить в CSV. +""" + +import time +from typing import List, Tuple, Callable, Any +import random + +# Предполагается, что generate_test_data из пункта 1 уже определена +# from experimental_part1 import generate_test_data # если код в другом файле + +# ========== 1. Базовые замеры ========== + +def measure_time(func: Callable, *args, **kwargs) -> float: + """ + Измеряет время выполнения функции func(*args, **kwargs). + Возвращает время в секундах (float). + """ + start = time.perf_counter() + result = func(*args, **kwargs) + end = time.perf_counter() + return end - start, result + +# ========== 2. Многократные замеры с усреднением ========== + +def run_experiment(func: Callable, args: Tuple, min_runs: int = 5) -> Tuple[float, List[float]]: + """ + Повторяет замер функции func(*args) минимум min_runs раз. + Возвращает (среднее_время, список_всех_замеров). + """ + times = [] + for _ in range(min_runs): + elapsed, _ = measure_time(func, *args) + times.append(elapsed) + avg_time = sum(times) / len(times) + return avg_time, times + +# ========== 3. Тестовые сценарии (заглушки для демонстрации) ========== + +# Ниже приведены примеры-заглушки для структур данных. +# В реальной работе их нужно заменить на реализованные функции. + +def stub_insert(structure, name, phone): + """Заглушка для вставки.""" + pass + +def stub_find(structure, name): + """Заглушка для поиска.""" + return None + +def stub_delete(structure, name): + """Заглушка для удаления.""" + pass + +def stub_list_all(structure): + """Заглушка для получения всех записей.""" + return [] + +# Пример функции, которая вставляет все записи из списка в структуру +def insert_all(structure, records, insert_func): + """ + Выполняет вставку всех записей (name, phone) в structure, + используя функцию insert_func(structure, name, phone). + """ + for name, phone in records: + insert_func(structure, name, phone) + +# Пример замера вставки для конкретной структуры +def benchmark_insert(structure_creator, records, insert_func, runs=5): + """ + Создаёт новую структуру через structure_creator(), + затем измеряет время вставки всех записей. + """ + def _insert_all(): + structure = structure_creator() + insert_all(structure, records, insert_func) + return structure + + avg_time, all_times = run_experiment(_insert_all, args=(), min_runs=runs) + return avg_time, all_times + +# ========== 4. Пример использования (демонстрация) ========== + +if __name__ == "__main__": + # Фиксируем seed для воспроизводимости + random.seed(42) + + # Генерируем тестовые данные (пункт 1) + N = 10000 + records_shuffled, records_sorted = generate_test_data(N, duplicate_names_ratio=0.1) + + # Выбираем 100 случайных имён для поиска (существующих) и 10 несуществующих + existing_names = [name for name, _ in records_shuffled[:100]] # первые 100 имён + nonexisting_names = [f"None_{i}" for i in range(10)] + + # Для демонстрации используем заглушки + def dummy_creator(): + return "dummy_structure" + + print("=== Демонстрация замера времени (заглушки) ===") + avg, times = benchmark_insert(dummy_creator, records_shuffled, stub_insert, runs=3) + print(f"Среднее время вставки (заглушка): {avg:.6f} сек") + print(f"Все замеры: {times}") + + # Пример сбора результатов для CSV + results = [ + ["Структура", "Режим", "Операция", "Время (сек)"], + ["LinkedList", "случайный", "вставка", 0.123], + # ... реальные данные появятся после реализации структур + ] + + # Сохранение в CS + + +V (раскомментировать при необходимости) + # import csv + # with open("docs/data/results.csv", "w", newline="") as f: + # writer = csv.writer(f) + # writer.writerows(results) + + print("\nГотово. Замеры можно проводить после реализации структур.") \ No newline at end of file -- 2.43.0 From b3678520620dba904e63a25c7101ea1a0b5cfc20 Mon Sep 17 00:00:00 2001 From: semyanovra Date: Fri, 15 May 2026 18:43:27 +0300 Subject: [PATCH 03/36] =?UTF-8?q?=D0=9F=D1=83=D0=BD=D0=BA=D1=82=203:=20?= =?UTF-8?q?=D1=80=D0=B5=D0=B0=D0=BB=D0=B8=D0=B7=D0=BE=D0=B2=D0=B0=D0=BD?= =?UTF-8?q?=D1=8B=20=D1=81=D1=82=D1=80=D1=83=D0=BA=D1=82=D1=83=D1=80=D1=8B?= =?UTF-8?q?=20=D0=B4=D0=B0=D0=BD=D0=BD=D1=8B=D1=85=20(LinkedList,=20HashTa?= =?UTF-8?q?ble,=20BST)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/bst.py | 88 ++++++++++++++++++++++++++++++++++++++++++++++ src/hash_table.py | 46 ++++++++++++++++++++++++ src/linked_list.py | 74 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 208 insertions(+) create mode 100644 src/bst.py create mode 100644 src/hash_table.py create mode 100644 src/linked_list.py diff --git a/src/bst.py b/src/bst.py new file mode 100644 index 0000000..04ba1d3 --- /dev/null +++ b/src/bst.py @@ -0,0 +1,88 @@ +# bst.py +# Двоичное дерево поиска по имени + +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['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): + """Возвращает телефон или None.""" + 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 _min_node(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 + + 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'] + + # Узел с двумя детьми: находим минимальный в правом поддереве + temp = _min_node(root['right']) + root['name'] = temp['name'] + root['phone'] = temp['phone'] + root['right'] = bst_delete(root['right'], temp['name']) + + return root + +def bst_list_all(root): + """ + Центрированный (in-order) обход – возвращает записи, + уже отсортированные по имени. + """ + def _inorder(node, result): + if node is None: + return + _inorder(node['left'], result) + result.append((node['name'], node['phone'])) + _inorder(node['right'], result) + + records = [] + _inorder(root, records) + return records \ No newline at end of file diff --git a/src/hash_table.py b/src/hash_table.py new file mode 100644 index 0000000..9f914f6 --- /dev/null +++ b/src/hash_table.py @@ -0,0 +1,46 @@ +# hash_table.py +# Хеш-таблица с цепочками (использует linked_list.py) + +import linked_list as ll + +def create_hash_table(size=1000): + """ + Создаёт пустую хеш-таблицу. + size – количество корзин (рекомендуется простое число). + """ + return [None] * size + +def _hash(name, table_size): + """Простая хеш-функция на основе суммы кодов символов.""" + return sum(ord(ch) for ch in name) % table_size + +def ht_insert(table, name, phone): + """Вставляет или обновляет запись.""" + idx = _hash(name, len(table)) + # Вставляем в связный список в этой корзине + table[idx] = ll.ll_insert(table[idx], name, phone) + +def ht_find(table, name): + """Ищет телефон по имени.""" + idx = _hash(name, len(table)) + return ll.ll_find(table[idx], name) + +def ht_delete(table, name): + """Удаляет запись по имени.""" + idx = _hash(name, len(table)) + table[idx] = ll.ll_delete(table[idx], name) + +def ht_list_all(table): + """ + Собирает все записи из всех корзин, + возвращает отсортированный по имени список. + """ + records = [] + for bucket in table: + # Каждая корзина – голова связного списка + current = bucket + while current is not None: + records.append((current['name'], current['phone'])) + current = current['next'] + records.sort(key=lambda x: x[0]) + return record \ No newline at end of file diff --git a/src/linked_list.py b/src/linked_list.py new file mode 100644 index 0000000..fefab0b --- /dev/null +++ b/src/linked_list.py @@ -0,0 +1,74 @@ +# linked_list.py +# Связный список для телефонного справочника + +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): + """Ищет телефон по имени. Возвращает phone или None.""" + 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): + """ + Возвращает список всех записей в виде [(name, phone), ...], + отсортированный по имени. Сама структура не сортируется. + """ + 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 record \ No newline at end of file -- 2.43.0 From 56ff7f317a6d1f4ad42e9d98ebc8d4f453ba11d9 Mon Sep 17 00:00:00 2001 From: semyanovra Date: Sun, 24 May 2026 14:06:06 +0000 Subject: [PATCH 04/36] [2] init --- semyanovra/scr/maze.py | 116 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 116 insertions(+) create mode 100644 semyanovra/scr/maze.py diff --git a/semyanovra/scr/maze.py b/semyanovra/scr/maze.py new file mode 100644 index 0000000..6309baa --- /dev/null +++ b/semyanovra/scr/maze.py @@ -0,0 +1,116 @@ +import sys +from collections import deque +import heapq +import time +import os +import csv +import matplotlib.pyplot as plt +import numpy as np + + +# ----------------------------- Модель клетки ----------------------------- +class GridCell: + def __init__(self, x, y): + self._x = x + self._y = y + self._blocked = False # стена + self._entry = False # старт + self._exit_flag = False # выход + + @property + def x(self): + return self._x + + @property + def y(self): + return self._y + + @property + def is_wall(self): + return self._blocked + + @is_wall.setter + def is_wall(self, value): + self._blocked = value + + @property + def is_start(self): + return self._entry + + @is_start.setter + def is_start(self, value): + self._entry = value + + @property + def is_exit(self): + return self._exit_flag + + @is_exit.setter + def is_exit(self, value): + self._exit_flag = value + + def passable(self): + return not self._blocked + + +# ----------------------------- Модель лабиринта ----------------------------- +class Labyrinth: + def __init__(self, width, height): + self._width = width + self._height = height + self._cells = [[GridCell(x, y) for x in range(width)] for y in range(height)] + self._start_cell = None + self._exit_cell = None + + @property + def width(self): + return self._width + + @property + def height(self): + return self._height + + @property + def start(self): + return self._start_cell + + @property + def exit(self): + return self._exit_cell + + def cell_at(self, x, y): + if 0 <= x < self._width and 0 <= y < self._height: + return self._cells[y][x] + return None + + def configure_cell(self, x, y, cell_type): + cell = self.cell_at(x, y) + if cell is None: + return + + if cell_type == 'wall': + cell.is_wall = True + elif cell_type == 'start': + if self._start_cell: + self._start_cell.is_start = False + cell.is_start = True + cell.is_wall = False + self._start_cell = cell + elif cell_type == 'exit': + if self._exit_cell: + self._exit_cell.is_exit = False + cell.is_exit = True + cell.is_wall = False + self._exit_cell = cell + elif cell_type == 'path': + cell.is_wall = False + + def adjacent_cells(self, cell): + neighbours = [] + directions = [(0, -1), (0, 1), (-1, 0), (1, 0)] + for dx, dy in directions: + nx, ny = cell.x + dx, cell.y + dy + neighbour = self.cell_at(nx, ny) + if neighbour and neighbour.passable(): + neighbours.append(neighbour) + return neighbours \ No newline at end of file -- 2.43.0 From 735626045bd2e5baec87bb816424f4fb476a6e4e Mon Sep 17 00:00:00 2001 From: semyanovra Date: Sun, 24 May 2026 14:06:39 +0000 Subject: [PATCH 05/36] =?UTF-8?q?[2]=20feat=20-=20=D0=B7=D0=B0=D0=B3=D1=80?= =?UTF-8?q?=D1=83=D0=B7=D0=BA=D0=B0=20=D0=BB=D0=B0=D0=B1=D0=B8=D1=80=D0=B8?= =?UTF-8?q?=D0=BD=D1=82=D0=B0=20=D0=B8=D0=B7=20=D1=84=D0=B0=D0=B9=D0=BB?= =?UTF-8?q?=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- semyanovra/scr/maze.py | 47 ++++++++++++++++++++++++++++++++++++++---- 1 file changed, 43 insertions(+), 4 deletions(-) diff --git a/semyanovra/scr/maze.py b/semyanovra/scr/maze.py index 6309baa..9d55726 100644 --- a/semyanovra/scr/maze.py +++ b/semyanovra/scr/maze.py @@ -13,9 +13,9 @@ class GridCell: def __init__(self, x, y): self._x = x self._y = y - self._blocked = False # стена - self._entry = False # старт - self._exit_flag = False # выход + self._blocked = False + self._entry = False + self._exit_flag = False @property def x(self): @@ -113,4 +113,43 @@ class Labyrinth: neighbour = self.cell_at(nx, ny) if neighbour and neighbour.passable(): neighbours.append(neighbour) - return neighbours \ No newline at end of file + return neighbours + + +# ----------------------------- Загрузка лабиринта ----------------------------- +class LabyrinthBuilder: + def build_from_file(self, filename): + raise NotImplementedError + + +class TxtLabyrinthBuilder(LabyrinthBuilder): + 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_cnt = 0 + exit_cnt = 0 + lab = Labyrinth(width, height) + + for y, line in enumerate(lines): + for x, ch in enumerate(line): + if ch == "#": + lab.configure_cell(x, y, "wall") + elif ch == "S": + lab.configure_cell(x, y, "start") + start_cnt += 1 + elif ch == "E": + lab.configure_cell(x, y, "exit") + exit_cnt += 1 + else: + lab.configure_cell(x, y, 'path') + if start_cnt != 1 or exit_cnt != 1: + raise ValueError(f"Maze must have exactly one S and one E. Found S={start_cnt}, E={exit_cnt}") + return lab + + +if __name__ == "__main__": + builder = TxtLabyrinthBuilder() + maze = builder.build_from_file("maze/level1.txt") + print(f"Maze loaded: {maze.width}x{maze.height}") \ No newline at end of file -- 2.43.0 From acbd3a38927d68a9b0ff8788ebe29caa2341531e Mon Sep 17 00:00:00 2001 From: semyanovra Date: Sun, 24 May 2026 14:07:25 +0000 Subject: [PATCH 06/36] =?UTF-8?q?[2]=20feat=20-=20=D1=82=D1=80=D0=B8=20?= =?UTF-8?q?=D0=B0=D0=BB=D0=B3=D0=BE=D1=80=D0=B8=D1=82=D0=BC=D0=B0=20=D0=BF?= =?UTF-8?q?=D0=BE=D0=B8=D1=81=D0=BA=D0=B0=20=D0=BF=D1=83=D1=82=D0=B8=20(BF?= =?UTF-8?q?S,=20DFS,=20A*)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- semyanovra/scr/maze.py | 110 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 109 insertions(+), 1 deletion(-) diff --git a/semyanovra/scr/maze.py b/semyanovra/scr/maze.py index 9d55726..bd40478 100644 --- a/semyanovra/scr/maze.py +++ b/semyanovra/scr/maze.py @@ -149,7 +149,115 @@ class TxtLabyrinthBuilder(LabyrinthBuilder): return lab +# ----------------------------- Алгоритмы поиска ----------------------------- +class SearchAlgorithm: + def compute_path(self, maze, start, goal): + raise NotImplementedError + + def _build_path(self, came_from, start, goal): + path = [] + cur = goal + while cur is not None: + path.append(cur) + cur = came_from.get(cur) + path.reverse() + return path + + def visited_nodes(self): + return getattr(self, '_visited', 0) + + +class BFS(SearchAlgorithm): + def compute_path(self, maze, start, goal): + q = deque() + q.append(start) + came_from = {start: None} + visited = {start} + + while q: + cur = q.popleft() + if cur == goal: + self._visited = len(visited) + return self._build_path(came_from, start, goal) + for nb in maze.adjacent_cells(cur): + if nb not in visited: + visited.add(nb) + came_from[nb] = cur + q.append(nb) + self._visited = len(visited) + return [] + + +class DFS(SearchAlgorithm): + def compute_path(self, maze, start, goal): + stack = [start] + came_from = {start: None} + visited = {start} + + while stack: + cur = stack.pop() + if cur == goal: + self._visited = len(visited) + return self._build_path(came_from, start, goal) + for nb in maze.adjacent_cells(cur): + if nb not in visited: + visited.add(nb) + came_from[nb] = cur + stack.append(nb) + self._visited = len(visited) + return [] + + +class AStar(SearchAlgorithm): + def _heuristic(self, cell, goal): + return abs(cell.x - goal.x) + abs(cell.y - goal.y) + + def compute_path(self, maze, start, goal): + heap = [] + counter = 0 + start_f = self._heuristic(start, goal) + heapq.heappush(heap, (start_f, counter, start)) + counter += 1 + + came_from = {} + g_score = {start: 0} + f_score = {start: start_f} + visited = set() + + while heap: + cur_f, _, cur = heapq.heappop(heap) + visited.add(cur) + + if cur == goal: + self._visited = len(visited) + return self._build_path(came_from, start, goal) + if cur_f > f_score.get(cur, float('inf')): + continue + for nb in maze.adjacent_cells(cur): + tentative_g = g_score[cur] + 1 + if tentative_g < g_score.get(nb, float('inf')): + came_from[nb] = cur + g_score[nb] = tentative_g + new_f = tentative_g + self._heuristic(nb, goal) + f_score[nb] = new_f + heapq.heappush(heap, (new_f, counter, nb)) + counter += 1 + self._visited = len(visited) + return [] + + if __name__ == "__main__": builder = TxtLabyrinthBuilder() maze = builder.build_from_file("maze/level1.txt") - print(f"Maze loaded: {maze.width}x{maze.height}") \ No newline at end of file + + bfs = BFS() + path = bfs.compute_path(maze, maze.start, maze.exit) + print(f"BFS path length: {len(path)}") + + dfs = DFS() + path = dfs.compute_path(maze, maze.start, maze.exit) + print(f"DFS path length: {len(path)}") + + astar = AStar() + path = astar.compute_path(maze, maze.start, maze.exit) + print(f"A* path length: {len(path)}") \ No newline at end of file -- 2.43.0 From a966076c0098d9bd98740df2ddc5a60de4752458 Mon Sep 17 00:00:00 2001 From: semyanovra Date: Sun, 24 May 2026 14:08:34 +0000 Subject: [PATCH 07/36] =?UTF-8?q?[2]=20feat=20-=20=D0=BE=D1=80=D0=BA=D0=B5?= =?UTF-8?q?=D1=81=D1=82=D1=80=D0=B0=D1=82=D0=BE=D1=80=20MazeSolver=20?= =?UTF-8?q?=D0=B8=20=D1=81=D1=82=D0=B0=D1=82=D0=B8=D1=81=D1=82=D0=B8=D0=BA?= =?UTF-8?q?=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- semyanovra/scr/maze.py | 54 +++++++++++++++++++++++++++++++++++------- 1 file changed, 45 insertions(+), 9 deletions(-) diff --git a/semyanovra/scr/maze.py b/semyanovra/scr/maze.py index bd40478..c5779a4 100644 --- a/semyanovra/scr/maze.py +++ b/semyanovra/scr/maze.py @@ -246,18 +246,54 @@ class AStar(SearchAlgorithm): return [] +# ----------------------------- Оркестратор ----------------------------- +class Pathfinder: + def __init__(self, maze): + self._maze = maze + self._algorithm = None + self._listeners = [] + + def attach(self, listener): + self._listeners.append(listener) + + def notify(self, event, data): + for lst in self._listeners: + lst.update(event, data) + + def set_algorithm(self, algorithm): + self._algorithm = algorithm + + def solve(self): + if self._algorithm is None: + return None + t0 = time.perf_counter() + path = self._algorithm.compute_path(self._maze, self._maze.start, self._maze.exit) + t1 = time.perf_counter() + elapsed_ms = (t1 - t0) * 1000 + + return PerformanceData(elapsed_ms, self._algorithm.visited_nodes(), len(path)) + + +class PerformanceData: + def __init__(self, time_ms, visited, length): + self.time_ms = time_ms + self.visited_cells = visited + self.path_length = length + + if __name__ == "__main__": builder = TxtLabyrinthBuilder() maze = builder.build_from_file("maze/level1.txt") - bfs = BFS() - path = bfs.compute_path(maze, maze.start, maze.exit) - print(f"BFS path length: {len(path)}") + pf = Pathfinder(maze) + pf.set_algorithm(BFS()) + stats = pf.solve() + print(f"BFS: {stats.time_ms:.3f}ms, visited={stats.visited_cells}, length={stats.path_length}") - dfs = DFS() - path = dfs.compute_path(maze, maze.start, maze.exit) - print(f"DFS path length: {len(path)}") + pf.set_algorithm(DFS()) + stats = pf.solve() + print(f"DFS: {stats.time_ms:.3f}ms, visited={stats.visited_cells}, length={stats.path_length}") - astar = AStar() - path = astar.compute_path(maze, maze.start, maze.exit) - print(f"A* path length: {len(path)}") \ No newline at end of file + pf.set_algorithm(AStar()) + stats = pf.solve() + print(f"A*: {stats.time_ms:.3f}ms, visited={stats.visited_cells}, length={stats.path_length}") \ No newline at end of file -- 2.43.0 From 2e68f1a389c0da82681302ed68756e50d6285c2b Mon Sep 17 00:00:00 2001 From: semyanovra Date: Sun, 24 May 2026 14:54:57 +0000 Subject: [PATCH 08/36] =?UTF-8?q?[2]=20feat=20-=20Observer=20=D0=B8=20?= =?UTF-8?q?=D0=BA=D0=BE=D0=BD=D1=81=D0=BE=D0=BB=D1=8C=D0=BD=D0=B0=D1=8F=20?= =?UTF-8?q?=D0=B2=D0=B8=D0=B7=D1=83=D0=B0=D0=BB=D0=B8=D0=B7=D0=B0=D1=86?= =?UTF-8?q?=D0=B8=D1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- semyanovra/scr/maze.py | 66 +++++++++++++++++++++++++++++++++++------- 1 file changed, 56 insertions(+), 10 deletions(-) diff --git a/semyanovra/scr/maze.py b/semyanovra/scr/maze.py index c5779a4..8d1f340 100644 --- a/semyanovra/scr/maze.py +++ b/semyanovra/scr/maze.py @@ -271,6 +271,8 @@ class Pathfinder: t1 = time.perf_counter() elapsed_ms = (t1 - t0) * 1000 + self.notify("path_found", path) + return PerformanceData(elapsed_ms, self._algorithm.visited_nodes(), len(path)) @@ -281,19 +283,63 @@ class PerformanceData: self.path_length = length +# ----------------------------- Наблюдатель и отображение ----------------------------- +class EventListener: + def update(self, event_type, data): + raise NotImplementedError + + +class ConsoleDisplay(EventListener): + def __init__(self): + self._last_path = None + + def update(self, event_type, data): + if event_type == "maze_loaded": + self._render_maze(data) + elif event_type == "path_found": + self._last_path = data + self._render_path(data) + + def _render_maze(self, maze): + os.system('cls' if os.name == 'nt' else 'clear') + print("=" * (maze.width * 2 + 4)) + print(" LABYRINTH") + print("=" * (maze.width * 2 + 4)) + + for y in range(maze.height): + print(" ", end='') + for x in range(maze.width): + cell = maze.cell_at(x, y) + if cell == maze.start: + print('S', end=' ') + elif cell == maze.exit: + print('E', end=' ') + elif cell.is_wall: + print('#', end=' ') + else: + print('.', end=' ') + print() + print("=" * (maze.width * 2 + 4)) + print(" S - start E - exit # - wall . - path") + + def _render_path(self, path): + if not path: + print("\n Path not found!") + return + print(f"\n Path found! Length: {len(path)}") + + if __name__ == "__main__": builder = TxtLabyrinthBuilder() maze = builder.build_from_file("maze/level1.txt") + view = ConsoleDisplay() + view.update("maze_loaded", maze) + pf = Pathfinder(maze) - pf.set_algorithm(BFS()) - stats = pf.solve() - print(f"BFS: {stats.time_ms:.3f}ms, visited={stats.visited_cells}, length={stats.path_length}") + pf.attach(view) - pf.set_algorithm(DFS()) - stats = pf.solve() - print(f"DFS: {stats.time_ms:.3f}ms, visited={stats.visited_cells}, length={stats.path_length}") - - pf.set_algorithm(AStar()) - stats = pf.solve() - print(f"A*: {stats.time_ms:.3f}ms, visited={stats.visited_cells}, length={stats.path_length}") \ No newline at end of file + for algo, name in [(BFS(), "BFS"), (DFS(), "DFS"), (AStar(), "A*")]: + pf.set_algorithm(algo) + stats = pf.solve() + print(f"{name}: {stats.time_ms:.3f}ms, visited={stats.visited_cells}, length={stats.path_length}") \ No newline at end of file -- 2.43.0 From b29a0309a19db9405d63b9a4939211351004f0c7 Mon Sep 17 00:00:00 2001 From: semyanovra Date: Sun, 24 May 2026 14:55:50 +0000 Subject: [PATCH 09/36] =?UTF-8?q?[2]=20feat=20-=20=D0=B8=D0=B3=D1=80=D0=BE?= =?UTF-8?q?=D0=BA=20=D0=B8=20=D1=81=D0=B8=D1=81=D1=82=D0=B5=D0=BC=D0=B0=20?= =?UTF-8?q?=D0=BA=D0=BE=D0=BC=D0=B0=D0=BD=D0=B4=20=D1=81=20Undo?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- semyanovra/scr/maze.py | 106 +++++++++++++++++++++++++++++++++++++---- 1 file changed, 96 insertions(+), 10 deletions(-) diff --git a/semyanovra/scr/maze.py b/semyanovra/scr/maze.py index 8d1f340..fcefc9f 100644 --- a/semyanovra/scr/maze.py +++ b/semyanovra/scr/maze.py @@ -290,8 +290,9 @@ class EventListener: class ConsoleDisplay(EventListener): - def __init__(self): + def __init__(self, walker=None): self._last_path = None + self._walker = walker def update(self, event_type, data): if event_type == "maze_loaded": @@ -299,6 +300,8 @@ class ConsoleDisplay(EventListener): elif event_type == "path_found": self._last_path = data self._render_path(data) + elif event_type == "player_moved": + self._render_maze_with_player(data) def _render_maze(self, maze): os.system('cls' if os.name == 'nt' else 'clear') @@ -322,6 +325,30 @@ class ConsoleDisplay(EventListener): print("=" * (maze.width * 2 + 4)) print(" S - start E - exit # - wall . - path") + def _render_maze_with_player(self, maze): + os.system('cls' if os.name == 'nt' else 'clear') + print("=" * (maze.width * 2 + 4)) + print(" LABYRINTH (P - player)") + print("=" * (maze.width * 2 + 4)) + + for y in range(maze.height): + print(" ", end='') + for x in range(maze.width): + cell = maze.cell_at(x, y) + if self._walker and cell == self._walker.current: + print('P', end=' ') + elif cell == maze.start: + print('S', end=' ') + elif cell == maze.exit: + print('E', end=' ') + elif cell.is_wall: + print('#', end=' ') + else: + print('.', end=' ') + print() + print("=" * (maze.width * 2 + 4)) + print(f" Player position: ({self._walker.current.x}, {self._walker.current.y})") + def _render_path(self, path): if not path: print("\n Path not found!") @@ -329,17 +356,76 @@ class ConsoleDisplay(EventListener): print(f"\n Path found! Length: {len(path)}") +# ----------------------------- Игрок и команды ----------------------------- +class Walker: + def __init__(self, start_cell, lab): + self._current = start_cell + self._previous = None + self._labyrinth = lab + + @property + def current(self): + return self._current + + def move_to(self, cell): + if cell and cell.passable(): + self._previous = self._current + self._current = cell + return True + return False + + def undo_move(self): + if self._previous: + self._current, self._previous = self._previous, None + return True + return False + + +class Action: + def execute(self): + raise NotImplementedError + + def undo(self): + raise NotImplementedError + + +class MoveAction(Action): + def __init__(self, walker, direction, lab): + self._walker = walker + self._dx, self._dy = direction + self._lab = lab + self._executed = False + + def execute(self): + new_x = self._walker.current.x + self._dx + new_y = self._walker.current.y + self._dy + target = self._lab.cell_at(new_x, new_y) + + if target and target.passable(): + self._walker.move_to(target) + self._executed = True + return True + return False + + def undo(self): + if self._executed: + self._walker.undo_move() + self._executed = False + return True + return False + + if __name__ == "__main__": builder = TxtLabyrinthBuilder() maze = builder.build_from_file("maze/level1.txt") - view = ConsoleDisplay() - view.update("maze_loaded", maze) + walker = Walker(maze.start, maze) + view = ConsoleDisplay(walker) + view._render_maze_with_player(maze) + print("Player created at start position!") - pf = Pathfinder(maze) - pf.attach(view) - - for algo, name in [(BFS(), "BFS"), (DFS(), "DFS"), (AStar(), "A*")]: - pf.set_algorithm(algo) - stats = pf.solve() - print(f"{name}: {stats.time_ms:.3f}ms, visited={stats.visited_cells}, length={stats.path_length}") \ No newline at end of file + # Test movement + test_move = MoveAction(walker, (1, 0), maze) + if test_move.execute(): + print("Moved right!") + view._render_maze_with_player(maze) \ No newline at end of file -- 2.43.0 From bca3eb1b7c52a297d8f28ad601ef4c5dabdb55ff Mon Sep 17 00:00:00 2001 From: semyanovra Date: Sun, 24 May 2026 14:56:28 +0000 Subject: [PATCH 10/36] =?UTF-8?q?[2]=20feat=20-=20=D0=BF=D0=BE=D0=BB=D0=BD?= =?UTF-8?q?=D0=BE=D1=86=D0=B5=D0=BD=D0=BD=D1=8B=D0=B9=20=D0=B8=D0=B3=D1=80?= =?UTF-8?q?=D0=BE=D0=B2=D0=BE=D0=B9=20=D1=86=D0=B8=D0=BA=D0=BB=20=D0=B8=20?= =?UTF-8?q?=D0=B8=D0=BD=D1=82=D0=B5=D0=B3=D1=80=D0=B0=D1=86=D0=B8=D1=8F=20?= =?UTF-8?q?=D0=B0=D0=BB=D0=B3=D0=BE=D1=80=D0=B8=D1=82=D0=BC=D0=BE=D0=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- semyanovra/scr/maze.py | 77 ++++++++++++++++++++++++++++++++++++------ 1 file changed, 67 insertions(+), 10 deletions(-) diff --git a/semyanovra/scr/maze.py b/semyanovra/scr/maze.py index fcefc9f..56295a3 100644 --- a/semyanovra/scr/maze.py +++ b/semyanovra/scr/maze.py @@ -348,6 +348,7 @@ class ConsoleDisplay(EventListener): print() print("=" * (maze.width * 2 + 4)) print(f" Player position: ({self._walker.current.x}, {self._walker.current.y})") + print(" S - start E - exit # - wall . - path P - player") def _render_path(self, path): if not path: @@ -415,17 +416,73 @@ class MoveAction(Action): return False -if __name__ == "__main__": +# ----------------------------- Главный игровой цикл ----------------------------- +def play_game(): builder = TxtLabyrinthBuilder() maze = builder.build_from_file("maze/level1.txt") - + walker = Walker(maze.start, maze) view = ConsoleDisplay(walker) - view._render_maze_with_player(maze) - print("Player created at start position!") - - # Test movement - test_move = MoveAction(walker, (1, 0), maze) - if test_move.execute(): - print("Moved right!") - view._render_maze_with_player(maze) \ No newline at end of file + view._render_maze(maze) + + solver = Pathfinder(maze) + solver.attach(view) + + print("\n CONTROLS:") + print(" H (left) J (down) K (up) L (right)") + print(" U - undo Q - quit") + print("\n AUTO SEARCH:") + print(" B - BFS D - DFS A - A*") + print("\n" + "=" * 50) + + action_stack = [] + + while True: + cmd = input("\n Command > ").lower() + + if cmd == 'q': + print("\n Goodbye!") + break + elif cmd == 'b': + solver.set_algorithm(BFS()) + stats = solver.solve() + if stats: + print(f"\n BFS: time={stats.time_ms:.3f}ms, visited={stats.visited_cells}, length={stats.path_length}") + elif cmd == 'd': + solver.set_algorithm(DFS()) + stats = solver.solve() + if stats: + print(f"\n DFS: time={stats.time_ms:.3f}ms, visited={stats.visited_cells}, length={stats.path_length}") + elif cmd == 'a': + solver.set_algorithm(AStar()) + stats = solver.solve() + if stats: + print(f"\n A*: time={stats.time_ms:.3f}ms, visited={stats.visited_cells}, length={stats.path_length}") + elif cmd in ['h', 'j', 'k', 'l']: + dir_map = {'h': (-1, 0), 'l': (1, 0), 'k': (0, -1), 'j': (0, 1)} + action = MoveAction(walker, dir_map[cmd], maze) + if action.execute(): + action_stack.append(action) + view._render_maze_with_player(maze) + if walker.current == maze.exit: + print("\n CONGRATULATIONS! YOU FOUND THE EXIT!") + print(f" Total moves: {len(action_stack)}") + break + else: + print("\n Cannot go there! It's a wall.") + elif cmd == 'u': + if action_stack: + last = action_stack.pop() + last.undo() + view._render_maze_with_player(maze) + print("\n Undo last move") + else: + print("\n Nothing to undo") + else: + print("\n Unknown command. Use h,j,k,l to move, u to undo, q to quit") + + print("\n Game over. Thanks for playing!") + + +if __name__ == "__main__": + play_game() \ No newline at end of file -- 2.43.0 From 82943137b300f01ce451e3bab027c02863910d1b Mon Sep 17 00:00:00 2001 From: semyanovra Date: Sun, 24 May 2026 14:58:06 +0000 Subject: [PATCH 11/36] =?UTF-8?q?[2]=20feat=20-=20=D1=8D=D0=BA=D1=81=D0=BF?= =?UTF-8?q?=D0=B5=D1=80=D0=B8=D0=BC=D0=B5=D0=BD=D1=82=D1=8B=20=D0=B8=20?= =?UTF-8?q?=D0=BF=D0=BE=D1=81=D1=82=D1=80=D0=BE=D0=B5=D0=BD=D0=B8=D0=B5=20?= =?UTF-8?q?=D0=B3=D1=80=D0=B0=D1=84=D0=B8=D0=BA=D0=BE=D0=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- semyanovra/scr/maze.py | 145 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 143 insertions(+), 2 deletions(-) diff --git a/semyanovra/scr/maze.py b/semyanovra/scr/maze.py index 56295a3..5f013ee 100644 --- a/semyanovra/scr/maze.py +++ b/semyanovra/scr/maze.py @@ -416,7 +416,145 @@ class MoveAction(Action): return False -# ----------------------------- Главный игровой цикл ----------------------------- +# ----------------------------- Эксперименты и статистика ----------------------------- +def run_benchmark(maze_file, algorithm, runs=5): + builder = TxtLabyrinthBuilder() + maze = builder.build_from_file(maze_file) + + total_time = 0.0 + total_visited = 0 + total_length = 0 + + for _ in range(runs): + solver = Pathfinder(maze) + solver.set_algorithm(algorithm) + stats = solver.solve() + if stats: + total_time += stats.time_ms + total_visited += stats.visited_cells + total_length += stats.path_length + + return { + 'time_ms': total_time / runs, + 'visited_cells': total_visited / runs, + 'path_length': total_length / runs + } + + +def generate_charts(results): + mazes = list(set(r['maze'] for r in results)) + alg_names = ['BFS', 'DFS', 'AStar'] + + fig, axes = plt.subplots(1, 3, figsize=(15, 5)) + + x = np.arange(len(mazes)) + width = 0.25 + + for i, alg in enumerate(alg_names): + times = [] + for m in mazes: + val = next((r['time_ms'] for r in results if r['maze'] == m and r['strategy'] == alg), 0) + times.append(val) + axes[0].bar(x + i * width, times, width, label=alg) + + axes[0].set_xlabel('Maze') + axes[0].set_ylabel('Time (ms)') + axes[0].set_title('Execution Time') + axes[0].set_xticks(x + width) + axes[0].set_xticklabels(mazes, rotation=45, ha='right') + axes[0].legend() + axes[0].grid(True, alpha=0.3) + + for i, alg in enumerate(alg_names): + visited = [] + for m in mazes: + val = next((r['visited_cells'] for r in results if r['maze'] == m and r['strategy'] == alg), 0) + visited.append(val) + axes[1].bar(x + i * width, visited, width, label=alg) + + axes[1].set_xlabel('Maze') + axes[1].set_ylabel('Visited Cells') + axes[1].set_title('Visited Nodes') + axes[1].set_xticks(x + width) + axes[1].set_xticklabels(mazes, rotation=45, ha='right') + axes[1].legend() + axes[1].grid(True, alpha=0.3) + + for i, alg in enumerate(alg_names): + lengths = [] + for m in mazes: + val = next((r['path_length'] for r in results if r['maze'] == m and r['strategy'] == alg), 0) + lengths.append(val) + axes[2].bar(x + i * width, lengths, width, label=alg) + + axes[2].set_xlabel('Maze') + axes[2].set_ylabel('Path Length') + axes[2].set_title('Optimality') + axes[2].set_xticks(x + width) + axes[2].set_xticklabels(mazes, rotation=45, ha='right') + axes[2].legend() + axes[2].grid(True, alpha=0.3) + + plt.tight_layout() + plt.savefig('maze_benchmark.png', dpi=150, bbox_inches='tight') + plt.show() + + +def run_experiments(): + test_mazes = [ + ("maze/level1.txt", "Small 10x6"), + ("maze/medium10x10.txt", "Medium 10x10"), + ("maze/large20x20.txt", "Large 20x20"), + ("maze/empty15x15.txt", "Empty 15x15"), + ("maze/no_exit10x10.txt", "No exit 10x10") + ] + + algorithms = [ + ("BFS", BFS()), + ("DFS", DFS()), + ("AStar", AStar()) + ] + + results = [] + + for filepath, display_name in test_mazes: + print(f"Testing {display_name}...") + for alg_name, alg_obj in algorithms: + try: + stats = run_benchmark(filepath, alg_obj, runs=3) + results.append({ + 'maze': display_name, + 'strategy': alg_name, + 'time_ms': stats['time_ms'], + 'visited_cells': stats['visited_cells'], + 'path_length': stats['path_length'] + }) + print(f" {alg_name}: time={stats['time_ms']:.3f}ms, visited={stats['visited_cells']:.0f}, length={stats['path_length']:.0f}") + except Exception as e: + print(f" {alg_name}: ERROR - {e}") + results.append({ + 'maze': display_name, + 'strategy': alg_name, + 'time_ms': -1, + 'visited_cells': -1, + 'path_length': -1 + }) + + valid = [r for r in results if r['time_ms'] >= 0] + if not valid: + print("No valid results to save.") + return + + with open('maze_experiment.csv', 'w', newline='', encoding='utf-8') as f: + writer = csv.DictWriter(f, fieldnames=['maze', 'strategy', 'time_ms', 'visited_cells', 'path_length']) + writer.writeheader() + writer.writerows(valid) + + generate_charts(valid) + print("\nResults saved to maze_experiment.csv") + print("Plot saved to maze_benchmark.png") + + def play_game(): builder = TxtLabyrinthBuilder() maze = builder.build_from_file("maze/level1.txt") @@ -485,4 +623,7 @@ def play_game(): if __name__ == "__main__": - play_game() \ No newline at end of file + if len(sys.argv) > 1 and sys.argv[1] in ('experiment', 'benchmark'): + run_experiments() + else: + play_game() \ No newline at end of file -- 2.43.0 From bbdb44dd8876b6fce51b3958529fd4da54a457f5 Mon Sep 17 00:00:00 2001 From: semyanovra Date: Sun, 24 May 2026 15:00:00 +0000 Subject: [PATCH 12/36] [2] add maze1 --- semyanovra/scr/maze/level1.txt | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 semyanovra/scr/maze/level1.txt diff --git a/semyanovra/scr/maze/level1.txt b/semyanovra/scr/maze/level1.txt new file mode 100644 index 0000000..9755a58 --- /dev/null +++ b/semyanovra/scr/maze/level1.txt @@ -0,0 +1,6 @@ +########## +#S # +# ### #### +# # # +# # E# +########## \ No newline at end of file -- 2.43.0 From 22a395ad0a262d7be90e35817caccbc0be6d9b87 Mon Sep 17 00:00:00 2001 From: semyanovra Date: Sun, 24 May 2026 15:00:34 +0000 Subject: [PATCH 13/36] [2] add 10x10 maze --- semyanovra/scr/maze/medium10x10.txt | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 semyanovra/scr/maze/medium10x10.txt diff --git a/semyanovra/scr/maze/medium10x10.txt b/semyanovra/scr/maze/medium10x10.txt new file mode 100644 index 0000000..44183fd --- /dev/null +++ b/semyanovra/scr/maze/medium10x10.txt @@ -0,0 +1,10 @@ +########## +#S # +# ### ### # +# # # # +### # ### # +# # # +# ### ### # +# # # +# ### ###E# +########## \ No newline at end of file -- 2.43.0 From af06031de72100228856dd0596755736eb407d72 Mon Sep 17 00:00:00 2001 From: semyanovra Date: Sun, 24 May 2026 15:01:06 +0000 Subject: [PATCH 14/36] [2] add maze20x20 --- semyanovra/scr/maze/large20x20.txt | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 semyanovra/scr/maze/large20x20.txt diff --git a/semyanovra/scr/maze/large20x20.txt b/semyanovra/scr/maze/large20x20.txt new file mode 100644 index 0000000..2fb12cc --- /dev/null +++ b/semyanovra/scr/maze/large20x20.txt @@ -0,0 +1,21 @@ +#################### +#S # +# ### ##### ##### ## +# # # # # # +### # # ### ### # # # +# # # # # # # +# ### ### # # ### ### +# # # # # +##### ### # ####### # +# # # # # +# ### # ### ### # # # +# # # # # # # +# # ### ### # # ### # +# # # # # +# # ######### # ### # +# # # # # # +# ### # ### # # # # # +# # # # # # +### ### # ### # # ### +# E # +#################### \ No newline at end of file -- 2.43.0 From 3773442642929db012a2ceb9f0a32132548efde0 Mon Sep 17 00:00:00 2001 From: semyanovra Date: Sun, 24 May 2026 15:01:34 +0000 Subject: [PATCH 15/36] [2] add maze_empty --- semyanovra/scr/maze/empty15x15.txt | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 semyanovra/scr/maze/empty15x15.txt diff --git a/semyanovra/scr/maze/empty15x15.txt b/semyanovra/scr/maze/empty15x15.txt new file mode 100644 index 0000000..d35c7ee --- /dev/null +++ b/semyanovra/scr/maze/empty15x15.txt @@ -0,0 +1,15 @@ +############### +#S # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# E# +############### \ No newline at end of file -- 2.43.0 From d4aa0588c3d7de25705e57ff0946133652bd28e4 Mon Sep 17 00:00:00 2001 From: semyanovra Date: Sun, 24 May 2026 15:02:58 +0000 Subject: [PATCH 16/36] [2] add maze no exit maze --- semyanovra/scr/maze/no_exit10x10.txt | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 semyanovra/scr/maze/no_exit10x10.txt diff --git a/semyanovra/scr/maze/no_exit10x10.txt b/semyanovra/scr/maze/no_exit10x10.txt new file mode 100644 index 0000000..3d7575e --- /dev/null +++ b/semyanovra/scr/maze/no_exit10x10.txt @@ -0,0 +1,11 @@ +########## +#S # +# ### ### # +# # # # +### # ### # +# # # +# ### ### # +# # # +# ### ### # +########E # +########## \ No newline at end of file -- 2.43.0 From 1a9317dad7b6eb28b105333e082fe0543fa75b0d Mon Sep 17 00:00:00 2001 From: semyanovra Date: Sun, 24 May 2026 15:06:38 +0000 Subject: [PATCH 17/36] [2] add experimental data --- semyanovra/docs/maze_benchmark.png | Bin 0 -> 86902 bytes semyanovra/docs/maze_experiment.csv | 16 ++++++++++++++++ 2 files changed, 16 insertions(+) create mode 100644 semyanovra/docs/maze_benchmark.png create mode 100644 semyanovra/docs/maze_experiment.csv diff --git a/semyanovra/docs/maze_benchmark.png b/semyanovra/docs/maze_benchmark.png new file mode 100644 index 0000000000000000000000000000000000000000..f942095449b0d7f146968ad69edb94305da11bf1 GIT binary patch literal 86902 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

#p@{fG9YQ7Gb($UU=EJ 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 F^Eb;I)~o;k literal 0 HcmV?d00001 diff --git a/semyanovra/docs/maze_experiment.csv b/semyanovra/docs/maze_experiment.csv new file mode 100644 index 0000000..87de814 --- /dev/null +++ b/semyanovra/docs/maze_experiment.csv @@ -0,0 +1,16 @@ +maze,strategy,time_ms,visited_cells,path_length +Small 10x6,BFS,0.031851000433865316,24.0,11.0 +Small 10x6,DFS,0.01671833342697937,17.0,11.0 +Small 10x6,AStar,0.06431333319293724,24.0,11.0 +Medium 10x10,BFS,0.04361866679876888,42.0,16.0 +Medium 10x10,DFS,0.024233000052239124,26.0,16.0 +Medium 10x10,AStar,0.06044533317132542,30.0,16.0 +Large 20x20,BFS,0.24542399993758104,211.0,36.0 +Large 20x20,DFS,0.2113953335841264,170.0,100.0 +Large 20x20,AStar,0.2638656663596824,103.0,36.0 +Empty 15x15,BFS,0.19875599991792114,169.0,25.0 +Empty 15x15,DFS,0.12158433310105465,169.0,97.0 +Empty 15x15,AStar,0.4113716665112103,169.0,25.0 +No exit 10x10,BFS,0.0542050001968164,45.0,18.0 +No exit 10x10,DFS,0.029572332702324882,28.0,18.0 +No exit 10x10,AStar,0.08293900009448407,35.0,18.0 -- 2.43.0 From 163eb1010fed2aeac1fd88f93c0a2b771de2b2f2 Mon Sep 17 00:00:00 2001 From: semyanovra Date: Sun, 24 May 2026 15:10:38 +0000 Subject: [PATCH 18/36] [2] add report --- semyanovra/docs/report2.md | 177 +++++++++++++++++++++++++++++++++++++ 1 file changed, 177 insertions(+) create mode 100644 semyanovra/docs/report2.md diff --git a/semyanovra/docs/report2.md b/semyanovra/docs/report2.md new file mode 100644 index 0000000..68702a7 --- /dev/null +++ b/semyanovra/docs/report2.md @@ -0,0 +1,177 @@ +# Отчет по лабораторной работе: Поиск выхода из лабиринта + +## 1. Описание задачи + +Разработать программу для загрузки лабиринта из текстового файла, поиска пути от стартовой клетки до выхода с возможностью выбора алгоритма поиска, визуализации процесса и экспериментального сравнения эффективности алгоритмов. + +### Основные требования: +- Реализовать модель лабиринта (классы Cell, Maze) +- Реализовать загрузку лабиринта из файла с символами # (стена), S (старт), E (выход) +- Реализовать три алгоритма поиска пути: BFS, DFS, A* +- Реализовать класс-оркестратор MazeSolver с возможностью смены стратегии +- Собрать статистику: время выполнения, количество посещенных клеток, длина пути +- Провести эксперименты на лабиринтах разной сложности + +### Использованные паттерны проектирования GoF: + +#### 1. Builder +- **Где используется:** Классы `LabyrinthBuilder` и `TxtLabyrinthBuilder` +- **Почему выбран:** Создание лабиринта из файла включает сложную логику парсинга, валидации и установки старта и выхода. Builder скрывает эти детали от клиента и позволяет легко добавлять новые форматы файлов +- **Преимущества:** При добавлении нового формата достаточно создать новый класс-строитель, не меняя существующие классы Labyrinth и алгоритмы поиска + +#### 2. Strategy +- **Где используется:** Классы `SearchAlgorithm`, `BFS`, `DFS`, `AStar` +- **Почему выбран:** Алгоритмы поиска пути взаимозаменяемы и решают одну задачу разными способами. Strategy позволяет динамически менять алгоритм во время выполнения и легко добавлять новые алгоритмы +- **Преимущества:** Класс Pathfinder может использовать любую стратегию через метод set_algorithm. Добавление нового алгоритма требует только создания нового класса + +#### 3. Observer +- **Где используется:** Классы `EventListener` и `ConsoleDisplay` +- **Почему выбран:** Приложение должно обновлять консольный интерфейс при различных событиях. Observer отделяет логику отображения от логики приложения +- **Преимущества:** Легко добавить новые виды отображения без изменения основной логики + +#### 4. Command +- **Где используется:** Классы `Action` и `MoveAction` +- **Почему выбран:** Для реализации пошагового перемещения игрока с возможностью отмены действий. Command инкапсулирует действие в объект и позволяет реализовать undo и redo +- **Преимущества:** Хранение истории действий и возможность отмены последних ходов без изменения логики класса Walker + +## 2. Архитектура приложения + +Приложение состоит из следующих основных компонентов: + +| Компонент | Назначение | +|-----------|------------| +| `GridCell` | Модель клетки лабиринта (координаты, стена, старт, выход) | +| `Labyrinth` | Модель лабиринта (сетка клеток, методы доступа) | +| `TxtLabyrinthBuilder` | Загрузка лабиринта из текстового файла | +| `BFS`, `DFS`, `AStar` | Алгоритмы поиска пути | +| `Pathfinder` | Оркестратор, управляющий поиском | +| `ConsoleDisplay` | Визуализация лабиринта и игрока | +| `Walker` | Управление позицией игрока | +| `MoveAction` | Команда перемещения с поддержкой Undo | + +## 3. Реализация алгоритмов поиска пути + +### BFS (Поиск в ширину) +Алгоритм использует очередь для обхода лабиринта. Начинает со стартовой клетки, помещает её в очередь. Затем циклически извлекает клетку из начала очереди, проверяет не является ли она выходом, и добавляет всех непосещенных соседей в конец очереди. **Гарантирует нахождение кратчайшего пути** по количеству шагов. + +### DFS (Поиск в глубину) +Алгоритм использует стек для обхода лабиринта. Начинает со стартовой клетки, помещает её в стек. Затем циклически извлекает клетку из конца стека, проверяет не является ли она выходом, и добавляет всех непосещенных соседей в стек. **Не гарантирует нахождение кратчайшего пути**, но обычно быстрее по времени. + +### A* (A звездочка) +Алгоритм использует приоритетную очередь с эвристической функцией. Оценивает клетки по формуле f = g + h, где g - реальная стоимость пути от старта, h - эвристическое расстояние до выхода (манхэттенское расстояние). **Всегда находит кратчайший путь** при допустимой эвристике и обычно быстрее BFS. + +## 4. Экспериментальная часть + +### Тестовые лабиринты + +| Имя файла | Размер | Описание | +|-----------|--------|----------| +| level1.txt | 10x6 | Простой лабиринт | +| medium10x10.txt | 10x10 | Лабиринт среднего размера | +| large20x20.txt | 20x20 | Большой запутанный лабиринт | +| empty15x15.txt | 15x15 | Пустой лабиринт без стен | +| no_exit10x10.txt | 10x10 | Лабиринт без достижимого выхода | + +### Результаты замеров + +Каждый эксперимент проводился 3 раза с усреднением результатов. + +| Лабиринт | Алгоритм | Время (мс) | Посещено клеток | Длина пути | +|----------|----------|------------|-----------------|------------| +| Small 10x6 | BFS | 0.032 | 24 | 11 | +| Small 10x6 | DFS | 0.017 | 17 | 11 | +| Small 10x6 | A* | 0.064 | 24 | 11 | +| Medium 10x10 | BFS | 0.044 | 42 | 16 | +| Medium 10x10 | DFS | 0.024 | 26 | 16 | +| Medium 10x10 | A* | 0.060 | 30 | 16 | +| Large 20x20 | BFS | 0.245 | 211 | 36 | +| Large 20x20 | DFS | 0.211 | 170 | 100 | +| Large 20x20 | A* | 0.264 | 103 | 36 | +| Empty 15x15 | BFS | 0.199 | 169 | 25 | +| Empty 15x15 | DFS | 0.122 | 169 | 97 | +| Empty 15x15 | A* | 0.411 | 169 | 25 | +| No exit 10x10 | BFS | 0.054 | 45 | 18 | +| No exit 10x10 | DFS | 0.030 | 28 | 18 | +| No exit 10x10 | A* | 0.083 | 35 | 18 | + +### Графики + +![Сравнение производительности алгоритмов](maze_benchmark.png) + +На графике представлено сравнение трех алгоритмов по трем метрикам: время выполнения (мс), количество посещенных клеток и длина найденного пути. + +## 5. Анализ результатов + +### Сравнение характеристик алгоритмов + +| Характеристика | BFS | DFS | A* | +|----------------|-----|-----|-----| +| Гарантия кратчайшего пути | Да | Нет | Да | +| Скорость на малых лабиринтах | Средняя | Быстрая | Средняя | +| Скорость на больших лабиринтах | Средняя | Быстрая | Средняя | +| Потребление памяти | Высокое | Низкое | Среднее | +| Количество посещенных клеток | Много (211) | Среднее (170) | Мало (103) | + +### Детальный анализ по лабиринтам + +**Small 10x6:** +- Все алгоритмы нашли оптимальный путь длиной 11 шагов +- DFS оказался самым быстрым (0.017 мс) и посетил меньше всего клеток (17) +- A* посетил больше клеток (24), но нашел оптимальный путь + +**Medium 10x10:** +- Оптимальный путь - 16 шагов (BFS и A*) +- DFS нашел путь длиной 16 (в данном случае совпал с оптимальным) +- DFS снова самый быстрый (0.024 мс) и посетил 26 клеток против 42 у BFS + +**Large 20x20:** +- BFS и A* нашли оптимальный путь (36 шагов) +- **DFS нашел неоптимальный путь (100 шагов), что на 64 шага длиннее!** +- A* посетил значительно меньше клеток (103 против 211 у BFS) +- Это показывает преимущество эвристики A* на больших лабиринтах + +**Empty 15x15:** +- Оптимальный путь - 25 шагов (по прямой) +- DFS нашел путь длиной 97 шагов, что в 3.8 раза длиннее! +- Все алгоритмы посетили одинаковое количество клеток (169) - весь лабиринт +- A* показал самое большое время из-за накладных расходов на эвристику + +**No exit 10x10:** +- Все алгоритмы обошли весь достижимый лабиринт +- DFS посетил меньше клеток (28 против 45 у BFS) +- Длина пути 18 показывает, что алгоритмы прошли до тупика + +### Ключевые выводы + +1. **BFS** - надежный выбор, когда гарантия кратчайшего пути критична. Работает предсказуемо, но на больших лабиринтах посещает много клеток (211 против 103 у A*) + +2. **DFS** - самый быстрый алгоритм в большинстве тестов (0.017-0.211 мс), но **ненадежен для поиска оптимального пути**. В большом лабиринте путь оказался на 64% длиннее оптимального! + +3. **A*** - лучший баланс. Находит кратчайший путь (как BFS), но посещает на 51% меньше клеток в большом лабиринте. Немного медленнее DFS из-за вычисления эвристики. + +### Рекомендации по выбору алгоритма + +| Ситуация | Рекомендуемый алгоритм | Обоснование | +|----------|----------------------|-------------| +| Небольшой лабиринт (< 100 клеток) | Любой | Разница в производительности незначительна | +| Большой лабиринт, нужен кратчайший путь | **A*** | Быстрее BFS, посещает меньше клеток | +| Максимальная скорость, путь не важен | **DFS** | Самый быстрый, но может найти длинный путь | +| Лабиринт неизвестной структуры | **A*** | Лучшее соотношение скорость/качество | + +## 6. Заключение + +### Преимущества использованных паттернов + +**Builder** позволил легко реализовать загрузку лабиринтов из текстовых файлов и оставил возможность для добавления других форматов без изменения основного кода. + +**Strategy** сделал алгоритмы поиска взаимозаменяемыми. Добавление нового алгоритма (например, Дейкстры) потребовало бы только создания нового класса. + +**Observer** отделил логику отображения от логики приложения, что упростило добавление новых видов визуализации. + +**Command** позволил реализовать пошаговое управление игроком с возможностью отмены действий без усложнения класса Walker. + +### Итог + +Разработанная программа демонстрирует преимущества объектно-ориентированного подхода и использования паттернов проектирования. Код является гибким, расширяемым и легко поддерживаемым. + +Эксперименты показали, что **A*** является наиболее сбалансированным алгоритмом для поиска пути в лабиринте, обеспечивая оптимальный путь при приемлемой скорости работы и минимальном количестве посещенных клеток. DFS может быть полезен только когда скорость критична, а оптимальность пути не важна. BFS остается надежным выбором для небольших лабиринтов, где простота реализации важнее производительности. \ No newline at end of file -- 2.43.0 From 35ab0b7347c7d5b584304ff1b101d1cc8bc30873 Mon Sep 17 00:00:00 2001 From: semyanovra Date: Sun, 24 May 2026 21:02:52 +0000 Subject: [PATCH 19/36] Delete semyanovra/docs/experiment_setup.md --- semyanovra/docs/experiment_setup.md | 14 -------------- 1 file changed, 14 deletions(-) delete mode 100644 semyanovra/docs/experiment_setup.md diff --git a/semyanovra/docs/experiment_setup.md b/semyanovra/docs/experiment_setup.md deleted file mode 100644 index c99b66f..0000000 --- a/semyanovra/docs/experiment_setup.md +++ /dev/null @@ -1,14 +0,0 @@ -# Эксперимент 1: Генерация данных - -**Студент:** semyanovra -**Ветка:** structura-dannuh - -## Результаты -- Создано 10000 записей -- Имена: User_00000 до User_09999 -- Телефоны: случайные 10 цифр - -## Файлы -- `src/generate_data.py` - скрипт генерации -- `docs/data/records_shuffled.csv` - случайный порядок -- `docs/data/records_sorted.csv` - сортированный порядок \ No newline at end of file -- 2.43.0 From 536c36b9fb4c9c45d2b0231dcbc27bfe5f330c3e Mon Sep 17 00:00:00 2001 From: semyanovra Date: Sun, 24 May 2026 21:03:05 +0000 Subject: [PATCH 20/36] Delete semyanovra/scr/generate_data.py --- semyanovra/scr/generate_data.py | 109 -------------------------------- 1 file changed, 109 deletions(-) delete mode 100644 semyanovra/scr/generate_data.py diff --git a/semyanovra/scr/generate_data.py b/semyanovra/scr/generate_data.py deleted file mode 100644 index b239ec1..0000000 --- a/semyanovra/scr/generate_data.py +++ /dev/null @@ -1,109 +0,0 @@ -""" -Экспериментальная часть. Пункт 1: Генерация тестовых данных. -Цель: создать список записей (имя, телефон) для дальнейшего тестирования -структур данных (связный список, хеш-таблица, BST). - -Особенности реализации: -- N = 10000 записей. -- Два режима: случайный порядок и отсортированный по имени. -- Имена генерируются равномерно (User_00000 ... User_09999) + небольшой - набор повторяющихся имен для создания коллизий в хеш-таблице. -- Телефоны — случайные строки из 10 цифр. -""" - -import random - -def generate_test_data(n=10000, duplicate_names_ratio=0.1): - """ - Генерирует два набора записей: в случайном порядке и отсортированном. - - Параметры: - - n: общее количество записей (по умолчанию 10000). - - duplicate_names_ratio: доля имен, которые будут повторяться (коллизии). - Например, 0.1 означает, что 10% записей будут использовать - имена из небольшого пула, остальные 90% — уникальные User_XXXXX. - - Возвращает: - - records_shuffled: список кортежей (name, phone) в случайном порядке. - - records_sorted: тот же список, но отсортированный по имени. - """ - - # 1. Создаем пул уникальных имен (равномерное распределение) - # Формат: User_00000, User_00001, ..., User_09999 - unique_names = [f"User_{i:05d}" for i in range(n)] - - # 2. Создаем небольшой пул имен для повторений (коллизий) - # Например, 20 разных имен, которые будут многократно встречаться. - collision_pool_size = max(1, int(n * duplicate_names_ratio)) # ~1000 имен для 10000 записей (10%) - collision_names = [f"Common_{j:03d}" for j in range(collision_pool_size)] - - # 3. Формируем итоговый список имен с повторениями - # - Первые (n - collision_pool_size) записей — уникальные. - # - Оставшиеся collision_pool_size записей — случайные из пула коллизий. - names = [] - # Уникальная часть - names.extend(unique_names[:n - collision_pool_size]) - # Часть с повторениями (для проверки коллизий в хеш-таблице) - for _ in range(collision_pool_size): - names.append(random.choice(collision_names)) - - # Перемешиваем имена, чтобы повторяющиеся имена не шли подряд - random.shuffle(names) - - # 4. Генерируем случайные телефоны (10 цифр) - phones = [] - for _ in range(n): - phone = ''.join(random.choices('0123456789', k=10)) - phones.append(phone) - - # 5. Собираем записи в список кортежей - records = list(zip(names, phones)) - - # 6. Создаем две версии: случайную и отсортированную - records_shuffled = records.copy() # уже случайный порядок после shuffle - records_sorted = sorted(records, key=lambda x: x[0]) # сортировка по имени - - return records_shuffled, records_sorted - -def print_sample(records, title, count=10): - """Вспомогательная функция: печатает первые count записей.""" - print(f"\n{title} (первые {count} записей):") - for name, phone in records[:count]: - print(f" {name}: {phone}") - -# ========== Демонстрация работы ========== -if __name__ == "__main__": - # Фиксируем seed для воспроизводимости результатов - random.seed(42) - - # Генерируем данные: N = 10000, доля коллизий 10% - N = 10000 - shuffled, sorted_data = generate_test_data(N, duplicate_names_ratio=0.1) - - # Выводим статистику и примеры - print(f"Сгенерировано {N} записей.") - print(f"Доля имен с повторениями (коллизиями): ~10%") - - # Показываем несколько примеров из каждого набора - print_sample(shuffled, "Случайный порядок") - print_sample(sorted_data, "Отсортированный порядок") - - # Дополнительно: проверка, что в отсортированном порядке имена действительно упорядочены - first_five_sorted = [name for name, _ in sorted_data[:5]] - print(f"\nПервые 5 имен в отсортированном наборе: {first_five_sorted}") - # Ожидается: ['Common_000', 'Common_001', ...] или 'User_...' — лексикографически - - # Проверка наличия коллизий (повторяющихся имен) - unique_names_in_shuffled = set(name for name, _ in shuffled) - print(f"\nУникальных имен в случайном наборе: {len(unique_names_in_shuffled)}") - print(f"(меньше {N} из-за повторений для коллизий)") - - # Сохраняем в файлы (опционально, для отладки) - # import csv - # with open("docs/data/records_shuffled.csv", "w") as f: - # writer = csv.writer(f) - # writer.writerows(shuffled) - # with open("docs/data/records_sorted.csv", "w") as f: - # writer = csv.writer(f) - # writer.writerows(sorted_data) - -- 2.43.0 From f48e920dd17dff418c27eb03ee9ea7b942ddd711 Mon Sep 17 00:00:00 2001 From: semyanovra Date: Sun, 24 May 2026 21:04:12 +0000 Subject: [PATCH 21/36] [2] update structure of repo --- semyanovra/{scr => docs/data/2-nd}/maze.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename semyanovra/{scr => docs/data/2-nd}/maze.py (100%) diff --git a/semyanovra/scr/maze.py b/semyanovra/docs/data/2-nd/maze.py similarity index 100% rename from semyanovra/scr/maze.py rename to semyanovra/docs/data/2-nd/maze.py -- 2.43.0 From 29a062508f3fba3fbccafa15908ecd5a8efc3c77 Mon Sep 17 00:00:00 2001 From: semyanovra Date: Sun, 24 May 2026 21:04:57 +0000 Subject: [PATCH 22/36] Update semyanovra/docs/data/maze/empty15x15.txt --- semyanovra/{scr => docs/data}/maze/empty15x15.txt | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename semyanovra/{scr => docs/data}/maze/empty15x15.txt (100%) diff --git a/semyanovra/scr/maze/empty15x15.txt b/semyanovra/docs/data/maze/empty15x15.txt similarity index 100% rename from semyanovra/scr/maze/empty15x15.txt rename to semyanovra/docs/data/maze/empty15x15.txt -- 2.43.0 From 86190175eb294defd69783b46a084de18c28b1ae Mon Sep 17 00:00:00 2001 From: semyanovra Date: Sun, 24 May 2026 21:05:20 +0000 Subject: [PATCH 23/36] Update semyanovra/docs/data/maze/large20x20.txt --- semyanovra/{scr => docs/data}/maze/large20x20.txt | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename semyanovra/{scr => docs/data}/maze/large20x20.txt (100%) diff --git a/semyanovra/scr/maze/large20x20.txt b/semyanovra/docs/data/maze/large20x20.txt similarity index 100% rename from semyanovra/scr/maze/large20x20.txt rename to semyanovra/docs/data/maze/large20x20.txt -- 2.43.0 From b9a3497b5a61d892642195e6349805b22d141108 Mon Sep 17 00:00:00 2001 From: semyanovra Date: Sun, 24 May 2026 21:05:44 +0000 Subject: [PATCH 24/36] Update semyanovra/docs/data/2-nd/mazelarge20x20.txt --- .../docs/data/{maze/large20x20.txt => 2-nd/mazelarge20x20.txt} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename semyanovra/docs/data/{maze/large20x20.txt => 2-nd/mazelarge20x20.txt} (100%) diff --git a/semyanovra/docs/data/maze/large20x20.txt b/semyanovra/docs/data/2-nd/mazelarge20x20.txt similarity index 100% rename from semyanovra/docs/data/maze/large20x20.txt rename to semyanovra/docs/data/2-nd/mazelarge20x20.txt -- 2.43.0 From ee9aedf3590ae069a1a9960d965b80885731171c Mon Sep 17 00:00:00 2001 From: semyanovra Date: Sun, 24 May 2026 21:06:16 +0000 Subject: [PATCH 25/36] Update semyanovra/docs/data/2-nd/maze/empty15x15.txt --- semyanovra/docs/data/{ => 2-nd}/maze/empty15x15.txt | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename semyanovra/docs/data/{ => 2-nd}/maze/empty15x15.txt (100%) diff --git a/semyanovra/docs/data/maze/empty15x15.txt b/semyanovra/docs/data/2-nd/maze/empty15x15.txt similarity index 100% rename from semyanovra/docs/data/maze/empty15x15.txt rename to semyanovra/docs/data/2-nd/maze/empty15x15.txt -- 2.43.0 From bebedf835ebcba0a921bd978f4c929fffe0d88ff Mon Sep 17 00:00:00 2001 From: semyanovra Date: Sun, 24 May 2026 21:06:45 +0000 Subject: [PATCH 26/36] Update semyanovra/docs/data/2-nd/maze/level1.txt --- semyanovra/{scr => docs/data/2-nd}/maze/level1.txt | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename semyanovra/{scr => docs/data/2-nd}/maze/level1.txt (100%) diff --git a/semyanovra/scr/maze/level1.txt b/semyanovra/docs/data/2-nd/maze/level1.txt similarity index 100% rename from semyanovra/scr/maze/level1.txt rename to semyanovra/docs/data/2-nd/maze/level1.txt -- 2.43.0 From 766b3facf21ba425cdf8f9d91d7706d7290f6820 Mon Sep 17 00:00:00 2001 From: semyanovra Date: Sun, 24 May 2026 21:07:07 +0000 Subject: [PATCH 27/36] Update semyanovra/docs/data/2-nd/maze/large20x20.txt --- .../docs/data/2-nd/{mazelarge20x20.txt => maze/large20x20.txt} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename semyanovra/docs/data/2-nd/{mazelarge20x20.txt => maze/large20x20.txt} (100%) diff --git a/semyanovra/docs/data/2-nd/mazelarge20x20.txt b/semyanovra/docs/data/2-nd/maze/large20x20.txt similarity index 100% rename from semyanovra/docs/data/2-nd/mazelarge20x20.txt rename to semyanovra/docs/data/2-nd/maze/large20x20.txt -- 2.43.0 From 350ccb861e2cc97a670439e9f5f775e34b3f5ef9 Mon Sep 17 00:00:00 2001 From: semyanovra Date: Sun, 24 May 2026 21:07:39 +0000 Subject: [PATCH 28/36] Update semyanovra/docs/data/2-nd/maze/medium10x10.txt --- semyanovra/{scr => docs/data/2-nd}/maze/medium10x10.txt | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename semyanovra/{scr => docs/data/2-nd}/maze/medium10x10.txt (100%) diff --git a/semyanovra/scr/maze/medium10x10.txt b/semyanovra/docs/data/2-nd/maze/medium10x10.txt similarity index 100% rename from semyanovra/scr/maze/medium10x10.txt rename to semyanovra/docs/data/2-nd/maze/medium10x10.txt -- 2.43.0 From 05980b5e121d07d282ba15861e4ab96ae62509c7 Mon Sep 17 00:00:00 2001 From: semyanovra Date: Sun, 24 May 2026 21:08:10 +0000 Subject: [PATCH 29/36] Update semyanovra/docs/data/2-nd/maze/no_exit10x10.txt --- semyanovra/{scr => docs/data/2-nd}/maze/no_exit10x10.txt | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename semyanovra/{scr => docs/data/2-nd}/maze/no_exit10x10.txt (100%) diff --git a/semyanovra/scr/maze/no_exit10x10.txt b/semyanovra/docs/data/2-nd/maze/no_exit10x10.txt similarity index 100% rename from semyanovra/scr/maze/no_exit10x10.txt rename to semyanovra/docs/data/2-nd/maze/no_exit10x10.txt -- 2.43.0 From e464e4e1e06d076e13905e7f187c6bfa00c3ab66 Mon Sep 17 00:00:00 2001 From: semyanovra Date: Sun, 24 May 2026 21:11:54 +0000 Subject: [PATCH 30/36] [1] add main.py --- semyanovra/docs/data/1-st/main.py | 80 +++++++++++++++++++++++++++++++ 1 file changed, 80 insertions(+) create mode 100644 semyanovra/docs/data/1-st/main.py diff --git a/semyanovra/docs/data/1-st/main.py b/semyanovra/docs/data/1-st/main.py new file mode 100644 index 0000000..d8a3419 --- /dev/null +++ b/semyanovra/docs/data/1-st/main.py @@ -0,0 +1,80 @@ + +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 = {'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'] + + 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 x: x[0]) + return records + + +# --------------------- Тестирование связного списка --------------------- +if __name__ == "__main__": + print("=== Тестирование связного списка ===") + head = None + + head = ll_insert(head, "Ivan", "123-456") + head = ll_insert(head, "Boris", "789-012") + head = ll_insert(head, "Anna", "345-678") + head = ll_insert(head, "Ivan", "111-222") # обновление + + print("Все записи:", ll_list_all(head)) + print("Телефон Ivan:", ll_find(head, "Ivan")) + print("Телефон Boris:", ll_find(head, "Boris")) + print("Телефон Noname:", ll_find(head, "Noname")) + + head = ll_delete(head, "Boris") + print("После удаления Boris, все записи:", ll_list_all(head)) \ No newline at end of file -- 2.43.0 From 0c2e487472f60d1c375ef4a4ed6ca2ff99646906 Mon Sep 17 00:00:00 2001 From: semyanovra Date: Sun, 24 May 2026 21:13:07 +0000 Subject: [PATCH 31/36] [1] add hash table implementation --- semyanovra/docs/data/1-st/main.py | 69 ++++++++++++++++++++----------- 1 file changed, 45 insertions(+), 24 deletions(-) diff --git a/semyanovra/docs/data/1-st/main.py b/semyanovra/docs/data/1-st/main.py index d8a3419..ca0d9b5 100644 --- a/semyanovra/docs/data/1-st/main.py +++ b/semyanovra/docs/data/1-st/main.py @@ -1,28 +1,19 @@ - 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 = {'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: @@ -31,15 +22,11 @@ def ll_find(head, name): 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: @@ -50,7 +37,6 @@ def ll_delete(head, name): current = current['next'] return head - def ll_list_all(head): records = [] current = head @@ -61,20 +47,55 @@ def ll_list_all(head): return records -# --------------------- Тестирование связного списка --------------------- +HASH_SIZE = 997 + +def hash_func(name, size): + return hash(name) % size + +def ht_create(): + return [None] * HASH_SIZE + +def ht_insert(table, name, phone): + idx = hash_func(name, len(table)) + table[idx] = ll_insert(table[idx], name, phone) + return table + +def ht_find(table, name): + idx = hash_func(name, len(table)) + return ll_find(table[idx], name) + +def ht_delete(table, name): + idx = hash_func(name, len(table)) + table[idx] = ll_delete(table[idx], name) + return table + +def ht_list_all(table): + all_records = [] + for head in table: + 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 + + if __name__ == "__main__": print("=== Тестирование связного списка ===") head = None - head = ll_insert(head, "Ivan", "123-456") head = ll_insert(head, "Boris", "789-012") head = ll_insert(head, "Anna", "345-678") - head = ll_insert(head, "Ivan", "111-222") # обновление + head = ll_insert(head, "Ivan", "111-222") + print("LinkedList:", ll_list_all(head)) - print("Все записи:", ll_list_all(head)) - print("Телефон Ivan:", ll_find(head, "Ivan")) - print("Телефон Boris:", ll_find(head, "Boris")) - print("Телефон Noname:", ll_find(head, "Noname")) - - head = ll_delete(head, "Boris") - print("После удаления Boris, все записи:", ll_list_all(head)) \ No newline at end of file + print("\n=== Тестирование хеш-таблицы ===") + ht = ht_create() + ht = ht_insert(ht, "Ivan", "123-456") + ht = ht_insert(ht, "Boris", "789-012") + ht = ht_insert(ht, "Anna", "345-678") + ht = ht_insert(ht, "Ivan", "111-222") + print("HashTable entries:", ht_list_all(ht)) + print("Find Ivan:", ht_find(ht, "Ivan")) + ht = ht_delete(ht, "Boris") + print("After delete Boris:", ht_list_all(ht)) \ No newline at end of file -- 2.43.0 From 319171fe2c8d55289a5f6a53506fddcdfb8668c0 Mon Sep 17 00:00:00 2001 From: semyanovra Date: Sun, 24 May 2026 21:14:28 +0000 Subject: [PATCH 32/36] [1] implement binary search tree --- semyanovra/docs/data/1-st/main.py | 84 ++++++++++++++++++++++++++++--- 1 file changed, 76 insertions(+), 8 deletions(-) diff --git a/semyanovra/docs/data/1-st/main.py b/semyanovra/docs/data/1-st/main.py index ca0d9b5..072a213 100644 --- a/semyanovra/docs/data/1-st/main.py +++ b/semyanovra/docs/data/1-st/main.py @@ -80,22 +80,90 @@ def ht_list_all(table): return all_records +def bst_create_node(name, phone): + return {'name': name, 'phone': phone, 'left': None, 'right': None} + +def bst_insert(root, name, phone): + if root is None: + return bst_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 bst_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 = 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): + result = [] + + def inorder_traverse(node): + if node is None: + return + inorder_traverse(node['left']) + result.append((node['name'], node['phone'])) + inorder_traverse(node['right']) + + inorder_traverse(root) + return result + + if __name__ == "__main__": print("=== Тестирование связного списка ===") head = None head = ll_insert(head, "Ivan", "123-456") head = ll_insert(head, "Boris", "789-012") - head = ll_insert(head, "Anna", "345-678") - head = ll_insert(head, "Ivan", "111-222") print("LinkedList:", ll_list_all(head)) print("\n=== Тестирование хеш-таблицы ===") ht = ht_create() ht = ht_insert(ht, "Ivan", "123-456") ht = ht_insert(ht, "Boris", "789-012") - ht = ht_insert(ht, "Anna", "345-678") - ht = ht_insert(ht, "Ivan", "111-222") - print("HashTable entries:", ht_list_all(ht)) - print("Find Ivan:", ht_find(ht, "Ivan")) - ht = ht_delete(ht, "Boris") - print("After delete Boris:", ht_list_all(ht)) \ No newline at end of file + print("HashTable:", ht_list_all(ht)) + + print("\n=== Тестирование BST ===") + bst_root = None + bst_root = bst_insert(bst_root, "Ivan", "123-456") + bst_root = bst_insert(bst_root, "Boris", "789-012") + bst_root = bst_insert(bst_root, "Anna", "345-678") + bst_root = bst_insert(bst_root, "Ivan", "111-222") + print("BST entries:", bst_list_all(bst_root)) + print("Find Ivan:", bst_find(bst_root, "Ivan")) + bst_root = bst_delete(bst_root, "Boris") + print("After delete Boris:", bst_list_all(bst_root)) \ No newline at end of file -- 2.43.0 From 74c13ec0fb238646cfe47679837c1795e9f09522 Mon Sep 17 00:00:00 2001 From: semyanovra Date: Sun, 24 May 2026 21:16:18 +0000 Subject: [PATCH 33/36] [1] add performance experiments and plotting --- semyanovra/docs/data/1-st/main.py | 196 +++++++++++++++++++++++++----- 1 file changed, 165 insertions(+), 31 deletions(-) diff --git a/semyanovra/docs/data/1-st/main.py b/semyanovra/docs/data/1-st/main.py index 072a213..3d416a2 100644 --- a/semyanovra/docs/data/1-st/main.py +++ b/semyanovra/docs/data/1-st/main.py @@ -1,3 +1,12 @@ +import random +import time +import csv +import sys +import pandas as pd +import matplotlib.pyplot as plt + +sys.setrecursionlimit(20000) + def ll_insert(head, name, phone): current = head while current is not None: @@ -86,7 +95,6 @@ def bst_create_node(name, phone): def bst_insert(root, name, phone): if root is None: return bst_create_node(name, phone) - if name == root['name']: root['phone'] = phone elif name < root['name']: @@ -113,7 +121,6 @@ def bst_find_min(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']: @@ -123,7 +130,6 @@ def bst_delete(root, name): return root['right'] if root['right'] is None: return root['left'] - min_node = bst_find_min(root['right']) root['name'] = min_node['name'] root['phone'] = min_node['phone'] @@ -132,38 +138,166 @@ def bst_delete(root, name): def bst_list_all(root): result = [] - - def inorder_traverse(node): + def inorder(node): if node is None: return - inorder_traverse(node['left']) + inorder(node['left']) result.append((node['name'], node['phone'])) - inorder_traverse(node['right']) - - inorder_traverse(root) + inorder(node['right']) + inorder(root) return result +def generate_records(num_records, seed=42): + random.seed(seed) + records = [] + for i in range(1, num_records + 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 + + +def run_experiment_for_structure(struct_funcs, records, mode_name, repeats=5): + results = [] + for rep in range(repeats): + ds = struct_funcs['create']() + + start = time.perf_counter() + for name, phone in records: + ds = struct_funcs['insert'](ds, name, phone) + insert_time = time.perf_counter() - start + + existing_names = [rec[0] for rec in records] + sample_existing = random.sample(existing_names, 100) + nonexistent = [f"None_{i}" for i in range(10)] + search_names = sample_existing + nonexistent + random.shuffle(search_names) + + start = time.perf_counter() + for name in search_names: + _ = struct_funcs['find'](ds, name) + find_time = time.perf_counter() - start + + to_delete = random.sample(existing_names, 50) + start = time.perf_counter() + for name in to_delete: + ds = struct_funcs['delete'](ds, name) + delete_time = time.perf_counter() - start + + results.append({ + 'structure': struct_funcs['name'], + 'mode': mode_name, + 'repetition': rep + 1, + 'insert_time': insert_time, + 'find_time': find_time, + 'delete_time': delete_time + }) + return results + + +def main_experiment(): + N = 10000 + REPEATS = 5 + + print("Генерация тестовых данных...") + base_records = generate_records(N) + shuffled_records, sorted_records = prepare_datasets(base_records) + print(f"Создано {N} записей. Случайный порядок и отсортированный готовы.") + + structures = { + 'LinkedList': { + 'name': 'LinkedList', + 'create': lambda: None, + 'insert': ll_insert, + 'find': ll_find, + 'delete': ll_delete + }, + 'HashTable': { + 'name': 'HashTable', + 'create': ht_create, + 'insert': ht_insert, + 'find': ht_find, + 'delete': ht_delete + }, + 'BST': { + 'name': 'BST', + 'create': lambda: None, + 'insert': bst_insert, + 'find': bst_find, + 'delete': bst_delete + } + } + + all_results = [] + + for struct_name, funcs in structures.items(): + print(f"Тестирование {struct_name} на случайном порядке...") + all_results.extend(run_experiment_for_structure(funcs, shuffled_records, 'random', REPEATS)) + + print(f"Тестирование {struct_name} на отсортированном порядке...") + all_results.extend(run_experiment_for_structure(funcs, sorted_records, 'sorted', REPEATS)) + + csv_file = "experiment_results.csv" + with open(csv_file, 'w', newline='', encoding='utf-8') as f: + writer = csv.writer(f) + writer.writerow(['Structure', 'Mode', 'Repeat', 'Insert (sec)', 'Search (sec)', 'Delete (sec)']) + for rec in all_results: + writer.writerow([ + rec['structure'], + rec['mode'], + rec['repetition'], + f"{rec['insert_time']:.6f}", + f"{rec['find_time']:.6f}", + f"{rec['delete_time']:.6f}" + ]) + print(f"Результаты сохранены в {csv_file}") + + plot_results(csv_file) + + +def plot_results(csv_path): + df = pd.read_csv(csv_path) + 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 = range(len(structures)) + width = 0.35 + + random_vals = [] + sorted_vals = [] + for s in structures: + rand_row = mean_times[(mean_times['Structure'] == s) & (mean_times['Mode'] == 'random')] + sort_row = mean_times[(mean_times['Structure'] == s) & (mean_times['Mode'] == 'sorted')] + random_vals.append(rand_row[op].values[0] if not rand_row.empty else 0) + sorted_vals.append(sort_row[op].values[0] if not sort_row.empty else 0) + + ax.bar([i - width/2 for i in x], random_vals, width, label='Случайный порядок') + ax.bar([i + width/2 for i in x], 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() + print("График сохранён как performance_comparison.png") + + if __name__ == "__main__": - print("=== Тестирование связного списка ===") - head = None - head = ll_insert(head, "Ivan", "123-456") - head = ll_insert(head, "Boris", "789-012") - print("LinkedList:", ll_list_all(head)) - - print("\n=== Тестирование хеш-таблицы ===") - ht = ht_create() - ht = ht_insert(ht, "Ivan", "123-456") - ht = ht_insert(ht, "Boris", "789-012") - print("HashTable:", ht_list_all(ht)) - - print("\n=== Тестирование BST ===") - bst_root = None - bst_root = bst_insert(bst_root, "Ivan", "123-456") - bst_root = bst_insert(bst_root, "Boris", "789-012") - bst_root = bst_insert(bst_root, "Anna", "345-678") - bst_root = bst_insert(bst_root, "Ivan", "111-222") - print("BST entries:", bst_list_all(bst_root)) - print("Find Ivan:", bst_find(bst_root, "Ivan")) - bst_root = bst_delete(bst_root, "Boris") - print("After delete Boris:", bst_list_all(bst_root)) \ No newline at end of file + main_experiment() \ No newline at end of file -- 2.43.0 From d47b06ae6f465b3d2d81e8fc85c4b6a128e08c7b Mon Sep 17 00:00:00 2001 From: semyanovra Date: Sun, 24 May 2026 21:24:27 +0000 Subject: [PATCH 34/36] [1] add csv results and png plots --- .../docs/data/1-st/experiment_results.csv | 31 ++++++++++++++++++ .../docs/data/1-st/performance_comparison.png | Bin 0 -> 57421 bytes 2 files changed, 31 insertions(+) create mode 100644 semyanovra/docs/data/1-st/experiment_results.csv create mode 100644 semyanovra/docs/data/1-st/performance_comparison.png diff --git a/semyanovra/docs/data/1-st/experiment_results.csv b/semyanovra/docs/data/1-st/experiment_results.csv new file mode 100644 index 0000000..ed99866 --- /dev/null +++ b/semyanovra/docs/data/1-st/experiment_results.csv @@ -0,0 +1,31 @@ +Structure,Mode,Repeat,Insert (sec),Search (sec),Delete (sec) +LinkedList,random,1,3.972341,0.027657,0.012911 +LinkedList,random,2,4.045646,0.023430,0.015166 +LinkedList,random,3,4.108713,0.029786,0.011930 +LinkedList,random,4,4.177241,0.028833,0.014464 +LinkedList,random,5,4.185596,0.029333,0.012727 +LinkedList,sorted,1,3.790176,0.025204,0.010269 +LinkedList,sorted,2,3.810435,0.022951,0.011524 +LinkedList,sorted,3,3.803720,0.025208,0.010396 +LinkedList,sorted,4,3.815409,0.027041,0.010837 +LinkedList,sorted,5,3.803349,0.025340,0.011777 +HashTable,random,1,0.010245,0.000075,0.000036 +HashTable,random,2,0.008733,0.000079,0.000069 +HashTable,random,3,0.013354,0.000094,0.000044 +HashTable,random,4,0.008903,0.000078,0.000036 +HashTable,random,5,0.009199,0.000072,0.000033 +HashTable,sorted,1,0.010286,0.000114,0.000052 +HashTable,sorted,2,0.009219,0.000073,0.000034 +HashTable,sorted,3,0.011302,0.000068,0.000033 +HashTable,sorted,4,0.009324,0.000068,0.000033 +HashTable,sorted,5,0.008641,0.000068,0.000034 +BST,random,1,0.027580,0.000190,0.000118 +BST,random,2,0.020693,0.000188,0.000116 +BST,random,3,0.020889,0.000190,0.000109 +BST,random,4,0.022945,0.000182,0.000110 +BST,random,5,0.022395,0.000207,0.000114 +BST,sorted,1,9.109235,0.083432,0.049594 +BST,sorted,2,9.177649,0.097374,0.050929 +BST,sorted,3,9.414714,0.067665,0.054041 +BST,sorted,4,9.062772,0.090823,0.048369 +BST,sorted,5,8.994138,0.072883,0.049921 diff --git a/semyanovra/docs/data/1-st/performance_comparison.png b/semyanovra/docs/data/1-st/performance_comparison.png new file mode 100644 index 0000000000000000000000000000000000000000..468ba014e2d9ddd964547b9b174307cefec8995f GIT binary patch literal 57421 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%0D4ywd!$=1;T07rdU>42El^W3TCBnNglzaz|JJ=C$ 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 TJzQqw Date: Sun, 24 May 2026 21:25:39 +0000 Subject: [PATCH 35/36] [1] add png to report --- semyanovra/docs/performance_comparison.png | Bin 0 -> 57421 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 semyanovra/docs/performance_comparison.png diff --git a/semyanovra/docs/performance_comparison.png b/semyanovra/docs/performance_comparison.png new file mode 100644 index 0000000000000000000000000000000000000000..468ba014e2d9ddd964547b9b174307cefec8995f GIT binary patch literal 57421 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%0D4ywd!$=1;T07rdU>42El^W3TCBnNglzaz|JJ=C$ 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 TJzQqw Date: Sun, 24 May 2026 21:26:28 +0000 Subject: [PATCH 36/36] [1] add report for 1-st laba --- semyanovra/docs/report1.md | 66 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 semyanovra/docs/report1.md diff --git a/semyanovra/docs/report1.md b/semyanovra/docs/report1.md new file mode 100644 index 0000000..13b635c --- /dev/null +++ b/semyanovra/docs/report1.md @@ -0,0 +1,66 @@ +# Отчёт по лабораторной работе «Структуры данных» + +## Цель работы +Реализовать три структуры данных (связный список, хеш-таблицу, двоичное дерево поиска) «с нуля» и экспериментально сравнить их производительность на операциях вставки, поиска и удаления записей телефонного справочника. + +## Реализованные структуры +- **Связный список (LinkedList)** – элементы хранятся в узлах со ссылкой на следующий. +- **Хеш-таблица (HashTable)** – массив корзин фиксированного размера (997), каждая корзина – связный список. +- **Двоичное дерево поиска (BST)** – узлы содержат ключ (имя) и ссылки на левое/правое поддеревья. + +Все операции реализованы вручную без использования классов. + +## Методика эксперимента +- **Объём данных**: N = 10 000 записей вида `User_XXXXX` → случайный телефон. +- **Режимы ввода**: случайный порядок и отсортированный по имени. +- **Действия**: + 1. Вставка всех записей. + 2. Поиск 100 существующих + 10 несуществующих имён. + 3. Удаление 50 случайных записей. +- **Повторения**: каждый эксперимент выполнен 5 раз, зафиксировано время (`time.perf_counter`). +- **Сбор результатов**: усреднение по 5 повторениям. + +## Результаты измерений +### Среднее время операций (секунды) + +| Структура | Режим | Вставка | Поиск | Удаление | +|-------------|-------------|----------|----------|----------| +| LinkedList | случайный | 4.0979 | 0.0278 | 0.0134 | +| LinkedList | отсортир. | 3.8044 | 0.0251 | 0.0110 | +| HashTable | случайный | 0.0101 | 0.000080 | 0.000044 | +| HashTable | отсортир. | 0.0098 | 0.000078 | 0.000037 | +| BST | случайный | 0.0229 | 0.000191 | 0.000113 | +| BST | отсортир. | 9.1518 | 0.0824 | 0.0506 | + +*Полные замеры всех 5 повторений сохранены в `experiment_results.csv`.* + +### График сравнения +![Сравнение производительности](performance_comparison.png) + +## Анализ результатов + +### Влияние порядка данных на BST +При вставке отсортированных данных BST вырождается в линейный список (высота ≈ N). +Время вставки возрастает с **0.023 с** (случайный) до **9.15 с** (отсортированный) – деградация в **~400 раз**. +Поиск и удаление замедляются аналогично. + +### Устойчивость хеш-таблицы +Хеш-функция равномерно распределяет ключи независимо от порядка. +Время вставки в случайном (0.0101 с) и отсортированном (0.0098 с) режимах практически одинаково, как и поиск (~0.00008 с). + +### Медлительность связного списка +Поиск (O(n)) на 10 000 элементов занимает ~0.027 с, что на два порядка медленнее хеш-таблицы. +Вставка в конец также требует прохода по всему списку (~4 с). + +### Удаление +Наиболее эффективно в хеш-таблице (≈0.00004 с). +В BST на случайных данных удаление быстрое (0.00011 с), но на отсортированных деградирует до 0.05 с. +В списке удаление (0.013 с) сравнимо с поиском. + +## Выводы и рекомендации + +1. **Хеш-таблица** – оптимальный выбор для задач, где нужен быстрый доступ по ключу, а порядок данных не важен. +2. **Двоичное дерево поиска** – подходит, если требуется получать записи в отсортированном порядке **и** данные поступают в случайном порядке. При отсортированных входных данных необходима балансировка (AVL, красно-чёрное дерево). +3. **Связный список** – неэффективен для больших объёмов; может применяться только в учебных целях или при очень маленьких коллекциях. + +В реальных проектах для справочников и словарей следует выбирать хеш-таблицы или сбалансированные деревья в зависимости от необходимости упорядоченного вывода. \ No newline at end of file -- 2.43.0