Merge pull request '[1]SokolovEN' (#278) from SokolovEN/2026-rff_mp:SokolovEN into develop

Reviewed-on: UNN/2026-rff_mp#278
This commit is contained in:
AlexanderVah 2026-05-30 11:45:56 +00:00
commit 372cbe7db4
13 changed files with 1697 additions and 0 deletions

View File

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

View File

@ -0,0 +1,13 @@
maze,strategy,time_ms,visited_cells,path_length,success_rate
Small (10x10),BFS,0.052460000733844936,30.0,14.0,1.0
Small (10x10),DFS,0.0480999966384843,32.0,14.0,1.0
Small (10x10),A*,0.07206000154837966,23.0,14.0,1.0
Medium (50x50),BFS,0.2786600001854822,182.0,92.0,1.0
Medium (50x50),DFS,0.14713999989908189,93.0,92.0,1.0
Medium (50x50),A*,0.5699400004232302,182.0,92.0,1.0
Large (100x100),BFS,0.39185999776236713,201.0,149.0,1.0
Large (100x100),DFS,0.2371800015680492,151.0,149.0,1.0
Large (100x100),A*,0.5810399976326153,200.0,149.0,1.0
Empty,BFS,3.187239999533631,1834.0,86.0,1.0
Empty,DFS,1.9440599950030446,1797.0,922.0,1.0
Empty,A*,6.751939994865097,1834.0,86.0,1.0
1 maze strategy time_ms visited_cells path_length success_rate
2 Small (10x10) BFS 0.052460000733844936 30.0 14.0 1.0
3 Small (10x10) DFS 0.0480999966384843 32.0 14.0 1.0
4 Small (10x10) A* 0.07206000154837966 23.0 14.0 1.0
5 Medium (50x50) BFS 0.2786600001854822 182.0 92.0 1.0
6 Medium (50x50) DFS 0.14713999989908189 93.0 92.0 1.0
7 Medium (50x50) A* 0.5699400004232302 182.0 92.0 1.0
8 Large (100x100) BFS 0.39185999776236713 201.0 149.0 1.0
9 Large (100x100) DFS 0.2371800015680492 151.0 149.0 1.0
10 Large (100x100) A* 0.5810399976326153 200.0 149.0 1.0
11 Empty BFS 3.187239999533631 1834.0 86.0 1.0
12 Empty DFS 1.9440599950030446 1797.0 922.0 1.0
13 Empty A* 6.751939994865097 1834.0 86.0 1.0

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

View File

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

532
SokolovEN/docs/data/maze.py Normal file
View File

@ -0,0 +1,532 @@
import sys
from collections import deque
import heapq
import time
import os
from abc import ABC, abstractmethod
from typing import List, Optional, Dict, Any
DATA_PATH = r"C:\Users\user\Desktop\2026-rff_mp\SokolovEN\docs\data"
class Observer(ABC):
@abstractmethod
def update(self, event: str, data: Any = None):
pass
class Observable:
def __init__(self):
self._observers: List[Observer] = []
def attach(self, observer: Observer):
self._observers.append(observer)
def detach(self, observer: Observer):
self._observers.remove(observer)
def notify(self, event: str, data: Any = None):
for observer in self._observers:
observer.update(event, data)
class Tile:
def __init__(self, x: int, y: int):
self._x = x
self._y = y
self._wall = False
self._start = False
self._exit = False
@property
def x(self) -> int:
return self._x
@property
def y(self) -> int:
return self._y
@property
def is_wall(self) -> bool:
return self._wall
@is_wall.setter
def is_wall(self, v: bool):
self._wall = v
@property
def is_start(self) -> bool:
return self._start
@is_start.setter
def is_start(self, v: bool):
self._start = v
@property
def is_exit(self) -> bool:
return self._exit
@is_exit.setter
def is_exit(self, v: bool):
self._exit = v
def passable(self) -> bool:
return not self._wall
def __hash__(self):
return hash((self._x, self._y))
def __eq__(self, other):
if not isinstance(other, Tile):
return False
return self._x == other._x and self._y == other._y
class Maze:
def __init__(self, w: int, h: int):
self._w = w
self._h = h
self._cells = [[Tile(x, y) for x in range(w)] for y in range(h)]
self._start: Optional[Tile] = None
self._exit: Optional[Tile] = None
@property
def width(self) -> int:
return self._w
@property
def height(self) -> int:
return self._h
@property
def start(self) -> Optional[Tile]:
return self._start
@property
def exit(self) -> Optional[Tile]:
return self._exit
def get_cell(self, x: int, y: int) -> Optional[Tile]:
if 0 <= x < self._w and 0 <= y < self._h:
return self._cells[y][x]
return None
def set_cell(self, x: int, y: int, kind: str):
c = self.get_cell(x, y)
if not c:
return
if kind == 'wall':
c.is_wall = True
elif kind == 'start':
if self._start:
self._start.is_start = False
c.is_start = True
c.is_wall = False
self._start = c
elif kind == 'exit':
if self._exit:
self._exit.is_exit = False
c.is_exit = True
c.is_wall = False
self._exit = c
elif kind == 'path':
c.is_wall = False
def neighbours(self, cell: Tile) -> List[Tile]:
result = []
for dx, dy in [(0, -1), (0, 1), (-1, 0), (1, 0)]:
nx, ny = cell.x + dx, cell.y + dy
nb = self.get_cell(nx, ny)
if nb and nb.passable():
result.append(nb)
return result
class MazeLoader(ABC):
@abstractmethod
def load(self, filename: str) -> Maze:
pass
class TextMazeLoader(MazeLoader):
def load(self, filename: str) -> Maze:
with open(filename, 'r', encoding='utf-8') as f:
lines = [line.rstrip('\n') for line in f.readlines()]
h = len(lines)
w = max(len(line) for line in lines) if h else 0
start_count = 0
exit_count = 0
maze = Maze(w, h)
for y, line in enumerate(lines):
for x, ch in enumerate(line):
if ch == '#':
maze.set_cell(x, y, 'wall')
elif ch == 'S':
maze.set_cell(x, y, 'start')
start_count += 1
elif ch == 'E':
maze.set_cell(x, y, 'exit')
exit_count += 1
else:
maze.set_cell(x, y, 'path')
if start_count != 1 or exit_count != 1:
raise ValueError(f"Maze must have one S and one E. Found: S={start_count}, E={exit_count}")
return maze
class PathFinder(ABC):
def __init__(self):
self._visited = 0
@abstractmethod
def find(self, maze: Maze, start: Tile, goal: Tile) -> List[Tile]:
pass
def _reconstruct(self, parent: Dict[Tile, Optional[Tile]], start: Tile, goal: Tile) -> List[Tile]:
path = []
current = goal
while current is not None:
path.append(current)
current = parent.get(current)
path.reverse()
return path if path and path[0] == start else []
@property
def visited_count(self) -> int:
return self._visited
class BFS(PathFinder):
def find(self, maze: Maze, start: Tile, goal: Tile) -> List[Tile]:
queue = deque([start])
parent = {start: None}
visited = {start}
while queue:
current = queue.popleft()
if current == goal:
self._visited = len(visited)
return self._reconstruct(parent, start, goal)
for neighbor in maze.neighbours(current):
if neighbor not in visited:
visited.add(neighbor)
parent[neighbor] = current
queue.append(neighbor)
self._visited = len(visited)
return []
class DFS(PathFinder):
def find(self, maze: Maze, start: Tile, goal: Tile) -> List[Tile]:
stack = [start]
parent = {start: None}
visited = {start}
while stack:
current = stack.pop()
if current == goal:
self._visited = len(visited)
return self._reconstruct(parent, start, goal)
for neighbor in maze.neighbours(current):
if neighbor not in visited:
visited.add(neighbor)
parent[neighbor] = current
stack.append(neighbor)
self._visited = len(visited)
return []
class AStar(PathFinder):
def _heuristic(self, cell: Tile, goal: Tile) -> int:
return abs(cell.x - goal.x) + abs(cell.y - goal.y)
def find(self, maze: Maze, start: Tile, goal: Tile) -> List[Tile]:
heap = []
counter = 0
start_f = self._heuristic(start, goal)
heapq.heappush(heap, (start_f, counter, start))
counter += 1
parent = {}
g_score = {start: 0}
f_score = {start: start_f}
visited = set()
while heap:
current_f, _, current = heapq.heappop(heap)
visited.add(current)
if current == goal:
self._visited = len(visited)
return self._reconstruct(parent, start, goal)
if current_f > f_score.get(current, float('inf')):
continue
for neighbor in maze.neighbours(current):
tentative_g = g_score[current] + 1
if tentative_g < g_score.get(neighbor, float('inf')):
parent[neighbor] = current
g_score[neighbor] = tentative_g
new_f = tentative_g + self._heuristic(neighbor, goal)
f_score[neighbor] = new_f
heapq.heappush(heap, (new_f, counter, neighbor))
counter += 1
self._visited = len(visited)
return []
class MazeSolver(Observable):
def __init__(self, maze: Maze):
super().__init__()
self._maze = maze
self._algorithm: Optional[PathFinder] = None
def set_algorithm(self, algorithm: PathFinder):
self._algorithm = algorithm
def solve(self) -> Optional[Dict[str, Any]]:
if not self._algorithm:
raise ValueError("Algorithm not set")
start_time = time.perf_counter()
path = self._algorithm.find(self._maze, self._maze.start, self._maze.exit)
end_time = time.perf_counter()
elapsed_ms = (end_time - start_time) * 1000
return {
'time_ms': elapsed_ms,
'visited': self._algorithm.visited_count,
'path_length': len(path),
'path': path
}
class Command(ABC):
@abstractmethod
def execute(self) -> bool:
pass
@abstractmethod
def undo(self) -> bool:
pass
class MoveCommand(Command):
def __init__(self, player: 'Player', dx: int, dy: int, maze: Maze):
self._player = player
self._dx = dx
self._dy = dy
self._maze = maze
self._executed = False
def execute(self) -> bool:
new_x = self._player.position.x + self._dx
new_y = self._player.position.y + self._dy
target = self._maze.get_cell(new_x, new_y)
if target and target.passable():
self._player.move_to(target)
self._executed = True
return True
return False
def undo(self) -> bool:
if self._executed:
self._player.undo()
self._executed = False
return True
return False
class Player:
def __init__(self, start_tile: Tile):
self._position = start_tile
self._previous = None
@property
def position(self) -> Tile:
return self._position
def move_to(self, tile: Tile):
self._previous = self._position
self._position = tile
def undo(self):
if self._previous:
self._position, self._previous = self._previous, None
class ConsoleView(Observer):
def __init__(self, maze: Maze, player: Optional[Player] = None):
self._maze = maze
self._player = player
self._current_path: List[Tile] = []
def update(self, event: str, data: Any = None):
if event == "solving_finished":
self._current_path = data.get('path', [])
self._display_solution(data)
def _display_solution(self, stats: Dict):
os.system('cls' if os.name == 'nt' else 'clear')
print("=" * (self._maze.width * 2 + 4))
print("MAZE SOLUTION")
print("=" * (self._maze.width * 2 + 4))
for y in range(self._maze.height):
print(" ", end='')
for x in range(self._maze.width):
cell = self._maze.get_cell(x, y)
if cell == self._maze.start:
print('S', end=' ')
elif cell == self._maze.exit:
print('E', end=' ')
elif cell.is_wall:
print('#', end=' ')
elif self._current_path and cell in self._current_path:
print('', end=' ')
else:
print('.', end=' ')
print()
print("=" * (self._maze.width * 2 + 4))
print(f"Time: {stats['time_ms']:.3f} ms")
print(f"Visited: {stats['visited']}")
print(f"Path length: {stats['path_length']}")
def display_maze(self):
os.system('cls' if os.name == 'nt' else 'clear')
print("=" * (self._maze.width * 2 + 4))
print("MAZE")
print("=" * (self._maze.width * 2 + 4))
for y in range(self._maze.height):
print(" ", end='')
for x in range(self._maze.width):
cell = self._maze.get_cell(x, y)
if self._player and cell == self._player.position:
print('P', end=' ')
elif cell == self._maze.start:
print('S', end=' ')
elif cell == self._maze.exit:
print('E', end=' ')
elif cell.is_wall:
print('#', end=' ')
else:
print('.', end=' ')
print()
print("=" * (self._maze.width * 2 + 4))
print("S - start E - exit # - wall . - path P - player")
def interactive_mode(maze: Maze):
player = Player(maze.start)
view = ConsoleView(maze, player)
view.display_maze()
solver = MazeSolver(maze)
solver.attach(view)
commands_history: List[Command] = []
print("\nControls:")
print("H (←) J (↓) K (↑) L (→) - move")
print("U - undo")
print("B - BFS")
print("D - DFS")
print("A - A*")
print("Q - quit")
print("\n" + "=" * 50)
while True:
cmd = input("\n> ").lower().strip()
if cmd == 'q':
break
elif cmd == 'b':
solver.set_algorithm(BFS())
result = solver.solve()
if result:
print(f"BFS: {result['time_ms']:.3f} ms, visited={result['visited']}, length={result['path_length']}")
elif cmd == 'd':
solver.set_algorithm(DFS())
result = solver.solve()
if result:
print(f"DFS: {result['time_ms']:.3f} ms, visited={result['visited']}, length={result['path_length']}")
elif cmd == 'a':
solver.set_algorithm(AStar())
result = solver.solve()
if result:
print(f"A*: {result['time_ms']:.3f} ms, visited={result['visited']}, length={result['path_length']}")
elif cmd in ['h', 'j', 'k', 'l']:
dir_map = {'h': (-1, 0), 'l': (1, 0), 'k': (0, -1), 'j': (0, 1)}
dx, dy = dir_map[cmd]
move = MoveCommand(player, dx, dy, maze)
if move.execute():
commands_history.append(move)
view.display_maze()
if player.position == maze.exit:
print("\n*** YOU ESCAPED! ***")
print(f"Total moves: {len(commands_history)}")
break
else:
print("Blocked!")
elif cmd == 'u':
if commands_history:
last_command = commands_history.pop()
last_command.undo()
view.display_maze()
print("Undo successful")
else:
print("Nothing to undo")
else:
print("Unknown command")
def main():
if len(sys.argv) > 1 and sys.argv[1] == 'experiment':
import subprocess
subprocess.run([sys.executable, 'plots.py'])
return
loader = TextMazeLoader()
maze_file = os.path.join(DATA_PATH, "maze1.txt")
if not os.path.exists(maze_file):
print(f"ERROR: Maze file not found: {maze_file}")
print(f"Please create maze1.txt in: {DATA_PATH}")
return
maze = loader.load(maze_file)
interactive_mode(maze)
if __name__ == "__main__":
main()

View File

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

View File

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

View File

@ -0,0 +1,580 @@
import csv
import time
import os
import matplotlib.pyplot as plt
import numpy as np
from collections import deque
import heapq
from maze import DATA_PATH
class Tile:
def __init__(self, x: int, y: int):
self._x = x
self._y = y
self._wall = False
self._start = False
self._exit = False
@property
def x(self) -> int:
return self._x
@property
def y(self) -> int:
return self._y
@property
def is_wall(self) -> bool:
return self._wall
@is_wall.setter
def is_wall(self, v: bool):
self._wall = v
@property
def is_start(self) -> bool:
return self._start
@is_start.setter
def is_start(self, v: bool):
self._start = v
@property
def is_exit(self) -> bool:
return self._exit
@is_exit.setter
def is_exit(self, v: bool):
self._exit = v
def passable(self) -> bool:
return not self._wall
def __hash__(self):
return hash((self._x, self._y))
def __eq__(self, other):
if not isinstance(other, Tile):
return False
return self._x == other._x and self._y == other._y
class Maze:
def __init__(self, w: int, h: int):
self._w = w
self._h = h
self._cells = [[Tile(x, y) for x in range(w)] for y in range(h)]
self._start = None
self._exit = None
@property
def width(self) -> int:
return self._w
@property
def height(self) -> int:
return self._h
@property
def start(self):
return self._start
@property
def exit(self):
return self._exit
def get_cell(self, x: int, y: int):
if 0 <= x < self._w and 0 <= y < self._h:
return self._cells[y][x]
return None
def set_cell(self, x: int, y: int, kind: str):
c = self.get_cell(x, y)
if not c:
return
if kind == 'wall':
c.is_wall = True
elif kind == 'start':
if self._start:
self._start.is_start = False
c.is_start = True
c.is_wall = False
self._start = c
elif kind == 'exit':
if self._exit:
self._exit.is_exit = False
c.is_exit = True
c.is_wall = False
self._exit = c
elif kind == 'path':
c.is_wall = False
def neighbours(self, cell):
result = []
for dx, dy in [(0, -1), (0, 1), (-1, 0), (1, 0)]:
nx, ny = cell.x + dx, cell.y + dy
nb = self.get_cell(nx, ny)
if nb and nb.passable():
result.append(nb)
return result
class TextMazeLoader:
def load(self, filename: str):
with open(filename, 'r', encoding='utf-8') as f:
lines = [line.rstrip('\n') for line in f.readlines()]
h = len(lines)
w = max(len(line) for line in lines) if h else 0
start_count = 0
exit_count = 0
maze = Maze(w, h)
for y, line in enumerate(lines):
for x, ch in enumerate(line):
if ch == '#':
maze.set_cell(x, y, 'wall')
elif ch == 'S':
maze.set_cell(x, y, 'start')
start_count += 1
elif ch == 'E':
maze.set_cell(x, y, 'exit')
exit_count += 1
else:
maze.set_cell(x, y, 'path')
if start_count != 1 or exit_count != 1:
raise ValueError(f"Maze must have one S and one E. Found: S={start_count}, E={exit_count}")
return maze
class BFS:
def __init__(self):
self._visited = 0
def find(self, maze, start, goal):
from collections import deque
queue = deque([start])
parent = {start: None}
visited = {start}
while queue:
current = queue.popleft()
if current == goal:
self._visited = len(visited)
return self._reconstruct(parent, start, goal)
for neighbor in maze.neighbours(current):
if neighbor not in visited:
visited.add(neighbor)
parent[neighbor] = current
queue.append(neighbor)
self._visited = len(visited)
return []
def _reconstruct(self, parent, start, goal):
path = []
current = goal
while current is not None:
path.append(current)
current = parent.get(current)
path.reverse()
return path if path and path[0] == start else []
@property
def visited_count(self):
return self._visited
class DFS:
def __init__(self):
self._visited = 0
def find(self, maze, start, goal):
stack = [start]
parent = {start: None}
visited = {start}
while stack:
current = stack.pop()
if current == goal:
self._visited = len(visited)
return self._reconstruct(parent, start, goal)
for neighbor in maze.neighbours(current):
if neighbor not in visited:
visited.add(neighbor)
parent[neighbor] = current
stack.append(neighbor)
self._visited = len(visited)
return []
def _reconstruct(self, parent, start, goal):
path = []
current = goal
while current is not None:
path.append(current)
current = parent.get(current)
path.reverse()
return path if path and path[0] == start else []
@property
def visited_count(self):
return self._visited
class AStar:
def __init__(self):
self._visited = 0
def _heuristic(self, cell, goal):
return abs(cell.x - goal.x) + abs(cell.y - goal.y)
def find(self, maze, start, goal):
import heapq
heap = []
counter = 0
start_f = self._heuristic(start, goal)
heapq.heappush(heap, (start_f, counter, start))
counter += 1
parent = {}
g_score = {start: 0}
f_score = {start: start_f}
visited = set()
while heap:
current_f, _, current = heapq.heappop(heap)
visited.add(current)
if current == goal:
self._visited = len(visited)
return self._reconstruct(parent, start, goal)
if current_f > f_score.get(current, float('inf')):
continue
for neighbor in maze.neighbours(current):
tentative_g = g_score[current] + 1
if tentative_g < g_score.get(neighbor, float('inf')):
parent[neighbor] = current
g_score[neighbor] = tentative_g
new_f = tentative_g + self._heuristic(neighbor, goal)
f_score[neighbor] = new_f
heapq.heappush(heap, (new_f, counter, neighbor))
counter += 1
self._visited = len(visited)
return []
def _reconstruct(self, parent, start, goal):
path = []
current = goal
while current is not None:
path.append(current)
current = parent.get(current)
path.reverse()
return path if path and path[0] == start else []
@property
def visited_count(self):
return self._visited
class MazeSolver:
def __init__(self, maze):
self._maze = maze
self._algorithm = None
def set_algorithm(self, algorithm):
self._algorithm = algorithm
def solve(self):
if not self._algorithm:
raise ValueError("Algorithm not set")
start_time = time.perf_counter()
path = self._algorithm.find(self._maze, self._maze.start, self._maze.exit)
end_time = time.perf_counter()
elapsed_ms = (end_time - start_time) * 1000
return {
'time_ms': elapsed_ms,
'visited': self._algorithm.visited_count,
'path_length': len(path),
'path': path
}
DATA_PATH = r"C:\Users\user\Desktop\2026-rff_mp\SokolovEN\docs\data"
class ExperimentRunner:
def __init__(self):
self.algorithms = {
"BFS": BFS(),
"DFS": DFS(),
"A*": AStar()
}
self.loader = TextMazeLoader()
def run_benchmark(self, maze_file: str, algorithm: str, runs: int = 5):
try:
maze = self.loader.load(maze_file)
except Exception as e:
return None
total_time = 0.0
total_visited = 0
total_length = 0
successes = 0
for _ in range(runs):
solver = MazeSolver(maze)
solver.set_algorithm(self.algorithms[algorithm])
result = solver.solve()
if result and result['path_length'] > 0:
total_time += result['time_ms']
total_visited += result['visited']
total_length += result['path_length']
successes += 1
if successes == 0:
return None
return {
'time_ms': total_time / successes,
'visited_cells': total_visited / successes,
'path_length': total_length / successes,
'success_rate': successes / runs
}
def run_all_experiments(self, runs: int = 5):
mazes_list = [
(os.path.join(DATA_PATH, "small.txt"), "Small (10x10)"),
(os.path.join(DATA_PATH, "medium.txt"), "Medium (50x50)"),
(os.path.join(DATA_PATH, "large.txt"), "Large (100x100)"),
(os.path.join(DATA_PATH, "empty.txt"), "Empty"),
(os.path.join(DATA_PATH, "no_exit.txt"), "No exit")
]
results = []
print("running experiments")
print(f"Data path: {DATA_PATH}")
for maze_file, maze_name in mazes_list:
if not os.path.exists(maze_file):
print(f"\n[warn] File not found: {maze_file}")
continue
print(f"\nTesting: {maze_name}")
for algo_name in self.algorithms.keys():
stats = self.run_benchmark(maze_file, algo_name, runs)
if stats:
print(
f" {algo_name}: time={stats['time_ms']:.3f}ms, visited={stats['visited_cells']:.0f}, length={stats['path_length']:.0f}")
results.append({
'maze': maze_name,
'strategy': algo_name,
'time_ms': stats['time_ms'],
'visited_cells': stats['visited_cells'],
'path_length': stats['path_length'],
'success_rate': stats['success_rate']
})
else:
print(f" {algo_name}: no path found")
results.append({
'maze': maze_name,
'strategy': algo_name,
'time_ms': -1,
'visited_cells': -1,
'path_length': -1,
'success_rate': 0
})
return results
def create_visualizations(results):
valid_results = [r for r in results if r['time_ms'] > 0]
if not valid_results:
print("no valid results for visualization")
return
mazes = sorted(set(r['maze'] for r in valid_results))
algorithms = ['BFS', 'DFS', 'A*']
fig, axes = plt.subplots(1, 3, figsize=(15, 5))
fig.suptitle('pathfinding algorithms comparison', fontsize=14)
x = np.arange(len(mazes))
width = 0.25
# Time chart
for i, algo in enumerate(algorithms):
times = []
for maze in mazes:
val = next((r['time_ms'] for r in valid_results
if r['maze'] == maze and r['strategy'] == algo), 0)
times.append(val)
bars = axes[0].bar(x + i * width, times, width, label=algo, alpha=0.8)
for bar, val in zip(bars, times):
if val > 0:
axes[0].text(bar.get_x() + bar.get_width() / 2, bar.get_height() + 0.5,
f'{val:.1f}', ha='center', va='bottom', fontsize=7)
axes[0].set_title('execution Time (ms)')
axes[0].set_ylabel('time (ms)')
axes[0].set_xticks(x + width)
axes[0].set_xticklabels(mazes, rotation=45, ha='right', fontsize=8)
axes[0].legend()
axes[0].grid(alpha=0.3, axis='y')
# Visited cells chart
for i, algo in enumerate(algorithms):
visited = []
for maze in mazes:
val = next((r['visited_cells'] for r in valid_results
if r['maze'] == maze and r['strategy'] == algo), 0)
visited.append(val)
bars = axes[1].bar(x + i * width, visited, width, label=algo, alpha=0.8)
for bar, val in zip(bars, visited):
if val > 0:
axes[1].text(bar.get_x() + bar.get_width() / 2, bar.get_height(),
f'{val:.0f}', ha='center', va='bottom', fontsize=7)
axes[1].set_title('visited Cells')
axes[1].set_ylabel('count')
axes[1].set_xticks(x + width)
axes[1].set_xticklabels(mazes, rotation=45, ha='right', fontsize=8)
axes[1].legend()
axes[1].grid(alpha=0.3, axis='y')
# Path length chart
for i, algo in enumerate(algorithms):
lengths = []
for maze in mazes:
val = next((r['path_length'] for r in valid_results
if r['maze'] == maze and r['strategy'] == algo), 0)
lengths.append(val)
bars = axes[2].bar(x + i * width, lengths, width, label=algo, alpha=0.8)
for bar, val in zip(bars, lengths):
if val > 0:
axes[2].text(bar.get_x() + bar.get_width() / 2, bar.get_height(),
f'{val:.0f}', ha='center', va='bottom', fontsize=7)
axes[2].set_title('path Length')
axes[2].set_ylabel('steps')
axes[2].set_xticks(x + width)
axes[2].set_xticklabels(mazes, rotation=45, ha='right', fontsize=8)
axes[2].legend()
axes[2].grid(alpha=0.3, axis='y')
plt.tight_layout()
output_path = os.path.join(DATA_PATH, 'experiment_results.png')
plt.savefig(output_path, dpi=150, bbox_inches='tight')
print(f"\nPlot saved to: {output_path}")
plt.show()
def save_results_to_csv(results, filename='experiment_results.csv'):
if not results:
return
filepath = os.path.join(DATA_PATH, filename)
with open(filepath, 'w', newline='', encoding='utf-8') as f:
fieldnames = ['maze', 'strategy', 'time_ms', 'visited_cells', 'path_length', 'success_rate']
writer = csv.DictWriter(f, fieldnames=fieldnames)
writer.writeheader()
writer.writerows(results)
print(f"Results saved to: {filepath}")
def analyze_efficiency(results):
valid_results = [r for r in results if r['time_ms'] > 0]
if not valid_results:
print("no valid results for analysis")
return
algo_stats = {}
for algo in ['BFS', 'DFS', 'A*']:
algo_data = [r for r in valid_results if r['strategy'] == algo]
if algo_data:
algo_stats[algo] = {
'avg_time': sum(r['time_ms'] for r in algo_data) / len(algo_data),
'avg_visited': sum(r['visited_cells'] for r in algo_data) / len(algo_data),
'avg_length': sum(r['path_length'] for r in algo_data) / len(algo_data)
}
print("average values across all mazes")
print(f"{'Algorithm':<12} {'Time (ms)':<15} {'Visited':<15} {'Path length':<15}")
for algo, stats in algo_stats.items():
print(f"{algo:<12} {stats['avg_time']:<15.3f} {stats['avg_visited']:<15.1f} {stats['avg_length']:<15.1f}")
fastest = min(algo_stats.items(), key=lambda x: x[1]['avg_time'])
optimal = min(algo_stats.items(), key=lambda x: x[1]['avg_length'])
efficient = min(algo_stats.items(), key=lambda x: x[1]['avg_visited'])
print("conclusions:")
print(f" fastest algorithm: {fastest[0]} ({fastest[1]['avg_time']:.3f} ms avg)")
print(f" optimal path: {optimal[0]} ({optimal[1]['avg_length']:.1f} steps avg)")
print(f" most efficient (fewest visits): {efficient[0]} ({efficient[1]['avg_visited']:.0f} cells avg)")
print("=" * 70)
def main():
if not os.path.exists(DATA_PATH):
print(f"\nerr: directory not found: {DATA_PATH}")
print("please create the directory and place maze files there.")
print("\nexpected structure:")
print(f" {DATA_PATH}/")
print(" ├── small.txt")
print(" ├── medium.txt")
print(" ├── large.txt")
print(" ├── empty.txt")
print(" └── no_exit.txt")
return
runner = ExperimentRunner()
results = runner.run_all_experiments(runs=5)
if not results:
print("\nNo results. Check if maze files exist in:", DATA_PATH)
return
save_results_to_csv(results)
analyze_efficiency(results)
create_visualizations(results)
if __name__ == "__main__":
main()

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 152 KiB

109
SokolovEN/docs/results.csv Normal file
View File

@ -0,0 +1,109 @@
Структура,Режим,Операция,Время (сек)
LinkedList,случайный,insert,8.347676099999717
LinkedList,случайный,find,0.057291300000088086
LinkedList,случайный,delete,0.033828600000106235
HashTable,случайный,insert,0.019370900000012625
HashTable,случайный,find,0.00013010000020585721
HashTable,случайный,delete,6.919999987076153e-05
BST,случайный,insert,0.045051300000068295
BST,случайный,find,0.00039449999985663453
BST,случайный,delete,0.0002380000000812288
LinkedList,случайный,insert,8.37124969999968
LinkedList,случайный,find,0.05748009999979331
LinkedList,случайный,delete,0.033760199999960605
HashTable,случайный,insert,0.019708000000264292
HashTable,случайный,find,0.00012689999994108803
HashTable,случайный,delete,6.789999997636187e-05
BST,случайный,insert,0.04297489999999016
BST,случайный,find,0.00036730000010720687
BST,случайный,delete,0.00021289999995133257
LinkedList,случайный,insert,8.501181300000098
LinkedList,случайный,find,0.06136560000004465
LinkedList,случайный,delete,0.033985800000209565
HashTable,случайный,insert,0.018909500000063417
HashTable,случайный,find,0.00012770000012096716
HashTable,случайный,delete,6.809999968027114e-05
BST,случайный,insert,0.040991700000176934
BST,случайный,find,0.000367999999980384
BST,случайный,delete,0.0002165999999306223
LinkedList,случайный,insert,8.44069300000001
LinkedList,случайный,find,0.05868799999961993
LinkedList,случайный,delete,0.033578099999886035
HashTable,случайный,insert,0.017991799999890645
HashTable,случайный,find,0.00012880000031145755
HashTable,случайный,delete,6.720000010318472e-05
BST,случайный,insert,0.04100620000008348
BST,случайный,find,0.0003681999996842933
BST,случайный,delete,0.00021289999995133257
LinkedList,случайный,insert,8.554321999999956
LinkedList,случайный,find,0.059801599999900645
LinkedList,случайный,delete,0.03381919999992533
HashTable,случайный,insert,0.01814620000004652
HashTable,случайный,find,0.0001274000001103559
HashTable,случайный,delete,6.649999977526022e-05
BST,случайный,insert,0.04181910000033895
BST,случайный,find,0.0003727000002982095
BST,случайный,delete,0.00021440000000438886
LinkedList,отсортированный,insert,8.395491100000072
LinkedList,отсортированный,find,0.061166899999989255
LinkedList,отсортированный,delete,0.03749729999981355
HashTable,отсортированный,insert,0.08801780000021608
HashTable,отсортированный,find,0.00013059999992037774
HashTable,отсортированный,delete,7.719999985056347e-05
BST,отсортированный,insert,18.99293549999993
BST,отсортированный,find,0.17561569999998028
BST,отсортированный,delete,0.10195840000005774
LinkedList,отсортированный,insert,8.345957999999882
LinkedList,отсортированный,find,0.06071609999980865
LinkedList,отсортированный,delete,0.03802029999997103
HashTable,отсортированный,insert,0.017803299999741284
HashTable,отсортированный,find,0.00013059999992037774
HashTable,отсортированный,delete,7.66999996812956e-05
BST,отсортированный,insert,19.05815190000021
BST,отсортированный,find,0.17255539999996472
BST,отсортированный,delete,0.10364929999968808
LinkedList,отсортированный,insert,8.494904799999858
LinkedList,отсортированный,find,0.06991719999996349
LinkedList,отсортированный,delete,0.0375927000000047
HashTable,отсортированный,insert,0.018229599999813217
HashTable,отсортированный,find,0.00013249999983599992
HashTable,отсортированный,delete,8.08000004326459e-05
BST,отсортированный,insert,19.04517390000001
BST,отсортированный,find,0.17560349999985192
BST,отсортированный,delete,0.10177699999985634
LinkedList,отсортированный,insert,8.241154399999687
LinkedList,отсортированный,find,0.08371720000013738
LinkedList,отсортированный,delete,0.05189399999972011
HashTable,отсортированный,insert,0.09625940000023547
HashTable,отсортированный,find,0.0001606000000720087
HashTable,отсортированный,delete,8.960000013757963e-05
BST,отсортированный,insert,19.152932399999827
BST,отсортированный,find,0.17190189999973882
BST,отсортированный,delete,0.09978479999972478
LinkedList,отсортированный,insert,8.261084100000062
LinkedList,отсортированный,find,0.060434499999701075
LinkedList,отсортированный,delete,0.03753559999995559
HashTable,отсортированный,insert,0.018136799999865616
HashTable,отсортированный,find,0.00013009999975110986
HashTable,отсортированный,delete,7.819999973435188e-05
BST,отсортированный,insert,19.379212700000153
BST,отсортированный,find,0.17449820000001637
BST,отсортированный,delete,0.0995044000001144
LinkedList,случайный,insert (СРЕДНЕЕ),8.443024419999892
LinkedList,случайный,find (СРЕДНЕЕ),0.05892531999988933
LinkedList,случайный,delete (СРЕДНЕЕ),0.033794380000017554
LinkedList,отсортированный,insert (СРЕДНЕЕ),8.347718479999912
LinkedList,отсортированный,find (СРЕДНЕЕ),0.06719037999991997
LinkedList,отсортированный,delete (СРЕДНЕЕ),0.040507979999892994
HashTable,случайный,insert (СРЕДНЕЕ),0.0188252800000555
HashTable,случайный,find (СРЕДНЕЕ),0.00012818000013794517
HashTable,случайный,delete (СРЕДНЕЕ),6.77799998811679e-05
HashTable,отсортированный,insert (СРЕДНЕЕ),0.047689379999974336
HashTable,отсортированный,find (СРЕДНЕЕ),0.0001368799998999748
HashTable,отсортированный,delete (СРЕДНЕЕ),8.04999999672873e-05
BST,случайный,insert (СРЕДНЕЕ),0.04236864000013156
BST,случайный,find (СРЕДНЕЕ),0.0003741399999853456
BST,случайный,delete (СРЕДНЕЕ),0.00021895999998378102
BST,отсортированный,insert (СРЕДНЕЕ),19.125681280000027
BST,отсортированный,find (СРЕДНЕЕ),0.17403493999991043
BST,отсортированный,delete (СРЕДНЕЕ),0.10133477999988827
1 Структура Режим Операция Время (сек)
2 LinkedList случайный insert 8.347676099999717
3 LinkedList случайный find 0.057291300000088086
4 LinkedList случайный delete 0.033828600000106235
5 HashTable случайный insert 0.019370900000012625
6 HashTable случайный find 0.00013010000020585721
7 HashTable случайный delete 6.919999987076153e-05
8 BST случайный insert 0.045051300000068295
9 BST случайный find 0.00039449999985663453
10 BST случайный delete 0.0002380000000812288
11 LinkedList случайный insert 8.37124969999968
12 LinkedList случайный find 0.05748009999979331
13 LinkedList случайный delete 0.033760199999960605
14 HashTable случайный insert 0.019708000000264292
15 HashTable случайный find 0.00012689999994108803
16 HashTable случайный delete 6.789999997636187e-05
17 BST случайный insert 0.04297489999999016
18 BST случайный find 0.00036730000010720687
19 BST случайный delete 0.00021289999995133257
20 LinkedList случайный insert 8.501181300000098
21 LinkedList случайный find 0.06136560000004465
22 LinkedList случайный delete 0.033985800000209565
23 HashTable случайный insert 0.018909500000063417
24 HashTable случайный find 0.00012770000012096716
25 HashTable случайный delete 6.809999968027114e-05
26 BST случайный insert 0.040991700000176934
27 BST случайный find 0.000367999999980384
28 BST случайный delete 0.0002165999999306223
29 LinkedList случайный insert 8.44069300000001
30 LinkedList случайный find 0.05868799999961993
31 LinkedList случайный delete 0.033578099999886035
32 HashTable случайный insert 0.017991799999890645
33 HashTable случайный find 0.00012880000031145755
34 HashTable случайный delete 6.720000010318472e-05
35 BST случайный insert 0.04100620000008348
36 BST случайный find 0.0003681999996842933
37 BST случайный delete 0.00021289999995133257
38 LinkedList случайный insert 8.554321999999956
39 LinkedList случайный find 0.059801599999900645
40 LinkedList случайный delete 0.03381919999992533
41 HashTable случайный insert 0.01814620000004652
42 HashTable случайный find 0.0001274000001103559
43 HashTable случайный delete 6.649999977526022e-05
44 BST случайный insert 0.04181910000033895
45 BST случайный find 0.0003727000002982095
46 BST случайный delete 0.00021440000000438886
47 LinkedList отсортированный insert 8.395491100000072
48 LinkedList отсортированный find 0.061166899999989255
49 LinkedList отсортированный delete 0.03749729999981355
50 HashTable отсортированный insert 0.08801780000021608
51 HashTable отсортированный find 0.00013059999992037774
52 HashTable отсортированный delete 7.719999985056347e-05
53 BST отсортированный insert 18.99293549999993
54 BST отсортированный find 0.17561569999998028
55 BST отсортированный delete 0.10195840000005774
56 LinkedList отсортированный insert 8.345957999999882
57 LinkedList отсортированный find 0.06071609999980865
58 LinkedList отсортированный delete 0.03802029999997103
59 HashTable отсортированный insert 0.017803299999741284
60 HashTable отсортированный find 0.00013059999992037774
61 HashTable отсортированный delete 7.66999996812956e-05
62 BST отсортированный insert 19.05815190000021
63 BST отсортированный find 0.17255539999996472
64 BST отсортированный delete 0.10364929999968808
65 LinkedList отсортированный insert 8.494904799999858
66 LinkedList отсортированный find 0.06991719999996349
67 LinkedList отсортированный delete 0.0375927000000047
68 HashTable отсортированный insert 0.018229599999813217
69 HashTable отсортированный find 0.00013249999983599992
70 HashTable отсортированный delete 8.08000004326459e-05
71 BST отсортированный insert 19.04517390000001
72 BST отсортированный find 0.17560349999985192
73 BST отсортированный delete 0.10177699999985634
74 LinkedList отсортированный insert 8.241154399999687
75 LinkedList отсортированный find 0.08371720000013738
76 LinkedList отсортированный delete 0.05189399999972011
77 HashTable отсортированный insert 0.09625940000023547
78 HashTable отсортированный find 0.0001606000000720087
79 HashTable отсортированный delete 8.960000013757963e-05
80 BST отсортированный insert 19.152932399999827
81 BST отсортированный find 0.17190189999973882
82 BST отсортированный delete 0.09978479999972478
83 LinkedList отсортированный insert 8.261084100000062
84 LinkedList отсортированный find 0.060434499999701075
85 LinkedList отсортированный delete 0.03753559999995559
86 HashTable отсортированный insert 0.018136799999865616
87 HashTable отсортированный find 0.00013009999975110986
88 HashTable отсортированный delete 7.819999973435188e-05
89 BST отсортированный insert 19.379212700000153
90 BST отсортированный find 0.17449820000001637
91 BST отсортированный delete 0.0995044000001144
92 LinkedList случайный insert (СРЕДНЕЕ) 8.443024419999892
93 LinkedList случайный find (СРЕДНЕЕ) 0.05892531999988933
94 LinkedList случайный delete (СРЕДНЕЕ) 0.033794380000017554
95 LinkedList отсортированный insert (СРЕДНЕЕ) 8.347718479999912
96 LinkedList отсортированный find (СРЕДНЕЕ) 0.06719037999991997
97 LinkedList отсортированный delete (СРЕДНЕЕ) 0.040507979999892994
98 HashTable случайный insert (СРЕДНЕЕ) 0.0188252800000555
99 HashTable случайный find (СРЕДНЕЕ) 0.00012818000013794517
100 HashTable случайный delete (СРЕДНЕЕ) 6.77799998811679e-05
101 HashTable отсортированный insert (СРЕДНЕЕ) 0.047689379999974336
102 HashTable отсортированный find (СРЕДНЕЕ) 0.0001368799998999748
103 HashTable отсортированный delete (СРЕДНЕЕ) 8.04999999672873e-05
104 BST случайный insert (СРЕДНЕЕ) 0.04236864000013156
105 BST случайный find (СРЕДНЕЕ) 0.0003741399999853456
106 BST случайный delete (СРЕДНЕЕ) 0.00021895999998378102
107 BST отсортированный insert (СРЕДНЕЕ) 19.125681280000027
108 BST отсортированный find (СРЕДНЕЕ) 0.17403493999991043
109 BST отсортированный delete (СРЕДНЕЕ) 0.10133477999988827

View File

@ -0,0 +1,257 @@
import random
import time
import csv
import sys
sys.setrecursionlimit(20000)
# 1. СВЯЗНЫЙ СПИСОК
def ll_insert(head, name, phone):
curr = head
while curr:
if curr['name'] == name:
curr['phone'] = phone
return head
curr = curr['next']
new_node = {'name': name, 'phone': phone, 'next': None}
if head is None:
return new_node
curr = head
while curr['next']:
curr = curr['next']
curr['next'] = new_node
return head
def ll_find(head, name):
curr = head
while curr:
if curr['name'] == name:
return curr['phone']
curr = curr['next']
return None
def ll_delete(head, name):
if head is None:
return None
if head['name'] == name:
return head['next']
curr = head
while curr['next']:
if curr['next']['name'] == name:
curr['next'] = curr['next']['next']
return head
curr = curr['next']
return head
def ll_list_all(head):
res = []
curr = head
while curr:
res.append((curr['name'], curr['phone']))
curr = curr['next']
res.sort(key=lambda x: x[0])
return res
# 2. ХЕШ-ТАБЛИЦА
BUCKET_COUNT = 1024
def ht_insert(buckets, name, phone):
idx = hash(name) % BUCKET_COUNT
buckets[idx] = ll_insert(buckets[idx], name, phone)
def ht_find(buckets, name):
idx = hash(name) % BUCKET_COUNT
return ll_find(buckets[idx], name)
def ht_delete(buckets, name):
idx = hash(name) % BUCKET_COUNT
buckets[idx] = ll_delete(buckets[idx], name)
def ht_list_all(buckets):
res = []
for head in buckets:
curr = head
while curr:
res.append((curr['name'], curr['phone']))
curr = curr['next']
res.sort(key=lambda x: x[0])
return res
def bst_insert(root, name, phone):
if root is None:
return {'name': name, 'phone': phone, 'left': None, 'right': None}
if name < root['name']:
root['left'] = bst_insert(root['left'], name, phone)
elif name > root['name']:
root['right'] = bst_insert(root['right'], name, phone)
else:
root['phone'] = phone
return root
def bst_find(root, name):
if root is None:
return None
if name == root['name']:
return root['phone']
elif name < root['name']:
return bst_find(root['left'], name)
else:
return bst_find(root['right'], name)
def _bst_find_min(node):
curr = node
while curr['left'] is not None:
curr = curr['left']
return curr
def bst_delete(root, name):
if root is None:
return None
if name < root['name']:
root['left'] = bst_delete(root['left'], name)
elif name > root['name']:
root['right'] = bst_delete(root['right'], name)
else:
if root['left'] is None:
return root['right']
elif root['right'] is None:
return root['left']
else:
successor = _bst_find_min(root['right'])
root['name'] = successor['name']
root['phone'] = successor['phone']
root['right'] = bst_delete(root['right'], successor['name'])
return root
def bst_list_all(root):
res = []
def inorder(node):
if node:
inorder(node['left'])
res.append((node['name'], node['phone']))
inorder(node['right'])
inorder(root)
return res
# ЭКСПЕРИМЕНТАЛЬНАЯ ЧАСТЬ
def run_experiments():
N = 10000
base_records = [(f"User_{i:05d}", f"100{i:05d}") for i in range(N)]
records_sorted = sorted(base_records, key=lambda x: x[0])
records_shuffled = base_records[:]
random.shuffle(records_shuffled)
all_names = [r[0] for r in base_records]
find_existing = random.sample(all_names, 100)
find_non_existing = [f"Missing_{i}" for i in range(10)]
delete_targets = random.sample(all_names, 50)
all_results = []
structures = ["LinkedList", "HashTable", "BST"]
data_modes = [("случайный", records_shuffled), ("отсортированный", records_sorted)]
for mode_name, records in data_modes:
print(f"\n Режим: {mode_name}")
for run in range(1, 6):
print(f" запуск {run}/5")
head = None
t = time.perf_counter()
for n, p in records: head = ll_insert(head, n, p)
t_ins = time.perf_counter() - t
t = time.perf_counter()
for n in find_existing + find_non_existing: ll_find(head, n)
t_find = time.perf_counter() - t
t = time.perf_counter()
for n in delete_targets: head = ll_delete(head, n)
t_del = time.perf_counter() - t
all_results.append(["LinkedList", mode_name, "insert", t_ins])
all_results.append(["LinkedList", mode_name, "find", t_find])
all_results.append(["LinkedList", mode_name, "delete", t_del])
buckets = [None] * BUCKET_COUNT
t = time.perf_counter()
for n, p in records: ht_insert(buckets, n, p)
t_ins = time.perf_counter() - t
t = time.perf_counter()
for n in find_existing + find_non_existing: ht_find(buckets, n)
t_find = time.perf_counter() - t
t = time.perf_counter()
for n in delete_targets: ht_delete(buckets, n)
t_del = time.perf_counter() - t
all_results.append(["HashTable", mode_name, "insert", t_ins])
all_results.append(["HashTable", mode_name, "find", t_find])
all_results.append(["HashTable", mode_name, "delete", t_del])
root = None
t = time.perf_counter()
for n, p in records: root = bst_insert(root, n, p)
t_ins = time.perf_counter() - t
t = time.perf_counter()
for n in find_existing + find_non_existing: bst_find(root, n)
t_find = time.perf_counter() - t
t = time.perf_counter()
for n in delete_targets: root = bst_delete(root, n)
t_del = time.perf_counter() - t
all_results.append(["BST", mode_name, "insert", t_ins])
all_results.append(["BST", mode_name, "find", t_find])
all_results.append(["BST", mode_name, "delete", t_del])
averages = []
for struct in structures:
for mode in ["случайный", "отсортированный"]:
for op in ["insert", "find", "delete"]:
times = [r[3] for r in all_results if r[0] == struct and r[1] == mode and r[2] == op]
avg = sum(times) / len(times)
averages.append([struct, mode, f"{op} (СРЕДНЕЕ)", avg])
final_csv_data = [["Структура", "Режим", "Операция", "Время (сек)"]] + all_results + averages
with open("results.csv", "w", newline="", encoding="utf-8-sig") as f:
writer = csv.writer(f)
writer.writerows(final_csv_data)
return all_results, averages
if __name__ == "__main__":
raw_data, avg_data = run_experiments()

View File

@ -0,0 +1,35 @@
import pandas as pd
import matplotlib.pyplot as plt
import numpy as np
df = pd.read_csv("results.csv", encoding="utf-8-sig")
df_avg = df[df["Операция"].str.contains("СРЕДНЕЕ")].copy()
df_avg["Операция"] = df_avg["Операция"].str.replace(" (СРЕДНЕЕ)", "")
modes = ["случайный", "отсортированный"]
operations = ["insert", "find", "delete"]
structures = ["LinkedList", "HashTable", "BST"]
x = np.arange(len(structures))
width = 0.35
fig, axes = plt.subplots(1, 3, figsize=(18, 5))
for i, op in enumerate(operations):
ax = axes[i]
for j, mode in enumerate(modes):
mask = (df_avg["Операция"] == op) & (df_avg["Режим"] == mode)
times = df_avg[mask]["Время (сек)"].values
label = "Случайные данные" if mode == "случайный" else "Отсортированные данные"
ax.bar(x + j * width, times, width, label=label)
ax.set_title(f"Операция: {op.upper()}")
ax.set_xticks(x + width / 2)
ax.set_xticklabels(structures)
ax.set_ylabel("Время (сек)")
ax.legend()
ax.grid(axis="y", linestyle="--", alpha=0.7)
plt.tight_layout()
plt.savefig("performance_comparison.png", dpi=300)
plt.show()