Задание 2

This commit is contained in:
sawa2caxap 2026-05-24 19:39:37 +03:00
parent 268d492389
commit d576fe4a6b
72 changed files with 1252 additions and 818 deletions

View File

@ -1,3 +0,0 @@
# Default ignored files
/shelf/
/workspace.xml

View File

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.11 (ds_project_archive)" project-jdk-type="Python SDK" />
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.11 (maze_project_submission) (2)" project-jdk-type="Python SDK" />
</project>

View File

@ -2,7 +2,7 @@
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/ds_project_archive.iml" filepath="$PROJECT_DIR$/.idea/ds_project_archive.iml" />
<module fileurl="file://$PROJECT_DIR$/.idea/maze_project_submission.iml" filepath="$PROJECT_DIR$/.idea/maze_project_submission.iml" />
</modules>
</component>
</project>

View File

@ -0,0 +1,42 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="AutoImportSettings">
<option name="autoReloadType" value="SELECTIVE" />
</component>
<component name="ChangeListManager">
<list default="true" id="a6ff989d-c5f6-4522-8b0a-933849f2d044" name="Changes" comment="" />
<option name="SHOW_DIALOG" value="false" />
<option name="HIGHLIGHT_CONFLICTS" value="true" />
<option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" />
<option name="LAST_RESOLUTION" value="IGNORE" />
</component>
<component name="MarkdownSettingsMigration">
<option name="stateVersion" value="1" />
</component>
<component name="ProjectColorInfo"><![CDATA[{
"associatedIndex": 2
}]]></component>
<component name="ProjectId" id="3EB20Mq0B865MSq8Kkl2evaRIZW" />
<component name="ProjectViewState">
<option name="hideEmptyMiddlePackages" value="true" />
<option name="showLibraryContents" value="true" />
</component>
<component name="PropertiesComponent"><![CDATA[{
"keyToString": {
"RunOnceActivity.OpenProjectViewOnStart": "true",
"RunOnceActivity.ShowReadmeOnStart": "true",
"last_opened_file_path": "C:/Users/vaz21/Downloads/Task 2 GLOBAL/maze_project_submission"
}
}]]></component>
<component name="SpellCheckerSettings" RuntimeDictionaries="0" Folders="0" CustomDictionaries="0" DefaultDictionary="application-level" UseSingleDictionary="true" transferred="true" />
<component name="TaskManager">
<task active="true" id="Default" summary="Default task">
<changelist id="a6ff989d-c5f6-4522-8b0a-933849f2d044" name="Changes" comment="" />
<created>1779637417749</created>
<option name="number" value="Default" />
<option name="presentableId" value="Default" />
<updated>1779637417749</updated>
</task>
<servers />
</component>
</project>

View File

@ -1,18 +1,24 @@
# Задание 1 — структуры данных
# Maze Solver Project
Процедурная реализация:
- linked_list.py
- hash_table.py
- bst.py
ООП-проект для поиска выхода из лабиринта с паттернами:
- Builder
- Strategy
- Observer
- Command
Эксперименты и отчёты:
- experiments.py
- plot_results.py
- results.csv
- docs/report.md
- docs/data/*.png
Запуск:
## Запуск
```bash
python main.py
```
## Эксперименты
```bash
python experiment.py
```
Результаты сохраняются в папку `experiment_results/`.
## Требования
```bash
pip install -r requirements.txt
```

View File

@ -1,118 +0,0 @@
from typing import Any, Dict, List, Optional
Node = Dict[str, Any]
def _make_node(name: str, phone: str) -> Node:
return {"name": name, "phone": phone, "left": None, "right": None}
def bst_insert(root: Optional[Node], name: str, phone: str) -> Node:
new_node = _make_node(name, phone)
if root is None:
return new_node
current = root
parent = None
while current is not None:
parent = current
if name < current["name"]:
current = current["left"]
elif name > current["name"]:
current = current["right"]
else:
current["phone"] = phone
return root
if name < parent["name"]:
parent["left"] = new_node
else:
parent["right"] = new_node
return root
def bst_find(root: Optional[Node], name: str) -> Optional[str]:
current = root
while current is not None:
if name < current["name"]:
current = current["left"]
elif name > current["name"]:
current = current["right"]
else:
return current["phone"]
return None
def _find_min_node(node: Node) -> Node:
current = node
while current["left"] is not None:
current = current["left"]
return current
def bst_delete(root: Optional[Node], name: str) -> Optional[Node]:
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
if current["left"] is None or current["right"] is None:
child = current["left"] if current["left"] is not None else current["right"]
if parent is None:
return child
if parent["left"] is current:
parent["left"] = child
else:
parent["right"] = child
return root
succ_parent = current
successor = current["right"]
while successor["left"] is not None:
succ_parent = successor
successor = successor["left"]
current["name"] = successor["name"]
current["phone"] = successor["phone"]
successor_child = successor["right"]
if succ_parent["left"] is successor:
succ_parent["left"] = successor_child
else:
succ_parent["right"] = successor_child
return root
def bst_list_all(root: Optional[Node]) -> List[Dict[str, str]]:
result: List[Dict[str, str]] = []
stack: List[Node] = []
current = root
while current is not None or stack:
while current is not None:
stack.append(current)
current = current["left"]
current = stack.pop()
result.append({"name": current["name"], "phone": current["phone"]})
current = current["right"]
return result

View File

View File

@ -0,0 +1,7 @@
from abc import ABC, abstractmethod
class MazeBuilder(ABC):
@abstractmethod
def buildFromFile(self, filename):
raise NotImplementedError

View File

@ -0,0 +1,52 @@
from core.cell import Cell
from core.maze import Maze
from builders.maze_builder import MazeBuilder
class TextFileMazeBuilder(MazeBuilder):
def buildFromFile(self, filename):
with open(filename, "r", encoding="utf-8") as f:
lines = [line.rstrip("\n") for line in f]
if not lines:
raise ValueError("Maze file is empty")
width = max(len(line) for line in lines)
height = len(lines)
cells = []
startCell = None
exitCell = None
for y, line in enumerate(lines):
row = []
for x in range(width):
ch = line[x] if x < len(line) else "#"
if ch == "#":
cell = Cell(x, y, isWall=True)
elif ch == "S":
if startCell is not None:
raise ValueError("Multiple start cells found")
cell = Cell(x, y, isWall=False, isStart=True)
startCell = cell
elif ch == "E":
if exitCell is not None:
raise ValueError("Multiple exit cells found")
cell = Cell(x, y, isWall=False, isExit=True)
exitCell = cell
elif ch in (" ", "."):
cell = Cell(x, y, isWall=False)
elif ch.isdigit():
cell = Cell(x, y, isWall=False, weight=max(1, int(ch)))
else:
raise ValueError(f"Unsupported symbol '{ch}' at ({x}, {y})")
row.append(cell)
cells.append(row)
if startCell is None:
raise ValueError("Start cell 'S' not found")
if exitCell is None:
raise ValueError("Exit cell 'E' not found")
return Maze(cells, width, height, startCell, exitCell)

View File

View File

@ -0,0 +1,11 @@
from abc import ABC, abstractmethod
class Command(ABC):
@abstractmethod
def execute(self):
raise NotImplementedError
@abstractmethod
def undo(self):
raise NotImplementedError

View File

@ -0,0 +1,37 @@
from commands.command import Command
class MoveCommand(Command):
DIRECTION_TO_DELTA = {
"W": (0, -1),
"A": (-1, 0),
"S": (0, 1),
"D": (1, 0),
}
def __init__(self, player, maze, direction):
self.player = player
self.maze = maze
self.direction = direction.upper()
self.previousCell = None
def execute(self):
if self.direction not in self.DIRECTION_TO_DELTA:
return False
dx, dy = self.DIRECTION_TO_DELTA[self.direction]
current = self.player.currentCell
new_cell = self.maze.getCell(current.x + dx, current.y + dy)
if new_cell is None or not new_cell.isPassable():
return False
self.previousCell = current
self.player.setCell(new_cell)
return True
def undo(self):
if self.previousCell is None:
return False
self.player.setCell(self.previousCell)
return True

View File

View File

@ -0,0 +1,30 @@
from commands.move_command import MoveCommand
class GameController:
def __init__(self, maze, player, view):
self.maze = maze
self.player = player
self.view = view
self.history = []
def move(self, direction):
command = MoveCommand(self.player, self.maze, direction)
if command.execute():
self.history.append(command)
self.view.update({"type": "move", "direction": direction})
self.view.render(self.maze, player_position=self.player.currentCell)
return True
print("Cannot move there")
return False
def undo(self):
if not self.history:
print("Nothing to undo")
return False
command = self.history.pop()
if command.undo():
self.view.update({"type": "undo"})
self.view.render(self.maze, player_position=self.player.currentCell)
return True
return False

View File

26
BolonkinNM/core/cell.py Normal file
View File

@ -0,0 +1,26 @@
from dataclasses import dataclass
@dataclass
class Cell:
x: int
y: int
isWall: bool = False
isStart: bool = False
isExit: bool = False
weight: int = 1
def isPassable(self):
return not self.isWall
def __repr__(self):
parts = [f"Cell({self.x}, {self.y}"]
if self.isWall:
parts.append("WALL")
if self.isStart:
parts.append("START")
if self.isExit:
parts.append("EXIT")
if self.weight != 1:
parts.append(f"w={self.weight}")
return ", ".join(parts) + ")"

49
BolonkinNM/core/maze.py Normal file
View File

@ -0,0 +1,49 @@
class Maze:
def __init__(self, cells, width, height, startCell=None, exitCell=None):
self.cells = cells
self.width = width
self.height = height
self.startCell = startCell
self.exitCell = exitCell
def getCell(self, x, y):
if 0 <= x < self.width and 0 <= y < self.height:
return self.cells[y][x]
return None
def getNeighbors(self, cell):
neighbors = []
for dx, dy in ((0, -1), (0, 1), (-1, 0), (1, 0)):
nx, ny = cell.x + dx, cell.y + dy
neighbor = self.getCell(nx, ny)
if neighbor is not None and neighbor.isPassable():
neighbors.append(neighbor)
return neighbors
def render_lines(self, player_position=None, path=None):
path_set = {(c.x, c.y) for c in path} if path else set()
player_pos = None if player_position is None else (player_position.x, player_position.y)
lines = []
for y in range(self.height):
row = []
for x in range(self.width):
cell = self.cells[y][x]
if player_pos == (x, y):
row.append("P")
elif cell.isStart:
row.append("S")
elif cell.isExit:
row.append("E")
elif cell.isWall:
row.append("#")
elif (x, y) in path_set:
row.append("*")
elif cell.weight > 1:
row.append(str(cell.weight))
else:
row.append(" ")
lines.append("".join(row))
return lines
def render(self, player_position=None, path=None):
return "\n".join(self.render_lines(player_position=player_position, path=path))

View File

@ -0,0 +1,6 @@
class Player:
def __init__(self, currentCell):
self.currentCell = currentCell
def setCell(self, cell):
self.currentCell = cell

View File

@ -0,0 +1,11 @@
from dataclasses import dataclass, field
@dataclass
class SearchStats:
timeMs: float
visitedCells: int
pathLength: int
path: list = field(default_factory=list)
found: bool = False
algorithm: str = ""

View File

@ -0,0 +1 @@
Place report files and experiment outputs here.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

View File

@ -1,109 +0,0 @@
Структура,Режим,Операция,Замер,Время (сек)
LinkedList,случайный,insert,1,4.2622492010
LinkedList,случайный,find,1,0.0314994130
LinkedList,случайный,delete,1,0.0149069000
LinkedList,случайный,insert,2,4.0154580330
LinkedList,случайный,find,2,0.0393284500
LinkedList,случайный,delete,2,0.0210732100
LinkedList,случайный,insert,3,4.0436019780
LinkedList,случайный,find,3,0.0344933660
LinkedList,случайный,delete,3,0.0152639850
LinkedList,случайный,insert,4,3.7182993220
LinkedList,случайный,find,4,0.0327698850
LinkedList,случайный,delete,4,0.0149959540
LinkedList,случайный,insert,5,3.7082228200
LinkedList,случайный,find,5,0.0303762490
LinkedList,случайный,delete,5,0.0141406560
LinkedList,случайный,insert,среднее,3.9495662708
LinkedList,случайный,find,среднее,0.0336934726
LinkedList,случайный,delete,среднее,0.0160761410
HashTable,случайный,insert,1,0.2059865770
HashTable,случайный,find,1,0.0014966100
HashTable,случайный,delete,1,0.0006891700
HashTable,случайный,insert,2,0.2024331460
HashTable,случайный,find,2,0.0015934880
HashTable,случайный,delete,2,0.0007212620
HashTable,случайный,insert,3,0.2126128040
HashTable,случайный,find,3,0.0016566220
HashTable,случайный,delete,3,0.0008358420
HashTable,случайный,insert,4,0.2157934910
HashTable,случайный,find,4,0.0015542810
HashTable,случайный,delete,4,0.0007269120
HashTable,случайный,insert,5,0.2079924580
HashTable,случайный,find,5,0.0013696990
HashTable,случайный,delete,5,0.0006616050
HashTable,случайный,insert,среднее,0.2089636952
HashTable,случайный,find,среднее,0.0015341400
HashTable,случайный,delete,среднее,0.0007269582
BST,случайный,insert,1,0.0166981280
BST,случайный,find,1,0.0001569360
BST,случайный,delete,1,0.0000917280
BST,случайный,insert,2,0.0184119040
BST,случайный,find,2,0.0001517110
BST,случайный,delete,2,0.0001163770
BST,случайный,insert,3,0.0174662270
BST,случайный,find,3,0.0001582930
BST,случайный,delete,3,0.0000892660
BST,случайный,insert,4,0.0191369100
BST,случайный,find,4,0.0002087170
BST,случайный,delete,4,0.0001067050
BST,случайный,insert,5,0.0184276900
BST,случайный,find,5,0.0002767720
BST,случайный,delete,5,0.0001067660
BST,случайный,insert,среднее,0.0180281718
BST,случайный,find,среднее,0.0001904858
BST,случайный,delete,среднее,0.0001021684
LinkedList,отсортированный,insert,1,2.9875078340
LinkedList,отсортированный,find,1,0.0237300610
LinkedList,отсортированный,delete,1,0.0111698260
LinkedList,отсортированный,insert,2,3.0573987940
LinkedList,отсортированный,find,2,0.0243270360
LinkedList,отсортированный,delete,2,0.0115366030
LinkedList,отсортированный,insert,3,2.9641987260
LinkedList,отсортированный,find,3,0.0236313330
LinkedList,отсортированный,delete,3,0.0112848510
LinkedList,отсортированный,insert,4,3.0345914950
LinkedList,отсортированный,find,4,0.0240271220
LinkedList,отсортированный,delete,4,0.0112117310
LinkedList,отсортированный,insert,5,2.9481954700
LinkedList,отсортированный,find,5,0.0239006100
LinkedList,отсортированный,delete,5,0.0110857710
LinkedList,отсортированный,insert,среднее,2.9983784638
LinkedList,отсортированный,find,среднее,0.0239232324
LinkedList,отсортированный,delete,среднее,0.0112577564
HashTable,отсортированный,insert,1,0.1997087560
HashTable,отсортированный,find,1,0.0017550400
HashTable,отсортированный,delete,1,0.0008407980
HashTable,отсортированный,insert,2,0.1968675190
HashTable,отсортированный,find,2,0.0019886760
HashTable,отсортированный,delete,2,0.0008920910
HashTable,отсортированный,insert,3,0.1907563580
HashTable,отсортированный,find,3,0.0018447440
HashTable,отсортированный,delete,3,0.0008684640
HashTable,отсортированный,insert,4,0.2625327630
HashTable,отсортированный,find,4,0.0016053140
HashTable,отсортированный,delete,4,0.0008098670
HashTable,отсортированный,insert,5,0.1936840590
HashTable,отсортированный,find,5,0.0019015160
HashTable,отсортированный,delete,5,0.0009053780
HashTable,отсортированный,insert,среднее,0.2087098910
HashTable,отсортированный,find,среднее,0.0018190580
HashTable,отсортированный,delete,среднее,0.0008633196
BST,отсортированный,insert,1,4.2195800190
BST,отсортированный,find,1,0.0389314570
BST,отсортированный,delete,1,0.0190308920
BST,отсортированный,insert,2,4.1356184250
BST,отсортированный,find,2,0.0383339310
BST,отсортированный,delete,2,0.0194247740
BST,отсортированный,insert,3,4.1204731890
BST,отсортированный,find,3,0.0388593320
BST,отсортированный,delete,3,0.0215428460
BST,отсортированный,insert,4,4.2120902370
BST,отсортированный,find,4,0.0378190250
BST,отсортированный,delete,4,0.0188528460
BST,отсортированный,insert,5,4.1304951260
BST,отсортированный,find,5,0.0359927840
BST,отсортированный,delete,5,0.0179617110
BST,отсортированный,insert,среднее,4.1636513992
BST,отсортированный,find,среднее,0.0379873058
BST,отсортированный,delete,среднее,0.0193626138
1 Структура Режим Операция Замер Время (сек)
2 LinkedList случайный insert 1 4.2622492010
3 LinkedList случайный find 1 0.0314994130
4 LinkedList случайный delete 1 0.0149069000
5 LinkedList случайный insert 2 4.0154580330
6 LinkedList случайный find 2 0.0393284500
7 LinkedList случайный delete 2 0.0210732100
8 LinkedList случайный insert 3 4.0436019780
9 LinkedList случайный find 3 0.0344933660
10 LinkedList случайный delete 3 0.0152639850
11 LinkedList случайный insert 4 3.7182993220
12 LinkedList случайный find 4 0.0327698850
13 LinkedList случайный delete 4 0.0149959540
14 LinkedList случайный insert 5 3.7082228200
15 LinkedList случайный find 5 0.0303762490
16 LinkedList случайный delete 5 0.0141406560
17 LinkedList случайный insert среднее 3.9495662708
18 LinkedList случайный find среднее 0.0336934726
19 LinkedList случайный delete среднее 0.0160761410
20 HashTable случайный insert 1 0.2059865770
21 HashTable случайный find 1 0.0014966100
22 HashTable случайный delete 1 0.0006891700
23 HashTable случайный insert 2 0.2024331460
24 HashTable случайный find 2 0.0015934880
25 HashTable случайный delete 2 0.0007212620
26 HashTable случайный insert 3 0.2126128040
27 HashTable случайный find 3 0.0016566220
28 HashTable случайный delete 3 0.0008358420
29 HashTable случайный insert 4 0.2157934910
30 HashTable случайный find 4 0.0015542810
31 HashTable случайный delete 4 0.0007269120
32 HashTable случайный insert 5 0.2079924580
33 HashTable случайный find 5 0.0013696990
34 HashTable случайный delete 5 0.0006616050
35 HashTable случайный insert среднее 0.2089636952
36 HashTable случайный find среднее 0.0015341400
37 HashTable случайный delete среднее 0.0007269582
38 BST случайный insert 1 0.0166981280
39 BST случайный find 1 0.0001569360
40 BST случайный delete 1 0.0000917280
41 BST случайный insert 2 0.0184119040
42 BST случайный find 2 0.0001517110
43 BST случайный delete 2 0.0001163770
44 BST случайный insert 3 0.0174662270
45 BST случайный find 3 0.0001582930
46 BST случайный delete 3 0.0000892660
47 BST случайный insert 4 0.0191369100
48 BST случайный find 4 0.0002087170
49 BST случайный delete 4 0.0001067050
50 BST случайный insert 5 0.0184276900
51 BST случайный find 5 0.0002767720
52 BST случайный delete 5 0.0001067660
53 BST случайный insert среднее 0.0180281718
54 BST случайный find среднее 0.0001904858
55 BST случайный delete среднее 0.0001021684
56 LinkedList отсортированный insert 1 2.9875078340
57 LinkedList отсортированный find 1 0.0237300610
58 LinkedList отсортированный delete 1 0.0111698260
59 LinkedList отсортированный insert 2 3.0573987940
60 LinkedList отсортированный find 2 0.0243270360
61 LinkedList отсортированный delete 2 0.0115366030
62 LinkedList отсортированный insert 3 2.9641987260
63 LinkedList отсортированный find 3 0.0236313330
64 LinkedList отсортированный delete 3 0.0112848510
65 LinkedList отсортированный insert 4 3.0345914950
66 LinkedList отсортированный find 4 0.0240271220
67 LinkedList отсортированный delete 4 0.0112117310
68 LinkedList отсортированный insert 5 2.9481954700
69 LinkedList отсортированный find 5 0.0239006100
70 LinkedList отсортированный delete 5 0.0110857710
71 LinkedList отсортированный insert среднее 2.9983784638
72 LinkedList отсортированный find среднее 0.0239232324
73 LinkedList отсортированный delete среднее 0.0112577564
74 HashTable отсортированный insert 1 0.1997087560
75 HashTable отсортированный find 1 0.0017550400
76 HashTable отсортированный delete 1 0.0008407980
77 HashTable отсортированный insert 2 0.1968675190
78 HashTable отсортированный find 2 0.0019886760
79 HashTable отсортированный delete 2 0.0008920910
80 HashTable отсортированный insert 3 0.1907563580
81 HashTable отсортированный find 3 0.0018447440
82 HashTable отсортированный delete 3 0.0008684640
83 HashTable отсортированный insert 4 0.2625327630
84 HashTable отсортированный find 4 0.0016053140
85 HashTable отсортированный delete 4 0.0008098670
86 HashTable отсортированный insert 5 0.1936840590
87 HashTable отсортированный find 5 0.0019015160
88 HashTable отсортированный delete 5 0.0009053780
89 HashTable отсортированный insert среднее 0.2087098910
90 HashTable отсортированный find среднее 0.0018190580
91 HashTable отсортированный delete среднее 0.0008633196
92 BST отсортированный insert 1 4.2195800190
93 BST отсортированный find 1 0.0389314570
94 BST отсортированный delete 1 0.0190308920
95 BST отсортированный insert 2 4.1356184250
96 BST отсортированный find 2 0.0383339310
97 BST отсортированный delete 2 0.0194247740
98 BST отсортированный insert 3 4.1204731890
99 BST отсортированный find 3 0.0388593320
100 BST отсортированный delete 3 0.0215428460
101 BST отсортированный insert 4 4.2120902370
102 BST отсортированный find 4 0.0378190250
103 BST отсортированный delete 4 0.0188528460
104 BST отсортированный insert 5 4.1304951260
105 BST отсортированный find 5 0.0359927840
106 BST отсортированный delete 5 0.0179617110
107 BST отсортированный insert среднее 4.1636513992
108 BST отсортированный find среднее 0.0379873058
109 BST отсортированный delete среднее 0.0193626138

View File

@ -1,101 +1,249 @@
# Отчёт по заданию 1 — структуры данных
# Отчёт по работе «Поиск выхода из лабиринта»
## Цель работы
## 1. Цель работы
Разработать гибкую программу для загрузки лабиринта из файла, поиска пути от старта до выхода с возможностью выбора алгоритма, визуализации процесса и экспериментального сравнения алгоритмов. В работе использованы паттерны проектирования, чтобы отделить логику представления лабиринта, его загрузки, поиска пути и вывода результатов.
Реализовать три структуры данных с нуля в процедурном стиле:
## 2. Описание задачи
Лабиринт задаётся в текстовом файле символами:
- `#` — стена;
- пробел — проход;
- `S` — старт;
- `E` — выход.
- связный список;
- хеш-таблицу;
- двоичное дерево поиска.
Программа должна:
- загружать лабиринт;
- строить его внутреннюю модель;
- искать путь разными алгоритмами;
- собирать статистику поиска;
- визуализировать результат в консоли;
- сравнивать стратегии на разных типах лабиринтов.
Также были выполнены измерения времени для операций `insert`, `find`, `delete` и построены графики по результатам эксперимента.
## 3. Выбранные паттерны проектирования
## Реализованные структуры
### 3.1 Builder
Паттерн Builder используется для загрузки лабиринта из файла. Он скрывает детали парсинга и валидации, а клиент получает готовый объект `Maze`.
### Связный список
Преимущества:
- легко добавить новый формат загрузки;
- клиентский код не зависит от формата файла;
- создание лабиринта можно расширять без переписывания остальной программы.
Узел хранится как словарь:
### 3.2 Strategy
Паттерн Strategy используется для выбора алгоритма поиска пути. В программе реализованы `BFS`, `DFS`, `A*`, а при необходимости можно добавить Дейкстру или любую другую стратегию.
```python
{"name": "Имя", "phone": "123", "next": None}
Преимущества:
- алгоритм можно менять во время выполнения;
- код оркестратора не зависит от конкретного метода поиска;
- новые алгоритмы добавляются без изменения существующего кода.
### 3.3 Observer
Паттерн Observer используется для обновления консольного интерфейса при изменении состояния программы: загрузка лабиринта, поиск пути, движение игрока.
Преимущества:
- вывод отделён от логики;
- можно заменить консольный интерфейс на графический без изменения поискового кода;
- упрощается расширение визуализации.
### 3.4 Command
Паттерн Command используется для пошагового перемещения игрока и отмены последнего хода.
Преимущества:
- каждое действие оформляется как отдельный объект;
- легко реализовать undo;
- история ходов хранится отдельно от логики перемещения.
## 4. Диаграмма классов
Ниже приведена упрощённая диаграмма классов в формате Mermaid:
```mermaid
classDiagram
class Cell {
+int x
+int y
+bool isWall
+bool isStart
+bool isExit
+isPassable()
}
class Maze {
+cells
+width
+height
+startCell
+exitCell
+getCell(x, y)
+getNeighbors(cell)
}
class MazeBuilder {
<<interface>>
+buildFromFile(filename)
}
class TextFileMazeBuilder {
+buildFromFile(filename)
}
class PathFindingStrategy {
<<interface>>
+findPath(maze, start, exitCell)
}
class BFSStrategy {
+findPath(maze, start, exitCell)
}
class DFSStrategy {
+findPath(maze, start, exitCell)
}
class AStarStrategy {
+findPath(maze, start, exitCell)
}
class SearchStats {
+timeMs
+visitedCells
+pathLength
+path
}
class MazeSolver {
+maze
+strategy
+setStrategy(strategy)
+solve()
}
class Observer {
<<interface>>
+update(event)
}
class ConsoleView {
+update(event)
+render(maze, player_position, path)
}
class Command {
<<interface>>
+execute()
+undo()
}
class MoveCommand {
+execute()
+undo()
}
class Player {
+currentCell
+setCell(cell)
}
Maze <|-- TextFileMazeBuilder : creates
MazeBuilder <|.. TextFileMazeBuilder
PathFindingStrategy <|.. BFSStrategy
PathFindingStrategy <|.. DFSStrategy
PathFindingStrategy <|.. AStarStrategy
MazeSolver --> Maze
MazeSolver --> PathFindingStrategy
MazeSolver --> SearchStats
Observer <|.. ConsoleView
Command <|.. MoveCommand
MoveCommand --> Player
MoveCommand --> Maze
ConsoleView --> Maze
Maze --> Cell
```
### Хеш-таблица
## 5. Ключевые классы и их роль
Хранится как список бакетов фиксированной длины, где каждый бакет — голова связного списка или `None`.
### Cell
Хранит координаты клетки и её тип. Позволяет быстро проверять, является ли клетка проходимой.
### Двоичное дерево поиска
### Maze
Содержит двумерную карту клеток, размер лабиринта, а также ссылки на старт и выход. Даёт доступ к соседним клеткам по четырём направлениям.
Узел хранится как словарь:
### TextFileMazeBuilder
Читает текстовый файл, создаёт объекты `Cell`, определяет старт и выход, затем возвращает готовый `Maze`.
```python
{"name": "Имя", "phone": "123", "left": None, "right": None}
```
### BFSStrategy
Ищет кратчайший путь по числу шагов. Подходит для случая, когда все переходы одинаковой стоимости.
Для BST использованы итеративные операции, чтобы корректно работать и на отсортированных данных.
### DFSStrategy
Быстро исследует пространство, но не гарантирует кратчайший путь. Полезен как сравнительный алгоритм.
## Методика эксперимента
### AStarStrategy
Использует эвристику Манхэттенского расстояния. Обычно посещает меньше клеток, чем BFS, если эвристика удачно направляет поиск к цели.
- Количество записей: `N = 10000`
- Режимы данных:
- случайный порядок;
- отсортированный порядок.
- Каждое измерение повторялось **5 раз**.
- В CSV сохранены:
- все отдельные замеры;
- среднее время для каждой операции, структуры и режима.
### MazeSolver
Оркестратор, который хранит лабиринт и текущую стратегию. Вызывает поиск, измеряет время и собирает статистику.
Операции:
### SearchStats
Содержит итог поиска: время выполнения, количество посещённых клеток и длину пути.
- вставка всех записей;
- поиск 100 существующих и 10 отсутствующих имён;
- удаление 50 случайных имён.
### ConsoleView
Реализует наблюдателя и умеет выводить лабиринт и найденный путь в консоль.
## Графики
### MoveCommand
Оформляет ход игрока как объект-команду. Поддерживает отмену последнего перемещения.
![insert](data/insert.png)
## 6. Экспериментальная часть
![find](data/find.png)
### 6.1 Подготовка тестовых лабиринтов
Для сравнения стратегий использовались следующие типы лабиринтов:
- маленький 10×10 с простым путём;
- средний 50×50 с тупиками;
- большой 100×100 со сложной структурой;
- пустой лабиринт без стен;
- лабиринт без выхода.
![delete](data/delete.png)
### 6.2 Методика измерений
Для каждой стратегии и каждого лабиринта поиск запускался несколько раз, после чего вычислялись средние значения:
- время поиска в миллисекундах;
- количество посещённых клеток;
- длина найденного пути.
## Средние результаты
Результаты сохранялись в CSV-файл в двух вариантах:
- сырой набор измерений;
- усреднённая таблица.
| Режим | Операция | LinkedList | HashTable | BST | Лучший результат |
|---|---:|---:|---:|---:|---|
| случайный | insert | 3.949566 | 0.208964 | 0.018028 | BST |
| случайный | find | 0.033693 | 0.001534 | 0.000190 | BST |
| случайный | delete | 0.016076 | 0.000727 | 0.000102 | BST |
| отсортированный | insert | 2.998378 | 0.208710 | 4.163651 | HashTable |
| отсортированный | find | 0.023923 | 0.001819 | 0.037987 | HashTable |
| отсортированный | delete | 0.011258 | 0.000863 | 0.019363 | HashTable |
## 7. Анализ эффективности
## Анализ результатов
### BFS
BFS гарантирует кратчайший путь по числу шагов, если все переходы имеют одинаковую стоимость. На простых и пустых лабиринтах работает стабильно и предсказуемо. Минус — может посещать много клеток, особенно на больших лабиринтах.
### Влияние порядка входных данных на BST
### DFS
DFS может быстро найти какой-то путь, но он не обязательно будет кратчайшим. На сложных лабиринтах иногда работает быстро, но на других может уйти далеко от цели и пройти лишние области.
На случайных данных BST работает значительно быстрее, чем на отсортированных. Это связано с тем, что при случайной вставке дерево остаётся ближе к сбалансированному состоянию.
### A*
A* использует эвристику и обычно показывает хороший баланс между скоростью и качеством пути. На больших и запутанных лабиринтах часто посещает меньше клеток, чем BFS, потому что поиск направлен в сторону выхода.
На отсортированных данных дерево вырождается в цепочку, поэтому вставка становится медленной, а поиск и удаление тоже деградируют по времени.
### Лабиринт без пути
Если пути нет, все алгоритмы вынуждены исследовать доступную область. В этом случае длина пути равна 0, а различия между алгоритмами проявляются в количестве просмотренных клеток и времени выполнения.
### Почему хеш-таблица почти не чувствительна к порядку
### Вывод по выбору алгоритма
- BFS стоит выбирать, когда нужен гарантированно кратчайший путь и веса переходов одинаковы.
- DFS полезен как простой и быстрый по реализации вариант, но без гарантии оптимальности.
- A* подходит для практических задач, где нужно ускорить поиск и сократить число посещённых клеток.
- При взвешенных переходах лучше использовать Дейкстру или взвешенный A*.
Хеш-таблица распределяет элементы по бакетам через хеш-функцию, поэтому сам порядок входа почти не влияет на скорость. Влияние может появляться только из-за коллизий, но в целом поведение остаётся близким к постоянному времени.
## 8. Роль ООП и паттернов
ООП и паттерны сделали код более гибким и расширяемым. Благодаря этому:
- можно заменить алгоритм поиска без переписывания логики программы;
- можно добавить новый формат загрузки лабиринта;
- можно поменять способ визуализации;
- можно расширить управление игроком и добавить отмену действий.
### Почему связный список всегда медленен при поиске
Без паттернов пришлось бы связывать загрузку, поиск, отображение и управление в один большой блок кода. Это усложнило бы отладку и дальнейшие изменения.
Поиск в связном списке выполняется последовательным просмотром элементов. Поэтому при большом количестве записей приходится проходить много узлов, и операция остаётся линейной по времени.
## 9. Вывод
В ходе работы была создана расширяемая программа для поиска пути в лабиринте. Использование паттернов Builder, Strategy, Observer и Command позволило разделить обязанности между классами, упростить поддержку кода и сделать архитектуру удобной для дальнейшего развития. Эксперименты показали, что выбор алгоритма сильно зависит от типа лабиринта: BFS даёт кратчайший путь, DFS иногда быстрее в реализации, а A* чаще всего наиболее практичен на больших картах.
### Как удаление работает в каждой структуре
- В связном списке нужно сначала найти нужный узел, затем переназначить ссылку.
- В хеш-таблице сначала выбирается бакет, затем удаление выполняется внутри короткой цепочки.
- В BST удаление зависит от числа потомков: если потомок один или ноль, операция простая; если два — нужно найти преемника.
## Вывод
Для частых вставок и особенно частого поиска в реальной задаче чаще всего лучше подходит **хеш-таблица**.
Если важно получать данные в отсортированном виде, удобнее использовать **BST**.
**Связный список** подходит для маленьких объёмов данных или очень простых сценариев, но при большом числе записей он проигрывает по скорости поиска.
## 10. Приложения
- Листинги ключевых классов.
- CSV-файлы с результатами экспериментов.
- Графики сравнений.
- Файлы с тестовыми лабиринтами.

225
BolonkinNM/experiment.py Normal file
View File

@ -0,0 +1,225 @@
from pathlib import Path
from statistics import mean
import csv
import random
import matplotlib.pyplot as plt
from core.cell import Cell
from core.maze import Maze
from solver.maze_solver import MazeSolver
from strategies.astar_strategy import AStarStrategy
from strategies.bfs_strategy import BFSStrategy
from strategies.dfs_strategy import DFSStrategy
from strategies.dijkstra_strategy import DijkstraStrategy
BASE_DIR = Path(__file__).resolve().parent
OUT_DIR = BASE_DIR / "experiment_results"
def build_maze_from_symbols(lines):
height = len(lines)
width = max(len(line) for line in lines)
cells = []
start = None
exit_cell = None
for y, line in enumerate(lines):
row = []
for x in range(width):
ch = line[x] if x < len(line) else "#"
if ch == "#":
cell = Cell(x, y, isWall=True)
elif ch == "S":
cell = Cell(x, y, isWall=False, isStart=True)
start = cell
elif ch == "E":
cell = Cell(x, y, isWall=False, isExit=True)
exit_cell = cell
elif ch == " " or ch == ".":
cell = Cell(x, y, isWall=False)
elif ch.isdigit():
cell = Cell(x, y, isWall=False, weight=int(ch))
else:
raise ValueError(f"Unknown symbol '{ch}' at {x},{y}")
row.append(cell)
cells.append(row)
return Maze(cells, width, height, start, exit_cell)
def generate_empty_maze(width, height):
lines = [" " * width for _ in range(height)]
lines = [list(row) for row in lines]
lines[1][1] = "S"
lines[height - 2][width - 2] = "E"
return build_maze_from_symbols(["".join(row) for row in lines])
def generate_simple_maze(width, height):
grid = [["#" for _ in range(width)] for _ in range(height)]
for x in range(1, width - 1):
grid[1][x] = " "
for y in range(1, height - 1):
grid[y][width - 2] = " "
grid[1][1] = "S"
grid[height - 2][width - 2] = "E"
return build_maze_from_symbols(["".join(row) for row in grid])
def generate_branching_maze(width, height, seed=42, wall_density=0.30):
rng = random.Random(seed)
grid = [["#" for _ in range(width)] for _ in range(height)]
x, y = 1, 1
grid[y][x] = "S"
while (x, y) != (width - 2, height - 2):
candidates = []
for dx, dy in [(1, 0), (0, 1)]:
nx, ny = x + dx, y + dy
if 1 <= nx < width - 1 and 1 <= ny < height - 1:
candidates.append((nx, ny))
if not candidates:
break
x, y = rng.choice(candidates)
grid[y][x] = " "
grid[height - 2][width - 2] = "E"
# carve extra corridors and dead ends
for yy in range(1, height - 1):
for xx in range(1, width - 1):
if grid[yy][xx] == "#" and rng.random() > wall_density:
grid[yy][xx] = " "
grid[1][1] = "S"
grid[height - 2][width - 2] = "E"
return build_maze_from_symbols(["".join(row) for row in grid])
def generate_no_path_maze(width, height):
grid = [[" " for _ in range(width)] for _ in range(height)]
for x in range(width):
grid[height // 2][x] = "#"
grid[1][1] = "S"
grid[height - 2][width - 2] = "E"
return build_maze_from_symbols(["".join(row) for row in grid])
def generate_weighted_maze(width, height, seed=123):
rng = random.Random(seed)
grid = [[" " for _ in range(width)] for _ in range(height)]
for y in range(height):
for x in range(width):
r = rng.random()
if r < 0.12:
grid[y][x] = "#"
elif r < 0.25:
grid[y][x] = "3"
elif r < 0.40:
grid[y][x] = "2"
else:
grid[y][x] = "1"
# ensure path-ish
for x in range(width):
grid[1][x] = "1"
for y in range(1, height):
grid[y][width - 2] = "1"
grid[1][1] = "S"
grid[height - 2][width - 2] = "E"
return build_maze_from_symbols(["".join(row) for row in grid])
def bench_one_maze(maze_name, maze, strategies, repeats=5):
summary_rows = []
raw_rows = []
for strategy_name, strategy_factory in strategies:
times, visiteds, lengths = [], [], []
for run in range(1, repeats + 1):
solver = MazeSolver(maze)
solver.setStrategy(strategy_factory())
stats = solver.solve()
raw_rows.append([maze_name, strategy_name, run, f"{stats.timeMs:.6f}", stats.visitedCells, stats.pathLength])
times.append(stats.timeMs)
visiteds.append(stats.visitedCells)
lengths.append(stats.pathLength)
summary_rows.append([maze_name, strategy_name, f"{mean(times):.6f}", f"{mean(visiteds):.2f}", f"{mean(lengths):.2f}", repeats])
return summary_rows, raw_rows
def save_csv(path, rows):
with open(path, "w", newline="", encoding="utf-8") as f:
csv.writer(f).writerows(rows)
def plot_summary(summary_rows):
by_maze = {}
for row in summary_rows[1:]:
maze_name, strategy, avg_time, avg_visited, avg_len, runs = row
by_maze.setdefault(maze_name, []).append((strategy, float(avg_time), float(avg_visited), float(avg_len)))
for maze_name, items in by_maze.items():
items.sort(key=lambda t: t[0])
strategies = [i[0] for i in items]
x = list(range(len(strategies)))
plt.figure(figsize=(8, 4))
plt.bar(x, [i[1] for i in items])
plt.xticks(x, strategies)
plt.ylabel("ms")
plt.title(f"{maze_name} — avg time")
plt.tight_layout()
plt.savefig(OUT_DIR / f"{maze_name}_time.png", dpi=150)
plt.close()
plt.figure(figsize=(8, 4))
plt.bar(x, [i[2] for i in items])
plt.xticks(x, strategies)
plt.ylabel("cells")
plt.title(f"{maze_name} — visited cells")
plt.tight_layout()
plt.savefig(OUT_DIR / f"{maze_name}_visited.png", dpi=150)
plt.close()
plt.figure(figsize=(8, 4))
plt.bar(x, [i[3] for i in items])
plt.xticks(x, strategies)
plt.ylabel("cells")
plt.title(f"{maze_name} — path length")
plt.tight_layout()
plt.savefig(OUT_DIR / f"{maze_name}_length.png", dpi=150)
plt.close()
def main():
OUT_DIR.mkdir(exist_ok=True)
strategies = [
("BFS", BFSStrategy),
("DFS", DFSStrategy),
("A*", AStarStrategy),
("Dijkstra", DijkstraStrategy),
]
mazes = [
("small_10x10", generate_simple_maze(10, 10)),
("medium_50x50", generate_branching_maze(50, 50)),
("large_100x100", generate_branching_maze(100, 100, seed=99, wall_density=0.35)),
("empty_30x30", generate_empty_maze(30, 30)),
("no_path_30x30", generate_no_path_maze(30, 30)),
("weighted_30x30", generate_weighted_maze(30, 30)),
]
summary = [["maze", "strategy", "avg_time_ms", "avg_visited_cells", "avg_path_length", "runs"]]
raw = [["maze", "strategy", "run", "time_ms", "visited_cells", "path_length"]]
for maze_name, maze in mazes:
s_rows, r_rows = bench_one_maze(maze_name, maze, strategies, repeats=5)
summary.extend(s_rows)
raw.extend(r_rows)
save_csv(OUT_DIR / "summary.csv", summary)
save_csv(OUT_DIR / "raw.csv", raw)
plot_summary(summary)
print("Saved to", OUT_DIR.resolve())
if __name__ == "__main__":
main()

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

View File

@ -0,0 +1,121 @@
maze,strategy,run,time_ms,visited_cells,path_length
small_10x10,BFS,1,0.044300,15,15
small_10x10,BFS,2,0.022800,15,15
small_10x10,BFS,3,0.020400,15,15
small_10x10,BFS,4,0.020300,15,15
small_10x10,BFS,5,0.018700,15,15
small_10x10,DFS,1,0.031200,15,15
small_10x10,DFS,2,0.022000,15,15
small_10x10,DFS,3,0.021200,15,15
small_10x10,DFS,4,0.020800,15,15
small_10x10,DFS,5,0.020500,15,15
small_10x10,A*,1,0.048900,15,15
small_10x10,A*,2,0.034700,15,15
small_10x10,A*,3,0.029400,15,15
small_10x10,A*,4,0.029100,15,15
small_10x10,A*,5,0.029300,15,15
small_10x10,Dijkstra,1,0.037900,15,15
small_10x10,Dijkstra,2,0.028500,15,15
small_10x10,Dijkstra,3,0.026800,15,15
small_10x10,Dijkstra,4,0.026400,15,15
small_10x10,Dijkstra,5,0.026700,15,15
medium_50x50,BFS,1,2.105800,1579,95
medium_50x50,BFS,2,1.928700,1579,95
medium_50x50,BFS,3,1.969500,1579,95
medium_50x50,BFS,4,1.938800,1579,95
medium_50x50,BFS,5,1.943600,1579,95
medium_50x50,DFS,1,1.927300,1277,647
medium_50x50,DFS,2,1.856300,1277,647
medium_50x50,DFS,3,1.890100,1277,647
medium_50x50,DFS,4,1.868000,1277,647
medium_50x50,DFS,5,1.865500,1277,647
medium_50x50,A*,1,2.359000,927,95
medium_50x50,A*,2,2.193700,927,95
medium_50x50,A*,3,2.178400,927,95
medium_50x50,A*,4,2.181800,927,95
medium_50x50,A*,5,2.174500,927,95
medium_50x50,Dijkstra,1,3.534700,1579,95
medium_50x50,Dijkstra,2,3.435500,1579,95
medium_50x50,Dijkstra,3,3.457600,1579,95
medium_50x50,Dijkstra,4,3.417300,1579,95
medium_50x50,Dijkstra,5,3.538000,1579,95
large_100x100,BFS,1,8.624100,5566,195
large_100x100,BFS,2,7.706900,5566,195
large_100x100,BFS,3,9.723300,5566,195
large_100x100,BFS,4,7.585700,5566,195
large_100x100,BFS,5,8.031300,5566,195
large_100x100,DFS,1,5.512400,3543,1531
large_100x100,DFS,2,5.329300,3543,1531
large_100x100,DFS,3,5.223300,3543,1531
large_100x100,DFS,4,5.729900,3543,1531
large_100x100,DFS,5,5.497400,3543,1531
large_100x100,A*,1,2.101500,853,195
large_100x100,A*,2,2.264500,853,195
large_100x100,A*,3,2.064100,853,195
large_100x100,A*,4,2.031700,853,195
large_100x100,A*,5,2.046500,853,195
large_100x100,Dijkstra,1,25.021300,5571,195
large_100x100,Dijkstra,2,13.541100,5571,195
large_100x100,Dijkstra,3,12.884100,5571,195
large_100x100,Dijkstra,4,13.481800,5571,195
large_100x100,Dijkstra,5,12.748000,5571,195
empty_30x30,BFS,1,1.234300,896,55
empty_30x30,BFS,2,1.163400,896,55
empty_30x30,BFS,3,1.145700,896,55
empty_30x30,BFS,4,1.177300,896,55
empty_30x30,BFS,5,1.175100,896,55
empty_30x30,DFS,1,1.338000,842,815
empty_30x30,DFS,2,1.296500,842,815
empty_30x30,DFS,3,1.296700,842,815
empty_30x30,DFS,4,1.280100,842,815
empty_30x30,DFS,5,1.290800,842,815
empty_30x30,A*,1,2.183400,784,55
empty_30x30,A*,2,2.522900,784,55
empty_30x30,A*,3,1.985000,784,55
empty_30x30,A*,4,1.972100,784,55
empty_30x30,A*,5,2.088600,784,55
empty_30x30,Dijkstra,1,2.080400,896,55
empty_30x30,Dijkstra,2,2.100100,896,55
empty_30x30,Dijkstra,3,2.130700,896,55
empty_30x30,Dijkstra,4,2.073600,896,55
empty_30x30,Dijkstra,5,2.095900,896,55
no_path_30x30,BFS,1,0.645900,450,0
no_path_30x30,BFS,2,0.566600,450,0
no_path_30x30,BFS,3,0.566000,450,0
no_path_30x30,BFS,4,0.583500,450,0
no_path_30x30,BFS,5,0.568900,450,0
no_path_30x30,DFS,1,0.692100,450,0
no_path_30x30,DFS,2,0.676900,450,0
no_path_30x30,DFS,3,0.703500,450,0
no_path_30x30,DFS,4,0.722300,450,0
no_path_30x30,DFS,5,0.672000,450,0
no_path_30x30,A*,1,1.112700,450,0
no_path_30x30,A*,2,1.130000,450,0
no_path_30x30,A*,3,1.096100,450,0
no_path_30x30,A*,4,1.111400,450,0
no_path_30x30,A*,5,1.183500,450,0
no_path_30x30,Dijkstra,1,1.023300,450,0
no_path_30x30,Dijkstra,2,1.011700,450,0
no_path_30x30,Dijkstra,3,1.127200,450,0
no_path_30x30,Dijkstra,4,1.110200,450,0
no_path_30x30,Dijkstra,5,1.043900,450,0
weighted_30x30,BFS,1,1.074700,788,55
weighted_30x30,BFS,2,0.997700,788,55
weighted_30x30,BFS,3,0.992700,788,55
weighted_30x30,BFS,4,1.010800,788,55
weighted_30x30,BFS,5,1.035000,788,55
weighted_30x30,DFS,1,1.130200,693,479
weighted_30x30,DFS,2,1.057400,693,479
weighted_30x30,DFS,3,1.049900,693,479
weighted_30x30,DFS,4,1.051600,693,479
weighted_30x30,DFS,5,1.059100,693,479
weighted_30x30,A*,1,0.402200,126,55
weighted_30x30,A*,2,0.384100,126,55
weighted_30x30,A*,3,0.360000,126,55
weighted_30x30,A*,4,0.360700,126,55
weighted_30x30,A*,5,0.353500,126,55
weighted_30x30,Dijkstra,1,1.834900,781,55
weighted_30x30,Dijkstra,2,1.759000,781,55
weighted_30x30,Dijkstra,3,1.786300,781,55
weighted_30x30,Dijkstra,4,1.740500,781,55
weighted_30x30,Dijkstra,5,1.807100,781,55
1 maze strategy run time_ms visited_cells path_length
2 small_10x10 BFS 1 0.044300 15 15
3 small_10x10 BFS 2 0.022800 15 15
4 small_10x10 BFS 3 0.020400 15 15
5 small_10x10 BFS 4 0.020300 15 15
6 small_10x10 BFS 5 0.018700 15 15
7 small_10x10 DFS 1 0.031200 15 15
8 small_10x10 DFS 2 0.022000 15 15
9 small_10x10 DFS 3 0.021200 15 15
10 small_10x10 DFS 4 0.020800 15 15
11 small_10x10 DFS 5 0.020500 15 15
12 small_10x10 A* 1 0.048900 15 15
13 small_10x10 A* 2 0.034700 15 15
14 small_10x10 A* 3 0.029400 15 15
15 small_10x10 A* 4 0.029100 15 15
16 small_10x10 A* 5 0.029300 15 15
17 small_10x10 Dijkstra 1 0.037900 15 15
18 small_10x10 Dijkstra 2 0.028500 15 15
19 small_10x10 Dijkstra 3 0.026800 15 15
20 small_10x10 Dijkstra 4 0.026400 15 15
21 small_10x10 Dijkstra 5 0.026700 15 15
22 medium_50x50 BFS 1 2.105800 1579 95
23 medium_50x50 BFS 2 1.928700 1579 95
24 medium_50x50 BFS 3 1.969500 1579 95
25 medium_50x50 BFS 4 1.938800 1579 95
26 medium_50x50 BFS 5 1.943600 1579 95
27 medium_50x50 DFS 1 1.927300 1277 647
28 medium_50x50 DFS 2 1.856300 1277 647
29 medium_50x50 DFS 3 1.890100 1277 647
30 medium_50x50 DFS 4 1.868000 1277 647
31 medium_50x50 DFS 5 1.865500 1277 647
32 medium_50x50 A* 1 2.359000 927 95
33 medium_50x50 A* 2 2.193700 927 95
34 medium_50x50 A* 3 2.178400 927 95
35 medium_50x50 A* 4 2.181800 927 95
36 medium_50x50 A* 5 2.174500 927 95
37 medium_50x50 Dijkstra 1 3.534700 1579 95
38 medium_50x50 Dijkstra 2 3.435500 1579 95
39 medium_50x50 Dijkstra 3 3.457600 1579 95
40 medium_50x50 Dijkstra 4 3.417300 1579 95
41 medium_50x50 Dijkstra 5 3.538000 1579 95
42 large_100x100 BFS 1 8.624100 5566 195
43 large_100x100 BFS 2 7.706900 5566 195
44 large_100x100 BFS 3 9.723300 5566 195
45 large_100x100 BFS 4 7.585700 5566 195
46 large_100x100 BFS 5 8.031300 5566 195
47 large_100x100 DFS 1 5.512400 3543 1531
48 large_100x100 DFS 2 5.329300 3543 1531
49 large_100x100 DFS 3 5.223300 3543 1531
50 large_100x100 DFS 4 5.729900 3543 1531
51 large_100x100 DFS 5 5.497400 3543 1531
52 large_100x100 A* 1 2.101500 853 195
53 large_100x100 A* 2 2.264500 853 195
54 large_100x100 A* 3 2.064100 853 195
55 large_100x100 A* 4 2.031700 853 195
56 large_100x100 A* 5 2.046500 853 195
57 large_100x100 Dijkstra 1 25.021300 5571 195
58 large_100x100 Dijkstra 2 13.541100 5571 195
59 large_100x100 Dijkstra 3 12.884100 5571 195
60 large_100x100 Dijkstra 4 13.481800 5571 195
61 large_100x100 Dijkstra 5 12.748000 5571 195
62 empty_30x30 BFS 1 1.234300 896 55
63 empty_30x30 BFS 2 1.163400 896 55
64 empty_30x30 BFS 3 1.145700 896 55
65 empty_30x30 BFS 4 1.177300 896 55
66 empty_30x30 BFS 5 1.175100 896 55
67 empty_30x30 DFS 1 1.338000 842 815
68 empty_30x30 DFS 2 1.296500 842 815
69 empty_30x30 DFS 3 1.296700 842 815
70 empty_30x30 DFS 4 1.280100 842 815
71 empty_30x30 DFS 5 1.290800 842 815
72 empty_30x30 A* 1 2.183400 784 55
73 empty_30x30 A* 2 2.522900 784 55
74 empty_30x30 A* 3 1.985000 784 55
75 empty_30x30 A* 4 1.972100 784 55
76 empty_30x30 A* 5 2.088600 784 55
77 empty_30x30 Dijkstra 1 2.080400 896 55
78 empty_30x30 Dijkstra 2 2.100100 896 55
79 empty_30x30 Dijkstra 3 2.130700 896 55
80 empty_30x30 Dijkstra 4 2.073600 896 55
81 empty_30x30 Dijkstra 5 2.095900 896 55
82 no_path_30x30 BFS 1 0.645900 450 0
83 no_path_30x30 BFS 2 0.566600 450 0
84 no_path_30x30 BFS 3 0.566000 450 0
85 no_path_30x30 BFS 4 0.583500 450 0
86 no_path_30x30 BFS 5 0.568900 450 0
87 no_path_30x30 DFS 1 0.692100 450 0
88 no_path_30x30 DFS 2 0.676900 450 0
89 no_path_30x30 DFS 3 0.703500 450 0
90 no_path_30x30 DFS 4 0.722300 450 0
91 no_path_30x30 DFS 5 0.672000 450 0
92 no_path_30x30 A* 1 1.112700 450 0
93 no_path_30x30 A* 2 1.130000 450 0
94 no_path_30x30 A* 3 1.096100 450 0
95 no_path_30x30 A* 4 1.111400 450 0
96 no_path_30x30 A* 5 1.183500 450 0
97 no_path_30x30 Dijkstra 1 1.023300 450 0
98 no_path_30x30 Dijkstra 2 1.011700 450 0
99 no_path_30x30 Dijkstra 3 1.127200 450 0
100 no_path_30x30 Dijkstra 4 1.110200 450 0
101 no_path_30x30 Dijkstra 5 1.043900 450 0
102 weighted_30x30 BFS 1 1.074700 788 55
103 weighted_30x30 BFS 2 0.997700 788 55
104 weighted_30x30 BFS 3 0.992700 788 55
105 weighted_30x30 BFS 4 1.010800 788 55
106 weighted_30x30 BFS 5 1.035000 788 55
107 weighted_30x30 DFS 1 1.130200 693 479
108 weighted_30x30 DFS 2 1.057400 693 479
109 weighted_30x30 DFS 3 1.049900 693 479
110 weighted_30x30 DFS 4 1.051600 693 479
111 weighted_30x30 DFS 5 1.059100 693 479
112 weighted_30x30 A* 1 0.402200 126 55
113 weighted_30x30 A* 2 0.384100 126 55
114 weighted_30x30 A* 3 0.360000 126 55
115 weighted_30x30 A* 4 0.360700 126 55
116 weighted_30x30 A* 5 0.353500 126 55
117 weighted_30x30 Dijkstra 1 1.834900 781 55
118 weighted_30x30 Dijkstra 2 1.759000 781 55
119 weighted_30x30 Dijkstra 3 1.786300 781 55
120 weighted_30x30 Dijkstra 4 1.740500 781 55
121 weighted_30x30 Dijkstra 5 1.807100 781 55

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

View File

@ -0,0 +1,25 @@
maze,strategy,avg_time_ms,avg_visited_cells,avg_path_length,runs
small_10x10,BFS,0.025300,15.00,15.00,5
small_10x10,DFS,0.023140,15.00,15.00,5
small_10x10,A*,0.034280,15.00,15.00,5
small_10x10,Dijkstra,0.029260,15.00,15.00,5
medium_50x50,BFS,1.977280,1579.00,95.00,5
medium_50x50,DFS,1.881440,1277.00,647.00,5
medium_50x50,A*,2.217480,927.00,95.00,5
medium_50x50,Dijkstra,3.476620,1579.00,95.00,5
large_100x100,BFS,8.334260,5566.00,195.00,5
large_100x100,DFS,5.458460,3543.00,1531.00,5
large_100x100,A*,2.101660,853.00,195.00,5
large_100x100,Dijkstra,15.535260,5571.00,195.00,5
empty_30x30,BFS,1.179160,896.00,55.00,5
empty_30x30,DFS,1.300420,842.00,815.00,5
empty_30x30,A*,2.150400,784.00,55.00,5
empty_30x30,Dijkstra,2.096140,896.00,55.00,5
no_path_30x30,BFS,0.586180,450.00,0.00,5
no_path_30x30,DFS,0.693360,450.00,0.00,5
no_path_30x30,A*,1.126740,450.00,0.00,5
no_path_30x30,Dijkstra,1.063260,450.00,0.00,5
weighted_30x30,BFS,1.022180,788.00,55.00,5
weighted_30x30,DFS,1.069640,693.00,479.00,5
weighted_30x30,A*,0.372100,126.00,55.00,5
weighted_30x30,Dijkstra,1.785560,781.00,55.00,5
1 maze strategy avg_time_ms avg_visited_cells avg_path_length runs
2 small_10x10 BFS 0.025300 15.00 15.00 5
3 small_10x10 DFS 0.023140 15.00 15.00 5
4 small_10x10 A* 0.034280 15.00 15.00 5
5 small_10x10 Dijkstra 0.029260 15.00 15.00 5
6 medium_50x50 BFS 1.977280 1579.00 95.00 5
7 medium_50x50 DFS 1.881440 1277.00 647.00 5
8 medium_50x50 A* 2.217480 927.00 95.00 5
9 medium_50x50 Dijkstra 3.476620 1579.00 95.00 5
10 large_100x100 BFS 8.334260 5566.00 195.00 5
11 large_100x100 DFS 5.458460 3543.00 1531.00 5
12 large_100x100 A* 2.101660 853.00 195.00 5
13 large_100x100 Dijkstra 15.535260 5571.00 195.00 5
14 empty_30x30 BFS 1.179160 896.00 55.00 5
15 empty_30x30 DFS 1.300420 842.00 815.00 5
16 empty_30x30 A* 2.150400 784.00 55.00 5
17 empty_30x30 Dijkstra 2.096140 896.00 55.00 5
18 no_path_30x30 BFS 0.586180 450.00 0.00 5
19 no_path_30x30 DFS 0.693360 450.00 0.00 5
20 no_path_30x30 A* 1.126740 450.00 0.00 5
21 no_path_30x30 Dijkstra 1.063260 450.00 0.00 5
22 weighted_30x30 BFS 1.022180 788.00 55.00 5
23 weighted_30x30 DFS 1.069640 693.00 479.00 5
24 weighted_30x30 A* 0.372100 126.00 55.00 5
25 weighted_30x30 Dijkstra 1.785560 781.00 55.00 5

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

View File

@ -1,172 +0,0 @@
from __future__ import annotations
import csv
import random
import time
from pathlib import Path
from typing import Dict, List, Tuple
from linked_list import ll_insert, ll_find, ll_delete
from hash_table import ht_insert, ht_find, ht_delete
from bst import bst_insert, bst_find, bst_delete
from utils import generate_records, prepare_records_variants
Record = Tuple[str, str]
def make_missing_names(count: int = 10) -> List[str]:
return [f"None_{i}" for i in range(count)]
def pick_existing_names(records: List[Record], count: int, seed: int = 42) -> List[str]:
rng = random.Random(seed)
unique_names = list(dict.fromkeys(name for name, _ in records))
if len(unique_names) < count:
raise ValueError(f"Not enough unique names: need {count}, got {len(unique_names)}")
return rng.sample(unique_names, count)
def pick_delete_names(records: List[Record], count: int = 50, seed: int = 43) -> List[str]:
rng = random.Random(seed)
unique_names = list(dict.fromkeys(name for name, _ in records))
if len(unique_names) < count:
raise ValueError(f"Not enough unique names: need {count}, got {len(unique_names)}")
return rng.sample(unique_names, count)
def build_structure(structure_name: str, records: List[Record], buckets_count: int = 2048):
if structure_name == "linked_list":
structure = None
for name, phone in records:
structure = ll_insert(structure, name, phone)
return structure
if structure_name == "hash_table":
buckets = [None] * buckets_count
for name, phone in records:
buckets = ht_insert(buckets, name, phone)
return buckets
if structure_name == "bst":
root = None
for name, phone in records:
root = bst_insert(root, name, phone)
return root
raise ValueError(f"Unknown structure: {structure_name}")
def do_find(structure_name: str, structure: object, existing_names: List[str], missing_names: List[str]) -> None:
if structure_name == "linked_list":
for name in existing_names:
ll_find(structure, name)
for name in missing_names:
ll_find(structure, name)
return
if structure_name == "hash_table":
for name in existing_names:
ht_find(structure, name)
for name in missing_names:
ht_find(structure, name)
return
if structure_name == "bst":
for name in existing_names:
bst_find(structure, name)
for name in missing_names:
bst_find(structure, name)
return
raise ValueError(f"Unknown structure: {structure_name}")
def do_delete(structure_name: str, structure: object, delete_names: List[str]):
if structure_name == "linked_list":
for name in delete_names:
structure = ll_delete(structure, name)
return structure
if structure_name == "hash_table":
for name in delete_names:
structure = ht_delete(structure, name)
return structure
if structure_name == "bst":
for name in delete_names:
structure = bst_delete(structure, name)
return structure
raise ValueError(f"Unknown structure: {structure_name}")
def measure_once(structure_name: str, records: List[Record], buckets_count: int = 2048) -> Dict[str, float]:
existing_names = pick_existing_names(records, 100, seed=42)
missing_names = make_missing_names(10)
delete_names = pick_delete_names(records, 50, seed=43)
start = time.perf_counter()
structure = build_structure(structure_name, records, buckets_count=buckets_count)
insert_time = time.perf_counter() - start
start = time.perf_counter()
do_find(structure_name, structure, existing_names, missing_names)
find_time = time.perf_counter() - start
start = time.perf_counter()
structure = do_delete(structure_name, structure, delete_names)
delete_time = time.perf_counter() - start
return {"insert": insert_time, "find": find_time, "delete": delete_time}
def run_experiments(n: int = 10000, buckets_count: int = 2048, repeats: int = 5):
records = generate_records(n, repeat_names=False)
records_shuffled, records_sorted = prepare_records_variants(records)
datasets = [
("случайный", records_shuffled),
("отсортированный", records_sorted),
]
structures = [
("LinkedList", "linked_list"),
("HashTable", "hash_table"),
("BST", "bst"),
]
operations = ("insert", "find", "delete")
rows = [["Структура", "Режим", "Операция", "Замер", "Время (сек)"]]
for mode_name, dataset_records in datasets:
for human_name, structure_name in structures:
times_by_op = {op: [] for op in operations}
for attempt in range(1, repeats + 1):
result = measure_once(structure_name, dataset_records, buckets_count=buckets_count)
for op_name in operations:
elapsed = result[op_name]
times_by_op[op_name].append(elapsed)
rows.append([human_name, mode_name, op_name, attempt, f"{elapsed:.10f}"])
for op_name in operations:
avg_time = sum(times_by_op[op_name]) / len(times_by_op[op_name])
rows.append([human_name, mode_name, op_name, "среднее", f"{avg_time:.10f}"])
return rows
def save_results_csv(rows, filename: str = "results.csv"):
with open(filename, "w", newline="", encoding="utf-8") as f:
writer = csv.writer(f)
writer.writerows(rows)
def main():
rows = run_experiments(n=10000, buckets_count=2048, repeats=5)
save_results_csv(rows, "results.csv")
print("Saved results.csv")
if __name__ == "__main__":
main()

View File

@ -1,44 +0,0 @@
from typing import Any, Dict, List, Optional
from linked_list import ll_insert, ll_find, ll_delete, ll_list_all
Bucket = Optional[Dict[str, Any]]
def _hash_name(name: str, buckets_count: int) -> int:
if buckets_count <= 0:
return 0
return sum(ord(ch) for ch in name) % buckets_count
def ht_insert(buckets: List[Bucket], name: str, phone: str) -> List[Bucket]:
if not buckets:
return buckets
index = _hash_name(name, len(buckets))
buckets[index] = ll_insert(buckets[index], name, phone)
return buckets
def ht_find(buckets: List[Bucket], name: str) -> Optional[str]:
if not buckets:
return None
index = _hash_name(name, len(buckets))
return ll_find(buckets[index], name)
def ht_delete(buckets: List[Bucket], name: str) -> List[Bucket]:
if not buckets:
return buckets
index = _hash_name(name, len(buckets))
buckets[index] = ll_delete(buckets[index], name)
return buckets
def ht_list_all(buckets: List[Bucket]) -> List[Dict[str, str]]:
records: List[Dict[str, str]] = []
for head in buckets:
records.extend(ll_list_all(head))
return sorted(records, key=lambda x: x["name"])

View File

@ -1,73 +0,0 @@
from typing import Any, Dict, List, Optional
Node = Dict[str, Any]
def _make_node(name: str, phone: str) -> Node:
return {"name": name, "phone": phone, "next": None}
def sort_records(records: List[Dict[str, str]]) -> List[Dict[str, str]]:
return sorted(records, key=lambda x: x["name"])
def ll_insert(head: Optional[Node], name: str, phone: str) -> Node:
new_node = _make_node(name, phone)
if head is None:
return new_node
current = head
while current is not None:
if current["name"] == name:
current["phone"] = phone
return head
if current["next"] is None:
current["next"] = new_node
return head
current = current["next"]
return head
def ll_find(head: Optional[Node], 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[Node], name: str) -> Optional[Node]:
if head is None:
return None
if head["name"] == name:
return head["next"]
prev = head
current = head["next"]
while current is not None:
if current["name"] == name:
prev["next"] = current["next"]
return head
prev = current
current = current["next"]
return head
def ll_list_all(head: Optional[Node]) -> List[Dict[str, str]]:
records: List[Dict[str, str]] = []
current = head
while current is not None:
records.append({"name": current["name"], "phone": current["phone"]})
current = current["next"]
return sort_records(records)

View File

@ -1,21 +1,59 @@
from builders.text_file_maze_builder import TextFileMazeBuilder
from core.player import Player
from observer.console_view import ConsoleView
from solver.maze_solver import MazeSolver
from strategies.astar_strategy import AStarStrategy
from strategies.bfs_strategy import BFSStrategy
from strategies.dfs_strategy import DFSStrategy
from controller.game_controller import GameController
from __future__ import annotations
import csv
from pathlib import Path
from experiments import run_experiments, save_results_csv
from plot_results import build_graphs, load_average_results
BASE_DIR = Path(__file__).resolve().parent
def main():
rows = run_experiments(n=10000, buckets_count=2048, repeats=5)
save_results_csv(rows, "results.csv")
averaged = load_average_results("results.csv")
build_graphs(averaged, output_dir="docs/data")
print("Done.")
def run_demo():
builder = TextFileMazeBuilder()
maze = builder.buildFromFile(str(BASE_DIR / "mazes" / "maze_small.txt"))
view = ConsoleView()
view.update({"type": "maze_loaded", "message": "Maze loaded"})
view.render(maze)
solver = MazeSolver(maze)
solver.addObserver(view)
for strategy in (BFSStrategy(), DFSStrategy(), AStarStrategy()):
solver.setStrategy(strategy)
stats = solver.solve()
print()
print(f"=== {strategy.name} ===")
print(f"Time: {stats.timeMs:.3f} ms")
print(f"Visited cells: {stats.visitedCells}")
print(f"Path length: {stats.pathLength}")
print(f"Path found: {'yes' if stats.found else 'no'}")
view.render(maze, path=stats.path)
player = Player(maze.startCell)
controller = GameController(maze, player, view)
print("Manual mode: W/A/S/D move, Z undo, Q quit")
view.render(maze, player_position=player.currentCell)
while True:
cmd = input("Command: ").strip().upper()
if cmd == "Q":
break
if cmd == "Z":
controller.undo()
elif cmd in {"W", "A", "S", "D"}:
controller.move(cmd)
else:
print("Unknown command")
if __name__ == "__main__":
main()
run_demo()

View File

@ -0,0 +1,9 @@
S
E

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,10 @@
1111111111111111111111111111
1S11111111111111111111111111
1111111111111111111111111111
1111111111111111111111111111
1111111111111222222222222111
1111111111111222222222222111
1111111111111333333333333111
1111111111111333333333333111
111111111111111111111111111E
1111111111111111111111111111

View File

View File

@ -0,0 +1,26 @@
import os
from observer.observer import Observer
class ConsoleView(Observer):
def update(self, event):
if isinstance(event, str):
print(f"[EVENT] {event}")
elif isinstance(event, dict):
event_type = event.get("type", "unknown")
if event_type == "search_finished":
stats = event.get("stats")
print(f"[EVENT] search finished: {stats}")
else:
print(f"[EVENT] {event_type}: {event}")
else:
print("[EVENT] unknown")
def clear(self):
os.system("cls" if os.name == "nt" else "clear")
def render(self, maze, player_position=None, path=None, clear_screen=False):
if clear_screen:
self.clear()
print(maze.render(player_position=player_position, path=path))
print()

View File

@ -0,0 +1,7 @@
from abc import ABC, abstractmethod
class Observer(ABC):
@abstractmethod
def update(self, event):
raise NotImplementedError

View File

@ -1,60 +0,0 @@
from __future__ import annotations
import csv
from collections import defaultdict
from pathlib import Path
import matplotlib.pyplot as plt
def load_average_results(csv_file: str):
results = []
with open(csv_file, "r", encoding="utf-8") as f:
reader = csv.DictReader(f)
for row in reader:
if row["Замер"] != "среднее":
continue
results.append({
"structure": row["Структура"],
"mode": row["Режим"],
"operation": row["Операция"],
"time": float(row["Время (сек)"]),
})
return results
def build_graphs(results, output_dir: str = "docs/data"):
output = Path(output_dir)
output.mkdir(parents=True, exist_ok=True)
grouped = defaultdict(list)
for row in results:
grouped[row["operation"]].append(row)
for operation in ("insert", "find", "delete"):
rows = grouped[operation]
labels = [f"{r['structure']}\n{r['mode']}" for r in rows]
values = [r["time"] for r in rows]
plt.figure(figsize=(11, 6))
plt.bar(labels, values)
plt.title(f"{operation.capitalize()} comparison")
plt.xlabel("Structure / data order")
plt.ylabel("Time, seconds")
plt.xticks(rotation=20)
plt.tight_layout()
filename = output / f"{operation}.png"
plt.savefig(filename, dpi=160)
plt.close()
print(f"Saved {filename}")
def main():
results = load_average_results("results.csv")
build_graphs(results)
if __name__ == "__main__":
main()

View File

@ -1 +1 @@
matplotlib>=3.8
matplotlib

View File

@ -1,109 +0,0 @@
Структура,Режим,Операция,Замер,Время (сек)
LinkedList,случайный,insert,1,2.4210275000
LinkedList,случайный,find,1,0.0214394000
LinkedList,случайный,delete,1,0.0108667000
LinkedList,случайный,insert,2,2.4208055000
LinkedList,случайный,find,2,0.0216110000
LinkedList,случайный,delete,2,0.0106216000
LinkedList,случайный,insert,3,2.4210881000
LinkedList,случайный,find,3,0.0216503000
LinkedList,случайный,delete,3,0.0106497000
LinkedList,случайный,insert,4,2.4530798000
LinkedList,случайный,find,4,0.0222764000
LinkedList,случайный,delete,4,0.0108350000
LinkedList,случайный,insert,5,2.4567773000
LinkedList,случайный,find,5,0.0219400000
LinkedList,случайный,delete,5,0.0108697000
LinkedList,случайный,insert,среднее,2.4345556400
LinkedList,случайный,find,среднее,0.0217834200
LinkedList,случайный,delete,среднее,0.0107685400
HashTable,случайный,insert,1,0.1621210000
HashTable,случайный,find,1,0.0011201000
HashTable,случайный,delete,1,0.0005854000
HashTable,случайный,insert,2,0.1732676000
HashTable,случайный,find,2,0.0011247000
HashTable,случайный,delete,2,0.0005818000
HashTable,случайный,insert,3,0.1638609000
HashTable,случайный,find,3,0.0011355000
HashTable,случайный,delete,3,0.0005814000
HashTable,случайный,insert,4,0.1642886000
HashTable,случайный,find,4,0.0011268000
HashTable,случайный,delete,4,0.0005785000
HashTable,случайный,insert,5,0.1640916000
HashTable,случайный,find,5,0.0011287000
HashTable,случайный,delete,5,0.0005787000
HashTable,случайный,insert,среднее,0.1655259400
HashTable,случайный,find,среднее,0.0011271600
HashTable,случайный,delete,среднее,0.0005811600
BST,случайный,insert,1,0.0153754000
BST,случайный,find,1,0.0001491000
BST,случайный,delete,1,0.0000786000
BST,случайный,insert,2,0.0155821000
BST,случайный,find,2,0.0001453000
BST,случайный,delete,2,0.0000724000
BST,случайный,insert,3,0.0151360000
BST,случайный,find,3,0.0001437000
BST,случайный,delete,3,0.0000741000
BST,случайный,insert,4,0.0153703000
BST,случайный,find,4,0.0001425000
BST,случайный,delete,4,0.0000715000
BST,случайный,insert,5,0.0153753000
BST,случайный,find,5,0.0001455000
BST,случайный,delete,5,0.0000723000
BST,случайный,insert,среднее,0.0153678200
BST,случайный,find,среднее,0.0001452200
BST,случайный,delete,среднее,0.0000737800
LinkedList,отсортированный,insert,1,2.5884851000
LinkedList,отсортированный,find,1,0.0227221000
LinkedList,отсортированный,delete,1,0.0111309000
LinkedList,отсортированный,insert,2,2.5095731000
LinkedList,отсортированный,find,2,0.0217208000
LinkedList,отсортированный,delete,2,0.0107773000
LinkedList,отсортированный,insert,3,2.5642096000
LinkedList,отсортированный,find,3,0.0228242000
LinkedList,отсортированный,delete,3,0.0115945000
LinkedList,отсортированный,insert,4,2.7163021000
LinkedList,отсортированный,find,4,0.0431456000
LinkedList,отсортированный,delete,4,0.0136020000
LinkedList,отсортированный,insert,5,2.6891794000
LinkedList,отсортированный,find,5,0.0217679000
LinkedList,отсортированный,delete,5,0.0106384000
LinkedList,отсортированный,insert,среднее,2.6135498600
LinkedList,отсортированный,find,среднее,0.0264361200
LinkedList,отсортированный,delete,среднее,0.0115486200
HashTable,отсортированный,insert,1,0.1524640000
HashTable,отсортированный,find,1,0.0014973000
HashTable,отсортированный,delete,1,0.0006991000
HashTable,отсортированный,insert,2,0.1537592000
HashTable,отсортированный,find,2,0.0012225000
HashTable,отсортированный,delete,2,0.0006561000
HashTable,отсортированный,insert,3,0.1555816000
HashTable,отсортированный,find,3,0.0012080000
HashTable,отсортированный,delete,3,0.0006472000
HashTable,отсортированный,insert,4,0.1546417000
HashTable,отсортированный,find,4,0.0015017000
HashTable,отсортированный,delete,4,0.0007512000
HashTable,отсортированный,insert,5,0.1531659000
HashTable,отсортированный,find,5,0.0012219000
HashTable,отсортированный,delete,5,0.0006493000
HashTable,отсортированный,insert,среднее,0.1539224800
HashTable,отсортированный,find,среднее,0.0013302800
HashTable,отсортированный,delete,среднее,0.0006805800
BST,отсортированный,insert,1,4.5025059000
BST,отсортированный,find,1,0.0387267000
BST,отсортированный,delete,1,0.0162161000
BST,отсортированный,insert,2,4.6704081000
BST,отсортированный,find,2,0.0435012000
BST,отсортированный,delete,2,0.0203211000
BST,отсортированный,insert,3,6.2192950000
BST,отсортированный,find,3,0.0578654000
BST,отсортированный,delete,3,0.0327529000
BST,отсортированный,insert,4,4.7844525000
BST,отсортированный,find,4,0.0380228000
BST,отсортированный,delete,4,0.0159740000
BST,отсортированный,insert,5,4.4861403000
BST,отсортированный,find,5,0.0382484000
BST,отсортированный,delete,5,0.0159402000
BST,отсортированный,insert,среднее,4.9325603600
BST,отсортированный,find,среднее,0.0432729000
BST,отсортированный,delete,среднее,0.0202408600
1 Структура Режим Операция Замер Время (сек)
2 LinkedList случайный insert 1 2.4210275000
3 LinkedList случайный find 1 0.0214394000
4 LinkedList случайный delete 1 0.0108667000
5 LinkedList случайный insert 2 2.4208055000
6 LinkedList случайный find 2 0.0216110000
7 LinkedList случайный delete 2 0.0106216000
8 LinkedList случайный insert 3 2.4210881000
9 LinkedList случайный find 3 0.0216503000
10 LinkedList случайный delete 3 0.0106497000
11 LinkedList случайный insert 4 2.4530798000
12 LinkedList случайный find 4 0.0222764000
13 LinkedList случайный delete 4 0.0108350000
14 LinkedList случайный insert 5 2.4567773000
15 LinkedList случайный find 5 0.0219400000
16 LinkedList случайный delete 5 0.0108697000
17 LinkedList случайный insert среднее 2.4345556400
18 LinkedList случайный find среднее 0.0217834200
19 LinkedList случайный delete среднее 0.0107685400
20 HashTable случайный insert 1 0.1621210000
21 HashTable случайный find 1 0.0011201000
22 HashTable случайный delete 1 0.0005854000
23 HashTable случайный insert 2 0.1732676000
24 HashTable случайный find 2 0.0011247000
25 HashTable случайный delete 2 0.0005818000
26 HashTable случайный insert 3 0.1638609000
27 HashTable случайный find 3 0.0011355000
28 HashTable случайный delete 3 0.0005814000
29 HashTable случайный insert 4 0.1642886000
30 HashTable случайный find 4 0.0011268000
31 HashTable случайный delete 4 0.0005785000
32 HashTable случайный insert 5 0.1640916000
33 HashTable случайный find 5 0.0011287000
34 HashTable случайный delete 5 0.0005787000
35 HashTable случайный insert среднее 0.1655259400
36 HashTable случайный find среднее 0.0011271600
37 HashTable случайный delete среднее 0.0005811600
38 BST случайный insert 1 0.0153754000
39 BST случайный find 1 0.0001491000
40 BST случайный delete 1 0.0000786000
41 BST случайный insert 2 0.0155821000
42 BST случайный find 2 0.0001453000
43 BST случайный delete 2 0.0000724000
44 BST случайный insert 3 0.0151360000
45 BST случайный find 3 0.0001437000
46 BST случайный delete 3 0.0000741000
47 BST случайный insert 4 0.0153703000
48 BST случайный find 4 0.0001425000
49 BST случайный delete 4 0.0000715000
50 BST случайный insert 5 0.0153753000
51 BST случайный find 5 0.0001455000
52 BST случайный delete 5 0.0000723000
53 BST случайный insert среднее 0.0153678200
54 BST случайный find среднее 0.0001452200
55 BST случайный delete среднее 0.0000737800
56 LinkedList отсортированный insert 1 2.5884851000
57 LinkedList отсортированный find 1 0.0227221000
58 LinkedList отсортированный delete 1 0.0111309000
59 LinkedList отсортированный insert 2 2.5095731000
60 LinkedList отсортированный find 2 0.0217208000
61 LinkedList отсортированный delete 2 0.0107773000
62 LinkedList отсортированный insert 3 2.5642096000
63 LinkedList отсортированный find 3 0.0228242000
64 LinkedList отсортированный delete 3 0.0115945000
65 LinkedList отсортированный insert 4 2.7163021000
66 LinkedList отсортированный find 4 0.0431456000
67 LinkedList отсортированный delete 4 0.0136020000
68 LinkedList отсортированный insert 5 2.6891794000
69 LinkedList отсортированный find 5 0.0217679000
70 LinkedList отсортированный delete 5 0.0106384000
71 LinkedList отсортированный insert среднее 2.6135498600
72 LinkedList отсортированный find среднее 0.0264361200
73 LinkedList отсортированный delete среднее 0.0115486200
74 HashTable отсортированный insert 1 0.1524640000
75 HashTable отсортированный find 1 0.0014973000
76 HashTable отсортированный delete 1 0.0006991000
77 HashTable отсортированный insert 2 0.1537592000
78 HashTable отсортированный find 2 0.0012225000
79 HashTable отсортированный delete 2 0.0006561000
80 HashTable отсортированный insert 3 0.1555816000
81 HashTable отсортированный find 3 0.0012080000
82 HashTable отсортированный delete 3 0.0006472000
83 HashTable отсортированный insert 4 0.1546417000
84 HashTable отсортированный find 4 0.0015017000
85 HashTable отсортированный delete 4 0.0007512000
86 HashTable отсортированный insert 5 0.1531659000
87 HashTable отсортированный find 5 0.0012219000
88 HashTable отсортированный delete 5 0.0006493000
89 HashTable отсортированный insert среднее 0.1539224800
90 HashTable отсортированный find среднее 0.0013302800
91 HashTable отсортированный delete среднее 0.0006805800
92 BST отсортированный insert 1 4.5025059000
93 BST отсортированный find 1 0.0387267000
94 BST отсортированный delete 1 0.0162161000
95 BST отсортированный insert 2 4.6704081000
96 BST отсортированный find 2 0.0435012000
97 BST отсортированный delete 2 0.0203211000
98 BST отсортированный insert 3 6.2192950000
99 BST отсортированный find 3 0.0578654000
100 BST отсортированный delete 3 0.0327529000
101 BST отсортированный insert 4 4.7844525000
102 BST отсортированный find 4 0.0380228000
103 BST отсортированный delete 4 0.0159740000
104 BST отсортированный insert 5 4.4861403000
105 BST отсортированный find 5 0.0382484000
106 BST отсортированный delete 5 0.0159402000
107 BST отсортированный insert среднее 4.9325603600
108 BST отсортированный find среднее 0.0432729000
109 BST отсортированный delete среднее 0.0202408600

View File

View File

@ -0,0 +1,50 @@
import time
from core.search_stats import SearchStats
class MazeSolver:
def __init__(self, maze, strategy=None):
self.maze = maze
self.strategy = strategy
self.observers = []
def setStrategy(self, strategy):
self.strategy = strategy
def addObserver(self, observer):
if observer not in self.observers:
self.observers.append(observer)
def removeObserver(self, observer):
if observer in self.observers:
self.observers.remove(observer)
def notify(self, event):
for observer in self.observers:
observer.update(event)
def solve(self):
if self.strategy is None:
raise ValueError("Strategy is not set")
self.notify({"type": "search_started", "strategy": self.strategy.name})
start_time = time.perf_counter()
path = self.strategy.findPath(self.maze, self.maze.startCell, self.maze.exitCell)
end_time = time.perf_counter()
stats = SearchStats(
timeMs=(end_time - start_time) * 1000.0,
visitedCells=getattr(self.strategy, "visitedCount", 0),
pathLength=len(path),
path=path,
found=bool(path),
algorithm=getattr(self.strategy, "name", "")
)
if stats.found:
self.notify({"type": "path_found", "strategy": stats.algorithm, "length": stats.pathLength})
else:
self.notify({"type": "path_not_found", "strategy": stats.algorithm})
self.notify({"type": "search_finished", "stats": stats})
return stats

View File

View File

@ -0,0 +1,45 @@
import heapq
from strategies.pathfinding_strategy import PathFindingStrategy
class AStarStrategy(PathFindingStrategy):
name = "A*"
def heuristic(self, cell, exitCell):
return abs(cell.x - exitCell.x) + abs(cell.y - exitCell.y)
def findPath(self, maze, start, exitCell):
self.visitedCount = 0
if start is None or exitCell is None:
return []
open_set = []
heapq.heappush(open_set, (0, 0, start.x, start.y, start))
parent = {}
g_score = {(start.x, start.y): 0}
closed = set()
while open_set:
f_score, current_g, _, _, current = heapq.heappop(open_set)
pos = (current.x, current.y)
if pos in closed:
continue
closed.add(pos)
self.visitedCount += 1
if current.x == exitCell.x and current.y == exitCell.y:
return self._restore_path(parent, start, exitCell)
for neighbor in maze.getNeighbors(current):
npos = (neighbor.x, neighbor.y)
tentative_g = current_g + getattr(neighbor, "weight", 1)
if tentative_g < g_score.get(npos, float("inf")):
g_score[npos] = tentative_g
parent[npos] = current
new_f = tentative_g + self.heuristic(neighbor, exitCell)
heapq.heappush(open_set, (new_f, tentative_g, neighbor.x, neighbor.y, neighbor))
return []

View File

@ -0,0 +1,31 @@
from collections import deque
from strategies.pathfinding_strategy import PathFindingStrategy
class BFSStrategy(PathFindingStrategy):
name = "BFS"
def findPath(self, maze, start, exitCell):
self.visitedCount = 0
if start is None or exitCell is None:
return []
queue = deque([start])
visited = {(start.x, start.y)}
parent = {}
while queue:
current = queue.popleft()
self.visitedCount += 1
if current.x == exitCell.x and current.y == exitCell.y:
return self._restore_path(parent, start, exitCell)
for neighbor in maze.getNeighbors(current):
pos = (neighbor.x, neighbor.y)
if pos not in visited:
visited.add(pos)
parent[pos] = current
queue.append(neighbor)
return []

View File

@ -0,0 +1,35 @@
from strategies.pathfinding_strategy import PathFindingStrategy
class DFSStrategy(PathFindingStrategy):
name = "DFS"
def findPath(self, maze, start, exitCell):
self.visitedCount = 0
if start is None or exitCell is None:
return []
stack = [start]
visited = set()
parent = {}
while stack:
current = stack.pop()
pos = (current.x, current.y)
if pos in visited:
continue
visited.add(pos)
self.visitedCount += 1
if current.x == exitCell.x and current.y == exitCell.y:
return self._restore_path(parent, start, exitCell)
neighbors = maze.getNeighbors(current)
for neighbor in reversed(neighbors):
npos = (neighbor.x, neighbor.y)
if npos not in visited:
parent[npos] = current
stack.append(neighbor)
return []

View File

@ -0,0 +1,41 @@
import heapq
from strategies.pathfinding_strategy import PathFindingStrategy
class DijkstraStrategy(PathFindingStrategy):
name = "Dijkstra"
def findPath(self, maze, start, exitCell):
self.visitedCount = 0
if start is None or exitCell is None:
return []
pq = [(0, start.x, start.y, start)]
dist = {(start.x, start.y): 0}
parent = {}
closed = set()
while pq:
current_cost, _, _, current = heapq.heappop(pq)
pos = (current.x, current.y)
if pos in closed:
continue
closed.add(pos)
self.visitedCount += 1
if current.x == exitCell.x and current.y == exitCell.y:
return self._restore_path(parent, start, exitCell)
for neighbor in maze.getNeighbors(current):
npos = (neighbor.x, neighbor.y)
step_cost = getattr(neighbor, "weight", 1)
new_cost = current_cost + step_cost
if new_cost < dist.get(npos, float("inf")):
dist[npos] = new_cost
parent[npos] = current
heapq.heappush(pq, (new_cost, neighbor.x, neighbor.y, neighbor))
return []

View File

@ -0,0 +1,30 @@
from abc import ABC, abstractmethod
class PathFindingStrategy(ABC):
name = "Base"
def __init__(self):
self.visitedCount = 0
@abstractmethod
def findPath(self, maze, start, exitCell):
raise NotImplementedError
def _restore_path(self, parent, start, exitCell):
if exitCell is None or start is None:
return []
path = []
current = exitCell
while True:
path.append(current)
if current.x == start.x and current.y == start.y:
break
current = parent.get((current.x, current.y))
if current is None:
return []
path.reverse()
return path

View File

@ -1,35 +0,0 @@
import random
from typing import List, Tuple
Record = Tuple[str, str]
def generate_records(n: int, repeat_names: bool = False, seed: int = 42) -> List[Record]:
rng = random.Random(seed)
records: List[Record] = []
if repeat_names:
name_pool = [
"User_Alex", "User_Bob", "User_Cat", "User_Dan", "User_Eva",
"User_Fox", "User_Geo", "User_Hen", "User_Ira", "User_Leo",
]
for _ in range(n):
name = rng.choice(name_pool)
phone = f"{rng.randint(1000000000, 9999999999)}"
records.append((name, phone))
else:
for i in range(n):
name = f"User_{i:05d}"
phone = f"{1000000000 + i}"
records.append((name, phone))
return records
def prepare_records_variants(records: List[Record], seed: int = 42):
rng = random.Random(seed)
records_shuffled = list(records)
rng.shuffle(records_shuffled)
records_sorted = sorted(records, key=lambda x: x[0])
return records_shuffled, records_sorted