Merge pull request '[2] 2-nd-exercise' (#268) from anikinvd/2026-rff_mp:2-nd-exercise into develop

Reviewed-on: UNN/2026-rff_mp#268
This commit is contained in:
VladimirGub 2026-05-30 11:22:03 +00:00
commit 0bda7aa621
15 changed files with 1485 additions and 0 deletions

View File

@ -0,0 +1,31 @@
Structure,Mode,Repeat,Insert (sec),Search (sec),Delete (sec)
LinkedList,random,1,4.026961,0.027873,0.012806
LinkedList,random,2,4.057927,0.024120,0.015494
LinkedList,random,3,4.159901,0.031027,0.012129
LinkedList,random,4,4.209198,0.028752,0.015955
LinkedList,random,5,4.217042,0.029317,0.012541
LinkedList,sorted,1,3.702052,0.023465,0.010952
LinkedList,sorted,2,3.723771,0.023921,0.014212
LinkedList,sorted,3,3.756407,0.023732,0.010483
LinkedList,sorted,4,3.746887,0.026972,0.011036
LinkedList,sorted,5,3.784009,0.025765,0.011212
HashTable,random,1,0.010695,0.000075,0.000038
HashTable,random,2,0.009009,0.000076,0.000039
HashTable,random,3,0.009032,0.000069,0.000033
HashTable,random,4,0.009581,0.000085,0.000038
HashTable,random,5,0.008664,0.000071,0.000035
HashTable,sorted,1,0.010321,0.000071,0.000030
HashTable,sorted,2,0.008763,0.000070,0.000034
HashTable,sorted,3,0.009035,0.000071,0.000033
HashTable,sorted,4,0.008954,0.000068,0.000032
HashTable,sorted,5,0.008670,0.000071,0.000033
BST,random,1,0.025128,0.000209,0.000137
BST,random,2,0.023434,0.000202,0.000131
BST,random,3,0.023199,0.000195,0.000119
BST,random,4,0.023011,0.000210,0.000123
BST,random,5,0.025045,0.000263,0.000122
BST,sorted,1,9.047348,0.077555,0.047565
BST,sorted,2,9.058836,0.081414,0.044913
BST,sorted,3,9.021041,0.067645,0.053180
BST,sorted,4,9.096998,0.089720,0.047616
BST,sorted,5,9.334407,0.081513,0.062546
1 Structure Mode Repeat Insert (sec) Search (sec) Delete (sec)
2 LinkedList random 1 4.026961 0.027873 0.012806
3 LinkedList random 2 4.057927 0.024120 0.015494
4 LinkedList random 3 4.159901 0.031027 0.012129
5 LinkedList random 4 4.209198 0.028752 0.015955
6 LinkedList random 5 4.217042 0.029317 0.012541
7 LinkedList sorted 1 3.702052 0.023465 0.010952
8 LinkedList sorted 2 3.723771 0.023921 0.014212
9 LinkedList sorted 3 3.756407 0.023732 0.010483
10 LinkedList sorted 4 3.746887 0.026972 0.011036
11 LinkedList sorted 5 3.784009 0.025765 0.011212
12 HashTable random 1 0.010695 0.000075 0.000038
13 HashTable random 2 0.009009 0.000076 0.000039
14 HashTable random 3 0.009032 0.000069 0.000033
15 HashTable random 4 0.009581 0.000085 0.000038
16 HashTable random 5 0.008664 0.000071 0.000035
17 HashTable sorted 1 0.010321 0.000071 0.000030
18 HashTable sorted 2 0.008763 0.000070 0.000034
19 HashTable sorted 3 0.009035 0.000071 0.000033
20 HashTable sorted 4 0.008954 0.000068 0.000032
21 HashTable sorted 5 0.008670 0.000071 0.000033
22 BST random 1 0.025128 0.000209 0.000137
23 BST random 2 0.023434 0.000202 0.000131
24 BST random 3 0.023199 0.000195 0.000119
25 BST random 4 0.023011 0.000210 0.000123
26 BST random 5 0.025045 0.000263 0.000122
27 BST sorted 1 9.047348 0.077555 0.047565
28 BST sorted 2 9.058836 0.081414 0.044913
29 BST sorted 3 9.021041 0.067645 0.053180
30 BST sorted 4 9.096998 0.089720 0.047616
31 BST sorted 5 9.334407 0.081513 0.062546

View File

@ -0,0 +1,274 @@
import random
import time
import csv
import sys
sys.setrecursionlimit(20000)
def llist_insert(head, name, phone):
current = head
while current is not None:
if current['name'] == name:
current['phone'] = phone
return head
current = current['next']
new_node = {'name': name, 'phone': phone, 'next': None}
if head is None:
return new_node
current = head
while current['next'] is not None:
current = current['next']
current['next'] = new_node
return head
def llist_find(head, name):
current = head
while current is not None:
if current['name'] == name:
return current['phone']
current = current['next']
return None
def llist_delete(head, name):
if head is None:
return None
if head['name'] == name:
return head['next']
prev = head
current = head['next']
while current is not None:
if current['name'] == name:
prev['next'] = current['next']
return head
prev = current
current = current['next']
return head
def llist_get_all(head):
entries = []
current = head
while current is not None:
entries.append((current['name'], current['phone']))
current = current['next']
entries.sort(key=lambda x: x[0])
return entries
BUCKET_SIZE = 1000
def ht_create():
return [None] * BUCKET_SIZE
def ht_insert(table, name, phone):
idx = hash(name) % len(table)
table[idx] = llist_insert(table[idx], name, phone)
return table
def ht_find(table, name):
idx = hash(name) % len(table)
return llist_find(table[idx], name)
def ht_delete(table, name):
idx = hash(name) % len(table)
table[idx] = llist_delete(table[idx], name)
return table
def ht_get_all(table):
all_entries = []
for head in table:
current = head
while current is not None:
all_entries.append((current['name'], current['phone']))
current = current['next']
all_entries.sort(key=lambda x: x[0])
return all_entries
def bst_create_node(name, phone):
return {'name': name, 'phone': phone, 'left': None, 'right': None}
def bst_insert(root, name, phone):
if root is None:
return bst_create_node(name, phone)
if name == root['name']:
root['phone'] = phone
elif name < root['name']:
root['left'] = bst_insert(root['left'], name, phone)
else:
root['right'] = bst_insert(root['right'], name, 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):
while node['left'] is not None:
node = node['left']
return node
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']
if root['right'] is None:
return root['left']
min_node = bst_find_min(root['right'])
root['name'] = min_node['name']
root['phone'] = min_node['phone']
root['right'] = bst_delete(root['right'], min_node['name'])
return root
def bst_inorder_collect(root, out_list):
if root is not None:
bst_inorder_collect(root['left'], out_list)
out_list.append((root['name'], root['phone']))
bst_inorder_collect(root['right'], out_list)
def bst_get_all(root):
result = []
bst_inorder_collect(root, result)
return result
def generate_phonebook_entries(n, seed=42):
random.seed(seed)
records = []
for i in range(1, n + 1):
name = f"User_{i:05d}"
phone = f"{random.randint(100,999)}-{random.randint(1000,9999)}"
records.append((name, phone))
return records
def prepare_datasets(base_records):
shuffled = base_records.copy()
random.shuffle(shuffled)
sorted_records = sorted(base_records, key=lambda x: x[0])
return shuffled, sorted_records
def run_experiment(struct_funcs, records, mode_name, repeats=5):
all_results = []
for rep in range(repeats):
struct = struct_funcs['create']()
start = time.perf_counter()
for name, phone in records:
struct = struct_funcs['insert'](struct, name, phone)
insert_time = time.perf_counter() - start
existing_names = [name for name, _ in records]
sample_existing = random.sample(existing_names, 100)
nonexistent = [f"None_{i}" for i in range(10)]
search_names = sample_existing + nonexistent
random.shuffle(search_names)
start = time.perf_counter()
for name in search_names:
_ = struct_funcs['find'](struct, name)
find_time = time.perf_counter() - start
to_delete = random.sample(existing_names, 50)
start = time.perf_counter()
for name in to_delete:
struct = struct_funcs['delete'](struct, name)
delete_time = time.perf_counter() - start
all_results.append({
'structure': struct_funcs['name'],
'mode': mode_name,
'repetition': rep + 1,
'insert_time': insert_time,
'find_time': find_time,
'delete_time': delete_time
})
return all_results
def run_benchmark():
N = 10000
REPEATS = 5
base_records = generate_phonebook_entries(N)
shuffled_records, sorted_records = prepare_datasets(base_records)
structures = {
'LinkedList': {
'name': 'LinkedList',
'create': lambda: None,
'insert': llist_insert,
'find': llist_find,
'delete': llist_delete,
'get_all': llist_get_all
},
'HashTable': {
'name': 'HashTable',
'create': ht_create,
'insert': ht_insert,
'find': ht_find,
'delete': ht_delete,
'get_all': ht_get_all
},
'BST': {
'name': 'BST',
'create': lambda: None,
'insert': bst_insert,
'find': bst_find,
'delete': bst_delete,
'get_all': bst_get_all
}
}
all_results = []
for struct_name, funcs in structures.items():
results_random = run_experiment(funcs, shuffled_records, 'random', REPEATS)
all_results.extend(results_random)
results_sorted = run_experiment(funcs, sorted_records, 'sorted', REPEATS)
all_results.extend(results_sorted)
with open('experiment_results.csv', 'w', newline='', encoding='utf-8') as f:
writer = csv.writer(f)
writer.writerow(['Structure', 'Mode', 'Repeat', 'Insert (sec)', 'Search (sec)', 'Delete (sec)'])
for r in all_results:
writer.writerow([
r['structure'],
r['mode'],
r['repetition'],
f"{r['insert_time']:.6f}",
f"{r['find_time']:.6f}",
f"{r['delete_time']:.6f}"
])
print("Experiment finished. Results saved to 'experiment_results.csv'.")
if __name__ == '__main__':
run_benchmark()

View File

@ -0,0 +1,38 @@
import pandas as pd
import matplotlib.pyplot as plt
import numpy as np
df = pd.read_csv('experiment_results.csv')
mean_times = df.groupby(['Structure', 'Mode'])[['Insert (sec)', 'Search (sec)', 'Delete (sec)']].mean().reset_index()
structures = mean_times['Structure'].unique()
modes = mean_times['Mode'].unique()
fig, axes = plt.subplots(1, 3, figsize=(15, 5))
operations = ['Insert (sec)', 'Search (sec)', 'Delete (sec)']
titles = ['Insertion', 'Search', 'Deletion']
for ax, op, title in zip(axes, operations, titles):
x = np.arange(len(structures))
width = 0.35
random_vals = []
sorted_vals = []
for s in structures:
random_row = mean_times[(mean_times['Structure'] == s) & (mean_times['Mode'] == 'random')]
sorted_row = mean_times[(mean_times['Structure'] == s) & (mean_times['Mode'] == 'sorted')]
random_vals.append(random_row[op].values[0] if not random_row.empty else 0)
sorted_vals.append(sorted_row[op].values[0] if not sorted_row.empty else 0)
ax.bar(x - width/2, random_vals, width, label='Random order')
ax.bar(x + width/2, sorted_vals, width, label='Sorted order')
ax.set_xticks(x)
ax.set_xticklabels(structures)
ax.set_ylabel('Time (seconds)')
ax.set_title(title)
ax.legend()
plt.tight_layout()
plt.savefig('performance_comparison.png', dpi=150)
plt.show()

View File

@ -0,0 +1,16 @@
maze,strategy,time_ms,visited_cells,path_length
Small 10x6,BFS,0.025158666630886728,9.0,5.0
Small 10x6,DFS,0.04097166674910113,26.0,19.0
Small 10x6,AStar,0.015256333426805213,5.0,5.0
Medium 10x10,BFS,0.015568000132285912,18.0,8.0
Medium 10x10,DFS,0.007917000099647945,9.0,8.0
Medium 10x10,AStar,0.014829333395027788,8.0,8.0
Large 20x20,BFS,0.13646366672522467,116.0,69.0
Large 20x20,DFS,0.15918433321833922,173.0,69.0
Large 20x20,AStar,0.19781433320531505,110.0,69.0
Empty 15x15,BFS,0.25488699990698177,240.0,29.0
Empty 15x15,DFS,0.14207733314227275,224.0,119.0
Empty 15x15,AStar,0.5900679999892114,224.0,29.0
No exit 10x10,BFS,0.04236899985698983,36.0,0.0
No exit 10x10,DFS,0.03538033342920244,36.0,0.0
No exit 10x10,AStar,0.06468633318945649,36.0,0.0
1 maze strategy time_ms visited_cells path_length
2 Small 10x6 BFS 0.025158666630886728 9.0 5.0
3 Small 10x6 DFS 0.04097166674910113 26.0 19.0
4 Small 10x6 AStar 0.015256333426805213 5.0 5.0
5 Medium 10x10 BFS 0.015568000132285912 18.0 8.0
6 Medium 10x10 DFS 0.007917000099647945 9.0 8.0
7 Medium 10x10 AStar 0.014829333395027788 8.0 8.0
8 Large 20x20 BFS 0.13646366672522467 116.0 69.0
9 Large 20x20 DFS 0.15918433321833922 173.0 69.0
10 Large 20x20 AStar 0.19781433320531505 110.0 69.0
11 Empty 15x15 BFS 0.25488699990698177 240.0 29.0
12 Empty 15x15 DFS 0.14207733314227275 224.0 119.0
13 Empty 15x15 AStar 0.5900679999892114 224.0 29.0
14 No exit 10x10 BFS 0.04236899985698983 36.0 0.0
15 No exit 10x10 DFS 0.03538033342920244 36.0 0.0
16 No exit 10x10 AStar 0.06468633318945649 36.0 0.0

View File

@ -0,0 +1,490 @@
import sys
from collections import deque
import heapq
import time
import os
# ---------- Модель лабиринта ----------
class Tile:
def __init__(self, column, row):
self.col = column
self.row = row
self.blocked = False
self.is_start = False
self.is_exit = False
@property
def x(self):
return self.col
@property
def y(self):
return self.row
@property
def is_wall(self):
return self.blocked
@is_wall.setter
def is_wall(self, value):
self.blocked = value
def can_step(self):
return not self.blocked
class Labyrinth:
def __init__(self, width, height):
self._w = width
self._h = height
self._grid = [[Tile(x, y) for x in range(width)] for y in range(height)]
self._start_tile = None
self._exit_tile = None
@property
def width(self):
return self._w
@property
def height(self):
return self._h
@property
def start(self):
return self._start_tile
@property
def exit(self):
return self._exit_tile
def get_tile(self, x, y):
if 0 <= x < self._w and 0 <= y < self._h:
return self._grid[y][x]
return None
def set_tile_type(self, x, y, kind):
tile = self.get_tile(x, y)
if tile is None:
return
if kind == 'wall':
tile.blocked = True
elif kind == 'start':
if self._start_tile:
self._start_tile.is_start = False
tile.is_start = True
tile.blocked = False
self._start_tile = tile
elif kind == 'exit':
if self._exit_tile:
self._exit_tile.is_exit = False
tile.is_exit = True
tile.blocked = False
self._exit_tile = tile
elif kind == 'path':
tile.blocked = False
def neighbours(self, tile):
"""Возвращает список проходимых соседей"""
result = []
directions = [(0, -1), (0, 1), (-1, 0), (1, 0)] # вверх, вниз, влево, вправо
for dx, dy in directions:
nx, ny = tile.x + dx, tile.y + dy
nb = self.get_tile(nx, ny)
if nb and nb.can_step():
result.append(nb)
return result
# ---------- Загрузка лабиринта ----------
class MazeLoader:
def load(self, filename):
raise NotImplementedError
class TxtMazeLoader(MazeLoader):
def load(self, filename):
with open(filename, 'r') as f:
lines = [line.rstrip('\n') for line in f.readlines()]
h = len(lines)
w = max(len(line) for line in lines) if h > 0 else 0
start_cnt = 0
exit_cnt = 0
lab = Labyrinth(w, h)
for y, line in enumerate(lines):
for x, ch in enumerate(line):
if ch == "#":
lab.set_tile_type(x, y, "wall")
elif ch == "S":
lab.set_tile_type(x, y, "start")
start_cnt += 1
elif ch == "E":
lab.set_tile_type(x, y, "exit")
exit_cnt += 1
else:
lab.set_tile_type(x, y, 'path')
if start_cnt != 1 or exit_cnt != 1:
raise ValueError(f"Maze error: S={start_cnt}, E={exit_cnt} (need exactly one each)")
return lab
# ---------- Стратегии поиска пути ----------
class SearchStrategy:
def find_path(self, lab, start, goal):
raise NotImplementedError
def _rebuild_path(self, came_from, start, goal):
path = []
cur = goal
while cur is not None:
path.append(cur)
cur = came_from.get(cur)
path.reverse()
return path
def visited_cells(self):
return getattr(self, '_visited', 0)
class BFS(SearchStrategy):
def find_path(self, lab, start, goal):
q = deque()
q.append(start)
parent = {start: None}
visited = {start}
while q:
cur = q.popleft()
if cur == goal:
self._visited = len(visited)
return self._rebuild_path(parent, start, goal)
for nb in lab.neighbours(cur):
if nb not in visited:
visited.add(nb)
parent[nb] = cur
q.append(nb)
self._visited = len(visited)
return []
class DFS(SearchStrategy):
def find_path(self, lab, start, goal):
stack = [start]
parent = {start: None}
visited = {start}
while stack:
cur = stack.pop()
if cur == goal:
self._visited = len(visited)
return self._rebuild_path(parent, start, goal)
for nb in lab.neighbours(cur):
if nb not in visited:
visited.add(nb)
parent[nb] = cur
stack.append(nb)
self._visited = len(visited)
return []
class AStar(SearchStrategy):
def _heuristic(self, a, b):
return abs(a.x - b.x) + abs(a.y - b.y)
def find_path(self, lab, start, goal):
heap = []
counter = 0
start_f = self._heuristic(start, goal)
heapq.heappush(heap, (start_f, counter, start))
counter += 1
parent = {}
g = {start: 0}
f = {start: start_f}
visited = set()
while heap:
cur_f, _, cur = heapq.heappop(heap)
visited.add(cur)
if cur == goal:
self._visited = len(visited)
return self._rebuild_path(parent, start, goal)
if cur_f > f.get(cur, float('inf')):
continue
for nb in lab.neighbours(cur):
new_g = g[cur] + 1
if new_g < g.get(nb, float('inf')):
parent[nb] = cur
g[nb] = new_g
new_f = new_g + self._heuristic(nb, goal)
f[nb] = new_f
heapq.heappush(heap, (new_f, counter, nb))
counter += 1
self._visited = len(visited)
return []
# ---------- Статистика ----------
class SearchStats:
def __init__(self, time_ms, visited, path_len):
self.time_ms = time_ms
self.visited_cells = visited
self.path_length = path_len
# ---------- Наблюдатель ----------
class Observer:
def notify(self, event, data):
raise NotImplementedError
class ConsoleDisplay(Observer):
def __init__(self, player=None):
self._last_path = None
self._player = player
def notify(self, event, data):
if event == "maze_loaded":
self._draw_maze(data)
elif event == "path_found":
self._last_path = data
self._show_path(data)
elif event == "player_moved":
self._draw_maze_with_player(data)
def _draw_maze(self, lab):
os.system('cls' if os.name == 'nt' else 'clear')
print("=" * (lab.width * 2 + 4))
print(" LABYRINTH")
print("=" * (lab.width * 2 + 4))
for y in range(lab.height):
print(" ", end='')
for x in range(lab.width):
cell = lab.get_tile(x, y)
if cell == lab.start:
print('S', end=' ')
elif cell == lab.exit:
print('E', end=' ')
elif cell.is_wall:
print('#', end=' ')
else:
print('.', end=' ')
print()
print("=" * (lab.width * 2 + 4))
print(" S - start E - exit # - wall . - free")
def _draw_maze_with_player(self, lab):
os.system('cls' if os.name == 'nt' else 'clear')
print("=" * (lab.width * 2 + 4))
print(" LABYRINTH (P = player)")
print("=" * (lab.width * 2 + 4))
for y in range(lab.height):
print(" ", end='')
for x in range(lab.width):
cell = lab.get_tile(x, y)
if self._player and cell == self._player.position:
print('P', end=' ')
elif cell == lab.start:
print('S', end=' ')
elif cell == lab.exit:
print('E', end=' ')
elif cell.is_wall:
print('#', end=' ')
else:
print('.', end=' ')
print()
print("=" * (lab.width * 2 + 4))
print(f" Player at: ({self._player.position.x}, {self._player.position.y})")
print(" S - start E - exit # - wall . - free P - player")
def _show_path(self, path):
if not path:
print("\n No route found!")
return
print(f"\n Route found! Length = {len(path)}")
# ---------- Игрок и команды ----------
class Player:
def __init__(self, start_cell, lab):
self._pos = start_cell
self._prev = None
self._lab = lab
@property
def position(self):
return self._pos
def move(self, target):
if target and target.can_step():
self._prev = self._pos
self._pos = target
return True
return False
def undo(self):
if self._prev:
self._pos, self._prev = self._prev, None
return True
return False
class Action:
def execute(self):
raise NotImplementedError
def revert(self):
raise NotImplementedError
class MoveAction(Action):
def __init__(self, player, direction, lab):
self._player = player
self._dx, self._dy = direction
self._lab = lab
self._done = False
def execute(self):
new_x = self._player.position.x + self._dx
new_y = self._player.position.y + self._dy
target = self._lab.get_tile(new_x, new_y)
if target and target.can_step():
self._player.move(target)
self._done = True
return True
return False
def revert(self):
if self._done:
self._player.undo()
self._done = False
return True
return False
# ---------- Решатель лабиринта ----------
class LabyrinthSolver:
def __init__(self, lab):
self._lab = lab
self._strategy = None
self._watchers = []
def attach(self, observer):
self._watchers.append(observer)
def _broadcast(self, event, data):
for obs in self._watchers:
obs.notify(event, data)
def set_algorithm(self, strategy):
self._strategy = strategy
def solve(self):
if self._strategy is None:
return None
t0 = time.perf_counter()
path = self._strategy.find_path(self._lab, self._lab.start, self._lab.exit)
t1 = time.perf_counter()
elapsed_ms = (t1 - t0) * 1000
self._broadcast("path_found", path)
return SearchStats(elapsed_ms, self._strategy.visited_cells(), len(path))
def run_experiment(maze_file, algorithm, runs=5):
loader = TxtMazeLoader()
lab = loader.load(maze_file)
total_time = 0.0
total_visited = 0
total_length = 0
for _ in range(runs):
solver = LabyrinthSolver(lab)
solver.set_algorithm(algorithm)
stats = solver.solve()
if stats:
total_time += stats.time_ms
total_visited += stats.visited_cells
total_length += stats.path_length
return {
'time_ms': total_time / runs,
'visited_cells': total_visited / runs,
'path_length': total_length / runs
}
# ---------- Точка входа ----------
if __name__ == "__main__":
if len(sys.argv) > 1 and sys.argv[1] == 'experiment':
print("Running experiments...")
sys.exit(0)
loader = TxtMazeLoader()
lab = loader.load("maze1.txt")
player = Player(lab.start, lab)
view = ConsoleDisplay(player)
view.notify("maze_loaded", lab)
solver = LabyrinthSolver(lab)
solver.attach(view)
print("\n CONTROLS:")
print(" H (left) J (down) K (up) L (right)")
print(" U - undo Q - quit")
print("\n AUTO SEARCH:")
print(" B - BFS D - DFS A - A*")
print("\n" + "=" * 50)
history = []
while True:
cmd = input("\n Command > ").lower()
if cmd == 'q':
print("\n Goodbye!")
break
elif cmd == 'b':
solver.set_algorithm(BFS())
stats = solver.solve()
print(f"\n BFS: time={stats.time_ms:.3f}ms, visited={stats.visited_cells}, length={stats.path_length}")
elif cmd == 'd':
solver.set_algorithm(DFS())
stats = solver.solve()
print(f"\n DFS: time={stats.time_ms:.3f}ms, visited={stats.visited_cells}, length={stats.path_length}")
elif cmd == 'a':
solver.set_algorithm(AStar())
stats = solver.solve()
print(f"\n A*: time={stats.time_ms:.3f}ms, visited={stats.visited_cells}, length={stats.path_length}")
elif cmd in ['h', 'j', 'k', 'l']:
dir_map = {'h': (-1, 0), 'l': (1, 0), 'k': (0, -1), 'j': (0, 1)}
action = MoveAction(player, dir_map[cmd], lab)
if action.execute():
history.append(action)
view.notify("player_moved", lab)
if player.position == lab.exit:
print("\n *** VICTORY! EXIT REACHED ***")
print(f" Moves made: {len(history)}")
break
else:
print("\n Blocked by wall!")
elif cmd == 'u':
if history:
act = history.pop()
act.revert()
view.notify("player_moved", lab)
print("\n Undo successful")
else:
print("\n Nothing to undo")
else:
print("\n Unknown command. Use h,j,k,l to move, u to undo, q to quit")
print("\n Game over. Thanks for playing!")

View File

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

View File

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

View File

@ -0,0 +1,21 @@
####################
#S.................#
#.####.###########.#
#.#..#.#.........#.#
#.#.##.#.#######.#.#
#.#....#.#.....#.#.#
#.######.#.###.#.#.#
#........#.#...#.#.#
##########.#.###.#.#
#..........#.....#.#
#.################.#
#.#..............#.#
#.#.############.#.#
#.#.#..........#.#.#
#.#.#.########.#.#.#
#...#........#...#.#
#.###########.###.#.#
#.................#.#
#.#################.#
#E.................#
####################

View File

@ -0,0 +1,15 @@
S...............
................
................
................
................
................
................
................
................
................
................
................
................
...............E
................

View File

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

View File

@ -0,0 +1,370 @@
import sys
import csv
from collections import deque
import heapq
import time
import matplotlib.pyplot as plt
import numpy as np
# ---------- Модель ----------
class Node:
def __init__(self, x, y):
self.x = x
self.y = y
self.wall = False
self.start_flag = False
self.exit_flag = False
@property
def is_wall(self):
return self.wall
@is_wall.setter
def is_wall(self, val):
self.wall = val
@property
def is_start(self):
return self.start_flag
@is_start.setter
def is_start(self, val):
self.start_flag = val
@property
def is_exit(self):
return self.exit_flag
@is_exit.setter
def is_exit(self, val):
self.exit_flag = val
def passable(self):
return not self.wall
class Grid:
def __init__(self, w, h):
self.w = w
self.h = h
self.cells = [[Node(x, y) for x in range(w)] for y in range(h)]
self.start_node = None
self.exit_node = None
def get(self, x, y):
if 0 <= x < self.w and 0 <= y < self.h:
return self.cells[y][x]
return None
def set_type(self, x, y, typ):
cell = self.get(x, y)
if not cell:
return
if typ == 'wall':
cell.is_wall = True
elif typ == 'start':
if self.start_node:
self.start_node.is_start = False
cell.is_start = True
cell.is_wall = False
self.start_node = cell
elif typ == 'exit':
if self.exit_node:
self.exit_node.is_exit = False
cell.is_exit = True
cell.is_wall = False
self.exit_node = cell
elif typ == 'path':
cell.is_wall = False
def neighbors(self, node):
res = []
dirs = [(0, -1), (0, 1), (-1, 0), (1, 0)]
for dx, dy in dirs:
nx, ny = node.x + dx, node.y + dy
nb = self.get(nx, ny)
if nb and nb.passable():
res.append(nb)
return res
class Loader:
def load(self, fname):
raise NotImplementedError
class TxtLoader(Loader):
def load(self, fname):
with open(fname, 'r') as f:
lines = [line.rstrip('\n') for line in f.readlines()]
h = len(lines)
w = max(len(line) for line in lines) if h > 0 else 0
start_cnt = 0
exit_cnt = 0
grid = Grid(w, h)
for y, line in enumerate(lines):
for x, ch in enumerate(line):
if ch == "#":
grid.set_type(x, y, "wall")
elif ch == "S":
grid.set_type(x, y, "start")
start_cnt += 1
elif ch == "E":
grid.set_type(x, y, "exit")
exit_cnt += 1
else:
grid.set_type(x, y, 'path')
if start_cnt != 1 or exit_cnt != 1:
raise ValueError(f"Bad maze: S={start_cnt}, E={exit_cnt}")
return grid
# ---------- Поисковые стратегии ----------
class SearchAlgo:
def search(self, grid, start, goal):
raise NotImplementedError
def _reconstruct(self, parent, start, goal):
path = []
cur = goal
while cur:
path.append(cur)
cur = parent.get(cur)
path.reverse()
return path
def visited_count(self):
return getattr(self, '_visited_num', 0)
class BFSAlgo(SearchAlgo):
def search(self, grid, start, goal):
q = deque([start])
parent = {start: None}
seen = {start}
while q:
cur = q.popleft()
if cur == goal:
self._visited_num = len(seen)
return self._reconstruct(parent, start, goal)
for nb in grid.neighbors(cur):
if nb not in seen:
seen.add(nb)
parent[nb] = cur
q.append(nb)
self._visited_num = len(seen)
return []
class DFSAlgo(SearchAlgo):
def search(self, grid, start, goal):
stack = [start]
parent = {start: None}
seen = {start}
while stack:
cur = stack.pop()
if cur == goal:
self._visited_num = len(seen)
return self._reconstruct(parent, start, goal)
for nb in grid.neighbors(cur):
if nb not in seen:
seen.add(nb)
parent[nb] = cur
stack.append(nb)
self._visited_num = len(seen)
return []
class AStarAlgo(SearchAlgo):
def _h(self, a, b):
return abs(a.x - b.x) + abs(a.y - b.y)
def search(self, grid, start, goal):
heap = []
cnt = 0
start_f = self._h(start, goal)
heapq.heappush(heap, (start_f, cnt, start))
cnt += 1
parent = {}
g_score = {start: 0}
f_score = {start: start_f}
seen = set()
while heap:
cur_f, _, cur = heapq.heappop(heap)
seen.add(cur)
if cur == goal:
self._visited_num = len(seen)
return self._reconstruct(parent, start, goal)
if cur_f > f_score.get(cur, float('inf')):
continue
for nb in grid.neighbors(cur):
tentative_g = g_score[cur] + 1
if tentative_g < g_score.get(nb, float('inf')):
parent[nb] = cur
g_score[nb] = tentative_g
new_f = tentative_g + self._h(nb, goal)
f_score[nb] = new_f
heapq.heappush(heap, (new_f, cnt, nb))
cnt += 1
self._visited_num = len(seen)
return []
class Solver:
def __init__(self, grid):
self.grid = grid
self.algo = None
def set_algo(self, algo):
self.algo = algo
def solve(self):
if not self.algo:
return None
t0 = time.perf_counter()
path = self.algo.search(self.grid, self.grid.start_node, self.grid.exit_node)
t1 = time.perf_counter()
elapsed = (t1 - t0) * 1000
return {
'time_ms': elapsed,
'visited_cells': self.algo.visited_count(),
'path_length': len(path)
}
def experiment(maze_file, algo, runs=5):
loader = TxtLoader()
grid = loader.load(maze_file)
total_t = 0.0
total_v = 0
total_l = 0
for _ in range(runs):
s = Solver(grid)
s.set_algo(algo)
stats = s.solve()
if stats:
total_t += stats['time_ms']
total_v += stats['visited_cells']
total_l += stats['path_length']
return {
'time_ms': total_t / runs,
'visited_cells': total_v / runs,
'path_length': total_l / runs
}
def make_plots(results):
mazes = list(set(r['maze'] for r in results))
algos = ['BFS', 'DFS', 'AStar']
fig, axes = plt.subplots(1, 3, figsize=(15, 5))
x = np.arange(len(mazes))
width = 0.25
# Время
for i, algo in enumerate(algos):
times = []
for m in mazes:
val = next((r['time_ms'] for r in results if r['maze'] == m and r['strategy'] == algo), 0)
times.append(val)
axes[0].bar(x + i * width, times, width, label=algo)
axes[0].set_xlabel('Maze')
axes[0].set_ylabel('Time (ms)')
axes[0].set_title('Execution time')
axes[0].set_xticks(x + width)
axes[0].set_xticklabels(mazes, rotation=45, ha='right')
axes[0].legend()
axes[0].grid(True, alpha=0.3)
# Посещённые клетки
for i, algo in enumerate(algos):
visited = []
for m in mazes:
val = next((r['visited_cells'] for r in results if r['maze'] == m and r['strategy'] == algo), 0)
visited.append(val)
axes[1].bar(x + i * width, visited, width, label=algo)
axes[1].set_xlabel('Maze')
axes[1].set_ylabel('Visited cells')
axes[1].set_title('Visited cells comparison')
axes[1].set_xticks(x + width)
axes[1].set_xticklabels(mazes, rotation=45, ha='right')
axes[1].legend()
axes[1].grid(True, alpha=0.3)
# Длина пути
for i, algo in enumerate(algos):
lengths = []
for m in mazes:
val = next((r['path_length'] for r in results if r['maze'] == m and r['strategy'] == algo), 0)
lengths.append(val)
axes[2].bar(x + i * width, lengths, width, label=algo)
axes[2].set_xlabel('Maze')
axes[2].set_ylabel('Path length')
axes[2].set_title('Path length comparison')
axes[2].set_xticks(x + width)
axes[2].set_xticklabels(mazes, rotation=45, ha='right')
axes[2].legend()
axes[2].grid(True, alpha=0.3)
plt.tight_layout()
plt.savefig('performance_plot.png', dpi=150, bbox_inches='tight')
plt.show()
if __name__ == "__main__":
test_mazes = [
("maze1.txt", "Small 10x6"),
("maze10x10.txt", "Medium 10x10"),
("maze20x20.txt", "Large 20x20"),
("maze_empty.txt", "Empty 15x15"),
("maze_no_exit.txt", "No exit 10x10")
]
algorithms = [
("BFS", BFSAlgo()),
("DFS", DFSAlgo()),
("AStar", AStarAlgo())
]
results = []
for fname, name in test_mazes:
print(f"Benchmarking {name}...")
for algo_name, algo in algorithms:
try:
stat = experiment(fname, algo, runs=3)
results.append({
'maze': name,
'strategy': algo_name,
'time_ms': stat['time_ms'],
'visited_cells': stat['visited_cells'],
'path_length': stat['path_length']
})
print(f" {algo_name}: time={stat['time_ms']:.3f}ms, visited={stat['visited_cells']:.0f}, length={stat['path_length']:.0f}")
except Exception as e:
print(f" {algo_name}: failed - {e}")
results.append({
'maze': name,
'strategy': algo_name,
'time_ms': -1,
'visited_cells': -1,
'path_length': -1
})
valid = [r for r in results if r['time_ms'] >= 0]
with open('experiment_data.csv', 'w', newline='', encoding='utf-8') as f:
writer = csv.DictWriter(f, fieldnames=['maze', 'strategy', 'time_ms', 'visited_cells', 'path_length'])
writer.writeheader()
writer.writerows(valid)
if valid:
make_plots(valid)
print("\nData saved to experiment_data.csv")
print("Plot saved to performance_plot.png")

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

View File

@ -0,0 +1,78 @@
# Лабораторная работа: Сравнение структур данных для телефонного справочника
## 1. Введение
В рамках работы были реализованы три структуры данных «с нуля» на языке Python без использования классов, в процедурной парадигме:
- **Связный список** — узлы хранятся в виде словарей `{'name', 'phone', 'next'}`.
- **Хеш-таблица** — массив фиксированного размера (1000 корзин), в каждой из которых хранится связный список (отдельные цепочки).
- **Двоичное дерево поиска (BST)** — узлы имеют поля `left`, `right`.
Для каждой структуры реализованы операции `insert`, `find`, `delete`, `list_all` (возвращает записи, отсортированные по имени).
Цель эксперимента — измерить производительность операций на наборе из **10000 записей** при двух режимах подачи данных: **случайный порядок** и **отсортированный по имени**. Каждый опыт повторялся 5 раз, результаты усреднены. Измерялось общее время:
- вставки всех 10000 записей;
- поиска 110 записей (100 существующих + 10 несуществующих);
- удаления 50 случайных записей.
## 2. Результаты измерений
В таблице приведены средние значения времени (в секундах) для каждой структуры и режима.
| Структура | Режим | Вставка (с) | Поиск (с) | Удаление (с) |
|----------------|-------------|------------|-----------|--------------|
| **LinkedList** | случайный | 4.1342 | 0.0282 | 0.0138 |
| LinkedList | сортир. | 3.7426 | 0.0248 | 0.0116 |
| **HashTable** | случайный | 0.00940 | 0.000075 | 0.000037 |
| HashTable | сортир. | 0.00915 | 0.000070 | 0.000032 |
| **BST** | случайный | 0.02396 | 0.000216 | 0.000126 |
| BST | сортир. | 9.1117 | 0.0796 | 0.0512 |
*Графическое представление результатов* приведено на рисунке `performance_comparison.png`, где для каждой операции построены столбчатые диаграммы с группировкой по структурам и режимам.
## 3. Анализ результатов
### 3.1. Влияние порядка данных на BST
Двоичное дерево поиска чувствительно к порядку поступления ключей. При вставке в отсортированном порядке дерево вырождается в линейный список (все узлы уходят в правое поддерево). Высота становится O(n), что приводит к резкому падению производительности:
- Вставка на отсортированных данных (9.11 с) **медленнее в 380 раз**, чем на случайных (0.024 с).
- Поиск замедляется в ~370 раз, удаление — в ~406 раз.
Фактически BST на отсортированных данных работает хуже даже связного списка из‑за рекурсивных вызовов и накладных расходов.
### 3.2. Устойчивость хеш-таблицы к порядку
Хеш-функция равномерно распределяет имена по корзинам вне зависимости от порядка поступления. Поэтому времена вставки, поиска и удаления практически идентичны для случайного и отсортированного режимов:
- Вставка: 0.00940 с (случ.) vs 0.00915 с (сорт.) — разница в пределах погрешности.
- Поиск и удаление также стабильны.
Средняя сложность O(1) подтверждается на практике.
### 3.3. Связный список — линейная сложность на всех операциях
Связный список не обеспечивает прямого доступа к элементам. Для поиска, обновления или удаления требуется последовательный проход, что даёт O(n).
- Вставка 10000 элементов занимает около 4 секунд (даже больше, чем BST на случайных данных).
- Поиск (~0.028 с) на порядок медленнее, чем в хеш-таблице и BST на случайных данных.
- Порядок входных данных почти не влияет на производительность (разница менее 10%), так как в любом случае приходится обходить список до конца для вставки новых уникальных имён.
### 3.4. Сравнение удаления
- **Связный список**: удаление требует сначала найти элемент (O(n)), затем переставить ссылки. Время ~0.0120.014 с, что близко ко времени поиска.
- **Хеш-таблица**: удаление за O(1) в среднем — достаточно вычислить хеш и удалить из короткого списка корзины. Время ~0.000030.00004 с.
- **BST**: на случайных данных удаление очень быстрое (0.000126 с) благодаря логарифмической высоте. На отсортированных данных время возрастает до 0.051 с (деградация до O(n)).
## 4. Выводы и рекомендации
На основе полученных результатов можно сделать следующие выводы о применимости структур в реальных задачах:
- **Хеш-таблица** — лучший выбор, когда требуется максимальная скорость всех операций (вставка, поиск, удаление) и не важен порядок хранения. Она стабильна, не чувствительна к порядку входных данных и показывает среднее время O(1). Идеальна для реализации словарей, кэшей, индексов по ключу.
- **Двоичное дерево поиска** — подходит, когда необходимо часто получать данные в отсортированном виде (например, вывод справочника по алфавиту) и гарантируется, что данные не будут поступать в отсортированном порядке (иначе дерево вырождается). В реальных проектах вместо простого BST следует использовать самобалансирующиеся деревья (AVL, красно-чёрные), которые сохраняют логарифмическую высоту при любых порядках. В эксперименте BST на случайных данных показал отличные результаты, близкие к хеш-таблице.
- **Связный список** — из‑за линейной сложности основных операций непригоден для хранения больших объёмов данных (тысячи и более записей). Может применяться лишь для очень маленьких коллекций, при частых вставках в начало (здесь не рассматривалось) или в учебных целях.
Таким образом, для телефонного справочника с 10000 записей наиболее эффективной является **хеш-таблица**, обеспечивающая мгновенный доступ по имени. Если же требуется ещё и алфавитный вывод без дополнительной сортировки, стоит использовать **сбалансированное дерево поиска**.

View File

@ -0,0 +1,125 @@
# Лабораторная работа: Поиск выхода из лабиринта
## 1. Постановка задачи
Разработать программу для загрузки лабиринта из текстового файла, поиска пути от стартовой клетки до выхода с возможностью выбора алгоритма поиска, визуализации процесса и экспериментального сравнения эффективности алгоритмов.
### Основные требования
- Реализовать модель лабиринта (классы `Cell`, `Maze`)
- Реализовать загрузку лабиринта из файла с символами `#` (стена), `S` (старт), `E` (выход)
- Реализовать три алгоритма поиска пути: BFS, DFS, A*
- Реализовать класс-оркестратор `MazeSolver` с возможностью смены стратегии
- Собрать статистику: время выполнения, количество посещённых клеток, длина пути
- Провести эксперименты на лабиринтах разной сложности
### Использованные паттерны проектирования GoF
| Паттерн | Где используется | Преимущества |
|---------|----------------|---------------|
| **Builder** | `MazeBuilder`, `TextFileMazeBuilder` | Скрывает детали парсинга, позволяет легко добавлять новые форматы файлов |
| **Strategy** | `PathFindingStrategy`, `BFSStrategy`, `DFSStrategy`, `AStarStrategy` | Позволяет динамически менять алгоритм поиска, упрощает добавление новых |
| **Observer** | `Observer`, `ConsoleView` | Отделяет отображение от логики, легко добавить новые виды вывода |
| **Command** | `Command`, `MoveCommand` | Реализует пошаговое перемещение с возможностью отмены (undo/redo) |
## 2. Архитектура приложения
Основные компоненты:
- **Модель** `Cell`, `Maze` (хранение сетки, проверка стен, получение соседей)
- **Загрузка** `MazeBuilder`, `TextFileMazeBuilder` (парсинг `.txt`‑файлов)
- **Алгоритмы** `BFSStrategy`, `DFSStrategy`, `AStarStrategy` (реализация поиска пути)
- **Оркестрация** `MazeSolver` (управление стратегией, сбор статистики, уведомление наблюдателей)
- **Визуализация** `ConsoleView` (отрисовка лабиринта, игрока, пути)
- **Интерактив** `Player`, `MoveCommand` (перемещение, история ходов)
## 3. Реализация алгоритмов поиска пути
| Алгоритм | Структура данных | Гарантия кратчайшего пути | Особенности |
|----------|-----------------|---------------------------|-------------|
| **BFS** | Очередь (`deque`) | Да | Обходит лабиринт по слоям, гарантирует минимум шагов |
| **DFS** | Стек | Нет | Углубляется до конца, затем возвращается; экономичен по памяти |
| **A*** | Приоритетная очередь (`heapq`) + эвристика | Да (при допустимой эвристике) | Использует манхэттенское расстояние, обычно быстрее BFS |
## 4. Экспериментальная часть
### Тестовые лабиринты
| Имя | Размер | Описание |
|-----|--------|----------|
| `Small 10x6` | 10×6 | Простой лабиринт из условия |
| `Medium 10x10` | 10×10 | Лабиринт среднего размера со случайными стенами |
| `Large 20x20` | 20×20 | Большой запутанный лабиринт |
| `Empty 15x15` | 15×15 | Пустой лабиринт (без стен) |
| `No exit 10x10` | 10×10 | Лабиринт без достижимого выхода |
Каждый алгоритм запускался **3 раза** на каждом лабиринте, результаты усреднены.
### Результаты замеров
| Лабиринт | Алгоритм | Время (мс) | Посещено клеток | Длина пути |
|----------|----------|------------|-----------------|------------|
| Small 10x6 | BFS | 0.025 | 9 | 5 |
| Small 10x6 | DFS | 0.041 | 26 | 19 |
| Small 10x6 | A* | 0.015 | 5 | 5 |
| Medium 10x10 | BFS | 0.016 | 18 | 8 |
| Medium 10x10 | DFS | 0.008 | 9 | 8 |
| Medium 10x10 | A* | 0.015 | 8 | 8 |
| Large 20x20 | BFS | 0.136 | 116 | 69 |
| Large 20x20 | DFS | 0.159 | 173 | 69 |
| Large 20x20 | A* | 0.198 | 110 | 69 |
| Empty 15x15 | BFS | 0.255 | 240 | 29 |
| Empty 15x15 | DFS | 0.142 | 224 | 119 |
| Empty 15x15 | A* | 0.590 | 224 | 29 |
| No exit 10x10 | BFS | 0.042 | 36 | 0 |
| No exit 10x10 | DFS | 0.035 | 36 | 0 |
| No exit 10x10 | A* | 0.065 | 36 | 0 |
### Графики
![Сравнение производительности алгоритмов](performance_plot.png)
На графике представлено сравнение трёх алгоритмов по трём метрикам: время выполнения, количество посещённых клеток и длина найденного пути.
## 5. Анализ результатов
### Сравнение характеристик
- **BFS**
- Гарантирует кратчайший путь (во всех лабиринтах, где путь существует, длина совпадает с A*).
- Посещает довольно много клеток (например, 240 в пустом лабиринте).
- Время стабильно, но на больших лабиринтах уступает DFS по скорости.
- **DFS**
- Самый быстрый на средних и больших лабиринтах (0.0080.159 мс).
- Не находит кратчайший путь: в пустом лабиринте длина пути 119 вместо 29.
- Посещает среднее количество клеток (224 в пустом, 173 в большом).
- **A***
- Всегда находит оптимальный путь (как BFS).
- Посещает **наименьшее** число клеток среди всех алгоритмов (5 в маленьком лабиринте, 110 в большом).
- Время работы на пустом лабиринте выше из‑за накладных расходов на эвристику и приоритетную очередь (0.590 мс против 0.142 мс у DFS).
- На сложных лабиринтах (Large 20x20) время сравнимо с BFS и даже немного больше из‑за более сложных операций с кучей.
### Ключевые выводы
1. **A* показывает лучший баланс** между оптимальностью пути и количеством посещённых клеток. Он особенно эффективен, когда требуется минимальное разрастание поиска.
2. **DFS самый быстрый**, если не важна длина пути (например, для проверки существования выхода).
3. **BFS** остаётся простым и предсказуемым, но уступает A* по числу посещений.
4. В лабиринте без выхода все алгоритмы корректно обходят всю достижимую область (36 клеток) и возвращают пустой путь.
### Рекомендации по выбору алгоритма
| Сценарий | Рекомендуемый алгоритм |
|----------|------------------------|
| Нужен **гарантированно кратчайший путь** и не важна скорость | BFS или A* (A* предпочтительнее) |
| **Скорость критична**, путь может быть неоптимальным | DFS |
| Нужен **компромисс** (оптимальность + мало посещений) | A* |
| Проверка **существования пути** (без восстановления маршрута) | DFS |
## 6. Заключение
Применение паттернов проектирования (Builder, Strategy, Observer, Command) позволило создать гибкую, расширяемую и легко тестируемую программу. Реализованные алгоритмы поиска пути были экспериментально сравнены на лабиринтах разной сложности. Полученные результаты подтверждают теоретические оценки: A* даёт наилучшее сочетание оптимальности и эффективности по числу посещённых клеток, DFS выигрывает по скорости, а BFS остаётся простым базовым решением.
Программа также предоставляет интерактивный режим с ручным управлением игроком и возможностью отмены ходов.