From 43ea04de237340fa1c94b53a169e3c8b242631ad Mon Sep 17 00:00:00 2001 From: FamutdinovMD Date: Sun, 24 May 2026 23:39:14 +0300 Subject: [PATCH] =?UTF-8?q?=D0=9B=D0=B0=D0=B1=D0=BE=D1=80=D0=B0=D1=82?= =?UTF-8?q?=D0=BE=D1=80=D0=BD=D0=B0=D1=8F=20=D1=80=D0=B0=D0=B1=D0=BE=D1=82?= =?UTF-8?q?=D0=B0=20=E2=84=962?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- famutdinovmd/.gitignore | 36 +++ famutdinovmd/2 | 1 - famutdinovmd/builders.py | 61 +++++ famutdinovmd/commands.py | 71 ++++++ famutdinovmd/experiments.py | 100 +++++++++ famutdinovmd/main.py | 182 +++++++++++++++ famutdinovmd/mazes/empty.txt | 1 + famutdinovmd/mazes/large.txt | 15 ++ famutdinovmd/mazes/medium.txt | 11 + famutdinovmd/mazes/no_exit.txt | 5 + famutdinovmd/mazes/small.txt | 10 + famutdinovmd/models.py | 86 +++++++ famutdinovmd/observers.py | 73 ++++++ famutdinovmd/report 1.ipynb | 349 ----------------------------- famutdinovmd/requirements.txt | 3 + famutdinovmd/solver.py | 53 +++++ famutdinovmd/strategies.py | 107 +++++++++ famutdinovmd/tasks/1/BinaryTree.py | 80 ------- famutdinovmd/tasks/1/HashTable.py | 61 ----- famutdinovmd/tasks/1/LinkedList.py | 62 ----- famutdinovmd/visualize.py | 88 ++++++++ 21 files changed, 902 insertions(+), 553 deletions(-) create mode 100644 famutdinovmd/.gitignore delete mode 160000 famutdinovmd/2 create mode 100644 famutdinovmd/builders.py create mode 100644 famutdinovmd/commands.py create mode 100644 famutdinovmd/experiments.py create mode 100644 famutdinovmd/main.py create mode 100644 famutdinovmd/mazes/empty.txt create mode 100644 famutdinovmd/mazes/large.txt create mode 100644 famutdinovmd/mazes/medium.txt create mode 100644 famutdinovmd/mazes/no_exit.txt create mode 100644 famutdinovmd/mazes/small.txt create mode 100644 famutdinovmd/models.py create mode 100644 famutdinovmd/observers.py delete mode 100644 famutdinovmd/report 1.ipynb create mode 100644 famutdinovmd/requirements.txt create mode 100644 famutdinovmd/solver.py create mode 100644 famutdinovmd/strategies.py delete mode 100644 famutdinovmd/tasks/1/BinaryTree.py delete mode 100644 famutdinovmd/tasks/1/HashTable.py delete mode 100644 famutdinovmd/tasks/1/LinkedList.py create mode 100644 famutdinovmd/visualize.py diff --git a/famutdinovmd/.gitignore b/famutdinovmd/.gitignore new file mode 100644 index 0000000..072b395 --- /dev/null +++ b/famutdinovmd/.gitignore @@ -0,0 +1,36 @@ +# Python +__pycache__/ +*.py[cod] +*$py.class +*.so +.Python +env/ +venv/ +ENV/ +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg + +# IDE +.vscode/ +.idea/ +*.swp +*.swo +*~ + +# Project specific +experiment_results.csv +experiment_results.png +*.log \ No newline at end of file diff --git a/famutdinovmd/2 b/famutdinovmd/2 deleted file mode 160000 index 522c158..0000000 --- a/famutdinovmd/2 +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 522c1583c9a12cd18587e96e021544cddef379ff diff --git a/famutdinovmd/builders.py b/famutdinovmd/builders.py new file mode 100644 index 0000000..821f7c5 --- /dev/null +++ b/famutdinovmd/builders.py @@ -0,0 +1,61 @@ +from abc import ABC, abstractmethod +from models import Cell, Maze + + +class MazeBuilder(ABC): + """Абстрактный строитель лабиринта""" + + @abstractmethod + def build_from_file(self, filename: str) -> Maze: + """Построить лабиринт из файла""" + pass + + +class TextFileMazeBuilder(MazeBuilder): + """Строитель лабиринта из текстового файла""" + + WALL_CHAR = '#' + START_CHAR = 'S' + EXIT_CHAR = 'E' + PASS_CHAR = ' ' + + def build_from_file(self, filename: str) -> Maze: + """Читает текстовый файл и создаёт лабиринт""" + with open(filename, 'r', encoding='utf-8') as f: + lines = [line.rstrip('\n') for line in f.readlines()] + + if not lines: + raise ValueError("Файл с лабиринтом пуст") + + height = len(lines) + width = max(len(line) for line in lines) + + maze = Maze(width, height) + + for y, line in enumerate(lines): + for x, ch in enumerate(line): + if x >= width: + continue + + cell = Cell(x, y) + + if ch == self.WALL_CHAR: + cell.is_wall = True + elif ch == self.START_CHAR: + cell.is_start = True + elif ch == self.EXIT_CHAR: + cell.is_exit = True + elif ch == self.PASS_CHAR: + pass # проходимая клетка (всё уже настроено) + else: + cell.is_wall = True # неизвестный символ считаем стеной + + maze.set_cell(x, y, cell) + + # Валидация + if maze.start is None: + raise ValueError("В лабиринте нет стартовой клетки (S)") + if maze.exit is None: + raise ValueError("В лабиринте нет выхода (E)") + + return maze \ No newline at end of file diff --git a/famutdinovmd/commands.py b/famutdinovmd/commands.py new file mode 100644 index 0000000..88611fc --- /dev/null +++ b/famutdinovmd/commands.py @@ -0,0 +1,71 @@ +from abc import ABC, abstractmethod +from typing import Optional +from models import Cell, Maze + + +class Player: + """Игрок, перемещающийся по лабиринту""" + + def __init__(self, start_cell: Cell): + self.current_cell = start_cell + + def move_to(self, new_cell: Cell) -> None: + """Перемещает игрока в новую клетку""" + self.current_cell = new_cell + + +class Command(ABC): + """Абстрактная команда""" + + @abstractmethod + def execute(self) -> bool: + """Выполняет команду""" + pass + + @abstractmethod + def undo(self) -> None: + """Отменяет команду""" + pass + + +class MoveCommand(Command): + """Команда перемещения игрока""" + + def __init__(self, player: Player, maze: Maze, direction: str): + self.player = player + self.maze = maze + self.direction = direction + self.previous_cell: Optional[Cell] = None + self.new_cell: Optional[Cell] = None + + def _get_target_cell(self) -> Optional[Cell]: + """Возвращает целевую клетку в зависимости от направления""" + x, y = self.player.current_cell.x, self.player.current_cell.y + + if self.direction == 'w': + y -= 1 + elif self.direction == 's': + y += 1 + elif self.direction == 'a': + x -= 1 + elif self.direction == 'd': + x += 1 + else: + return None + + return self.maze.get_cell(x, y) + + def execute(self) -> bool: + """Выполняет перемещение""" + self.previous_cell = self.player.current_cell + self.new_cell = self._get_target_cell() + + if self.new_cell and self.new_cell.is_passable(): + self.player.move_to(self.new_cell) + return True + return False + + def undo(self) -> None: + """Отменяет перемещение""" + if self.previous_cell: + self.player.move_to(self.previous_cell) \ No newline at end of file diff --git a/famutdinovmd/experiments.py b/famutdinovmd/experiments.py new file mode 100644 index 0000000..34207a7 --- /dev/null +++ b/famutdinovmd/experiments.py @@ -0,0 +1,100 @@ +import csv +import time +from typing import List, Dict +from models import Maze +from builders import TextFileMazeBuilder +from strategies import BFSStrategy, DFSStrategy, AStarStrategy +from solver import MazeSolver + + +def run_experiment(maze: Maze, strategy_name: str, strategy, repeats: int = 5) -> Dict: + """Запускает эксперимент для одной стратегии""" + times = [] + visited_counts = [] + path_lengths = [] + path_found = True + + for _ in range(repeats): + solver = MazeSolver(maze, strategy) + path, stats = solver.solve() + + times.append(stats.time_ms) + visited_counts.append(stats.visited_cells) + path_lengths.append(stats.path_length) + path_found = stats.path_found + + return { + 'strategy': strategy_name, + 'time_mean': sum(times) / len(times), + 'time_min': min(times), + 'time_max': max(times), + 'visited_mean': sum(visited_counts) / len(visited_counts), + 'path_length_mean': sum(path_lengths) / len(path_lengths) if path_found else 0, + 'path_found': path_found + } + + +def run_all_experiments(maze_files: List[str], repeats: int = 5) -> List[Dict]: + """Запускает эксперименты для всех лабиринтов и стратегий""" + builder = TextFileMazeBuilder() + strategies = [ + ('BFS', BFSStrategy()), + ('DFS', DFSStrategy()), + ('A*', AStarStrategy()) + ] + + results = [] + + for maze_file in maze_files: + try: + maze = builder.build_from_file(maze_file) + except (ValueError, FileNotFoundError) as e: + print(f"❌ Ошибка: {e}") + continue + + print(f"\n📊 Лабиринт: {maze_file}") + print(f" Размер: {maze.width}×{maze.height}") + print(f" Старт: ({maze.start.x}, {maze.start.y})") + print(f" Выход: ({maze.exit.x}, {maze.exit.y})") + + for strategy_name, strategy in strategies: + print(f" 🧪 Тестирование: {strategy_name}") + result = run_experiment(maze, strategy_name, strategy, repeats) + result['maze_file'] = maze_file.split('/')[-1] + result['maze_size'] = f"{maze.width}×{maze.height}" + results.append(result) + + status = "✅" if result['path_found'] else "❌" + print(f" {status} Время: {result['time_mean']:.2f} мс, " + f"Посещено: {result['visited_mean']:.0f}, " + f"Путь: {result['path_length_mean']:.0f}") + + return results + + +def save_results_to_csv(results: List[Dict], filename: str = "experiment_results.csv") -> None: + """Сохраняет результаты в CSV файл""" + with open(filename, 'w', newline='', encoding='utf-8') as f: + writer = csv.DictWriter(f, fieldnames=[ + 'maze_file', 'maze_size', 'strategy', + 'time_mean', 'time_min', 'time_max', + 'visited_mean', 'path_length_mean', 'path_found' + ]) + writer.writeheader() + writer.writerows(results) + print(f"\n💾 Результаты сохранены в {filename}") + + +def print_results_table(results: List[Dict]) -> None: + """Выводит результаты в виде таблицы""" + print("\n" + "=" * 80) + print("РЕЗУЛЬТАТЫ ЭКСПЕРИМЕНТОВ") + print("=" * 80) + + for res in results: + print(f"\n📁 Лабиринт: {res['maze_file']}") + print(f" 📐 Размер: {res['maze_size']}") + print(f" 🎯 Стратегия: {res['strategy']}") + print(f" ⏱️ Время (ср): {res['time_mean']:.2f} мс") + print(f" 📍 Посещено: {res['visited_mean']:.0f} клеток") + print(f" 🛤️ Длина пути: {res['path_length_mean']:.0f}") \ No newline at end of file diff --git a/famutdinovmd/main.py b/famutdinovmd/main.py new file mode 100644 index 0000000..ebbb055 --- /dev/null +++ b/famutdinovmd/main.py @@ -0,0 +1,182 @@ +import os +from builders import TextFileMazeBuilder +from strategies import BFSStrategy, DFSStrategy, AStarStrategy +from solver import MazeSolver +from observers import ConsoleView +from commands import Player +from experiments import run_all_experiments, save_results_to_csv, print_results_table + + +def create_test_mazes(): + """Создаёт тестовые лабиринты в папке mazes/""" + os.makedirs("mazes", exist_ok=True) + + # Маленький лабиринт 10×10 + small = """########## +#S # +# ### ## # +# # # +### # #### +# # # +# ### # # +# # # +# # E# +##########""" + + # Средний лабиринт 20×11 + medium = """#################### +#S # +# # # # # # # # # # +# # +# # # # # # # # # # +# # +# # # # # # # # # # +# # +# # # # # # # # # # +# E# +####################""" + + # Большой лабиринт 30×15 + large = """############################## +#S # +# # # # # # # # # # # # # # # +# # +# # # # # # # # # # # # # # # +# # +# # # # # # # # # # # # # # # +# # +# # # # # # # # # # # # # # # +# # +# # # # # # # # # # # # # # # +# # +# # # # # # # # # # # # # # # +# E# +##############################""" + + # Пустой лабиринт (без стен) + empty = "S" + " " * 28 + "E" + + # Лабиринт без выхода + no_exit = """####### +#S # +# ### # +# # # +#######""" + + # Сохранение файлов + with open("mazes/small.txt", "w") as f: + f.write(small) + with open("mazes/medium.txt", "w") as f: + f.write(medium) + with open("mazes/large.txt", "w") as f: + f.write(large) + with open("mazes/empty.txt", "w") as f: + f.write(empty) + with open("mazes/no_exit.txt", "w") as f: + f.write(no_exit) + + print("✅ Тестовые лабиринты созданы в папке 'mazes/'") + + +def demo_maze_solver(): + """Демонстрация работы MazeSolver с разными стратегиями""" + print("\n" + "=" * 60) + print("ДЕМОНСТРАЦИЯ РАБОТЫ MAZE SOLVER") + print("=" * 60) + + builder = TextFileMazeBuilder() + view = ConsoleView() + + try: + maze = builder.build_from_file("mazes/small.txt") + view.update("maze_loaded", {"maze": maze}) + + strategies = [ + ("BFS", BFSStrategy(), "BFS (поиск в ширину)"), + ("DFS", DFSStrategy(), "DFS (поиск в глубину)"), + ("A*", AStarStrategy(), "A* (A-star поиск)") + ] + + for name, strategy, description in strategies: + print(f"\n--- {description} ---") + solver = MazeSolver(maze, strategy) + view.update("search_start", {"algorithm": description}) + + path, stats = solver.solve() + + if stats.path_found: + view.update("path_found", {"maze": maze, "path": path, "stats": stats}) + else: + view.update("no_path", {"stats": stats}) + + except Exception as e: + print(f"❌ Ошибка: {e}") + + +def demo_player_controls(): + """Демонстрация управления игроком (Command + Observer)""" + print("\n" + "=" * 60) + print("ДЕМОНСТРАЦИЯ УПРАВЛЕНИЯ (Command + Observer)") + print("=" * 60) + + builder = TextFileMazeBuilder() + view = ConsoleView() + + try: + maze = builder.build_from_file("mazes/small.txt") + player = Player(maze.start) + + view.update("maze_loaded", {"maze": maze}) + view.render(maze, player_position=player.current_cell) + + print("\n💡 Для управления игроком в консоли введите W/A/S/D") + print(" (это демонстрация работы паттернов Command и Observer)") + + except Exception as e: + print(f"❌ Ошибка: {e}") + + +def run_experiments(): + """Запуск экспериментов для сравнения алгоритмов""" + print("\n" + "=" * 60) + print("ЭКСПЕРИМЕНТАЛЬНОЕ СРАВНЕНИЕ АЛГОРИТМОВ") + print("=" * 60) + + maze_files = [ + "mazes/small.txt", + "mazes/medium.txt", + "mazes/large.txt", + "mazes/empty.txt", + "mazes/no_exit.txt" + ] + + results = run_all_experiments(maze_files, repeats=5) + save_results_to_csv(results) + print_results_table(results) + + +def main(): + """Главная функция""" + print("=" * 60) + print("🎯 ОБЪЕКТНО-ОРИЕНТИРОВАННАЯ РЕАЛИЗАЦИЯ ПОИСКА В ЛАБИРИНТЕ") + print("📚 Применённые паттерны: Builder, Strategy, Observer, Command") + print("=" * 60) + + # Создание тестовых лабиринтов + create_test_mazes() + + # Демонстрация работы + demo_maze_solver() + demo_player_controls() + + # Эксперименты + run_experiments() + + print("\n" + "=" * 60) + print("✅ Программа завершена!") + print("📊 Для построения графиков запустите: python visualize.py") + print("=" * 60) + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/famutdinovmd/mazes/empty.txt b/famutdinovmd/mazes/empty.txt new file mode 100644 index 0000000..172bb4f --- /dev/null +++ b/famutdinovmd/mazes/empty.txt @@ -0,0 +1 @@ +S E \ No newline at end of file diff --git a/famutdinovmd/mazes/large.txt b/famutdinovmd/mazes/large.txt new file mode 100644 index 0000000..143173c --- /dev/null +++ b/famutdinovmd/mazes/large.txt @@ -0,0 +1,15 @@ +############################## +#S # +# # # # # # # # # # # # # # # +# # +# # # # # # # # # # # # # # # +# # +# # # # # # # # # # # # # # # +# # +# # # # # # # # # # # # # # # +# # +# # # # # # # # # # # # # # # +# # +# # # # # # # # # # # # # # # +# E# +############################## \ No newline at end of file diff --git a/famutdinovmd/mazes/medium.txt b/famutdinovmd/mazes/medium.txt new file mode 100644 index 0000000..e52ac72 --- /dev/null +++ b/famutdinovmd/mazes/medium.txt @@ -0,0 +1,11 @@ +#################### +#S # +# # # # # # # # # # +# # +# # # # # # # # # # +# # +# # # # # # # # # # +# # +# # # # # # # # # # +# E# +#################### \ No newline at end of file diff --git a/famutdinovmd/mazes/no_exit.txt b/famutdinovmd/mazes/no_exit.txt new file mode 100644 index 0000000..c9a85c0 --- /dev/null +++ b/famutdinovmd/mazes/no_exit.txt @@ -0,0 +1,5 @@ +####### +#S # +# ### # +# # # +####### \ No newline at end of file diff --git a/famutdinovmd/mazes/small.txt b/famutdinovmd/mazes/small.txt new file mode 100644 index 0000000..9cbc84e --- /dev/null +++ b/famutdinovmd/mazes/small.txt @@ -0,0 +1,10 @@ +########## +#S # +# ### ## # +# # # +### # #### +# # # +# ### # # +# # # +# # E# +########## \ No newline at end of file diff --git a/famutdinovmd/models.py b/famutdinovmd/models.py new file mode 100644 index 0000000..002b977 --- /dev/null +++ b/famutdinovmd/models.py @@ -0,0 +1,86 @@ +from typing import List, Optional + + +class Cell: + """Клетка лабиринта""" + + def __init__(self, x: int, y: int): + self.x = x + self.y = y + self.is_wall = False + self.is_start = False + self.is_exit = False + + def is_passable(self) -> bool: + """Проверяет, можно ли пройти через клетку""" + return not self.is_wall + + def __eq__(self, other) -> bool: + if not isinstance(other, Cell): + return False + return self.x == other.x and self.y == other.y + + def __hash__(self): + return hash((self.x, self.y)) + + def __repr__(self): + return f"Cell({self.x}, {self.y})" + + +class Maze: + """Лабиринт (сетка клеток)""" + + def __init__(self, width: int, height: int): + self.width = width + self.height = height + self._cells: List[List[Optional[Cell]]] = [[None for _ in range(width)] for _ in range(height)] + self.start: Optional[Cell] = None + self.exit: Optional[Cell] = None + + def set_cell(self, x: int, y: int, cell: Cell) -> None: + """Устанавливает клетку в указанные координаты""" + if 0 <= x < self.width and 0 <= y < self.height: + self._cells[y][x] = cell + if cell.is_start: + self.start = cell + if cell.is_exit: + self.exit = cell + + def get_cell(self, x: int, y: int) -> Optional[Cell]: + """Возвращает клетку по координатам""" + if 0 <= x < self.width and 0 <= y < self.height: + return self._cells[y][x] + return None + + def get_neighbors(self, cell: Cell) -> List[Cell]: + """Возвращает список соседних проходимых клеток""" + neighbors = [] + directions = [(0, -1), (0, 1), (-1, 0), (1, 0)] # вверх, вниз, влево, вправо + + for dx, dy in directions: + nx, ny = cell.x + dx, cell.y + dy + neighbor = self.get_cell(nx, ny) + if neighbor and neighbor.is_passable(): + neighbors.append(neighbor) + + return neighbors + + def __str__(self) -> str: + """Строковое представление лабиринта""" + result = [] + for y in range(self.height): + row = [] + for x in range(self.width): + cell = self.get_cell(x, y) + if cell is None: + row.append('?') + elif cell.is_start: + row.append('S') + elif cell.is_exit: + row.append('E') + elif cell.is_wall: + row.append('#') + else: + row.append(' ') + result.append(''.join(row)) + return '\n'.join(result) \ No newline at end of file diff --git a/famutdinovmd/observers.py b/famutdinovmd/observers.py new file mode 100644 index 0000000..b736d37 --- /dev/null +++ b/famutdinovmd/observers.py @@ -0,0 +1,73 @@ +from abc import ABC, abstractmethod +from typing import List, Optional +from models import Cell, Maze + + +class Observer(ABC): + """Абстрактный наблюдатель""" + + @abstractmethod + def update(self, event: str, data: dict) -> None: + """Обработка события""" + pass + + +class ConsoleView(Observer): + """Консольная визуализация лабиринта""" + + def render(self, maze: Maze, player_position: Optional[Cell] = None, + path: Optional[List[Cell]] = None) -> None: + """Отрисовывает лабиринт в консоли""" + path_set = set(path) if path else set() + + print("\n+" + "-" * maze.width + "+") + + for y in range(maze.height): + row = [] + for x in range(maze.width): + cell = maze.get_cell(x, y) + if cell is None: + row.append('?') + elif player_position and cell == player_position: + row.append('@') + elif cell.is_start: + row.append('S') + elif cell.is_exit: + row.append('E') + elif cell in path_set: + row.append('*') + elif cell.is_wall: + row.append('#') + else: + row.append(' ') + print("|" + ''.join(row) + "|") + + print("+" + "-" * maze.width + "+") + + def update(self, event: str, data: dict) -> None: + """Обработка событий от MazeSolver""" + if event == "maze_loaded": + maze = data.get('maze') + print("\n📦 Лабиринт загружен:") + self.render(maze) + + elif event == "search_start": + algorithm = data.get('algorithm', 'Unknown') + print(f"\n🔍 Начинаем поиск алгоритмом: {algorithm}") + + elif event == "path_found": + maze = data.get('maze') + path = data.get('path') + stats = data.get('stats') + print(f"\n✅ Путь найден! {stats}") + self.render(maze, path=path) + + elif event == "no_path": + stats = data.get('stats') + print(f"\n❌ {stats}") + + elif event == "player_moved": + maze = data.get('maze') + player = data.get('player') + if player: + self.render(maze, player_position=player.current_cell) \ No newline at end of file diff --git a/famutdinovmd/report 1.ipynb b/famutdinovmd/report 1.ipynb deleted file mode 100644 index 9ac1fa0..0000000 --- a/famutdinovmd/report 1.ipynb +++ /dev/null @@ -1,349 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "id": "2acfa743", - "metadata": {}, - "source": [ - "# 0. Подготовим окружение" - ] - }, - { - "cell_type": "code", - "execution_count": 8, - "id": "4689b73e", - "metadata": {}, - "outputs": [], - "source": [ - "import sys\n", - "import os\n", - "sys.path.insert(0, os.path.abspath( '../task1'))\n", - "sys.path.insert(0, os.path.abspath( '../'))" - ] - }, - { - "cell_type": "markdown", - "id": "37cc11a5", - "metadata": {}, - "source": [ - "# 1. Генерация тестовых данных\n", - "\n", - "Создадим список records из N=10000 элементов. Каждый элемент — кортеж (name, phone). \n", - "Имена возъмём случайные из небольшого набора (чтобы были повторения и коллизии). \n", - "Для проверки влияния порядка подготовим два варианта: \n", - "\n", - "_records_shuffled_ — случайный порядок. \n", - "_records_sorted_ — отсортированный по имени (по алфавиту)." - ] - }, - { - "cell_type": "code", - "execution_count": 9, - "id": "a3b5c31b", - "metadata": {}, - "outputs": [], - "source": [ - "from util.randomNames import generate_test_data\n", - "from util.timeTester import test\n", - "\n", - "records_shuffled = generate_test_data(N=10000)\n", - "records_sorted = generate_test_data(N=10000, _sorted=True)" - ] - }, - { - "cell_type": "markdown", - "id": "c2f4989c", - "metadata": {}, - "source": [ - "# 2. Проведение замеров" - ] - }, - { - "cell_type": "code", - "execution_count": 10, - "id": "df12d41d", - "metadata": {}, - "outputs": [], - "source": [ - "# Подготовим функции СД, которые будем тестировать\n", - "from structures.LinkedList import *\n", - "from structures.HashTable import *\n", - "from structures.BinaryTree import *\n", - "\n", - "func_list = {\"Связанный список\" : (ll_insert, ll_find, ll_delete),\n", - " \"Хэш-таблица\" : (ht_insert, ht_find, ht_delete),\n", - " \"Бинарное дерево\" : (bst_insert, bst_find, bst_delete)}" - ] - }, - { - "cell_type": "code", - "execution_count": 11, - "id": "cc8d0436", - "metadata": {}, - "outputs": [], - "source": [ - "# Проведём замеры\n", - "report = [[\"Структура\", \"Режим\", \"Вставка\", \"Поиск\", \"Удаление\"]]\n", - "records = {\"Cлучайный\" : records_shuffled, \"Отсортированный\" : records_sorted}\n", - "\n", - "TEST_ITERATIONS_NUM = 5\n", - "\n", - "for _ in range(TEST_ITERATIONS_NUM):\n", - " for mode, data in records.items():\n", - " for struct_name, fns in func_list.items():\n", - " result = test(data, *fns)\n", - " row = [struct_name, mode,\n", - " result[\"insert_time\"],\n", - " result[\"find_time\"],\n", - " result[\"delete_time\"]]\n", - " report.append(row)" - ] - }, - { - "cell_type": "code", - "execution_count": 12, - "id": "2eedf056", - "metadata": {}, - "outputs": [], - "source": [ - "# Сохраним данные в csv\n", - "import csv\n", - "with open(\"data/task1/results.csv\", \"w\", newline=\"\") as f:\n", - " writer = csv.writer(f)\n", - " writer.writerows(report)" - ] - }, - { - "cell_type": "markdown", - "id": "94335af1", - "metadata": {}, - "source": [ - "# 3. Построение графиков и их анализ" - ] - }, - { - "cell_type": "code", - "execution_count": 13, - "id": "cad64d2f", - "metadata": {}, - "outputs": [ - { - "data": { - "image/png": "iVBORw0KGgoAAAANSUhEUgAABdEAAAHqCAYAAADrpwd3AAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjYuMywgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy/P9b71AAAACXBIWXMAAA9hAAAPYQGoP6dpAACcZElEQVR4nOzde1xVZfr///eWo2migoEUIFqjkKUF5YDiYUwMyzx+pCysPBSDpUJpovLJLCPLbOcokqUxTqXMZ0jtwCQ4o6RJNiDYNFGaoZjBGFqSppx/f/hl/9zuvREU3IKv5+OxHtO+17Xu+157kGvti3uvZaitra0VAAAAAAAAAACw0MbeEwAAAAAAAAAA4EpFER0AAAAAAAAAABsoogMAAAAAAAAAYANFdAAAAAAAAAAAbKCIDgAAAAAAAACADRTRAQAAAAAAAACwgSI6AAAAAAAAAAA2UEQHAAAAAAAAAMAGiugAAAAAAAAAANhAER1owVJSUmQwGMy2Ll26aPDgwfroo4/sPT0AANCErOX987du3brZe5oAAABAq0MRHWgF3n77bWVnZ2vXrl1avXq1HBwcNHLkSH344Yf2nhoAAGhidXn//K1///72nhoAAK3eyy+/LIPBoA8++MBiX0lJiZydnTV69OjLPzEAzcrR3hMAcOl69+6t4OBg0+u7775bnTp10vr16zVy5Eg7zgwAADS18/N+nY4dO+qHH36ww4wAALh6TJ06VQsXLtSf/vQn3XfffWb7Vq1apcrKSj355JN2mh2A5sJKdKAVcnV1lbOzs5ycnExt5eXlWrRokQICAuTq6ip3d3cNGTJEu3btkqQLfj188ODBkqQzZ87oqaeeUt++feXm5qbOnTsrJCREmzdvtpjHucc7ODjI29tbDz/8sP773/+aYg4ePCiDwaCUlBRTW2lpqW699VYFBASopKTE1L5y5UoNHDhQ1113ndq1a6dbbrlFL7/8siorK5v4HQQAoOU7c+aM4uPj5e/vL2dnZ11//fWaPn26fvnlF7O4bt266ZFHHjFr+8tf/mL19jAXup6Qzub/hQsXml6fPn1aQ4cOVdeuXfXNN9808VkCAHB5de7cWQ8++KC2bt2qgoICU3tFRYXeeOMN3XzzzRo6dKgdZwigOVBEB1qB6upqVVVVqbKyUj/88INmzZqlU6dOaeLEiZKkqqoqRURE6Pnnn9e9996rjRs3KiUlRaGhoSoqKpIks6+DL1iwQJL0/vvvm9qSkpIknf3wfPz4cT399NPatGmT1q9frwEDBmjs2LFat26dxdymTJmi7OxsZWVlafbs2UpNTdWjjz5q81xKS0v1hz/8QZWVldq2bZu8vLxM+w4cOKCJEyfqL3/5iz766CNNmTJFr7zyih5//PEmey8BAGgNamtrNXr0aC1dulRRUVH6+OOPFRcXpz//+c/6wx/+oPLycpvHlpWVac6cOXJwcDBrb8j1xPlOnz6te++9V19//bW2bdumXr16Nel5AgBgD3Urzf/0pz+Z2lJTU/Xf//7XYhX6I488YnWh2vl/wE5NTVV4eLi6du2qtm3bKiAgQHPnztWpU6eszsHWAriDBw+aYmpra5WUlKS+ffuqbdu26tSpk8aPH6/vv//erK/Bgwerd+/eFmMsXbrUos/G/PG9oqJCL7zwgnr16iUXFxd16dJFjz76qH766Ser5wRcybidC9AK/P73vzd77eLiohUrVmj48OGSpPXr12vbtm168803NXXqVFPcubd6ObePulVit912m0USdHNz09tvv216XV1draFDh+rnn3+W0WjUpEmTzOJvuOEGU98DBgzQp59+arZa7VylpaUaOnSo1QK6JC1btsz03zU1NQoLC5O7u7seffRRvfrqq+rUqZP1NwgAgKtMRkaGtmzZopdfflmzZ8+WJA0bNkw+Pj6KjIzUunXrNG3aNKvHPvvss3JwcNDo0aOVk5Njam/I9cS5Tp8+rZEjR1JABwC0OrfeeqsGDhyodevWKTExUW5ubvrTn/6kTp06KSoqyiK+bdu2+uc//2l6/Yc//MEiZv/+/RoxYoRmzZqldu3a6ZtvvtGSJUv0xRdfmB17rilTpphy8scff6wXXnjBbP/jjz+ulJQUzZgxQ0uWLNHx48e1aNEihYaGau/evfL09LyUt0GS7T++19TUaNSoUdqxY4fmzJmj0NBQHTp0SM8++6wGDx6snJwctW3b9pLHBy4XiuhAK7Bu3ToFBARIOluI3rhxo6ZPn67q6mo98cQT+vvf/y5XV1dNnjy5Scb7v//7PxmNRu3du9fsr+Kurq4WsTU1NaqqqlJ1dbW++OIL7dy5U8OGDbOIO3bsmIYOHaovv/xS//nPfywK6JKUl5enZ599Vp999pmOHz9utm/fvn3q169fE5wdAAAtX92H7fNXiv3P//yPJk+erH/84x9Wi+hfffWVVqxYoXfeeUd///vfzfY15nri9OnTuu+++/SPf/xDH3/8MQV0AECr8+STT+p//ud/9Pbbb6tfv37617/+paefflrXXHONWVx5ebmcnJzMFq61aWN5Y4i6b4RLZ1eQ9+/fXwEBARo0aJC+/PJL3Xrrrab9FRUVks6uCq/r9/xbpn3++ed688039eqrryouLs7UHhYWpt/97ndatmyZlixZcgnvwFm2/vj+17/+VZ988onS0tI0duxYU3ufPn10xx13KCUlRX/84x8veXzgcuF2LkArEBAQoODgYAUHB+vuu+/WG2+8ofDwcM2ZM0e//PKLfvrpJ3l7e1tN1I31/vvva8KECbr++uv1zjvvKDs7W//61780efJknTlzxiL++eefl5OTk1xdXTVw4EDdeOONMhqNFnHz5s1TRUWFvLy8lJCQYLG/qKhIYWFhOnLkiF5//XXt2LFD//rXv7Ry5UpJZz+sAwCAs44dOyZHR0d16dLFrN1gMMjLy0vHjh2zetz06dMVFhamyMhIi32NuZ4wGo366quv1KtXLy1atEhVVVUXdyIAAFyhRo8eLR8fH61YsUJGo1EODg6aPn26RdzJkyctCuvWfP/995o4caK8vLzk4OAgJycnDRo0SJLM7r0u/f+ff60tZKvz0UcfyWAw6KGHHlJVVZVp8/LyUp8+fbR9+3aLY86Nq6qqUk1NTb1zrvvj+6uvvqr27dtbjN+xY0eNHDnSrM++ffvKy8vL6vjAlYyV6EArdeutt2rLli3at2+funTpop07d6qmpuaSC+nvvPOO/P39lZqaKoPBYGq3dW/VadOm6bHHHlNtba1+/PFHvfjiiwoJCVF+fr6uvfZaU1z37t21bds27d27VxEREVqzZo2mTJli2r9p0yadOnVK77//vvz8/Ezt+fn5l3Q+AAC0Ru7u7qqqqtJPP/1kVkivra1VSUmJ7rjjDotj3n33XWVnZ9vMrY25nujcubO2bdumiooK3XnnnXruuef0/PPPX9I5AQBwJXF0dNQf//hHzZs3TwcOHNDo0aMtbocqSUeOHJG3t3e9fZ08eVJhYWFydXXVCy+8oN/97ne65pprdPjwYY0dO9Zi0VhpaakkycPDw2af//3vf1VbW2vzli3du3c3e/2f//xHTk5O9c7zfOf+8f38b7D997//1S+//CJnZ2erx9adA9BSUEQHWqm6D8BdunRRRESE1q9fr5SUlEu+pYvBYJCzs7NZAb2kpESbN2+2Gu/t7a3g4GDT69raWo0ZM0bZ2dkKDw83tT/zzDPy8vKSl5eXnnzySc2cOdP0NbO6caWz93s/t68333zzks4HAIDWaOjQoXr55Zf1zjvvKDY21tSelpamU6dOaejQoWbxv/76q2bPnq2ZM2cqMDDQap+NuZ54/PHHTbdwSUxM1NNPP63w8HCFhYVd4pkBAHDlmDZtmhYtWqQzZ85oxowZFvsrKytVUFBg9Rte5/rnP/+pH3/8Udu3bzetPpekX375xWr8/v37JUk33nijzT49PDxkMBi0Y8cOs8/Rdc5v69GjhzZs2GDW9s477+j111+32v+F/vju4eEhd3d3ffLJJ1b3n7uoDmgJKKIDrcBXX31l+pr0sWPH9P777yszM1NjxoyRv7+/fHx89Pbbbys6OlrffvuthgwZopqaGu3evVsBAQG6//77GzzWvffeq/fff18xMTEaP368Dh8+rOeff15du3Y1JfJz/fDDD/r8889NK9ETExPl4uJiuoe7NUuWLNE///lPPfjgg9q1a5ecnJw0bNgwOTs764EHHtCcOXN05swZrVq1Sj///HPj3zAAAFq5YcOGafjw4XrmmWdUVlam/v3768svv9Szzz6r2267zeKhZ5s3b5anp6eeffZZm30+8MADF3U9MWvWLP3973/XQw89pL1796pjx45NeaoAANhNhw4d1L59e910000aMmSIxf6MjAydOXPG5kO461hbNCZJb7zxhtX4TZs2qV27dgoKCrLZ57333quXXnpJR44c0YQJEy50KnJ1dTVbACfJ5i1XGvLH93vvvVcbNmxQdXU1zy9Dq0ARHWgFHn30UdN/u7m5yd/fX8uWLVNMTIyks18zS09PV2JiotavXy+j0ahrr71Wffr00d13393osY4ePark5GStXbtW3bt319y5c/XDDz/oueees4hfs2aN1qxZI4PBoM6dO6tPnz76+9//Lh8fH5tjuLq66t1339Wdd96phIQEvfTSS+rVq5fS0tK0YMECjR07Vu7u7po4caLi4uIUERHRqHMAAKC1MxgM2rRpkxYuXKi3335bixcvloeHh6KiovTiiy9afEivrq62ej/Tc13s9YTBYFBK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1Pb222/rf/7nf0z/4F1dXXX69Gmb/ffp00ePP/64brnlFt1000164YUX1L17d33wwQdW4//85z/rzJkzGjVqVENP95Lt27dPa9euVWxs7GUbEwAAAJcP18eNw/UxAOBKRU5vHHL6xaGIfpWora2VdPZrJxdSVlZm9S93daZOnaq3335bknT06FF9/PHHmjx5smn/zTffrL/97W+qrKy0evypU6c0Z84cBQYGqmPHjmrfvr2++eYbq3+V++2337RgwQK98sorcnS0vIV/UlKS2rdvb9pefPFFi5iPPvpI7du3V8eOHXXLLbdo5cqVF3wP5syZo8cff1zdu3e/YCwAAABaHq6PuT4GALQO5HRy+uVAEf0qcdNNN8lgMKigoOCCsT/++KO8vb1t7p80aZK+//57ZWdn65133lG3bt0UFhZm2v/aa6/p008/Vbt27dS+fXtFR0ebHT979mylpaVp8eLF2rFjh/Lz83XLLbeooqLCYqxXXnlFPXv21MiRI63O5cEHH1R+fr5pO38sSRoyZIjy8/P1+eefKzo6WjNmzNA//vEPm+eXlZWlHTt2aMGCBTZjAAAA0LJxfcz1MQCgdSCnk9MvB8s/c6BV6ty5s4YPH66VK1dqxowZFn91++WXX9SxY0edOnVKBQUFio+Pt9mXu7u7Ro8erbffflvZ2dl69NFHzfaHhYWppKRERUVFqq6u1vvvv2/217IdO3bokUce0ZgxYySdvV/UwYMHLcYpLi7WqlWrtH37dptzcXNz04033mh2nudr166dKaZXr1567bXXlJeXZ/WvfLW1tXrqqaeUkJCgTp062RwX51noZu8ZXJqFJ+w9AwAArgwXk9Pb+0j9X5WOnpYcL7wCrNn8mCd539bgcK6PuT4GgFatsTn9SsnnEjn9/yGnX1nsvhI9KSlJ/v7+cnV1VVBQkHbs2GEztri4WBMnTlTPnj3Vpk0bi6fU1vnll180ffp0de3aVa6urgoICFB6enoznUHLkZSUpOrqat15551KS0vT/v37VVBQoOXLlyskJETffPONHnjgAXXs2FERERH19jV16lT9+c9/VkFBgR5++GGL/Q4ODvL399eNN96o6667zmzfjTfeqPfff1/5+fnau3evJk6cqJqaGos+Vq5cqTFjxuj222+/pPOuqanRmTNndPLkSX3wwQc6dOiQbrnlFqux//jHP3TixAnFxMRc0pgAAAC48nF9zPUxAKB1IKeT05ubXVeip6amatasWUpKSlL//v31xhtvKCIiQl9//bV8fX0t4svLy9WlSxfNnz9fr732mtU+KyoqNGzYMF133XX629/+phtuuEGHDx/Wtdde29ync8Xz9/fXnj17tHjxYj311FMqLi5Wly5dFBQUpFWrVmnhwoWqqqrS1q1bL/hU4Lvuuktdu3bVzTffXO/XYKx57bXXNHnyZIWGhsrDw0PPPPOMysrKLOJqamq0ePHiRvVtzYcffqi2bdvK0dFRvr6+SkxM1PDhw61+zefUqVN66aWXzB4sAQAAgNaJ62OujwEArQM5nZze3Ay1dXfft4N+/frp9ttv16pVq0xtAQEBGj16tBITE+s9dvDgwerbt6+MRqNZe3Jysl555RV98803cnJyuqh5lZWVyc3NTSdOnFCHDh3M9p05c0aFhYWm1fNXq99++03e3t5au3atxo4da+/poIlc9M83t3MBGqS+/ILmwXsONNJF5PQz7X1U2P9V+V/fRa72/vp3I7763dS4Pm596rs2Jr9cfrznQCM1MqdfUflcIqejSTVFTrfb7VwqKiqUm5ur8PBws/bw8HDt2rXrovv94IMPFBISounTp8vT01O9e/fWiy++qOrq6kudMnT2L2U//vijEhIS5Obmpvvuu8/eUwIAAADshutjAABaB3I66mO327mUlpaqurpanp6eZu2enp4qKSm56H6///57/fOf/9SDDz6o9PR07d+/X9OnT1dVVZX+93//1+ox5eXlKi8vN7229jULnFVUVCR/f3/dcMMNSklJsfqgAgAAAOBqwfUxAACtAzkd9bH7T4PBYP4VkdraWou2xqipqdF1112n1atXy8HBQUFBQfrxxx/1yiuv2CyiJyYm6rnnnrvoMa8m3bp1kx3vAAQAAABcUbg+BgCgdSCnoz52u52Lh4eHHBwcLFadHz161GJ1emN07dpVv/vd7+Tg4GBqCwgIUElJiSoqKqweEx8frxMnTpi2w4cPX/T4AAAAAAAAAIDWw25FdGdnZwUFBSkzM9OsPTMzU6GhoRfdb//+/fXdd9+ppqbG1LZv3z517drV5tNnXVxc1KFDB7MNAAAAAAAAAAC7FdElKS4uTm+99ZbWrl2rgoICxcbGqqioSNHR0ZLOrhCfNGmS2TH5+fnKz8/XyZMn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lzWtS5cuOnv2rH744QcdPHhQeXl5uv/+++2W88477+jbb78td3udPn1aDRo0cDl/9+7d2rJli91y27RpI0l2y77rrrvs2hcbG6sjR46oqKhI6enpMsaoVatWdsvZunWr3TJKtoO/v7/uuusujRw50tp2GRkZuvXWW+3OVHfp0kXFxcX6+uuvnbbd/P9HCZRuV3lKvu+GDRvqzJkzdr8giI6OVkpKik6cOOHy89u2bVNAQICCg4N14cIFTZ8+3Zq3ZcsW3X///WratKkCAwOVkJCg06dP69y5cw7r76oflGjWrJldOWc6d+6sgIAANWvWTD169FBmZma5617RvlDi5MmT+vHHH3XvvfeWW85d58+f1/jx4/X666+rbt261vSMjAzl5+e7rMfd/pCZmanu3bsrLy9P3bp1q5I2V8aiRYtUr149ZWZm6ocffrCbd9NNN1nfYa9evZSfn293XKiMyZMn2/WJsscgV9zpl126dJG3979j6He/+50uXryob775xpqWm5vr9FckZRlj9MILL2jSpEkKCgqqxBr+27x589SpUyddf/31CggI0IIFC9xeX9Q+ZDqZXoJMJ9PJ9PKR6ajpyHQyvQSZTqaT6eUj0y8Pg+iVUL9+fd1444268cYbdccdd2jRokXy8/PTBx984FD2hRde0IgRIxQWFuZ0WTNmzLCCfu/everUqZM179y5c4qPj1dAQID+53/+R2lpaVq9erWkS5ePVaR0YAQGBtrVs27dOruyxhiHgCkdPMXFxZKktWvX2i3n4MGD+sc//lFuO7777jtFRUW5nF9cXKyHH37Ybrl79+7VkSNHdPfdd1e4niXLqFOnjnbv3m23jIyMDL311lsO22H//v366KOPtGTJEi1ZssTlNijhanrLli3l5eXl9gGv5PvetWuXoqOj9cc//tGaN3HiREVFRVnB6Cw4O3XqpD179mjDhg06ffq0FixYIEn6/vvv9dBDD6l9+/ZKTk7W7t27NXv2bEmyu1ywon5QYtu2bXblnFmxYoX27t2r999/X1lZWUpISCh33SvaF0r8lsspy/P666+rdevWevjhhytVj7v9Yf/+/Ro4cKAef/xxPfnkk9a+ciWkpqZqxowZ+vDDDxUbG6uBAwda+60krVu3zvoOS45Pv3X7Dho0yK5PhIeHV/gZd/plw4YN3drOP/74o1t1vvPOOzp3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", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "import pandas as pd\n", - "import numpy as np\n", - "import matplotlib.pyplot as plt\n", - "\n", - "df = pd.read_csv('data/task1/results.csv')\n", - "mean_times = df.groupby(['Структура', 'Режим'])[['Вставка', 'Поиск', 'Удаление']].mean().reset_index()\n", - "structures = mean_times['Структура'].unique()\n", - "modes = mean_times['Режим'].unique()\n", - "\n", - "fig, axes = plt.subplots(1, 3, figsize=(15, 5))\n", - "operations = ['Вставка', 'Поиск', 'Удаление']\n", - "\n", - "for ax, op in zip(axes, operations):\n", - " # a\n", - " x = np.arange(len(structures))\n", - " width = 0.35\n", - " \n", - " random_vals = []\n", - " sorted_vals = []\n", - " for s in structures:\n", - " random_row = mean_times[(mean_times['Структура']==s) & (mean_times['Режим']=='Cлучайный')]\n", - " sorted_row = mean_times[(mean_times['Структура']==s) & (mean_times['Режим']=='Отсортированный')]\n", - " random_vals.append(random_row[op].values[0] if not random_row.empty else 0)\n", - " sorted_vals.append(sorted_row[op].values[0] if not sorted_row.empty else 0)\n", - " \n", - " ax.bar(x - width/2, random_vals, width, label='Случайный')\n", - " ax.bar(x + width/2, sorted_vals, width, label='Отсортированный')\n", - " ax.set_xticks(x)\n", - " ax.set_xticklabels(structures)\n", - " ax.set_ylabel('Время (сек)')\n", - " ax.set_title(op)\n", - " ax.legend()\n", - "\n", - "plt.tight_layout()\n", - "plt.savefig('data/task1/performance_plot.png', dpi=150)\n", - "plt.show()" - ] - }, - { - "cell_type": "code", - "execution_count": 14, - "id": "1d86131d", - "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "
\n", - "\n", - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
СтруктураРежимВставкаПоискУдаление
0Бинарное деревоCлучайный0.0119210.0001500.000137
1Бинарное деревоОтсортированный0.1221710.0016270.000873
2Связанный списокCлучайный0.0900390.0008930.000605
3Связанный списокОтсортированный0.1624470.0017070.000915
4Хэш-таблицаCлучайный0.0448310.0006180.000324
5Хэш-таблицаОтсортированный0.0493690.0005270.000272
\n", - "
" - ], - "text/plain": [ - " Структура Режим Вставка Поиск Удаление\n", - "0 Бинарное дерево Cлучайный 0.011921 0.000150 0.000137\n", - "1 Бинарное дерево Отсортированный 0.122171 0.001627 0.000873\n", - "2 Связанный список Cлучайный 0.090039 0.000893 0.000605\n", - "3 Связанный список Отсортированный 0.162447 0.001707 0.000915\n", - "4 Хэш-таблица Cлучайный 0.044831 0.000618 0.000324\n", - "5 Хэш-таблица Отсортированный 0.049369 0.000527 0.000272" - ] - }, - "execution_count": 14, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "df = pd.read_csv('data/task1/results.csv')\n", - "df.groupby(['Структура', 'Режим'])[['Вставка', 'Поиск', 'Удаление']].mean().reset_index()" - ] - }, - { - "cell_type": "markdown", - "id": "c9a486a5", - "metadata": {}, - "source": [ - "# 4. Анализ результатов\n", - "---\n", - "### 4.1 Влияние порядка данных на вставку в BST\n", - "При вставке элементов в отсортированном порядке в бинарное дерево оно превращается в связный список - это связанно с тем, что все элементы вставляются в одну ветвь дерева. Сложность всех операций приблтижается к **O(n)**. Вставка в BST на отсортированных данных заняла 0.122171c вместо 0.011921с, разница более чем в 10 раз. Причём, время вставки даже хуже, чем у чистого связнного списка - это связанно с дополнительными расходами бинарного дерева. Поиск так же ухудшился, примерно в 10 раз, а с ним ухудшилось и удаление.\n", - "\n", - "### 4.2 Почему хэш-таблица почти не чувствительна к порядку\n", - "По графикам видно, что для хэш-таблицы время операций почти не изменяется. Исключение составляет лишь поиск, его время больше на отсортированных данных. Это связано с особенностями теста - поиск 10 несуществующих записей ухудшают результат для отсортированных данных. Все эти наблюдения связаны с механизмом работы хэш-таблицы - она распределяет данные по корзинам независимо от порядка поступления. Получается, что сложность всех операций **O(1)**\n", - "\n", - "### 4.3 Почему связный список всегда медленен при поиске\n", - "Для поиска в связном списке нужно просматривать все элементы по порядку, так что сложность всех операций **O(n)**\n", - "\n", - "### 4.4 Сравнение удаления\n", - "\n", - "- **Связаный список** удаление требует сначала найти элемент за O(n), затем переставить ссылки за O(1). Время удаления (0.000605 с) близко ко времени поиска, что логично.\n", - "- **Хеш-таблица:** при удалении, поиск корзины за O(1) и поиск в коротком связаном списке за O(n) удаляется элемент. Время удаления (0.000324) меньше, чем в списке.\n", - "- **BST:** на случайных данных удаление очень быстрое (0.000137 с) благодаря логарифмической высоте. На отсортированных данных время возрастает до 0.000873, что отражает деградацию до O(n)." - ] - }, - { - "cell_type": "markdown", - "id": "a7ed5470", - "metadata": {}, - "source": [ - "# 5. Вывод\n", - "На основе полученных результатов можно сформулировать следующие рекомендации:\n", - "\n", - "- Хеш-таблица – хороший выбор, если приоритетом является максимальная скорость вставки, поиска и удаления по ключу, а порядок элементов не имеет значения. Время операций близко к **O(1)** и практически не зависит от упорядоченности входных данных. Идеальна для кэшей, словарей и частых запросов по идентификатору.\n", - "\n", - "- Двоичное дерево поиска – следует применять, когда необходимо получать данные в отсортированном порядке. На случайных данных демонстрирует хорошую производительность **O(log n)**, однако при поступлении заранее отсортированных элементов вырождается в связный список с падением скорости до **O(n)**.\n", - "\n", - "- Связный список – демонстрирует линейную сложность поиска и удаления **O(n)** что делает его непригодным для задач с частым доступом к произвольным элементам. Может быть оправдан только в узких случаях, где вставки и удаления происходят исключительно в начале или конце коллекции (очереди, стеки) и не требуется поиск.\n", - "\n", - "Таким образом, для реальных задач чаще всего выбирают хеш-таблицы или сбалансированные деревья в зависимости от требований к упорядоченности данных.\n" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.11.2" - } - }, - "nbformat": 4, - "nbformat_minor": 5 -} diff --git a/famutdinovmd/requirements.txt b/famutdinovmd/requirements.txt new file mode 100644 index 0000000..ca0f8d7 --- /dev/null +++ b/famutdinovmd/requirements.txt @@ -0,0 +1,3 @@ +matplotlib>=3.5.0 +pandas>=1.5.0 +numpy>=1.21.0 \ No newline at end of file diff --git a/famutdinovmd/solver.py b/famutdinovmd/solver.py new file mode 100644 index 0000000..b7388a1 --- /dev/null +++ b/famutdinovmd/solver.py @@ -0,0 +1,53 @@ +import time +from dataclasses import dataclass +from typing import List, Optional, Tuple +from models import Cell, Maze +from strategies import PathFindingStrategy + + +@dataclass +class SearchStats: + """Статистика поиска""" + time_ms: float + visited_cells: int + path_length: int + path_found: bool = True + + def __str__(self) -> str: + if not self.path_found: + return f"Путь не найден (время: {self.time_ms:.2f} мс)" + return (f"Время: {self.time_ms:.2f} мс, " + f"Посещено клеток: {self.visited_cells}, " + f"Длина пути: {self.path_length}") + + +class MazeSolver: + """Оркестратор решения лабиринта""" + + def __init__(self, maze: Maze, strategy: Optional[PathFindingStrategy] = None): + self.maze = maze + self._strategy = strategy + + def set_strategy(self, strategy: PathFindingStrategy) -> None: + """Устанавливает стратегию поиска""" + self._strategy = strategy + + def solve(self) -> Tuple[List[Cell], SearchStats]: + """Выполняет поиск пути с текущей стратегией""" + if self._strategy is None: + raise ValueError("Стратегия не установлена") + + start_time = time.perf_counter() + path = self._strategy.find_path(self.maze, self.maze.start, self.maze.exit) + end_time = time.perf_counter() + + time_ms = (end_time - start_time) * 1000 + + stats = SearchStats( + time_ms=time_ms, + visited_cells=len(path) if path else 0, + path_length=len(path) if path else 0, + path_found=bool(path) + ) + + return path, stats \ No newline at end of file diff --git a/famutdinovmd/strategies.py b/famutdinovmd/strategies.py new file mode 100644 index 0000000..5dd6e3a --- /dev/null +++ b/famutdinovmd/strategies.py @@ -0,0 +1,107 @@ +from abc import ABC, abstractmethod +from collections import deque +from heapq import heappush, heappop +from typing import List, Dict, Optional +from models import Cell, Maze + + +class PathFindingStrategy(ABC): + """Абстрактная стратегия поиска пути""" + + @abstractmethod + def find_path(self, maze: Maze, start: Cell, exit_cell: Cell) -> List[Cell]: + """Находит путь от start до exit_cell""" + pass + + +class BFSStrategy(PathFindingStrategy): + """Поиск в ширину (BFS) - гарантирует кратчайший путь""" + + def find_path(self, maze: Maze, start: Cell, exit_cell: Cell) -> List[Cell]: + queue = deque([start]) + visited = {start} + parent: Dict[Cell, Optional[Cell]] = {start: None} + + while queue: + current = queue.popleft() + + if current == exit_cell: + return self._reconstruct_path(parent, current) + + for neighbor in maze.get_neighbors(current): + if neighbor not in visited: + visited.add(neighbor) + parent[neighbor] = current + queue.append(neighbor) + + return [] # Путь не найден + + def _reconstruct_path(self, parent: Dict[Cell, Optional[Cell]], current: Cell) -> List[Cell]: + """Восстанавливает путь от start до current""" + path = [] + while current is not None: + path.append(current) + current = parent.get(current) + return list(reversed(path)) + + +class DFSStrategy(PathFindingStrategy): + """Поиск в глубину (DFS) - быстрый, но не обязательно кратчайший""" + + def find_path(self, maze: Maze, start: Cell, exit_cell: Cell) -> List[Cell]: + stack = [(start, [start])] + visited = {start} + + while stack: + current, path = stack.pop() + + if current == exit_cell: + return path + + for neighbor in maze.get_neighbors(current): + if neighbor not in visited: + visited.add(neighbor) + stack.append((neighbor, path + [neighbor])) + + return [] + + +class AStarStrategy(PathFindingStrategy): + """A* поиск - оптимальный баланс скорости и кратчайшего пути""" + + def _heuristic(self, cell: Cell, exit_cell: Cell) -> int: + """Манхэттенское расстояние (эвристика)""" + return abs(cell.x - exit_cell.x) + abs(cell.y - exit_cell.y) + + def find_path(self, maze: Maze, start: Cell, exit_cell: Cell) -> List[Cell]: + counter = 0 # для разрешения конфликтов в куче + open_set = [(self._heuristic(start, exit_cell), counter, start)] + + g_score: Dict[Cell, float] = {start: 0} + parent: Dict[Cell, Optional[Cell]] = {start: None} + + while open_set: + _, _, current = heappop(open_set) + + if current == exit_cell: + return self._reconstruct_path(parent, current) + + for neighbor in maze.get_neighbors(current): + tentative_g = g_score[current] + 1 + + if neighbor not in g_score or tentative_g < g_score[neighbor]: + parent[neighbor] = current + g_score[neighbor] = tentative_g + counter += 1 + f = tentative_g + self._heuristic(neighbor, exit_cell) + heappush(open_set, (f, counter, neighbor)) + + return [] + + def _reconstruct_path(self, parent: Dict[Cell, Optional[Cell]], current: Cell) -> List[Cell]: + """Восстанавливает путь от start до current""" + path = [] + while current is not None: + path.append(current) + current = parent.get(current) + return list(reversed(path)) \ No newline at end of file diff --git a/famutdinovmd/tasks/1/BinaryTree.py b/famutdinovmd/tasks/1/BinaryTree.py deleted file mode 100644 index 17c11b8..0000000 --- a/famutdinovmd/tasks/1/BinaryTree.py +++ /dev/null @@ -1,80 +0,0 @@ -""" -Двоичное дерево поиска - -Узел — словарь: -{'name': 'Имя', 'phone': '123', 'left': None, 'right': None}. -""" - -def bst_insert(root, name, phone): - """Итеративно вставляет, возвращает новый корень (если корень меняется).""" - if root == None: - return {'name': name, 'phone': phone, 'left': None, 'right': None} - current = root - while True: - if current['name'] == name: - current['phone'] == phone - elif current['name'] < name: - if current['left'] == None: - current['left'] = bst_insert(None, name, phone) - return root - else: - current = current['left'] - else: - if current['right'] == None: - current['right'] = bst_insert(None, name, phone) - return root - else: - current = current['right'] - - - -def find_node_to_delete(root: dict, name: str) -> dict|None: - """Поиск в ширину.""" - while root != None: - if root['name'] == name: - return root - elif name < root['name']: - root = root['left'] - else: - root = root['right'] - return None - -def find_minimal_child(root: dict) -> dict|None: - while root['left']: - root = root['left'] - return root - -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: - # Случай 1: нет детей или один ребенок - if root['left'] is None: - return root['right'] - elif root['right'] is None: - return root['left'] - - # Случай 2: два ребенка - min_node = find_minimal_child(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): - """Центрированный обход. - Рекурсивно собирает записи в отсортированном порядке.""" - if root is None: - return [] - node_values = {"name": root['name'], "phone": root['phone']} - return bst_list_all(root['left']) + [node_values] + bst_list_all(root['right']) - - \ No newline at end of file diff --git a/famutdinovmd/tasks/1/HashTable.py b/famutdinovmd/tasks/1/HashTable.py deleted file mode 100644 index 74a337a..0000000 --- a/famutdinovmd/tasks/1/HashTable.py +++ /dev/null @@ -1,61 +0,0 @@ -""" -Хеш-таблица - -Хранится как список buckets фиксированной длины, -каждый элемент — голова связного списка (или None). -""" - -from LinkedList import * - -def hash_fun(name, size) : - """Принимает имя и возвращает индекс бакета для него.""" - if size <= 0: - raise ValueError("size должен быть больше 0") - hash_sum = 0 - base = 1103 - - for i, letter in enumerate(name): - power = len(name) - i - 1 # или просто i - hash_sum += ord(letter) * (base ** power) - return hash_sum % size - - -def ht_insert(buckets, name, phone, blen=50): - """Возвращает новый массив бакетов - Вычисляет индекс, вызывает ll_insert для соответствующего бакета. - Функция не меняет размер массива бакетов автоматически!""" - if buckets == []: - raise ValueError("Длинна buckets должна быть больше 0") - - size = len(buckets) - index = hash_fun(name, size) - buckets[index] = ll_insert(buckets[index], name, phone) - return buckets - -def ht_delete(buckets, name): - """Возвращает новый массив бакетов без элемента с именем name""" - if buckets == []: - raise ValueError("Длинна buckets должна быть больше 0") - - size = len(buckets) - index = hash_fun(name, size) - buckets[index] = ll_delete(buckets[index], name) - return buckets - - -def ht_find(buckets, name): - if buckets == []: - raise ValueError("Длинна buckets должна быть больше 0") - - size = len(buckets) - index = hash_fun(name, size) - return ll_find(buckets[index], name) - - -def ht_list_all(buckets): - """Собирает все записи из всех бакетов и сортирует""" - allRecords = [] - for bucket in buckets: - allRecords.extend(ll_list_all(bucket)) - return sorted(allRecords, key=lambda x: x[0]) - \ No newline at end of file diff --git a/famutdinovmd/tasks/1/LinkedList.py b/famutdinovmd/tasks/1/LinkedList.py deleted file mode 100644 index 833325c..0000000 --- a/famutdinovmd/tasks/1/LinkedList.py +++ /dev/null @@ -1,62 +0,0 @@ -""" -Связный список (LinkedListPhoneBook) - -Узел представляется словарём: -{'name': 'Имя', 'phone': '123', 'next': None}. -""" - - -def ll_insert(head, name, phone): - """ - Проходит до конца (или сразу добавляет в конец) и возвращает новую - голову (если вставка в начало) или изменяет список по ссылке. - Удобнее возвращать новую голову, если вставка может быть в начало. - """ - newNode = {'name': name, 'phone': phone, 'next': next} - if head == None: - return newNode - currentNode = head - while currentNode ['next'] != None: - currentNode = currentNode['next'] - currentNode ['next'] = newNode - return head - - -def ll_find(head, name): - """Ищет узел, возвращает телефон или None.""" - currentNode = head - while currentNode != None: - if currentNode['name'] == name: - return currentNode['phone'] - currentNode = currentNode['next'] - return None - - - -def ll_delete(head, name): - """Удаляет узел, возвращает новую голову.""" - if head == None: - return None - if head['name'] == name: - return head['next'] - - currentNode = head - while currentNode['next'] != None: - if currentNode['next']['name'] == name: - currentNode['next'] = currentNode['next']['next'] - return head - currentNode = currentNode['next'] - return head - - -def ll_list_all(head): - """Cобирает все записи в список и сортирует. - сортировка вынесена отдельно).""" - records = [] - currentNobe = head - while currentNobe != None: - records.append(currentNobe['name'], currentNobe['phone']) - currentNobe = currentNobe['next'] - records.sort(key=lambda item: item[0]) - return records - \ No newline at end of file diff --git a/famutdinovmd/visualize.py b/famutdinovmd/visualize.py new file mode 100644 index 0000000..ec1f734 --- /dev/null +++ b/famutdinovmd/visualize.py @@ -0,0 +1,88 @@ +import pandas as pd +import matplotlib.pyplot as plt +import numpy as np +from pathlib import Path + + +def plot_results(csv_file='experiment_results.csv'): + """Строит графики сравнения алгоритмов""" + + if not Path(csv_file).exists(): + print(f"❌ {csv_file} не найден. Сначала запустите main.py") + return + + # Загрузка данных + df = pd.read_csv(csv_file) + df = df[df['path_found'] == True] + + if df.empty: + print("❌ Нет данных для графиков") + return + + # Подготовка данных + mazes = [m.replace('.txt', '') for m in df['maze_file'].unique()] + strategies = df['strategy'].unique() + + # Создание графиков + fig, axes = plt.subplots(1, 3, figsize=(15, 5)) + fig.suptitle('Сравнение алгоритмов поиска в лабиринте', + fontsize=14, fontweight='bold') + + x = np.arange(len(mazes)) + width = 0.25 + colors = {'BFS': '#3498db', 'DFS': '#2ecc71', 'A*': '#e74c3c'} + + for i, strategy in enumerate(strategies): + times, visited, lengths = [], [], [] + + for maze in df['maze_file'].unique(): + data = df[(df['strategy'] == strategy) & (df['maze_file'] == maze)] + if not data.empty: + times.append(data['time_mean'].values[0]) + visited.append(data['visited_mean'].values[0]) + lengths.append(data['path_length_mean'].values[0]) + else: + times.append(0) + visited.append(0) + lengths.append(0) + + # График времени + axes[0].bar(x + i*width, times, width, label=strategy, + color=colors.get(strategy, 'gray'), alpha=0.7) + + # График посещённых клеток + axes[1].bar(x + i*width, visited, width, label=strategy, + color=colors.get(strategy, 'gray'), alpha=0.7) + + # График длины пути + axes[2].bar(x + i*width, lengths, width, label=strategy, + color=colors.get(strategy, 'gray'), alpha=0.7) + + # Настройка внешнего вида + axes[0].set_title('⏱️ Время выполнения (мс)') + 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) + + axes[1].set_title('📍 Посещённые клетки') + 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) + + axes[2].set_title('🛤️ Длина пути') + 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('experiment_results.png', dpi=150, bbox_inches='tight') + plt.show() + + print("✅ Графики сохранены в experiment_results.png") + + +if __name__ == "__main__": + plot_results() \ No newline at end of file