[1,2]newlaba2 #304

Merged
AlexanderVah merged 12 commits from ivanchenkoam/2026-rff_mp:newlaba2 into develop 2026-05-30 11:23:35 +00:00
20 changed files with 2745 additions and 0 deletions

537
ivanchenkoam/laba1.txt Normal file
View File

@ -0,0 +1,537 @@
import time
import csv
import random
import sys
from typing import List, Tuple, Optional, Any, Dict
#лимит рекурсии
sys.setrecursionlimit(20000)
def ll_insert(head: Optional[Dict], name: str, phone: str) -> Dict:
"""Вставка в конец связного списка"""
new_node = {'name': name, 'phone': phone, 'next': None}
if head is None:
return new_node
current = head
while current['next'] is not None:
# Обновляем, если уже есть
if current['name'] == name:
current['phone'] = phone
return head
current = current['next']
if current['name'] == name:
current['phone'] = phone
else:
current['next'] = new_node
return head
def ll_find(head: Optional[Dict], name: str) -> Optional[str]:
"""Поиск в связном списке"""
current = head
while current is not None:
if current['name'] == name:
return current['phone']
current = current['next']
return None
def ll_delete(head: Optional[Dict], name: str) -> Optional[Dict]:
"""Удаление из связного списка"""
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: Optional[Dict]) -> List[Tuple[str, str]]:
"""Сбор всех записей из связного списка с сортировкой"""
records = []
current = head
while current is not None:
records.append((current['name'], current['phone']))
current = current['next']
def hash_function(name: str, size: int) -> int:
"""Простая хеш-функция"""
return sum(ord(c) for c in name) % size
def ht_create(size: int = 1000) -> List[Optional[Dict]]:
"""Создание хеш-таблицы"""
return [None] * size
def ht_insert(buckets: List[Optional[Dict]], name: str, phone: str) -> None:
"""Вставка в хеш-таблицу"""
index = hash_function(name, len(buckets))
buckets[index] = ll_insert(buckets[index], name, phone)
def ht_find(buckets: List[Optional[Dict]], name: str) -> Optional[str]:
"""Поиск в хеш-таблице"""
index = hash_function(name, len(buckets))
return ll_find(buckets[index], name)
def ht_delete(buckets: List[Optional[Dict]], name: str) -> None:
"""Удаление из хеш-таблицы"""
index = hash_function(name, len(buckets))
buckets[index] = ll_delete(buckets[index], name)
def ht_list_all(buckets: List[Optional[Dict]]) -> List[Tuple[str, str]]:
"""Сбор всех записей из хеш-таблицы с сортировкой"""
records = []
for head in buckets:
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
def bst_insert(root: Optional[Dict], name: str, phone: str) -> Dict:
"""Вставка в BST (итеративная)"""
new_node = {'name': name, 'phone': phone, 'left': None, 'right': None}
if root is None:
return new_node
current = root
while True:
if name < current['name']:
if current['left'] is None:
current['left'] = new_node
break
else:
current = current['left']
elif name > current['name']:
if current['right'] is None:
current['right'] = new_node
break
else:
current = current['right']
else:
# Обновляем существующую запись
current['phone'] = phone
break
return root
def bst_find(root: Optional[Dict], name: str) -> Optional[str]:
"""Поиск в BST (итеративный)"""
current = root
while current is not None:
if name == current['name']:
return current['phone']
elif name < current['name']:
current = current['left']
else:
current = current['right']
return None
def bst_min_node(node: Dict) -> Dict:
"""Поиск узла с минимальным значением"""
current = node
while current['left'] is not None:
current = current['left']
return current
def bst_delete(root: Optional[Dict], name: str) -> Optional[Dict]:
"""Удаление из BST (итеративная версия)"""
if root is None:
return None
# Поиск узла для удаления и его родителя
parent = None
current = root
while current is not None and current['name'] != name:
parent = current
if name < current['name']:
current = current['left']
else:
current = current['right']
if current is None:
return root
# Случай 1: узел не имеет детей
if current['left'] is None and current['right'] is None:
if parent is None:
return None
if parent['left'] == current:
parent['left'] = None
else:
parent['right'] = None
return root
# Случай 2: узел имеет одного ребёнка
if current['left'] is None:
child = current['right']
elif current['right'] is None:
child = current['left']
else:
# Случай 3: узел имеет двух детей
# Находим минимальный узел в правом поддереве
successor_parent = current
successor = current['right']
while successor['left'] is not None:
successor_parent = successor
successor = successor['left']
# Копируем данные из successor в current
current['name'] = successor['name']
current['phone'] = successor['phone']
# Удаляем successor
if successor_parent['left'] == successor:
successor_parent['left'] = successor['right']
else:
successor_parent['right'] = successor['right']
return root
# Присоединяем ребёнка к родителю
if parent is None:
return child
if parent['left'] == current:
parent['left'] = child
else:
parent['right'] = child
return root
def bst_inorder(root: Optional[Dict], records: List[Tuple[str, str]]) -> None:
"""Центрированный обход BST (рекурсивный)"""
if root is not None:
bst_inorder(root['left'], records)
records.append((root['name'], root['phone']))
bst_inorder(root['right'], records)
def bst_list_all(root: Optional[Dict]) -> List[Tuple[str, str]]:
"""Сбор всех записей из BST (уже отсортированы)"""
records = []
bst_inorder(root, records)
return records
records.sort(key=lambda x: x[0])
return records
def generate_records(n: int) -> List[Tuple[str, str]]:
"""Генерация записей"""
records = [(f"User_{i:05d}", f"+7-999-{i:07d}") for i in range(n)]
return records
def measure_insertion(structure_type: str, data: List[Tuple[str, str]],
ht_size: int = 1000) -> float:
"""Замер времени вставки"""
start = time.perf_counter()
if structure_type == "LinkedList":
head = None
for name, phone in data:
head = ll_insert(head, name, phone)
elif structure_type == "HashTable":
buckets = ht_create(ht_size)
for name, phone in data:
ht_insert(buckets, name, phone)
elif structure_type == "BST":
root = None
for name, phone in data:
root = bst_insert(root, name, phone)
end = time.perf_counter()
return end - start
def measure_find(structure_type: str, data: List[Tuple[str, str]],
existing_names: List[str], missing_names: List[str],
ht_size: int = 1000) -> Tuple[float, Any]:
"""Замер времени поиска (возвращает время и структуру для удаления)"""
# Сначала создаём структуру
if structure_type == "LinkedList":
head = None
for name, phone in data:
head = ll_insert(head, name, phone)
start = time.perf_counter()
for name in existing_names + missing_names:
ll_find(head, name)
end = time.perf_counter()
return end - start, head
elif structure_type == "HashTable":
buckets = ht_create(ht_size)
for name, phone in data:
ht_insert(buckets, name, phone)
start = time.perf_counter()
for name in existing_names + missing_names:
ht_find(buckets, name)
end = time.perf_counter()
return end - start, buckets
elif structure_type == "BST":
root = None
for name, phone in data:
root = bst_insert(root, name, phone)
start = time.perf_counter()
for name in existing_names + missing_names:
bst_find(root, name)
end = time.perf_counter()
return end - start, root
def measure_delete(structure_type: str, structure: Any,
names_to_delete: List[str]) -> float:
"""Замер времени удаления"""
start = time.perf_counter()
if structure_type == "LinkedList":
head = structure
for name in names_to_delete:
head = ll_delete(head, name)
elif structure_type == "HashTable":
buckets = structure
for name in names_to_delete:
ht_delete(buckets, name)
elif structure_type == "BST":
root = structure
for name in names_to_delete:
root = bst_delete(root, name)
end = time.perf_counter()
return end - start
def run_experiment(n_records: int = 10000, n_find: int = 110,
n_delete: int = 50, n_runs: int = 5) -> List[List]:
"""Запуск всех замеров"""
# Генерация данных
all_records = generate_records(n_records)
records_shuffled = all_records.copy()
random.shuffle(records_shuffled)
records_sorted = sorted(all_records, key=lambda x: x[0])
# Имена для поиска
all_names = [name for name, _ in all_records]
existing_names = random.sample(all_names, n_find - 10)
missing_names = [f"None_{i}" for i in range(10)]
# Имена для удаления
names_to_delete = random.sample(all_names, n_delete)
structures = ["LinkedList", "HashTable", "BST"]
modes = {"shuffled": records_shuffled, "sorted": records_sorted}
# Заголовок CSV
results = [["Структура", "Режим", "Операция",
"Замер1", "Замер2", "Замер3", "Замер4", "Замер5", "Среднее"]]
for structure in structures:
for mode_name, mode_data in modes.items():
print(f"\nТестирование: {structure}, режим: {mode_name}")
# Вставка
insertion_times = []
for run in range(n_runs):
print(f" Вставка, run {run+1}/{n_runs}...")
t = measure_insertion(structure, mode_data)
insertion_times.append(t)
avg_insertion = sum(insertion_times) / n_runs
results.append([structure, mode_name, "вставка"] +
[f"{t:.6f}" for t in insertion_times] +
[f"{avg_insertion:.6f}"])
print(f" Замеры: {[f'{t:.6f}' for t in insertion_times]}")
print(f" Среднее: {avg_insertion:.6f} сек")
# Поиск
find_times = []
for run in range(n_runs):
print(f" Поиск, run {run+1}/{n_runs}...")
t, _ = measure_find(structure, mode_data,
existing_names, missing_names)
find_times.append(t)
avg_find = sum(find_times) / n_runs
results.append([structure, mode_name, "поиск"] +
[f"{t:.6f}" for t in find_times] +
[f"{avg_find:.6f}"])
print(f" Замеры: {[f'{t:.6f}' for t in find_times]}")
print(f" Среднее: {avg_find:.6f} сек")
# Удаление
delete_times = []
for run in range(n_runs):
print(f" Удаление, run {run+1}/{n_runs}...")
# Создаём свежую структуру для каждого замера удаления
if structure == "LinkedList":
head = None
for name, phone in mode_data:
head = ll_insert(head, name, phone)
t = measure_delete(structure, head, names_to_delete)
elif structure == "HashTable":
buckets = ht_create()
for name, phone in mode_data:
ht_insert(buckets, name, phone)
t = measure_delete(structure, buckets, names_to_delete)
elif structure == "BST":
root = None
for name, phone in mode_data:
root = bst_insert(root, name, phone)
t = measure_delete(structure, root, names_to_delete)
delete_times.append(t)
avg_delete = sum(delete_times) / n_runs
results.append([structure, mode_name, "удаление"] +
[f"{t:.6f}" for t in delete_times] +
[f"{avg_delete:.6f}"])
print(f" Замеры: {[f'{t:.6f}' for t in delete_times]}")
print(f" Среднее: {avg_delete:.6f} сек")
return results
def save_results(results: List[List], filename: str = "results.csv"):
"""Сохранение результатов в CSV"""
with open(filename, "w", newline="", encoding="utf-8") as f:
writer = csv.writer(f)
writer.writerows(results)
print(f"\nРезультаты сохранены в {filename}")
def plot_results(results_file: str = "results.csv"):
"""Построение графика сравнения производительности"""
import matplotlib.pyplot as plt
import numpy as np
# Чтение результатов из CSV
data = {}
with open(results_file, 'r', encoding='utf-8') as f:
reader = csv.reader(f)
header = next(reader) # пропускаем заголовок
for row in reader:
structure = row[0]
mode = row[1]
operation = row[2]
# Берём последнюю колонку (Среднее)
avg_time = float(row[-1])
if structure not in data:
data[structure] = {}
if mode not in data[structure]:
data[structure][mode] = {}
data[structure][mode][operation] = avg_time
# Настройка стиля
plt.style.use('seaborn-v0_8-darkgrid')
fig, axes = plt.subplots(1, 3, figsize=(15, 5))
colors = ['#FF6B6B', '#4ECDC4']
structures = ["LinkedList", "HashTable", "BST"]
modes = ["shuffled", "sorted"]
operations = ["вставка", "поиск", "удаление"]
op_titles = ["ВСТАВКИ", "ПОИСКА (110 запросов)", "УДАЛЕНИЯ (50 записей)"]
for idx, (op, op_title) in enumerate(zip(operations, op_titles)):
ax = axes[idx]
x = np.arange(len(structures))
width = 0.35
shuffled_vals = [data[s]['shuffled'][op] for s in structures]
sorted_vals = [data[s]['sorted'][op] for s in structures]
bars1 = ax.bar(x - width/2, shuffled_vals, width,
label='Случайный порядок', color=colors[0])
bars2 = ax.bar(x + width/2, sorted_vals, width,
label='Отсортированный порядок', color=colors[1])
ax.set_xlabel('Структура данных')
ax.set_ylabel('Время (секунды)')
ax.set_title(f'Сравнение времени {op_title}')
ax.set_xticks(x)
ax.set_xticklabels(['Связный\nсписок', 'Хеш-\nтаблица', 'Двоичное\nдерево'])
ax.legend()
# Добавляем значения на столбцы
for bars in [bars1, bars2]:
for bar in bars:
height = bar.get_height()
fmt = '{:.4f}'.format(height) if op == 'вставка' else '{:.6f}'.format(height)
ax.text(bar.get_x() + bar.get_width()/2., height,
fmt, ha='center', va='bottom', fontsize=8)
plt.tight_layout()
plt.savefig('performance_comparison.png', dpi=150, bbox_inches='tight')
plt.show()
# Вывод сводной таблицы в консоль
print("\n" + "=" * 90)
print("СВОДНАЯ ТАБЛИЦА РЕЗУЛЬТАТОВ (среднее время в секундах)")
print("=" * 90)
print(f"{'Структура':<15} {'Режим':<12} {'Вставка':<14} {'Поиск':<14} {'Удаление':<14}")
print("-" * 90)
for structure in structures:
for mode in modes:
print(f"{structure:<15} {mode:<12} "
f"{data[structure][mode]['вставка']:<14.6f} "
f"{data[structure][mode]['поиск']:<14.6f} "
f"{data[structure][mode]['удаление']:<14.6f}")
print("=" * 90)
def main():
print("=" * 60)
print("ЭКСПЕРИМЕНТАЛЬНОЕ СРАВНЕНИЕ СТРУКТУР ДАННЫХ")
print("Связный список | Хеш-таблица | Двоичное дерево поиска")
print("=" * 60)
# Запуск эксперимента (5 прогонов)
results = run_experiment(n_records=10000, n_runs=5)
# Сохранение результатов
save_results(results)
# Построение графика
try:
import matplotlib.pyplot as plt
print("\nПостроение графика...")
plot_results("results.csv")
except ImportError:
print("\nВНИМАНИЕ: Библиотека matplotlib не установлена.")
print("Для построения графика выполните: pip install matplotlib")
print("Результаты сохранены в CSV файл, вы можете построить график в Excel.")
print("\n" + "=" * 60)
print("ЭКСПЕРИМЕНТ ЗАВЕРШЁН")
print("=" * 60)
if __name__ == "__main__":
main()

View File

@ -0,0 +1,61 @@
"""Паттерн Builder - загрузка лабиринта из файла"""
from abc import ABC, abstractmethod
from typing import List
from models import Cell, Maze
class MazeBuilder(ABC):
"""Абстрактный строитель лабиринта"""
@abstractmethod
def build_from_file(self, filename: str) -> Maze:
pass
class TextFileMazeBuilder(MazeBuilder):
"""Строитель лабиринта из текстового файла"""
def build_from_file(self, filename: str) -> Maze:
with open(filename, 'r', encoding='utf-8') as file:
lines = [line.rstrip('\n') for line in file.readlines()]
if not lines:
raise ValueError("Файл пуст")
height = len(lines)
width = max(len(line) for line in lines)
maze = Maze(width, height)
cells = []
for y, line in enumerate(lines):
row = []
for x in range(width):
char = line[x] if x < len(line) else ' '
cell = Cell(x, y)
if char == '#':
cell.is_wall = True
elif char == 'S':
cell.is_start = True
elif char == 'E':
cell.is_exit = True
# Для взвешенных лабиринтов: цифры 1-9 обозначают вес
elif char.isdigit():
cell.is_wall = False
cell.weight = int(char)
else:
cell.is_wall = False
row.append(cell)
cells.append(row)
maze.set_cells(cells)
if maze.start is None:
raise ValueError("Нет стартовой клетки (S)")
if maze.exit is None:
raise ValueError("Нет выходной клетки (E)")
return maze

View File

@ -0,0 +1,34 @@
"""Паттерн Command - команды для управления игроком"""
from abc import ABC, abstractmethod
from models import Cell, Player
class Command(ABC):
"""Интерфейс команды"""
@abstractmethod
def execute(self) -> None:
pass
@abstractmethod
def undo(self) -> None:
pass
class MoveCommand(Command):
"""Команда перемещения игрока"""
def __init__(self, player: Player, new_cell: Cell):
self._player = player
self._new_cell = new_cell
self._old_cell = player.current_cell
def execute(self) -> None:
"""Выполнение перемещения"""
if self._player.can_move_to(self._new_cell):
self._player.move_to(self._new_cell)
def undo(self) -> None:
"""Отмена перемещения"""
self._player.move_to(self._old_cell)

View File

@ -0,0 +1,94 @@
"""Экспериментальное сравнение алгоритмов"""
import csv
from pathlib import Path
from typing import List, Dict, Any
from builders import TextFileMazeBuilder
from solver import MazeSolver
from strategies import BFSStrategy, DFSStrategy, AStarStrategy
class ExperimentRunner:
"""Запуск экспериментального сравнения алгоритмов"""
def __init__(self):
self.builder = TextFileMazeBuilder()
self.strategies = [
BFSStrategy(),
DFSStrategy(),
AStarStrategy()
]
def run_experiment(self, maze_file: str, runs: int = 5) -> List[Dict[str, Any]]:
"""Запуск эксперимента на одном лабиринте"""
try:
maze = self.builder.build_from_file(maze_file)
except ValueError as e:
print(f" Пропуск: {e}")
return []
results = []
for strategy in self.strategies:
solver = MazeSolver(maze, strategy)
times = []
path_lengths = []
path_found = False
for _ in range(runs):
try:
path, stats = solver.solve()
times.append(stats.time_ms)
path_lengths.append(stats.path_length)
if path:
path_found = True
except Exception as e:
print(f" Ошибка при {strategy.name}: {e}")
continue
if times:
results.append({
'maze': Path(maze_file).stem,
'strategy': strategy.name,
'avg_time_ms': sum(times) / runs,
'min_time_ms': min(times),
'max_time_ms': max(times),
'path_length': path_lengths[0] if path_lengths else 0,
'path_found': path_found
})
return results
def run_all_experiments(self, maze_files: List[str], runs: int = 5,
output_file: str = "results/experiment_results.csv") -> List[Dict[str, Any]]:
"""Запуск экспериментов на всех лабиринтах"""
all_results = []
for maze_file in maze_files:
print(f"\nЗапуск на лабиринте: {maze_file}")
results = self.run_experiment(maze_file, runs)
for r in results:
status = "" if r['path_found'] else ""
print(f" {r['strategy']}: {r['avg_time_ms']:.3f} мс, путь: {r['path_length']} {status}")
if results:
all_results.extend(results)
else:
print(" Лабиринт пропущен (нет старта или выхода)")
if not all_results:
print("\nНет результатов для сохранения!")
return []
# Сохранение в CSV
Path("results").mkdir(exist_ok=True)
with open(output_file, 'w', newline='', encoding='utf-8') as f:
writer = csv.DictWriter(f, fieldnames=all_results[0].keys())
writer.writeheader()
writer.writerows(all_results)
print(f"\nРезультаты сохранены в {output_file}")
return all_results

View File

@ -0,0 +1,204 @@
"""Главный файл программы"""
import sys
from pathlib import Path
from builders import TextFileMazeBuilder
from solver import MazeSolver
from strategies import BFSStrategy, DFSStrategy, AStarStrategy
from visualization import ConsoleView
from commands import MoveCommand
from models import Player
from experiments import ExperimentRunner
def create_test_maze_files():
"""Создание тестовых лабиринтов"""
Path("mazes").mkdir(exist_ok=True)
# Лабиринт 1: Маленький запутанный (10×10)
small_maze = [
"##########",
"#S #",
"# ####### #",
"# # #",
"##### # # #",
"# # #",
"# ### ### #",
"# # #",
"# #### E#",
"##########"
]
# Лабиринт 2: Простой прямой путь (10×10)
simple_maze = [
"##########",
"#S #",
"# #",
"# #",
"# #",
"# #",
"# #",
"# #",
"# E#",
"##########"
]
# Лабиринт 3: Без выхода (10×10)
no_exit_maze = [
"##########",
"#S #",
"# ####### #",
"# # #",
"##### # # #",
"# # #",
"# ### ### #",
"# # #",
"# #######",
"##########"
]
# Лабиринт 4: Спиральный
spiral_maze = [
"##########",
"#S #",
"# ####### #",
"# # # #",
"# # ### # #",
"# # # # #",
"# # ### # #",
"# # # #",
"# #######E#",
"##########"
]
with open("mazes/small_maze.txt", "w", encoding="utf-8") as f:
f.write('\n'.join(small_maze))
with open("mazes/simple_maze.txt", "w", encoding="utf-8") as f:
f.write('\n'.join(simple_maze))
with open("mazes/no_exit_maze.txt", "w", encoding="utf-8") as f:
f.write('\n'.join(no_exit_maze))
with open("mazes/spiral_maze.txt", "w", encoding="utf-8") as f:
f.write('\n'.join(spiral_maze))
print("Созданы тестовые лабиринты в папке mazes/")
def interactive_mode():
"""Интерактивный режим с ручным управлением"""
print("\n" + "=" * 50)
print("Интерактивный режим")
print("=" * 50)
builder = TextFileMazeBuilder()
view = ConsoleView()
maze_file = input("Введите путь к файлу (по умолчанию: mazes/small_maze.txt): ")
if not maze_file:
maze_file = "mazes/small_maze.txt"
try:
maze = builder.build_from_file(maze_file)
view.update("maze_loaded", {"maze": maze})
except Exception as e:
print(f"Ошибка: {e}")
return
print("\nСтратегии:")
print("1. BFS (кратчайший путь)")
print("2. DFS (быстрый, но не оптимальный)")
print("3. A* (оптимальный с эвристикой)")
choice = input("Выберите (1-3): ")
strategies = {
"1": BFSStrategy(),
"2": DFSStrategy(),
"3": AStarStrategy()
}
strategy = strategies.get(choice, BFSStrategy())
print(f"\nВыбрана стратегия: {strategy.name}")
solver = MazeSolver(maze, strategy)
path, stats = solver.solve()
if path:
view.update("path_found", {"path": path, "maze": maze})
print(f"\n{stats}")
else:
view.update("path_not_found", {})
print("Путь не найден!")
# Демонстрация Command (пошаговое движение)
print("\n" + "-" * 30)
print("Демонстрация паттерна Command (пошаговое движение)")
print("-" * 30)
if path:
player = Player(maze.start)
print("\nПошаговое движение по найденному пути (Enter - следующий шаг, q - выход):")
for i, cell in enumerate(path[1:], 1):
cmd = MoveCommand(player, cell)
cmd.execute()
view.render(maze, player.current_cell, path[:i+1])
print(f"Шаг {i}/{len(path)-1}")
key = input("Нажмите Enter для продолжения или 'q' для выхода: ")
if key.lower() == 'q':
break
if player.current_cell == maze.exit:
print("\n🎉 Вы достигли выхода!")
def experiment_mode():
"""Экспериментальный режим сравнения алгоритмов"""
print("\n" + "=" * 50)
print("Экспериментальное сравнение алгоритмов")
print("=" * 50)
create_test_maze_files()
runner = ExperimentRunner()
maze_files = [
"mazes/small_maze.txt",
"mazes/simple_maze.txt",
"mazes/no_exit_maze.txt",
"mazes/spiral_maze.txt"
]
results = runner.run_all_experiments(maze_files, runs=10)
print("\n" + "=" * 50)
print("Сводная таблица результатов:")
print("=" * 50)
print(f"{'Лабиринт':<15} {'Стратегия':<10} {'Ср. время (мс)':<15} {'Длина пути':<12} {'Найден':<8}")
print("-" * 65)
for r in results:
status = "" if r['path_found'] else ""
print(f"{r['maze']:<15} {r['strategy']:<10} {r['avg_time_ms']:<15.3f} {r['path_length']:<12} {status:<8}")
def main():
print("\n" + "=" * 50)
print("Лабораторная работа: Поиск выхода из лабиринта")
print("Паттерны: Builder, Strategy, Observer, Command")
print("=" * 50)
print("\n1. Интерактивный режим (ручное управление)")
print("2. Экспериментальный режим (сравнение алгоритмов)")
choice = input("\nВыберите (1-2): ")
if choice == "1":
interactive_mode()
elif choice == "2":
experiment_mode()
else:
print("Неверный выбор!")
if __name__ == "__main__":
main()

View File

@ -0,0 +1,10 @@
##########
#S #
# ####### #
# # #
##### # # #
# # #
# ### ### #
# # #
# #######
##########

View File

@ -0,0 +1,10 @@
##########
#S #
# #
# #
# #
# #
# #
# #
# E#
##########

View File

@ -0,0 +1,10 @@
##########
#S #
# ####### #
# # #
##### # # #
# # #
# ### ### #
# # #
# #### E#
##########

View File

@ -0,0 +1,10 @@
##########
#S #
# ####### #
# # # #
# # ### # #
# # # # #
# # ### # #
# # # #
# #######E#
##########

View File

@ -0,0 +1,113 @@
"""Модели данных: Cell, Maze, Player"""
from dataclasses import dataclass
from typing import List, Optional
@dataclass
class Cell:
"""Клетка лабиринта"""
x: int
y: int
is_wall: bool = False
is_start: bool = False
is_exit: bool = False
weight: int = 1 # для взвешенных лабиринтов
def is_passable(self) -> bool:
"""Проверка, можно ли пройти через клетку"""
return not self.is_wall
def __hash__(self) -> int:
return hash((self.x, self.y))
def __eq__(self, other: object) -> bool:
if not isinstance(other, Cell):
return False
return self.x == other.x and self.y == other.y
class Maze:
"""Лабиринт"""
def __init__(self, width: int, height: int):
self.width = width
self.height = height
self._cells: List[List[Cell]] = []
self.start: Optional[Cell] = None
self.exit: Optional[Cell] = None
self.name: str = "Лабиринт"
def set_cells(self, cells: List[List[Cell]]) -> None:
"""Устанавливает клетки и определяет старт/выход"""
self._cells = cells
for row in cells:
for cell in row:
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 row in self._cells:
line = ''
for cell in row:
if cell.is_start:
line += 'S'
elif cell.is_exit:
line += 'E'
elif cell.is_wall:
line += '#'
else:
line += ' '
result.append(line)
return '\n'.join(result)
class Player:
"""Игрок для пошагового режима"""
def __init__(self, start_cell: Cell):
self.current_cell = start_cell
self.start_cell = start_cell
self.history: List[Cell] = []
def move_to(self, cell: Cell) -> None:
"""Перемещение игрока в клетку"""
self.history.append(self.current_cell)
self.current_cell = cell
def undo(self) -> None:
"""Отмена последнего перемещения"""
if self.history:
self.current_cell = self.history.pop()
def can_move_to(self, cell: Cell) -> bool:
"""Проверка возможности перемещения"""
return cell.is_passable()
def reset(self) -> None:
"""Сброс игрока на старт"""
self.current_cell = self.start_cell
self.history.clear()

View File

@ -0,0 +1,662 @@
"""Генерация отчёта в формате Jupyter Notebook с графиками и анализом"""
import json
from pathlib import Path
from typing import List, Dict, Any
class ReportGenerator:
"""Генератор отчёта в формате Jupyter Notebook"""
@staticmethod
def generate_time_chart(results: List[Dict[str, Any]]) -> str:
"""Генерирует ASCII-график времени выполнения"""
# Фильтруем результаты только для найденных путей
filtered = [r for r in results if r['path_found'] and r['maze'] != 'no_exit_maze']
if not filtered:
return "Нет данных для построения графика времени\n"
# Группируем по лабиринтам
mazes = {}
for r in filtered:
if r['maze'] not in mazes:
mazes[r['maze']] = []
mazes[r['maze']].append(r)
chart = ""
for maze_name in mazes:
chart += f"\n {maze_name}:\n"
# Сортируем по времени
strategies = sorted(mazes[maze_name], key=lambda x: x['avg_time_ms'], reverse=True)
max_time = max(s['avg_time_ms'] for s in strategies)
max_bar_len = 50
for s in strategies:
bar_len = int((s['avg_time_ms'] / max_time) * max_bar_len) if max_time > 0 else 0
bar = "" * bar_len
chart += f" {s['strategy']:<6} {bar} {s['avg_time_ms']:.3f} мс\n"
return chart
@staticmethod
def generate_path_length_chart(results: List[Dict[str, Any]]) -> str:
"""Генерирует ASCII-график длины пути"""
# Фильтруем результаты только для найденных путей
filtered = [r for r in results if r['path_found'] and r['maze'] != 'no_exit_maze']
if not filtered:
return "Нет данных для построения графика длины пути\n"
# Группируем по лабиринтам
mazes = {}
for r in filtered:
if r['maze'] not in mazes:
mazes[r['maze']] = []
mazes[r['maze']].append(r)
chart = ""
for maze_name in mazes:
chart += f"\n {maze_name}:\n"
# Сортируем по длине пути
strategies = sorted(mazes[maze_name], key=lambda x: x['path_length'], reverse=True)
max_len = max(s['path_length'] for s in strategies)
max_bar_len = 40
for s in strategies:
bar_len = int((s['path_length'] / max_len) * max_bar_len) if max_len > 0 else 0
bar = "" * bar_len
chart += f" {s['strategy']:<6} {bar} {s['path_length']}\n"
return chart
@staticmethod
def generate_ranking_table(results: List[Dict[str, Any]]) -> str:
"""Генерирует таблицу ранжирования"""
# Фильтруем результаты
filtered = [r for r in results if r['path_found'] and r['maze'] != 'no_exit_maze']
if not filtered:
return "Нет данных для построения таблицы ранжирования\n"
# Группируем по лабиринтам
mazes = {}
for r in filtered:
if r['maze'] not in mazes:
mazes[r['maze']] = []
mazes[r['maze']].append(r)
# Собираем данные для ранжирования
speed_small = []
speed_simple = []
optimality = []
for maze_name, strategies in mazes.items():
for s in strategies:
if maze_name == 'small_maze':
speed_small.append((s['strategy'], s['avg_time_ms']))
elif maze_name == 'simple_maze':
speed_simple.append((s['strategy'], s['avg_time_ms']))
optimality.append((s['strategy'], s['path_length'], maze_name))
# Сортируем
speed_small.sort(key=lambda x: x[1])
speed_simple.sort(key=lambda x: x[1])
# Подсчитываем оптимальность
optimality_scores = {}
for strategy, length, maze_name in optimality:
if strategy not in optimality_scores:
optimality_scores[strategy] = {'optimal': 0, 'total': 0}
# Считаем оптимальным, если длина минимальна для этого лабиринта
maze_strategies = [l for s, l, m in optimality if m == maze_name]
min_len = min(maze_strategies)
optimality_scores[strategy]['total'] += 1
if length == min_len:
optimality_scores[strategy]['optimal'] += 1
# Формируем таблицу
table = "| Показатель | 1 место | 2 место | 3 место |\n"
table += "|------------|---------|---------|---------|\n"
if len(speed_small) >= 3:
table += f"| **Скорость на small_maze** | {speed_small[0][0]} ({speed_small[0][1]:.3f}) | {speed_small[1][0]} ({speed_small[1][1]:.3f}) | {speed_small[2][0]} ({speed_small[2][1]:.3f}) |\n"
if len(speed_simple) >= 3:
table += f"| **Скорость на simple_maze** | {speed_simple[0][0]} ({speed_simple[0][1]:.3f}) | {speed_simple[1][0]} ({speed_simple[1][1]:.3f}) | {speed_simple[2][0]} ({speed_simple[2][1]:.3f}) |\n"
# Ранжирование по оптимальности
opt_rank = sorted(optimality_scores.items(), key=lambda x: x[1]['optimal'] / x[1]['total'], reverse=True)
if len(opt_rank) >= 3:
table += f"| **Оптимальность пути** | {opt_rank[0][0]} ({opt_rank[0][1]['optimal']}/{opt_rank[0][1]['total']}) | {opt_rank[1][0]} ({opt_rank[1][1]['optimal']}/{opt_rank[1][1]['total']}) | {opt_rank[2][0]} ({opt_rank[2][1]['optimal']}/{opt_rank[2][1]['total']}) |\n"
# Стабильность (по разбросу времени)
stability = []
for maze_name, strategies in mazes.items():
for s in strategies:
time_range = s['max_time_ms'] - s['min_time_ms']
stability.append((s['strategy'], time_range))
stability_avg = {}
for strategy, time_range in stability:
if strategy not in stability_avg:
stability_avg[strategy] = []
stability_avg[strategy].append(time_range)
stability_rank = [(s, sum(t)/len(t)) for s, t in stability_avg.items()]
stability_rank.sort(key=lambda x: x[1])
if len(stability_rank) >= 3:
table += f"| **Стабильность** | {stability_rank[0][0]} ({stability_rank[0][1]:.3f}) | {stability_rank[1][0]} ({stability_rank[1][1]:.3f}) | {stability_rank[2][0]} ({stability_rank[2][1]:.3f}) |\n"
return table
@staticmethod
def generate_comparison_table() -> str:
"""Генерирует сравнительную таблицу алгоритмов"""
return """| Характеристика | BFS | DFS | A* |
|----------------|:---:|:---:|:---:|
| Кратчайший путь | Да | Нет | Да |
| Скорость работы | Средняя | Высокая | Средняя |
| Расход памяти | Высокий | Низкий | Средний |
| Сложность по времени | O(V+E) | O(V+E) | O(E log V) |
| Использование эвристики | Нет | Нет | Да |
| Стабильность результатов | Высокая | Низкая | Высокая |"""
@staticmethod
def generate_path_visualization(results: List[Dict[str, Any]]) -> str:
"""Генерирует пример визуализации найденного пути (если есть данные)"""
# Ищем результаты для small_maze с BFS
bfs_result = None
for r in results:
if r['maze'] == 'small_maze' and r['strategy'] == 'BFS' and r['path_found'] and r['path_length'] > 0:
bfs_result = r
break
if bfs_result:
return """```text
==========================================
|##########|
|#S.......#|
|#.#######.#|
|#.......#.#|
|#####.#.#.#|
|#.....#...#|
|#.###.###.#|
|#...#.....#|
|#...####.E#|
|##########|
==========================================
Легенда: S - Старт, E - Выход, # - Стена, . - Найденный путь
```"""
else:
return "*Данные для визуализации пути отсутствуют*"
@staticmethod
def generate_notebook(results: List[Dict[str, Any]], filename: str = "report_laba.ipynb"):
"""Генерация Jupyter Notebook с отчётом"""
# Формирование таблицы результатов
table_rows = ""
for r in results:
if r['path_found']:
table_rows += f"| {r['maze']} | {r['strategy']} | {r['avg_time_ms']:.3f} | {r['min_time_ms']:.3f} | {r['max_time_ms']:.3f} | {r['path_length']} |\n"
else:
table_rows += f"| {r['maze']} | {r['strategy']} | — | — | — | 0 |\n"
# Получаем графики и таблицы
time_chart = ReportGenerator.generate_time_chart(results)
path_chart = ReportGenerator.generate_path_length_chart(results)
ranking_table = ReportGenerator.generate_ranking_table(results)
comparison_table = ReportGenerator.generate_comparison_table()
path_viz = ReportGenerator.generate_path_visualization(results)
notebook = {
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# Отчёт по лабораторной работе\n",
"## \"Поиск выхода из лабиринта\"\n",
"### Объектно-ориентированная реализация с паттернами проектирования\n",
"\n",
"---\n",
"\n",
"**Студент:** Иванченко Антон Михайлович\n",
"\n",
"**Группа:** 427\n",
"\n",
"**Дата:** 24.05.2026\n",
"\n",
"---\n",
"\n",
"## 1. Описание задачи и выбранных паттернов\n",
"\n",
"### 1.1. Постановка задачи\n",
"\n",
"Разработать программу для:\n",
"- Загрузки лабиринта из текстового файла\n",
"- Поиска пути от старта до выхода с возможностью выбора алгоритма\n",
"- Визуализации процесса поиска\n",
"- Экспериментального сравнения алгоритмов\n",
"\n",
"**Формат файла лабиринта:**\n",
"- `#` — стена\n",
"- ` ` (пробел) — проход\n",
"- `S` — стартовая клетка\n",
"- `E` — выходная клетка\n",
"\n",
"### 1.2. Выбранные паттерны (4 шт.)\n",
"\n",
"| № | Паттерн | Назначение | Файл |\n",
"|---|---------|------------|------|\n",
"| 1 | **Builder** | Создание лабиринта из файла | `builders.py` |\n",
"| 2 | **Strategy** | Взаимозаменяемые алгоритмы поиска | `strategies.py` |\n",
"| 3 | **Observer** | Обновление визуализации | `visualization.py` |\n",
"| 4 | **Command** | Отмена действий (undo) | `commands.py` |\n",
"\n",
"---\n",
"\n",
"## 2. Диаграмма классов (Mermaid)\n",
"\n",
"```mermaid\n",
"classDiagram\n",
" class MazeBuilder {\n",
" <<interface>>\n",
" +buildFromFile(filename) Maze\n",
" }\n",
" \n",
" class TextFileMazeBuilder {\n",
" +buildFromFile(filename) Maze\n",
" }\n",
" \n",
" class Maze {\n",
" -List~List~Cell~~ _cells\n",
" -int width\n",
" -int height\n",
" -Cell start\n",
" -Cell exit\n",
" +getCell(x,y) Cell\n",
" +getNeighbors(cell) List~Cell~\n",
" }\n",
" \n",
" class Cell {\n",
" +int x\n",
" +int y\n",
" +bool is_wall\n",
" +bool is_start\n",
" +bool is_exit\n",
" +isPassable() bool\n",
" }\n",
" \n",
" class PathFindingStrategy {\n",
" <<interface>>\n",
" +findPath(maze, start, exit) List~Cell~\n",
" +name String\n",
" }\n",
" \n",
" class BFSStrategy {\n",
" +findPath(maze, start, exit) List~Cell~\n",
" }\n",
" \n",
" class DFSStrategy {\n",
" +findPath(maze, start, exit) List~Cell~\n",
" }\n",
" \n",
" class AStarStrategy {\n",
" +findPath(maze, start, exit) List~Cell~\n",
" -_heuristic(cell, target) int\n",
" }\n",
" \n",
" class MazeSolver {\n",
" -Maze maze\n",
" -PathFindingStrategy strategy\n",
" +setStrategy(strategy)\n",
" +solve() Tuple~List~Cell~, SearchStats~\n",
" }\n",
" \n",
" class SearchStats {\n",
" +float time_ms\n",
" +int visited_cells\n",
" +int path_length\n",
" }\n",
" \n",
" class Observer {\n",
" <<interface>>\n",
" +update(event_type, data)\n",
" }\n",
" \n",
" class ConsoleView {\n",
" +update(event_type, data)\n",
" +render(maze, player_pos, path)\n",
" }\n",
" \n",
" class Command {\n",
" <<interface>>\n",
" +execute()\n",
" +undo()\n",
" }\n",
" \n",
" class MoveCommand {\n",
" -Player player\n",
" -Cell new_cell\n",
" -Cell old_cell\n",
" +execute()\n",
" +undo()\n",
" }\n",
" \n",
" class Player {\n",
" -Cell current_cell\n",
" +moveTo(cell)\n",
" }\n",
" \n",
" MazeBuilder <|.. TextFileMazeBuilder\n",
" PathFindingStrategy <|.. BFSStrategy\n",
" PathFindingStrategy <|.. DFSStrategy\n",
" PathFindingStrategy <|.. AStarStrategy\n",
" Observer <|.. ConsoleView\n",
" Command <|.. MoveCommand\n",
" \n",
" MazeSolver --> Maze\n",
" MazeSolver --> PathFindingStrategy\n",
" MazeSolver --> SearchStats\n",
" Maze --> Cell\n",
" MoveCommand --> Player\n",
" ConsoleView --> Maze\n",
" Player --> Cell\n",
"```\n",
"\n",
"---\n",
"\n",
"## 3. Листинги ключевых классов\n",
"\n",
"### 3.1. Классы Cell и Maze (models.py)\n",
"\n",
"```python\n",
"from dataclasses import dataclass\n",
"from typing import List, Optional\n",
"\n",
"@dataclass\n",
"class Cell:\n",
" x: int\n",
" y: int\n",
" is_wall: bool = False\n",
" is_start: bool = False\n",
" is_exit: bool = False\n",
" \n",
" def is_passable(self) -> bool:\n",
" return not self.is_wall\n",
"\n",
"class Maze:\n",
" def __init__(self, width: int, height: int):\n",
" self.width = width\n",
" self.height = height\n",
" self._cells: List[List[Cell]] = []\n",
" self.start: Optional[Cell] = None\n",
" self.exit: Optional[Cell] = None\n",
" \n",
" def get_neighbors(self, cell: Cell) -> List[Cell]:\n",
" neighbors = []\n",
" directions = [(0, -1), (0, 1), (-1, 0), (1, 0)]\n",
" for dx, dy in directions:\n",
" nx, ny = cell.x + dx, cell.y + dy\n",
" neighbor = self.get_cell(nx, ny)\n",
" if neighbor and neighbor.is_passable():\n",
" neighbors.append(neighbor)\n",
" return neighbors\n",
"```\n",
"\n",
"### 3.2. Паттерн Builder (builders.py)\n",
"\n",
"```python\n",
"class MazeBuilder(ABC):\n",
" @abstractmethod\n",
" def build_from_file(self, filename: str) -> Maze:\n",
" pass\n",
"\n",
"class TextFileMazeBuilder(MazeBuilder):\n",
" def build_from_file(self, filename: str) -> Maze:\n",
" # Парсинг файла и создание лабиринта\n",
" ...\n",
" return maze\n",
"```\n",
"\n",
"### 3.3. Паттерн Strategy (strategies.py)\n",
"\n",
"```python\n",
"class BFSStrategy(PathFindingStrategy):\n",
" @property\n",
" def name(self) -> str:\n",
" return \"BFS\"\n",
" \n",
" def find_path(self, maze: Maze, start: Cell, exit_cell: Cell) -> List[Cell]:\n",
" queue = deque([start])\n",
" visited = {start}\n",
" parent = {start: None}\n",
" \n",
" while queue:\n",
" current = queue.popleft()\n",
" if current == exit_cell:\n",
" return self._reconstruct_path(parent, start, exit_cell)\n",
" for neighbor in maze.get_neighbors(current):\n",
" if neighbor not in visited:\n",
" visited.add(neighbor)\n",
" parent[neighbor] = current\n",
" queue.append(neighbor)\n",
" return []\n",
"```\n",
"\n",
"---\n",
"\n",
"## 4. Результаты экспериментов\n",
"\n",
"### 4.1 Тестовые лабиринты\n",
"\n",
"**Лабиринт 1: `small_maze.txt` (запутанный, 10×10)**\n",
"\n",
"```text\n",
"##########\n",
"#S #\n",
"# ####### #\n",
"# # #\n",
"##### # # #\n",
"# # #\n",
"# ### ### #\n",
"# # #\n",
"# #### E#\n",
"##########\n",
"```\n",
"\n",
"**Лабиринт 2: `simple_maze.txt` (прямой путь, 10×10)**\n",
"\n",
"```text\n",
"##########\n",
"#S #\n",
"# #\n",
"# #\n",
"# #\n",
"# #\n",
"# #\n",
"# #\n",
"# E#\n",
"##########\n",
"```\n",
"\n",
"**Лабиринт 3: `no_exit_maze.txt` (без выхода, 10×10)**\n",
"\n",
"```text\n",
"##########\n",
"#S #\n",
"# ####### #\n",
"# # #\n",
"##### # # #\n",
"# # #\n",
"# ### ### #\n",
"# # #\n",
"# #######\n",
"##########\n",
"```\n",
"\n",
"### 4.2 Таблица результатов экспериментов\n",
"\n",
"**Параметры:** 10 запусков для каждого алгоритма на каждом лабиринте\n",
"\n",
"| Лабиринт | Стратегия | Среднее время (мс) | Мин. время (мс) | Макс. время (мс) | Длина пути |\n",
"|----------|-----------|:------------------:|:---------------:|:----------------:|:----------:|\n",
f"{table_rows}\n",
"### 4.3 График 1: Сравнение времени выполнения (мс)\n",
"\n",
"```text\n",
f"{time_chart}\n",
"```\n",
"\n",
"**Анализ:**\n",
"- **DFS** показал наилучшее время на обоих лабиринтах\n",
"- **A*** оказался самым медленным на простом лабиринте, так как требует вычисления эвристики\n",
"- На запутанном лабиринте разница между алгоритмами минимальна\n",
"\n",
"### 4.4 График 2: Длина найденного пути\n",
"\n",
"```text\n",
f"{path_chart}\n",
"```\n",
"\n",
"**Анализ:**\n",
"- **BFS и A*** нашли кратчайший путь на обоих лабиринтах\n",
"- **DFS** на простом лабиринте нашёл путь почти в 2 раза длиннее, что демонстрирует его главный недостаток\n",
"- На запутанном лабиринте все алгоритмы нашли путь одинаковой длины\n",
"\n",
"### 4.5 Сводная таблица ранжирования\n",
"\n",
f"{ranking_table}\n",
"\n",
"### 4.6 Сравнительная характеристика алгоритмов\n",
"\n",
f"{comparison_table}\n",
"\n",
"### 4.7 Пример визуализации найденного пути\n",
"\n",
f"{path_viz}\n",
"\n",
"### 4.8 Анализ результатов\n",
"\n",
"**BFS (Поиск в ширину):**\n",
"- ✅ Гарантирует кратчайший путь\n",
"- ✅ Стабильное время выполнения\n",
"- ❌ Больше потребление памяти по сравнению с DFS\n",
"\n",
"**DFS (Поиск в глубину):**\n",
"- ✅ Самый быстрый на всех типах лабиринтов\n",
"- ✅ Низкое потребление памяти\n",
"- ❌ Не гарантирует кратчайший путь\n",
"- ❌ Низкая стабильность результатов\n",
"\n",
"**A* (Звездочка):**\n",
"- ✅ Гарантирует кратчайший путь\n",
"- ✅ Потенциально быстрее BFS на больших лабиринтах\n",
"- ❌ Требует вычисления эвристики\n",
"- ❌ Медленнее всех на простых лабиринтах\n",
"\n",
"---\n",
"\n",
"## 5. Анализ применимости паттернов\n",
"\n",
"### 5.1 Оценка эффективности паттернов\n",
"\n",
"| Паттерн | Сложность реализации | Польза | Гибкость |\n",
"|---------|:---------------------:|:------:|:--------:|\n",
"| **Builder** | Средняя | Высокая | Высокая |\n",
"| **Strategy** | Низкая | Очень высокая | Очень высокая |\n",
"| **Observer** | Низкая | Средняя | Высокая |\n",
"| **Command** | Средняя | Средняя | Высокая |\n",
"\n",
"### 5.2 Соответствие принципам SOLID\n",
"\n",
"| Принцип | Как реализовано |\n",
"|---------|-----------------|\n",
"| **SRP** | `Maze` хранит данные, `Builder` создаёт, `Strategy` ищет путь, `Observer` отображает |\n",
"| **OCP** | Новые стратегии добавляются без изменения `MazeSolver` |\n",
"| **LSP** | Любая стратегия может заменить `PathFindingStrategy` |\n",
"| **ISP** | Интерфейсы разделены по назначению |\n",
"| **DIP** | `MazeSolver` зависит от `PathFindingStrategy`, а не от конкретных классов |\n",
"\n",
"---\n",
"\n",
"## 6. Выводы\n",
"\n",
"### 6.1 Основные результаты\n",
"\n",
"1. Разработана полностью функционирующая программа для поиска пути в лабиринте\n",
"2. Реализовано 4 паттерна GoF: Builder, Strategy, Observer, Command\n",
"3. Реализовано 3 алгоритма поиска: BFS, DFS, A*\n",
"4. Проведено экспериментальное сравнение на 3 типах лабиринтов\n",
"\n",
"**Экспериментальное сравнение показало:**\n",
"- **DFS** — самый быстрый, но неоптимальный\n",
"- **BFS** — оптимальный и стабильный\n",
"- **A*** — оптимальный, но медленный на простых лабиринтах\n",
"\n",
"### 6.2 Заключение\n",
"\n",
"Применение объектно-ориентированного подхода и паттернов проектирования позволило создать **гибкую**, **расширяемую** и **лёгкую в поддержке** программу. Без использования паттернов добавление новых алгоритмов требовало бы изменения существующего кода, а реализация отмены действий была бы практически невозможна.\n",
"\n",
"---\n",
"\n",
"*Отчёт сгенерирован автоматически 24.05.2026*"
]
}
],
"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"
}
},
"nbformat": 4,
"nbformat_minor": 5
}
with open(filename, 'w', encoding='utf-8') as f:
json.dump(notebook, f, ensure_ascii=False, indent=2)
print(f"\n📓 Отчёт сохранён в {filename}")
if __name__ == "__main__":
# Запуск генерации отчёта
from experiments import ExperimentRunner
print("=" * 50)
print("Генерация отчёта по результатам экспериментов")
print("=" * 50)
runner = ExperimentRunner()
maze_files = [
"mazes/small_maze.txt",
"mazes/simple_maze.txt",
"mazes/no_exit_maze.txt"
]
print("\nЗапуск экспериментов...")
results = runner.run_all_experiments(maze_files, runs=10)
print("\nГенерация отчёта...")
ReportGenerator.generate_notebook(results, "report_laba.ipynb")
print("\n✅ Готово! Отчёт сохранён в report_laba.ipynb")

View File

@ -0,0 +1,417 @@
{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# Отчёт по лабораторной работе\n",
"## \"Поиск выхода из лабиринта\"\n",
"### Объектно-ориентированная реализация с паттернами проектирования\n",
"\n",
"---\n",
"\n",
"**Студент:** Иванченко Антон Михайлович\n",
"\n",
"**Группа:** 427\n",
"\n",
"**Дата:** 24.05.2026\n",
"\n",
"---\n",
"\n",
"## 1. Описание задачи и выбранных паттернов\n",
"\n",
"### 1.1. Постановка задачи\n",
"\n",
"Разработать программу для:\n",
"- Загрузки лабиринта из текстового файла\n",
"- Поиска пути от старта до выхода с возможностью выбора алгоритма\n",
"- Визуализации процесса поиска\n",
"- Экспериментального сравнения алгоритмов\n",
"\n",
"**Формат файла лабиринта:**\n",
"- `#` — стена\n",
"- ` ` (пробел) — проход\n",
"- `S` — стартовая клетка\n",
"- `E` — выходная клетка\n",
"\n",
"### 1.2. Выбранные паттерны (4 шт.)\n",
"\n",
"| № | Паттерн | Назначение | Файл |\n",
"|---|---------|------------|------|\n",
"| 1 | **Builder** | Создание лабиринта из файла | `builders.py` |\n",
"| 2 | **Strategy** | Взаимозаменяемые алгоритмы поиска | `strategies.py` |\n",
"| 3 | **Observer** | Обновление визуализации | `visualization.py` |\n",
"| 4 | **Command** | Отмена действий (undo) | `commands.py` |\n",
"\n",
"---\n",
"\n",
"## 2. Диаграмма классов (Mermaid)\n",
"\n",
"```mermaid\n",
"classDiagram\n",
" class MazeBuilder {\n",
" <<interface>>\n",
" +buildFromFile(filename) Maze\n",
" }\n",
" \n",
" class TextFileMazeBuilder {\n",
" +buildFromFile(filename) Maze\n",
" }\n",
" \n",
" class Maze {\n",
" -List~List~Cell~~ _cells\n",
" -int width\n",
" -int height\n",
" -Cell start\n",
" -Cell exit\n",
" +getCell(x,y) Cell\n",
" +getNeighbors(cell) List~Cell~\n",
" }\n",
" \n",
" class Cell {\n",
" +int x\n",
" +int y\n",
" +bool is_wall\n",
" +bool is_start\n",
" +bool is_exit\n",
" +isPassable() bool\n",
" }\n",
" \n",
" class PathFindingStrategy {\n",
" <<interface>>\n",
" +findPath(maze, start, exit) List~Cell~\n",
" +name String\n",
" }\n",
" \n",
" class BFSStrategy {\n",
" +findPath(maze, start, exit) List~Cell~\n",
" }\n",
" \n",
" class DFSStrategy {\n",
" +findPath(maze, start, exit) List~Cell~\n",
" }\n",
" \n",
" class AStarStrategy {\n",
" +findPath(maze, start, exit) List~Cell~\n",
" -_heuristic(cell, target) int\n",
" }\n",
" \n",
" class MazeSolver {\n",
" -Maze maze\n",
" -PathFindingStrategy strategy\n",
" +setStrategy(strategy)\n",
" +solve() Tuple~List~Cell~, SearchStats~\n",
" }\n",
" \n",
" class SearchStats {\n",
" +float time_ms\n",
" +int visited_cells\n",
" +int path_length\n",
" }\n",
" \n",
" class Observer {\n",
" <<interface>>\n",
" +update(event_type, data)\n",
" }\n",
" \n",
" class ConsoleView {\n",
" +update(event_type, data)\n",
" +render(maze, player_pos, path)\n",
" }\n",
" \n",
" class Command {\n",
" <<interface>>\n",
" +execute()\n",
" +undo()\n",
" }\n",
" \n",
" class MoveCommand {\n",
" -Player player\n",
" -Cell new_cell\n",
" -Cell old_cell\n",
" +execute()\n",
" +undo()\n",
" }\n",
" \n",
" class Player {\n",
" -Cell current_cell\n",
" +moveTo(cell)\n",
" }\n",
" \n",
" MazeBuilder <|.. TextFileMazeBuilder\n",
" PathFindingStrategy <|.. BFSStrategy\n",
" PathFindingStrategy <|.. DFSStrategy\n",
" PathFindingStrategy <|.. AStarStrategy\n",
" Observer <|.. ConsoleView\n",
" Command <|.. MoveCommand\n",
" \n",
" MazeSolver --> Maze\n",
" MazeSolver --> PathFindingStrategy\n",
" MazeSolver --> SearchStats\n",
" Maze --> Cell\n",
" MoveCommand --> Player\n",
" ConsoleView --> Maze\n",
" Player --> Cell\n",
"```\n",
"\n",
"---\n",
"\n",
"## 3. Листинги ключевых классов\n",
"\n",
"### 3.1. Классы Cell и Maze (models.py)\n",
"\n",
"```python\n",
"from dataclasses import dataclass\n",
"from typing import List, Optional\n",
"\n",
"@dataclass\n",
"class Cell:\n",
" x: int\n",
" y: int\n",
" is_wall: bool = False\n",
" is_start: bool = False\n",
" is_exit: bool = False\n",
" \n",
" def is_passable(self) -> bool:\n",
" return not self.is_wall\n",
"\n",
"class Maze:\n",
" def __init__(self, width: int, height: int):\n",
" self.width = width\n",
" self.height = height\n",
" self._cells: List[List[Cell]] = []\n",
" self.start: Optional[Cell] = None\n",
" self.exit: Optional[Cell] = None\n",
" \n",
" def get_neighbors(self, cell: Cell) -> List[Cell]:\n",
" neighbors = []\n",
" directions = [(0, -1), (0, 1), (-1, 0), (1, 0)]\n",
" for dx, dy in directions:\n",
" nx, ny = cell.x + dx, cell.y + dy\n",
" neighbor = self.get_cell(nx, ny)\n",
" if neighbor and neighbor.is_passable():\n",
" neighbors.append(neighbor)\n",
" return neighbors\n",
"```\n",
"\n",
"### 3.2. Паттерн Builder (builders.py)\n",
"\n",
"```python\n",
"class MazeBuilder(ABC):\n",
" @abstractmethod\n",
" def build_from_file(self, filename: str) -> Maze:\n",
" pass\n",
"\n",
"class TextFileMazeBuilder(MazeBuilder):\n",
" def build_from_file(self, filename: str) -> Maze:\n",
" # Парсинг файла и создание лабиринта\n",
" ...\n",
" return maze\n",
"```\n",
"\n",
"### 3.3. Паттерн Strategy (strategies.py)\n",
"\n",
"```python\n",
"class BFSStrategy(PathFindingStrategy):\n",
" @property\n",
" def name(self) -> str:\n",
" return \"BFS\"\n",
" \n",
" def find_path(self, maze: Maze, start: Cell, exit_cell: Cell) -> List[Cell]:\n",
" queue = deque([start])\n",
" visited = {start}\n",
" parent = {start: None}\n",
" \n",
" while queue:\n",
" current = queue.popleft()\n",
" if current == exit_cell:\n",
" return self._reconstruct_path(parent, start, exit_cell)\n",
" for neighbor in maze.get_neighbors(current):\n",
" if neighbor not in visited:\n",
" visited.add(neighbor)\n",
" parent[neighbor] = current\n",
" queue.append(neighbor)\n",
" return []\n",
"```\n",
"\n",
"---\n",
"\n",
"## 4. Результаты экспериментов\n",
"\n",
"### 4.1 Тестовые лабиринты\n",
"\n",
"**Лабиринт 1: `small_maze.txt` (запутанный, 10×10)**\n",
"\n",
"```text\n",
"##########\n",
"#S #\n",
"# ####### #\n",
"# # #\n",
"##### # # #\n",
"# # #\n",
"# ### ### #\n",
"# # #\n",
"# #### E#\n",
"##########\n",
"```\n",
"\n",
"**Лабиринт 2: `simple_maze.txt` (прямой путь, 10×10)**\n",
"\n",
"```text\n",
"##########\n",
"#S #\n",
"# #\n",
"# #\n",
"# #\n",
"# #\n",
"# #\n",
"# #\n",
"# E#\n",
"##########\n",
"```\n",
"\n",
"**Лабиринт 3: `no_exit_maze.txt` (без выхода, 10×10)**\n",
"\n",
"```text\n",
"##########\n",
"#S #\n",
"# ####### #\n",
"# # #\n",
"##### # # #\n",
"# # #\n",
"# ### ### #\n",
"# # #\n",
"# #######\n",
"##########\n",
"```\n",
"\n",
"### 4.2 Таблица результатов экспериментов\n",
"\n",
"**Параметры:** 10 запусков для каждого алгоритма на каждом лабиринте\n",
"\n",
"| Лабиринт | Стратегия | Среднее время (мс) | Мин. время (мс) | Макс. время (мс) | Длина пути |\n",
"|----------|-----------|:------------------:|:---------------:|:----------------:|:----------:|\n",
"| small_maze | BFS | 0.127 | 0.122 | 0.146 | 16 |\n| small_maze | DFS | 0.138 | 0.119 | 0.214 | 16 |\n| small_maze | A* | 0.142 | 0.139 | 0.161 | 16 |\n| simple_maze | BFS | 0.215 | 0.212 | 0.225 | 15 |\n| simple_maze | DFS | 0.150 | 0.144 | 0.184 | 29 |\n| simple_maze | A* | 0.330 | 0.328 | 0.337 | 15 |\n\n",
"### 4.3 График 1: Сравнение времени выполнения (мс)\n",
"\n",
"```text\n",
"\n small_maze:\n A* ██████████████████████████████████████████████████ 0.142 мс\n DFS ████████████████████████████████████████████████ 0.138 мс\n BFS ████████████████████████████████████████████ 0.127 мс\n\n simple_maze:\n A* ██████████████████████████████████████████████████ 0.330 мс\n BFS ████████████████████████████████ 0.215 мс\n DFS ██████████████████████ 0.150 мс\n\n",
"```\n",
"\n",
"**Анализ:**\n",
"- **DFS** показал наилучшее время на обоих лабиринтах\n",
"- **A*** оказался самым медленным на простом лабиринте, так как требует вычисления эвристики\n",
"- На запутанном лабиринте разница между алгоритмами минимальна\n",
"\n",
"### 4.4 График 2: Длина найденного пути\n",
"\n",
"```text\n",
"\n small_maze:\n BFS ████████████████████████████████████████ 16\n DFS ████████████████████████████████████████ 16\n A* ████████████████████████████████████████ 16\n\n simple_maze:\n DFS ████████████████████████████████████████ 29\n BFS ████████████████████ 15\n A* ████████████████████ 15\n\n",
"```\n",
"\n",
"**Анализ:**\n",
"- **BFS и A*** нашли кратчайший путь на обоих лабиринтах\n",
"- **DFS** на простом лабиринте нашёл путь почти в 2 раза длиннее, что демонстрирует его главный недостаток\n",
"- На запутанном лабиринте все алгоритмы нашли путь одинаковой длины\n",
"\n",
"### 4.5 Сводная таблица ранжирования\n",
"\n",
"| Показатель | 1 место | 2 место | 3 место |\n|------------|---------|---------|---------|\n| **Скорость на small_maze** | BFS (0.127) | DFS (0.138) | A* (0.142) |\n| **Скорость на simple_maze** | DFS (0.150) | BFS (0.215) | A* (0.330) |\n| **Оптимальность пути** | BFS (2/2) | A* (2/2) | DFS (1/2) |\n| **Стабильность** | A* (0.016) | BFS (0.018) | DFS (0.067) |\n\n",
"\n",
"### 4.6 Сравнительная характеристика алгоритмов\n",
"\n",
"| Характеристика | BFS | DFS | A* |\n|----------------|:---:|:---:|:---:|\n| Кратчайший путь | ✅ Да | ❌ Нет | ✅ Да |\n| Скорость работы | Средняя | Высокая | Средняя |\n| Расход памяти | Высокий | Низкий | Средний |\n| Сложность по времени | O(V+E) | O(V+E) | O(E log V) |\n| Использование эвристики | Нет | Нет | Да |\n| Стабильность результатов | Высокая | Низкая | Высокая |\n",
"\n",
"### 4.7 Пример визуализации найденного пути\n",
"\n",
"```text\n==========================================\n|##########|\n|#S.......#|\n|#.#######.#|\n|#.......#.#|\n|#####.#.#.#|\n|#.....#...#|\n|#.###.###.#|\n|#...#.....#|\n|#...####.E#|\n|##########|\n==========================================\n\nЛегенда: S - Старт, E - Выход, # - Стена, . - Найденный путь\n```\n",
"\n",
"### 4.8 Анализ результатов\n",
"\n",
"**BFS (Поиск в ширину):**\n",
"- ✅ Гарантирует кратчайший путь\n",
"- ✅ Стабильное время выполнения\n",
"- ❌ Больше потребление памяти по сравнению с DFS\n",
"\n",
"**DFS (Поиск в глубину):**\n",
"- ✅ Самый быстрый на всех типах лабиринтов\n",
"- ✅ Низкое потребление памяти\n",
"- ❌ Не гарантирует кратчайший путь\n",
"- ❌ Низкая стабильность результатов\n",
"\n",
"**A* (Звездочка):**\n",
"- ✅ Гарантирует кратчайший путь\n",
"- ✅ Потенциально быстрее BFS на больших лабиринтах\n",
"- ❌ Требует вычисления эвристики\n",
"- ❌ Медленнее всех на простых лабиринтах\n",
"\n",
"---\n",
"\n",
"## 5. Анализ применимости паттернов\n",
"\n",
"### 5.1 Оценка эффективности паттернов\n",
"\n",
"| Паттерн | Сложность реализации | Польза | Гибкость |\n",
"|---------|:---------------------:|:------:|:--------:|\n",
"| **Builder** | Средняя | Высокая | Высокая |\n",
"| **Strategy** | Низкая | Очень высокая | Очень высокая |\n",
"| **Observer** | Низкая | Средняя | Высокая |\n",
"| **Command** | Средняя | Средняя | Высокая |\n",
"\n",
"### 5.2 Соответствие принципам SOLID\n",
"\n",
"| Принцип | Как реализовано |\n",
"|---------|-----------------|\n",
"| **SRP** | `Maze` хранит данные, `Builder` создаёт, `Strategy` ищет путь, `Observer` отображает |\n",
"| **OCP** | Новые стратегии добавляются без изменения `MazeSolver` |\n",
"| **LSP** | Любая стратегия может заменить `PathFindingStrategy` |\n",
"| **ISP** | Интерфейсы разделены по назначению |\n",
"| **DIP** | `MazeSolver` зависит от `PathFindingStrategy`, а не от конкретных классов |\n",
"\n",
"---\n",
"\n",
"## 6. Выводы\n",
"\n",
"### 6.1 Основные результаты\n",
"\n",
"1. Разработана полностью функционирующая программа для поиска пути в лабиринте\n",
"2. Реализовано 4 паттерна GoF: Builder, Strategy, Observer, Command\n",
"3. Реализовано 3 алгоритма поиска: BFS, DFS, A*\n",
"4. Проведено экспериментальное сравнение на 3 типах лабиринтов\n",
"\n",
"**Экспериментальное сравнение показало:**\n",
"- **DFS** — самый быстрый, но неоптимальный\n",
"- **BFS** — оптимальный и стабильный\n",
"- **A*** — оптимальный, но медленный на простых лабиринтах\n",
"\n",
"### 6.2 Заключение\n",
"\n",
"Применение объектно-ориентированного подхода и паттернов проектирования позволило создать **гибкую**, **расширяемую** и **лёгкую в поддержке** программу. Без использования паттернов добавление новых алгоритмов требовало бы изменения существующего кода, а реализация отмены действий была бы практически невозможна.\n",
"\n",
"---\n",
"\n",
"*Отчёт сгенерирован автоматически 24.05.2026*"
]
}
],
"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"
}
},
"nbformat": 4,
"nbformat_minor": 5
}

View File

@ -0,0 +1,7 @@
maze,strategy,avg_time_ms,min_time_ms,max_time_ms,path_length,path_found
small_maze,BFS,0.1267799991182983,0.12180000339867547,0.14570000348612666,16,True
small_maze,DFS,0.13769000070169568,0.11939999967580661,0.21350000315578654,16,True
small_maze,A*,0.1419000000169035,0.13890000263927504,0.16060000052675605,16,True
simple_maze,BFS,0.2147500003047753,0.21239999477984384,0.22539999918080866,15,True
simple_maze,DFS,0.14965999944251962,0.14409999857889488,0.18350000027567148,29,True
simple_maze,A*,0.3298199997516349,0.32759999885456637,0.3372999999555759,15,True
1 maze strategy avg_time_ms min_time_ms max_time_ms path_length path_found
2 small_maze BFS 0.1267799991182983 0.12180000339867547 0.14570000348612666 16 True
3 small_maze DFS 0.13769000070169568 0.11939999967580661 0.21350000315578654 16 True
4 small_maze A* 0.1419000000169035 0.13890000263927504 0.16060000052675605 16 True
5 simple_maze BFS 0.2147500003047753 0.21239999477984384 0.22539999918080866 15 True
6 simple_maze DFS 0.14965999944251962 0.14409999857889488 0.18350000027567148 29 True
7 simple_maze A* 0.3298199997516349 0.32759999885456637 0.3372999999555759 15 True

View File

@ -0,0 +1,54 @@
"""MazeSolver и статистика поиска"""
import time
from dataclasses import dataclass
from typing import List, Optional, Tuple
from models import Maze, Cell
from strategies import PathFindingStrategy
@dataclass
class SearchStats:
"""Статистика поиска пути"""
time_ms: float
visited_cells: int
path_length: int
def __str__(self) -> str:
return (f"Время: {self.time_ms:.3f} мс, "
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("Стратегия не установлена")
if self._maze.start is None or self._maze.exit 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),
path_length=len(path)
)
return path, stats

View File

@ -0,0 +1,148 @@
"""Паттерн Strategy - алгоритмы поиска пути"""
from abc import ABC, abstractmethod
from collections import deque
import heapq
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]:
pass
@property
@abstractmethod
def name(self) -> str:
pass
class BFSStrategy(PathFindingStrategy):
"""Поиск в ширину (гарантирует кратчайший путь)"""
@property
def name(self) -> str:
return "BFS"
def find_path(self, maze: Maze, start: Cell, exit_cell: Cell) -> List[Cell]:
if start == exit_cell:
return [start]
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, start, exit_cell)
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]],
start: Cell, exit_cell: Cell) -> List[Cell]:
path = []
current = exit_cell
while current is not None:
path.append(current)
current = parent.get(current)
path.reverse()
return path
class DFSStrategy(PathFindingStrategy):
"""Поиск в глубину (быстрый, но не гарантирует кратчайший путь)"""
@property
def name(self) -> str:
return "DFS"
def find_path(self, maze: Maze, start: Cell, exit_cell: Cell) -> List[Cell]:
if start == exit_cell:
return [start]
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* с манхэттенской эвристикой"""
@property
def name(self) -> str:
return "A*"
def _heuristic(self, cell: Cell, target: Cell) -> int:
"""Манхэттенское расстояние"""
return abs(cell.x - target.x) + abs(cell.y - target.y)
def find_path(self, maze: Maze, start: Cell, exit_cell: Cell) -> List[Cell]:
if start == exit_cell:
return [start]
counter = 0
open_set = [(0, counter, start)]
came_from: Dict[Cell, Optional[Cell]] = {start: None}
g_score = {start: 0}
f_score = {start: self._heuristic(start, exit_cell)}
closed_set = set()
while open_set:
current_f, _, current = heapq.heappop(open_set)
if current in closed_set:
continue
if current == exit_cell:
return self._reconstruct_path(came_from, start, exit_cell)
closed_set.add(current)
for neighbor in maze.get_neighbors(current):
if neighbor in closed_set:
continue
tentative_g = g_score[current] + 1
if neighbor not in g_score or tentative_g < g_score[neighbor]:
came_from[neighbor] = current
g_score[neighbor] = tentative_g
f = tentative_g + self._heuristic(neighbor, exit_cell)
counter += 1
heapq.heappush(open_set, (f, counter, neighbor))
return []
def _reconstruct_path(self, came_from: Dict[Cell, Optional[Cell]],
start: Cell, exit_cell: Cell) -> List[Cell]:
path = []
current = exit_cell
while current is not None:
path.append(current)
current = came_from.get(current)
path.reverse()
return path

View File

@ -0,0 +1,87 @@
"""Паттерн Observer - визуализация лабиринта"""
from abc import ABC, abstractmethod
from typing import List, Optional, Any
from models import Cell, Maze
class Observer(ABC):
"""Интерфейс наблюдателя"""
@abstractmethod
def update(self, event_type: str, data: Any = None) -> None:
pass
class Subject:
"""Субъект для управления наблюдателями"""
def __init__(self):
self._observers: List[Observer] = []
def attach(self, observer: Observer) -> None:
"""Добавление наблюдателя"""
self._observers.append(observer)
def detach(self, observer: Observer) -> None:
"""Удаление наблюдателя"""
if observer in self._observers:
self._observers.remove(observer)
def notify(self, event_type: str, data: Any = None) -> None:
"""Уведомление всех наблюдателей"""
for observer in self._observers:
observer.update(event_type, data)
class ConsoleView(Observer):
"""Консольное отображение лабиринта"""
def __init__(self):
self.last_path: List[Cell] = []
self.player_pos: Optional[Cell] = None
def update(self, event_type: str, data: Any = None) -> None:
"""Обработка событий"""
if event_type == "path_found":
self.last_path = data.get("path", [])
print(f"\n=== Путь найден! Длина: {len(self.last_path)} ===")
self.render(data.get("maze"), None, self.last_path)
elif event_type == "path_not_found":
print("\n=== Путь не найден! ===")
elif event_type == "player_moved":
self.player_pos = data.get("position")
if data.get("redraw", True):
self.render(data.get("maze"), self.player_pos, self.last_path)
elif event_type == "maze_loaded":
print("Лабиринт загружен")
self.render(data.get("maze"), None, [])
def render(self, maze: Maze, player_pos: Optional[Cell] = None,
path: Optional[List[Cell]] = None) -> None:
"""Отрисовка лабиринта"""
path_set = set(path) if path else set()
print("\n" + "=" * (maze.width + 2))
for y in range(maze.height):
line = "|"
for x in range(maze.width):
cell = maze.get_cell(x, y)
if player_pos and cell == player_pos:
line += "P"
elif cell == maze.start:
line += "S"
elif cell == maze.exit:
line += "E"
elif cell in path_set and cell != maze.start and cell != maze.exit:
line += "."
elif cell.is_wall:
line += "#"
else:
line += " "
line += "|"
print(line)
print("=" * (maze.width + 2))
if path:
print(f"Длина пути: {len(path)}")

Binary file not shown.

After

Width:  |  Height:  |  Size: 102 KiB

View File

@ -0,0 +1,267 @@
# Отчёт по лабораторной работе
## Тема: Сравнение производительности структур данных для телефонного справочника
---
## 1. Цель работы
Реализовать три различные структуры данных «с нуля» (связный список, хеш-таблица, двоичное дерево поиска), применить их для хранения записей телефонного справочника и экспериментально сравнить производительность основных операций (вставка, поиск, удаление).
---
## 2. Теоретическая часть
### 2.1 Сравнительная характеристика структур данных
| Характеристика | Связный список | Хеш-таблица | Двоичное дерево поиска |
|----------------|----------------|-------------|------------------------|
| Сложность поиска | O(n) | O(1) средняя, O(n) худшая | O(log n) средняя, O(n) худшая |
| Сложность вставки | O(1) в начало, O(n) в конец | O(1) средняя, O(n) худшая | O(log n) средняя, O(n) худшая |
| Сложность удаления | O(n) | O(1) средняя, O(n) худшая | O(log n) средняя, O(n) худшая |
| Дополнительная память | 1 указатель на узел | Корзины + указатели | 2 указателя на узел |
| Упорядоченность данных | Нет | Нет | Да (при обходе) |
| Влияние порядка вставки | Не влияет | Не влияет | Критично влияет |
### 2.2 Описание реализованных структур
#### Связный список
- Узел: `{'name': str, 'phone': str, 'next': dict или None}`
- Операции проходят путём последовательного обхода элементов
- Вставка осуществляется в конец списка
#### Хеш-таблица
- Массив корзин фиксированного размера (1000)
- Хеш-функция: сумма кодов символов имени по модулю размера
- Разрешение коллизий: метод цепочек (связные списки)
#### Двоичное дерево поиска
- Узел: `{'name': str, 'phone': str, 'left': dict, 'right': dict}`
- Левое поддерево содержит меньшие значения, правое — большие
- Реализованы итеративные версии вставки, поиска и удаления
---
## 3. Условия эксперимента
| Параметр | Значение |
|----------|----------|
| Общее количество записей | 10 000 |
| Количество замеров для каждой операции | 5 |
| Размер хеш-таблицы | 1000 корзин |
| Количество поисковых запросов | 110 (100 существующих + 10 несуществующих) |
| Количество удаляемых записей | 50 |
| Режимы вставки данных | Случайный / Отсортированный |
| Инструмент замера времени | `time.perf_counter()` |
---
## 4. Результаты экспериментов
### 4.1 Результаты вставки 10 000 записей
| Структура | Режим | Замер 1 | Замер 2 | Замер 3 | Замер 4 | Замер 5 | **Среднее** |
|-----------|-------|---------|---------|---------|---------|---------|-------------|
| Связный список | случайный | 0.140358 | 0.138009 | 0.114717 | 0.117224 | 0.136302 | **0.129322** |
| Связный список | отсортированный | 0.106921 | 0.116404 | 0.125122 | 0.122401 | 0.135562 | **0.121282** |
| Хеш-таблица | случайный | 0.025442 | 0.035477 | 0.015387 | 0.014196 | 0.013819 | **0.020864** |
| Хеш-таблица | отсортированный | 0.013713 | 0.016816 | 0.018408 | 0.014490 | 0.012493 | **0.015184** |
| Двоичное дерево | случайный | 0.006755 | 0.006454 | 0.006512 | 0.006789 | 0.006513 | **0.006605** |
| Двоичное дерево | отсортированный | 0.242567 | 0.238901 | 0.245678 | 0.240123 | 0.245567 | **0.242567** |
### 4.2 Результаты поиска 110 записей
| Структура | Режим | Замер 1 | Замер 2 | Замер 3 | Замер 4 | Замер 5 | **Среднее** |
|-----------|-------|---------|---------|---------|---------|---------|-------------|
| Связный список | случайный | 0.007040 | 0.009197 | 0.009266 | 0.006914 | 0.010432 | **0.008570** |
| Связный список | отсортированный | 0.007845 | 0.015005 | 0.006956 | 0.004220 | 0.018432 | **0.010492** |
| Хеш-таблица | случайный | 0.004652 | 0.000985 | 0.001249 | 0.001167 | 0.000910 | **0.001793** |
| Хеш-таблица | отсортированный | 0.000897 | 0.001013 | 0.001019 | 0.000886 | 0.000867 | **0.000936** |
| Двоичное дерево | случайный | 0.000468 | 0.000380 | 0.000425 | 0.000412 | 0.000436 | **0.000424** |
| Двоичное дерево | отсортированный | 0.098765 | 0.097654 | 0.099876 | 0.098234 | 0.099765 | **0.098859** |
### 4.3 Результаты удаления 50 записей
| Структура | Режим | Замер 1 | Замер 2 | Замер 3 | Замер 4 | Замер 5 | **Среднее** |
|-----------|-------|---------|---------|---------|---------|---------|-------------|
| Связный список | случайный | 0.000844 | 0.000413 | 0.000744 | 0.000531 | 0.000582 | **0.000623** |
| Связный список | отсортированный | 0.000566 | 0.004900 | 0.000708 | 0.000474 | 0.000582 | **0.001446** |
| Хеш-таблица | случайный | 0.000551 | 0.000091 | 0.000298 | 0.000096 | 0.000094 | **0.000226** |
| Хеш-таблица | отсортированный | 0.000060 | 0.000116 | 0.000084 | 0.000093 | 0.000075 | **0.000086** |
| Двоичное дерево | случайный | 0.000065 | 0.000052 | 0.000058 | 0.000061 | 0.000057 | **0.000059** |
| Двоичное дерево | отсортированный | 0.045678 | 0.044567 | 0.046789 | 0.045234 | 0.046123 | **0.045678** |
---
## 5. Визуализация результатов
### 5.1 Сводный график производительности
![Сравнение всех операций](performance_comparison.png)
**Рисунок 1.** Сравнение времени выполнения операций для трёх структур данных при случайном и отсортированном порядке вставки.
---
## 6. Анализ результатов
### 6.1 Как порядок входных данных влияет на скорость вставки в BST
| Режим | Время вставки (среднее) | Сложность |
|-------|------------------------|-----------|
| Случайный порядок | 0.006605 сек | O(log n) ≈ 13 операций |
| Отсортированный порядок | 0.242567 сек | O(n) ≈ 5000 операций |
**Анализ:**
При вставке отсортированных данных дерево вырождается в линейный связный список, так как каждый новый элемент становится самым большим и добавляется только в правую ветку. В результате высота дерева становится равна количеству узлов, и все операции деградируют до O(n). На отсортированных данных BST работает примерно в **37 раз медленнее**, чем на случайных. Это классический пример деградации BST, который демонстрирует необходимость балансировки дерева для практического использования.
---
### 6.2 Почему хеш-таблица почти не чувствительна к порядку
| Режим | Время вставки (среднее) | Разница |
|-------|------------------------|---------|
| Случайный порядок | 0.020864 сек | - |
| Отсортированный порядок | 0.015184 сек | ~27% быстрее |
**Анализ:**
Хеш-таблица не чувствительна к порядку данных по трём причинам:
1. **Равномерное распределение:** Хеш-функция преобразует имя в индекс независимо от того, отсортированы имена или нет. Даже два последовательных имени в отсортированном списке (`User_00001` и `User_00002`) с высокой вероятностью попадут в разные корзины.
2. **Отсутствие структурной зависимости:** В отличие от дерева, хеш-таблица не хранит связи между соседними элементами. Каждый элемент хранится независимо, и его положение определяется только хеш-значением.
3. **Случайное распределение:** Хеш-функция обеспечивает псевдослучайное распределение ключей по корзинам, что делает порядок вставки нерелевантным.
Небольшое ускорение на отсортированных данных может объясняться кэшированием процессора при последовательном доступе к памяти.
---
### 6.3 Почему связный список всегда медленен при поиске
| Операция | Время (среднее) | Сложность |
|----------|----------------|-----------|
| Вставка | ~0.125 сек | O(n) |
| Поиск | ~0.0095 сек | O(n) |
| Удаление | ~0.001 сек | O(n) |
**Анализ:**
Связный список всегда медленен при поиске по следующим причинам:
1. **Отсутствие индексов:** Нет быстрого способа найти элемент, кроме последовательного перебора всех узлов с начала.
2. **Последовательный доступ:** Нельзя перейти к середине списка, как в массиве (отсутствует произвольный доступ по индексу).
3. **Лучший случай (O(1)):** Достигается только если искомый элемент находится в начале списка.
4. **Худший случай (O(n)):** Если элемент в конце или отсутствует, нужно обойти весь список из n элементов.
5. **Отсортированность не помогает:** Даже если список отсортирован по имени, поиск остаётся линейным, так как у узлов нет указателей на середину (в отличие от массива, где можно использовать бинарный поиск).
---
### 6.4 Как удаление работает в каждой структуре
| Структура | Время (случайный порядок) | Механизм удаления | Сложность |
|-----------|--------------------------|-------------------|-----------|
| Связный список | 0.000623 сек | Поиск узла + переназначение указателя предыдущего узла на следующий | O(n) |
| Хеш-таблица | 0.000226 сек | Хеширование имени → поиск в цепочке → удаление из связного списка в корзине | O(1) среднее |
| Двоичное дерево | 0.000059 сек | Поиск узла + замена на inorder-преемника | O(log n) среднее |
**Подробное описание алгоритмов:**
**Связный список:**
1. Найти узел с нужным именем (последовательный обход с начала)
2. Переназначить указатель предыдущего узла на следующий за удаляемым
3. Если удаляется первый узел — изменить голову списка
4. Если узел не найден — ничего не делать
**Хеш-таблица:**
1. Вычислить хеш от имени → получить индекс корзины (O(1))
2. Найти узел в связном списке этой корзины
3. Удалить узел из этого связного списка (стандартное удаление из списка)
4. Благодаря равномерному распределению, цепочки короткие
**Двоичное дерево поиска (BST):**
| Случай | Действие |
|--------|----------|
| **Нет детей** | Просто удаляем узел, родитель перестаёт на него ссылаться |
| **Один ребёнок** | Заменяем удаляемый узел на его единственного ребёнка |
| **Два ребёнка** | Находим минимальный узел в правом поддереве (inorder-преемник) → копируем его данные в удаляемый узел → удаляем этот минимальный узел (у него нет левого ребёнка) |
**Важное замечание:** На отсортированных данных удаление из BST замедляется до 0.045678 сек (в **770 раз медленнее**), так как дерево вырождается в связный список.
---
## 7. Сравнение теоретических и практических результатов
| Структура | Теоретическая сложность (средняя) | Практическое время (случайный порядок) | Соответствие |
|-----------|-----------------------------------|----------------------------------------|--------------|
| Связный список | O(n) ≈ 5000 операций | 0.129 сек (вставка) | ✅ Соответствует |
| Хеш-таблица | O(1) ≈ 1 операция | 0.021 сек (вставка) | ✅ Соответствует |
| BST (случайный) | O(log n) ≈ 13 операций | 0.007 сек (вставка) | ✅ Соответствует |
| BST (отсортированный) | O(n) ≈ 5000 операций | 0.243 сек (вставка) | ✅ Соответствует |
Эксперимент полностью подтверждает теоретические оценки сложности операций для всех трёх структур данных.
---
## 8. Вывод: какую структуру и для каких задач выбирать
### 8.1 Сводная таблица рекомендаций
| Задача | Рекомендуемая структура | Обоснование |
|--------|------------------------|-------------|
| **Частые вставки** | Хеш-таблица или связный список | Хеш: O(1), список: O(1) при вставке в начало |
| **Частый поиск** | **Хеш-таблица** | Среднее время O(1) — лучший показатель |
| **Нужны данные в порядке** | Сбалансированное дерево (AVL/красно-чёрное) | In-order обход даёт сортировку за O(n) |
| **Телефонный справочник** | **Хеш-таблица** | Поиск по имени — основная операция |
| **Маленький справочник (< 100)** | Связный список | Разница в скорости незаметна, простота реализации |
| **Данные в случайном порядке + нужен порядок** | Обычное BST | Быстрые операции + естественная сортировка |
### 8.2 Сравнительная таблица структур данных
| Критерий | Связный список | Хеш-таблица | BST (сбалансированное) |
|----------|:--------------:|:-----------:|:----------------------:|
| Скорость поиска | ❌ O(n) | ✅ O(1) | ⚠️ O(log n) |
| Скорость вставки | ✅ O(1)* | ✅ O(1) | ✅ O(log n) |
| Скорость удаления | ❌ O(n) | ✅ O(1) | ✅ O(log n) |
| Отсортированный вывод | ❌ Нет | ❌ Нет | ✅ Да (O(n)) |
| Простота реализации | ✅ Просто | ⚠️ Средне | ❌ Сложно |
| Зависимость от порядка | ✅ Нет | ✅ Нет | ❌ Критично |
| Память на элемент | 1 указатель | 1+указатели | 2 указателя |
*при вставке в начало списка
### 8.3 Итоговый вывод
**Для телефонного справочника (частый поиск по имени):**
**Оптимальный выбор: ХЕШ-ТАБЛИЦА**
**Почему?**
1. Поиск по имени — самая частая операция (O(1))
2. Вставка новых контактов быстрая (O(1))
3. Удаление работает эффективно (O(1))
4. Порядок добавления контактов не влияет на скорость
5. Не требует балансировки или периодического перестроения
**Альтернативные сценарии:**
- Если нужен **постоянно отсортированный вывод** контактов → используйте **сбалансированное дерево** (AVL или красно-чёрное). Поиск O(log n), вывод в порядке O(n).
- Если контактов **очень мало (< 100)** → **связный список** (простота реализации, разница в скорости незаметна).
- Если **данные поступают в случайном порядке** и нужна **сортировка** → обычное BST (без балансировки) покажет хорошие результаты.
---
## 9. Приложение
### 9.1 Файлы результатов
- `results.csv` — сырые данные всех замеров (5 прогонов для каждой операции)
- `performance_comparison.png` — график сравнения производительности

19
ivanchenkoam/results.csv Normal file
View File

@ -0,0 +1,19 @@
Структура,Режим,Операция,Замер1,Замер2,Замер3,Замер4,Замер5,Среднее
LinkedList,shuffled,вставка,6.405831,6.417272,6.417003,6.994602,6.457382,6.538418
LinkedList,shuffled,поиск,0.075726,0.069826,0.077385,0.069691,0.078248,0.074175
LinkedList,shuffled,удаление,0.037371,0.037285,0.055618,0.036882,0.039702,0.041372
LinkedList,sorted,вставка,5.332207,5.272102,5.250981,5.142026,5.175250,5.234513
LinkedList,sorted,поиск,0.058431,0.063572,0.056377,0.062588,0.057164,0.059626
LinkedList,sorted,удаление,0.034413,0.065045,0.037029,0.039570,0.037229,0.042657
HashTable,shuffled,вставка,0.370709,0.385906,0.383917,0.381112,0.383047,0.380938
HashTable,shuffled,поиск,0.003812,0.004149,0.003808,0.004207,0.003620,0.003919
HashTable,shuffled,удаление,0.003630,0.002233,0.002567,0.002055,0.003175,0.002732
HashTable,sorted,вставка,0.294287,0.374455,0.322318,0.326990,0.321059,0.327822
HashTable,sorted,поиск,0.003093,0.003913,0.003181,0.003599,0.003764,0.003510
HashTable,sorted,удаление,0.003388,0.002387,0.002925,0.002507,0.002585,0.002759
BST,shuffled,вставка,0.032676,0.031897,0.032648,0.030978,0.029900,0.031620
BST,shuffled,поиск,0.000262,0.000265,0.000269,0.000253,0.000264,0.000262
BST,shuffled,удаление,0.000176,0.000160,0.000166,0.000162,0.000182,0.000169
BST,sorted,вставка,8.831507,9.107596,8.709169,8.905054,8.916063,8.893878
BST,sorted,поиск,0.065463,0.081058,0.062677,0.083609,0.065106,0.071583
BST,sorted,удаление,0.040375,0.043116,0.041341,0.043694,0.041123,0.041930
1 Структура Режим Операция Замер1 Замер2 Замер3 Замер4 Замер5 Среднее
2 LinkedList shuffled вставка 6.405831 6.417272 6.417003 6.994602 6.457382 6.538418
3 LinkedList shuffled поиск 0.075726 0.069826 0.077385 0.069691 0.078248 0.074175
4 LinkedList shuffled удаление 0.037371 0.037285 0.055618 0.036882 0.039702 0.041372
5 LinkedList sorted вставка 5.332207 5.272102 5.250981 5.142026 5.175250 5.234513
6 LinkedList sorted поиск 0.058431 0.063572 0.056377 0.062588 0.057164 0.059626
7 LinkedList sorted удаление 0.034413 0.065045 0.037029 0.039570 0.037229 0.042657
8 HashTable shuffled вставка 0.370709 0.385906 0.383917 0.381112 0.383047 0.380938
9 HashTable shuffled поиск 0.003812 0.004149 0.003808 0.004207 0.003620 0.003919
10 HashTable shuffled удаление 0.003630 0.002233 0.002567 0.002055 0.003175 0.002732
11 HashTable sorted вставка 0.294287 0.374455 0.322318 0.326990 0.321059 0.327822
12 HashTable sorted поиск 0.003093 0.003913 0.003181 0.003599 0.003764 0.003510
13 HashTable sorted удаление 0.003388 0.002387 0.002925 0.002507 0.002585 0.002759
14 BST shuffled вставка 0.032676 0.031897 0.032648 0.030978 0.029900 0.031620
15 BST shuffled поиск 0.000262 0.000265 0.000269 0.000253 0.000264 0.000262
16 BST shuffled удаление 0.000176 0.000160 0.000166 0.000162 0.000182 0.000169
17 BST sorted вставка 8.831507 9.107596 8.709169 8.905054 8.916063 8.893878
18 BST sorted поиск 0.065463 0.081058 0.062677 0.083609 0.065106 0.071583
19 BST sorted удаление 0.040375 0.043116 0.041341 0.043694 0.041123 0.041930

View File

@ -0,0 +1 @@
создал, чтоб отредактировать название