Merge pull request '[1] for 1-st ex' (#228) from fomichevks/2026-rff_mp:fomichevks into develop

Reviewed-on: UNN/2026-rff_mp#228
This commit is contained in:
VladimirGub 2026-05-30 11:43:46 +00:00
commit 590204844a
14 changed files with 1799 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: 80 KiB

View File

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

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\Kirill\2026-rff_mp\fomichevks\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\Kirill\2026-rff_mp\fomichevks\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: 144 KiB

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

@ -0,0 +1,109 @@
Структура,Режим,Операция,Время (сек)
LinkedList,случайный,insert,3.450394299999971
LinkedList,случайный,find,0.02368320000005042
LinkedList,случайный,delete,0.0178195999997115
HashTable,случайный,insert,0.009212800000568677
HashTable,случайный,find,6.169999960548012e-05
HashTable,случайный,delete,3.860000015265541e-05
BST,случайный,insert,0.015902500000265718
BST,случайный,find,0.00014120000014372636
BST,случайный,delete,8.770000022195745e-05
LinkedList,случайный,insert,3.517222700000275
LinkedList,случайный,find,0.026108199999725912
LinkedList,случайный,delete,0.01791400000001886
HashTable,случайный,insert,0.009205899999869871
HashTable,случайный,find,5.869999949936755e-05
HashTable,случайный,delete,3.609999930631602e-05
BST,случайный,insert,0.017175400000269292
BST,случайный,find,0.0001505999998698826
BST,случайный,delete,9.200000022246968e-05
LinkedList,случайный,insert,3.435324000000037
LinkedList,случайный,find,0.026613500000166823
LinkedList,случайный,delete,0.020348300000478048
HashTable,случайный,insert,0.01095050000003539
HashTable,случайный,find,6.169999960548012e-05
HashTable,случайный,delete,3.9000000469968654e-05
BST,случайный,insert,0.015578700000332901
BST,случайный,find,0.0001768000001902692
BST,случайный,delete,0.00010370000018156134
LinkedList,случайный,insert,3.4476645999993707
LinkedList,случайный,find,0.025092200000472076
LinkedList,случайный,delete,0.017970900000364054
HashTable,случайный,insert,0.008620399999927031
HashTable,случайный,find,5.6599999879836105e-05
HashTable,случайный,delete,3.699999979289714e-05
BST,случайный,insert,0.016901099999813596
BST,случайный,find,0.0001353999996354105
BST,случайный,delete,8.39000003907131e-05
LinkedList,случайный,insert,3.4527680000001055
LinkedList,случайный,find,0.02482880000025034
LinkedList,случайный,delete,0.01792089999980817
HashTable,случайный,insert,0.00791659999958938
HashTable,случайный,find,0.00012760000026901253
HashTable,случайный,delete,6.010000015521655e-05
BST,случайный,insert,0.01643800000056217
BST,случайный,find,0.0001905999997688923
BST,случайный,delete,9.900000077323057e-05
LinkedList,отсортированный,insert,3.2146765999996205
LinkedList,отсортированный,find,0.02251799999976356
LinkedList,отсортированный,delete,0.016432399999757763
HashTable,отсортированный,insert,0.008092500000202563
HashTable,отсортированный,find,7.089999962772708e-05
HashTable,отсортированный,delete,4.069999977218686e-05
BST,отсортированный,insert,8.144065100000262
BST,отсортированный,find,0.07145860000036919
BST,отсортированный,delete,0.041536599999744794
LinkedList,отсортированный,insert,3.2909168000005593
LinkedList,отсортированный,find,0.1718697999995129
LinkedList,отсортированный,delete,0.03186750000077154
HashTable,отсортированный,insert,0.014283700000305544
HashTable,отсортированный,find,9.820000013860408e-05
HashTable,отсортированный,delete,5.8200000239594374e-05
BST,отсортированный,insert,7.79496620000009
BST,отсортированный,find,0.06252070000027743
BST,отсортированный,delete,0.04316579999976966
LinkedList,отсортированный,insert,3.3210246999997253
LinkedList,отсортированный,find,0.020591699999386037
LinkedList,отсортированный,delete,0.016228899999987334
HashTable,отсортированный,insert,0.007315800000469608
HashTable,отсортированный,find,5.450000026030466e-05
HashTable,отсортированный,delete,3.370000013092067e-05
BST,отсортированный,insert,8.219712999999501
BST,отсортированный,find,0.0645872999994026
BST,отсортированный,delete,0.04166759999952774
LinkedList,отсортированный,insert,3.3059798000003866
LinkedList,отсортированный,find,0.020161800000096264
LinkedList,отсортированный,delete,0.016405999999733467
HashTable,отсортированный,insert,0.008103499999378982
HashTable,отсортированный,find,6.690000009257346e-05
HashTable,отсортированный,delete,3.999999989900971e-05
BST,отсортированный,insert,9.020431099999769
BST,отсортированный,find,0.06939630000033503
BST,отсортированный,delete,0.04487580000022717
LinkedList,отсортированный,insert,3.5286267000001317
LinkedList,отсортированный,find,0.022289700000328594
LinkedList,отсортированный,delete,0.018663600000763836
HashTable,отсортированный,insert,0.010729900000114867
HashTable,отсортированный,find,7.849999929021578e-05
HashTable,отсортированный,delete,4.8600000809528865e-05
BST,отсортированный,insert,8.329646700000012
BST,отсортированный,find,0.06335099999978411
BST,отсортированный,delete,0.042559800000162795
LinkedList,случайный,insert (СРЕДНЕЕ),3.4606747199999517
LinkedList,случайный,find (СРЕДНЕЕ),0.025265180000133114
LinkedList,случайный,delete (СРЕДНЕЕ),0.018394740000076126
LinkedList,отсортированный,insert (СРЕДНЕЕ),3.3322449200000848
LinkedList,отсортированный,find (СРЕДНЕЕ),0.051486199999817475
LinkedList,отсортированный,delete (СРЕДНЕЕ),0.019919680000202788
HashTable,случайный,insert (СРЕДНЕЕ),0.00918123999999807
HashTable,случайный,find (СРЕДНЕЕ),7.325999977183528e-05
HashTable,случайный,delete (СРЕДНЕЕ),4.215999997541076e-05
HashTable,отсортированный,insert (СРЕДНЕЕ),0.009705080000094313
HashTable,отсортированный,find (СРЕДНЕЕ),7.379999988188501e-05
HashTable,отсортированный,delete (СРЕДНЕЕ),4.4240000170248096e-05
BST,случайный,insert (СРЕДНЕЕ),0.016399140000248735
BST,случайный,find (СРЕДНЕЕ),0.0001589199999216362
BST,случайный,delete (СРЕДНЕЕ),9.326000035798643e-05
BST,отсортированный,insert (СРЕДНЕЕ),8.301764419999927
BST,отсортированный,find (СРЕДНЕЕ),0.06626278000003367
BST,отсортированный,delete (СРЕДНЕЕ),0.04276111999988643
1 Структура Режим Операция Время (сек)
2 LinkedList случайный insert 3.450394299999971
3 LinkedList случайный find 0.02368320000005042
4 LinkedList случайный delete 0.0178195999997115
5 HashTable случайный insert 0.009212800000568677
6 HashTable случайный find 6.169999960548012e-05
7 HashTable случайный delete 3.860000015265541e-05
8 BST случайный insert 0.015902500000265718
9 BST случайный find 0.00014120000014372636
10 BST случайный delete 8.770000022195745e-05
11 LinkedList случайный insert 3.517222700000275
12 LinkedList случайный find 0.026108199999725912
13 LinkedList случайный delete 0.01791400000001886
14 HashTable случайный insert 0.009205899999869871
15 HashTable случайный find 5.869999949936755e-05
16 HashTable случайный delete 3.609999930631602e-05
17 BST случайный insert 0.017175400000269292
18 BST случайный find 0.0001505999998698826
19 BST случайный delete 9.200000022246968e-05
20 LinkedList случайный insert 3.435324000000037
21 LinkedList случайный find 0.026613500000166823
22 LinkedList случайный delete 0.020348300000478048
23 HashTable случайный insert 0.01095050000003539
24 HashTable случайный find 6.169999960548012e-05
25 HashTable случайный delete 3.9000000469968654e-05
26 BST случайный insert 0.015578700000332901
27 BST случайный find 0.0001768000001902692
28 BST случайный delete 0.00010370000018156134
29 LinkedList случайный insert 3.4476645999993707
30 LinkedList случайный find 0.025092200000472076
31 LinkedList случайный delete 0.017970900000364054
32 HashTable случайный insert 0.008620399999927031
33 HashTable случайный find 5.6599999879836105e-05
34 HashTable случайный delete 3.699999979289714e-05
35 BST случайный insert 0.016901099999813596
36 BST случайный find 0.0001353999996354105
37 BST случайный delete 8.39000003907131e-05
38 LinkedList случайный insert 3.4527680000001055
39 LinkedList случайный find 0.02482880000025034
40 LinkedList случайный delete 0.01792089999980817
41 HashTable случайный insert 0.00791659999958938
42 HashTable случайный find 0.00012760000026901253
43 HashTable случайный delete 6.010000015521655e-05
44 BST случайный insert 0.01643800000056217
45 BST случайный find 0.0001905999997688923
46 BST случайный delete 9.900000077323057e-05
47 LinkedList отсортированный insert 3.2146765999996205
48 LinkedList отсортированный find 0.02251799999976356
49 LinkedList отсортированный delete 0.016432399999757763
50 HashTable отсортированный insert 0.008092500000202563
51 HashTable отсортированный find 7.089999962772708e-05
52 HashTable отсортированный delete 4.069999977218686e-05
53 BST отсортированный insert 8.144065100000262
54 BST отсортированный find 0.07145860000036919
55 BST отсортированный delete 0.041536599999744794
56 LinkedList отсортированный insert 3.2909168000005593
57 LinkedList отсортированный find 0.1718697999995129
58 LinkedList отсортированный delete 0.03186750000077154
59 HashTable отсортированный insert 0.014283700000305544
60 HashTable отсортированный find 9.820000013860408e-05
61 HashTable отсортированный delete 5.8200000239594374e-05
62 BST отсортированный insert 7.79496620000009
63 BST отсортированный find 0.06252070000027743
64 BST отсортированный delete 0.04316579999976966
65 LinkedList отсортированный insert 3.3210246999997253
66 LinkedList отсортированный find 0.020591699999386037
67 LinkedList отсортированный delete 0.016228899999987334
68 HashTable отсортированный insert 0.007315800000469608
69 HashTable отсортированный find 5.450000026030466e-05
70 HashTable отсортированный delete 3.370000013092067e-05
71 BST отсортированный insert 8.219712999999501
72 BST отсортированный find 0.0645872999994026
73 BST отсортированный delete 0.04166759999952774
74 LinkedList отсортированный insert 3.3059798000003866
75 LinkedList отсортированный find 0.020161800000096264
76 LinkedList отсортированный delete 0.016405999999733467
77 HashTable отсортированный insert 0.008103499999378982
78 HashTable отсортированный find 6.690000009257346e-05
79 HashTable отсортированный delete 3.999999989900971e-05
80 BST отсортированный insert 9.020431099999769
81 BST отсортированный find 0.06939630000033503
82 BST отсортированный delete 0.04487580000022717
83 LinkedList отсортированный insert 3.5286267000001317
84 LinkedList отсортированный find 0.022289700000328594
85 LinkedList отсортированный delete 0.018663600000763836
86 HashTable отсортированный insert 0.010729900000114867
87 HashTable отсортированный find 7.849999929021578e-05
88 HashTable отсортированный delete 4.8600000809528865e-05
89 BST отсортированный insert 8.329646700000012
90 BST отсортированный find 0.06335099999978411
91 BST отсортированный delete 0.042559800000162795
92 LinkedList случайный insert (СРЕДНЕЕ) 3.4606747199999517
93 LinkedList случайный find (СРЕДНЕЕ) 0.025265180000133114
94 LinkedList случайный delete (СРЕДНЕЕ) 0.018394740000076126
95 LinkedList отсортированный insert (СРЕДНЕЕ) 3.3322449200000848
96 LinkedList отсортированный find (СРЕДНЕЕ) 0.051486199999817475
97 LinkedList отсортированный delete (СРЕДНЕЕ) 0.019919680000202788
98 HashTable случайный insert (СРЕДНЕЕ) 0.00918123999999807
99 HashTable случайный find (СРЕДНЕЕ) 7.325999977183528e-05
100 HashTable случайный delete (СРЕДНЕЕ) 4.215999997541076e-05
101 HashTable отсортированный insert (СРЕДНЕЕ) 0.009705080000094313
102 HashTable отсортированный find (СРЕДНЕЕ) 7.379999988188501e-05
103 HashTable отсортированный delete (СРЕДНЕЕ) 4.4240000170248096e-05
104 BST случайный insert (СРЕДНЕЕ) 0.016399140000248735
105 BST случайный find (СРЕДНЕЕ) 0.0001589199999216362
106 BST случайный delete (СРЕДНЕЕ) 9.326000035798643e-05
107 BST отсортированный insert (СРЕДНЕЕ) 8.301764419999927
108 BST отсортированный find (СРЕДНЕЕ) 0.06626278000003367
109 BST отсортированный delete (СРЕДНЕЕ) 0.04276111999988643

View File

@ -0,0 +1,137 @@
1. Цель работы
Реализовать три базовые структуры данных без использования объектно-ориентированных механизмов, применить их для хранения записей телефонного справочника, экспериментально измерить производительность операций вставки, поиска и удаления, а также проанализировать влияние порядка входных данных на время выполнения.
Связный список (LinkedListPhoneBook)
Узел: {'name': str, 'phone': str, 'next': dict | None}
Операции:
ll_insert: линейный проход до конца, обновление при совпадении имени, вставка нового узла в хвост. Возвращает голову списка.
ll_find: последовательный перебор до первого совпадения.
ll_delete: поиск предшественника удаляемого узла, переназначение ссылки next.
ll_list_all: сбор записей в список, явная сортировка по имени.
Хеш-таблица с цепочками (HashTable)
Структура: список из BUCKET_COUNT = 1024 элементов, каждый элемент — голова связного списка.
Хеширование: idx = hash(name) % BUCKET_COUNT
Операции: делегируют соответствующим ll_* функциям для конкретного бакета.
Узел: {'name': str, 'phone': str, 'left': dict | None, 'right': dict | None}
Операции:
bst_insert: рекурсивное сравнение имён, создание листа при достижении None.
bst_find: рекурсивный спуск влево/вправо в зависимости от результата сравнения.
bst_delete: три случая: 0 потомков, 1 потомок, 2 потомка. При двух потомках используется inorder-преемник (минимальный элемент правого поддерева).
bst_list_all: центрированный (in-order) обход, гарантирующий отсортированный вывод без дополнительной сортировки.
Влияние порядка входных данных на скорость вставки в BST
Двоичное дерево поиска (BST) поддерживает инвариант: left.name < root.name < right.name. При вставке новых узлов алгоритм рекурсивно спускается по дереву, выбирая левую или правую ветвь в зависимости от результата сравнения.
Случай 1: Случайный порядок данных
Ключи распределяются по дереву хаотично
Левые и правые поддеревья заполняются примерно равномерно
Высота дерева: h ≈ log₂(N) ≈ 14 для N=10 000
Сложность вставки одного элемента: O(log N)
Общая сложность вставки всех N элементов: O(N log N)
Случай 2: Отсортированный порядок данных
Каждый следующий ключ больше всех предыдущих
Алгоритм всегда выбирает правую ветвь
Дерево вырождается в линейную цепочку
Почему хеш-таблица почти не чувствительна к порядку?
Функция hash() в Python:
Детерминирована: один и тот же ключ → один и тот же хеш
Равномерно распределяет значения по пространству хешей
Не зависит от порядка вызова: hash("User_00001") всегда одинаков
Распределение по бакетам
При N=10 000 записей и 1024 бакетах:
Ожидаемая загрузка: α = N / BUCKET_COUNT ≈ 9.77 элементов на бакет
Даже если все ключи отсортированы, их хеши «размазываются» по всему диапазону
Внутри каждого бакета хранится короткий связный список (~10 элементов)
Почему связный список всегда медленен при поиске?
Связный список хранит элементы последовательно, без индексации
Для поиска элемента с именем X:
Начать с головы списка
Сравнить curr['name'] == X
Если не совпало → перейти к curr['next']
Повторять до нахождения или конца списка
Связный список не подходит для задач с частым поиском. Его удел очереди, стеки, или вспомогательная роль внутри других структур.
Как удаление работает в каждой структуре?
Связный список
def ll_delete(head, name):
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
Поиск узла (или его предшественника) — O(N)
Переназначение ссылки next — O(1)
Сборка мусора (автоматически в Python)
Хеш-таблица
def ht_delete(buckets, name):
idx = hash(name) % BUCKET_COUNT
buckets[idx] = ll_delete(buckets[idx], name)
Вычисление индекса бакета — O(1)
Поиск и удаление в связном списке бакета — O(L), где L ≈ 10
Итого: O(1) в среднем
Двоичное дерево поиска
def bst_delete(root, name):
# 1. Поиск узла
if name < root['name']:
root['left'] = bst_delete(root['left'], name)
elif name > root['name']:
root['right'] = bst_delete(root['right'], name)
else:
# 2. Три случая удаления
if root['left'] is None:
return root['right'] # 0 или 1 потомок
elif root['right'] is None:
return root['left']
else:
# 2 потомка: найти inorder-преемника
successor = _bst_find_min(root['right'])
root['name'] = successor['name']
root['phone'] = successor['phone']
root['right'] = bst_delete(root['right'], successor['name'])
return root
Поиск удаляемого узла — O(h)
Обработка случая:
0 потомков: просто удалить узел
1 потомок: «поднять» потомка на место удаляемого
2 потомка: найти минимум в правом поддереве (inorder-преемник), скопировать его данные, рекурсивно удалить преемника
Возврат обновлённого корня поддерева
Когда какую структуру использовать?
| Сценарий | Рекомендация |
|---|---|
| **Частый поиск** по имени | HashTable или BST (случайные данные) |
| **Данные приходят отсортированными** | HashTable (BST деградирует!) |
| **Нужен отсортированный список** | BST (in-order обход — бесплатный) |
| **Частые вставки/удаления + поиск** | HashTable |
| **Минимальная память, простота** | LinkedList (для малых N) |
| **Диапазонные запросы** (все имена AM) | BST |
### Сложности операций
| Структура | Insert | Find | Delete | List (sorted) |
|---|---|---|---|---|
| LinkedList | O(n) | O(n) | O(n) | O(n log n) |
| HashTable | O(1) avg | O(1) avg | O(1) avg | O(n log n) |
| BST (сбалансированный) | O(log n) | O(log n) | O(log n) | O(n) |
| BST (вырожденный) | O(n) | O(n) | O(n) | O(n) |
HashTable — лучший выбор для телефонного справочника при частых вставках и поисках. BST лучше HashTable только если нужен отсортированный вывод без дополнительной сортировки — но при условии случайного порядка вставки или использования самобалансирующегося дерева (AVL, Red-Black).

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()