forked from UNN/2026-rff_mp
Merge pull request '[1] 1-st-exercise' (#265) from KuznetsovYuM/2026-rff_mp:1-st-exercise into develop
Reviewed-on: UNN/2026-rff_mp#265
This commit is contained in:
commit
98d32c7cf1
278
KuznetsovYuM/docs/data/1-st-exercise/phonebook_structures.py
Normal file
278
KuznetsovYuM/docs/data/1-st-exercise/phonebook_structures.py
Normal file
|
|
@ -0,0 +1,278 @@
|
|||
def linked_list_add(head, name, phone):
|
||||
curr = head
|
||||
while curr is not None:
|
||||
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'] is not None:
|
||||
curr = curr['next']
|
||||
curr['next'] = new_node
|
||||
return head
|
||||
|
||||
|
||||
def linked_list_find(head, name):
|
||||
curr = head
|
||||
while curr is not None:
|
||||
if curr['name'] == name:
|
||||
return curr['phone']
|
||||
curr = curr['next']
|
||||
return None
|
||||
|
||||
|
||||
def linked_list_remove(head, name):
|
||||
if head is None:
|
||||
return None
|
||||
if head['name'] == name:
|
||||
return head['next']
|
||||
prev = head
|
||||
curr = head['next']
|
||||
while curr is not None:
|
||||
if curr['name'] == name:
|
||||
prev['next'] = curr['next']
|
||||
return head
|
||||
prev = curr
|
||||
curr = curr['next']
|
||||
return head
|
||||
|
||||
|
||||
def linked_list_collect_all(head):
|
||||
records = []
|
||||
curr = head
|
||||
while curr is not None:
|
||||
records.append((curr['name'], curr['phone']))
|
||||
curr = curr['next']
|
||||
records.sort(key=lambda pair: pair[0])
|
||||
return records
|
||||
|
||||
|
||||
|
||||
#HASH
|
||||
def _hash_bucket_index(key, table_size):
|
||||
return hash(key) % table_size
|
||||
|
||||
|
||||
def hash_table_create(bucket_count=10):
|
||||
return [None] * bucket_count
|
||||
|
||||
|
||||
def hash_table_put(table, name, phone):
|
||||
idx = _hash_bucket_index(name, len(table))
|
||||
table[idx] = linked_list_add(table[idx], name, phone)
|
||||
return table
|
||||
|
||||
|
||||
def hash_table_get(table, name):
|
||||
idx = _hash_bucket_index(name, len(table))
|
||||
return linked_list_find(table[idx], name)
|
||||
|
||||
|
||||
def hash_table_remove(table, name):
|
||||
idx = _hash_bucket_index(name, len(table))
|
||||
table[idx] = linked_list_remove(table[idx], name)
|
||||
return table
|
||||
|
||||
|
||||
def hash_table_collect_all(table):
|
||||
all_records = []
|
||||
for head in table:
|
||||
curr = head
|
||||
while curr is not None:
|
||||
all_records.append((curr['name'], curr['phone']))
|
||||
curr = curr['next']
|
||||
all_records.sort(key=lambda pair: pair[0])
|
||||
return all_records
|
||||
|
||||
|
||||
#BST
|
||||
def _bst_new_node(name, phone):
|
||||
return {'name': name, 'phone': phone, 'left': None, 'right': None}
|
||||
|
||||
|
||||
def bst_add(root, name, phone):
|
||||
"""Insert or update. Returns (possibly new) root."""
|
||||
if root is None:
|
||||
return _bst_new_node(name, phone)
|
||||
|
||||
if name == root['name']:
|
||||
root['phone'] = phone
|
||||
elif name < root['name']:
|
||||
root['left'] = bst_add(root['left'], name, phone)
|
||||
else:
|
||||
root['right'] = bst_add(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_minimum(node):
|
||||
while node['left'] is not None:
|
||||
node = node['left']
|
||||
return node
|
||||
|
||||
|
||||
def bst_remove(root, name):
|
||||
if root is None:
|
||||
return None
|
||||
|
||||
if name < root['name']:
|
||||
root['left'] = bst_remove(root['left'], name)
|
||||
elif name > root['name']:
|
||||
root['right'] = bst_remove(root['right'], name)
|
||||
else:
|
||||
if root['left'] is None:
|
||||
return root['right']
|
||||
if root['right'] is None:
|
||||
return root['left']
|
||||
|
||||
successor = _bst_find_minimum(root['right'])
|
||||
root['name'] = successor['name']
|
||||
root['phone'] = successor['phone']
|
||||
root['right'] = bst_remove(root['right'], successor['name'])
|
||||
return root
|
||||
|
||||
|
||||
def bst_collect_inorder(root):
|
||||
result = []
|
||||
def inorder(node):
|
||||
if node is None:
|
||||
return
|
||||
inorder(node['left'])
|
||||
result.append((node['name'], node['phone']))
|
||||
inorder(node['right'])
|
||||
inorder(root)
|
||||
return result
|
||||
|
||||
|
||||
|
||||
|
||||
#Benchmarking
|
||||
import random
|
||||
import time
|
||||
import csv
|
||||
import os
|
||||
import sys
|
||||
|
||||
sys.setrecursionlimit(20000)
|
||||
|
||||
def generate_test_data(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_ordered_and_shuffled(records):
|
||||
shuffled = records.copy()
|
||||
random.shuffle(shuffled)
|
||||
sorted_records = sorted(records, key=lambda x: x[0])
|
||||
return shuffled, sorted_records
|
||||
|
||||
def measure_operations(struct_ops, records, mode_name, repeats=5):
|
||||
results = []
|
||||
for rep in range(repeats):
|
||||
ds = struct_ops['create']()
|
||||
|
||||
start = time.perf_counter()
|
||||
for name, phone in records:
|
||||
ds = struct_ops['insert'](ds, name, phone)
|
||||
insert_time = time.perf_counter() - start
|
||||
|
||||
existing_names = [name for name, _ in records]
|
||||
sample_existing = random.sample(existing_names, 100)
|
||||
nonexistent = [f"Missing_{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_ops['find'](ds, name)
|
||||
find_time = time.perf_counter() - start
|
||||
|
||||
to_delete = random.sample(existing_names, 50)
|
||||
start = time.perf_counter()
|
||||
for name in to_delete:
|
||||
ds = struct_ops['delete'](ds, name)
|
||||
delete_time = time.perf_counter() - start
|
||||
|
||||
results.append({
|
||||
'structure': struct_ops['name'],
|
||||
'mode': mode_name,
|
||||
'repetition': rep+1,
|
||||
'insert_time': insert_time,
|
||||
'find_time': find_time,
|
||||
'delete_time': delete_time
|
||||
})
|
||||
return results
|
||||
|
||||
def run_full_benchmark():
|
||||
N = 10000
|
||||
base_records = generate_test_data(N)
|
||||
shuffled, sorted_records = prepare_ordered_and_shuffled(base_records)
|
||||
|
||||
structures = {
|
||||
'LinkedList': {
|
||||
'name': 'LinkedList',
|
||||
'create': lambda: None,
|
||||
'insert': linked_list_add,
|
||||
'find': linked_list_find,
|
||||
'delete': linked_list_remove,
|
||||
},
|
||||
'HashTable': {
|
||||
'name': 'HashTable',
|
||||
'create': lambda: hash_table_create(100),
|
||||
'insert': hash_table_put,
|
||||
'find': hash_table_get,
|
||||
'delete': hash_table_remove,
|
||||
},
|
||||
'BST': {
|
||||
'name': 'BST',
|
||||
'create': lambda: None,
|
||||
'insert': bst_add,
|
||||
'find': bst_find,
|
||||
'delete': bst_remove,
|
||||
}
|
||||
}
|
||||
|
||||
all_results = []
|
||||
for name, ops in structures.items():
|
||||
print(f"Benchmarking {name} on random order...")
|
||||
all_results.extend(measure_operations(ops, shuffled, 'random', repeats=5))
|
||||
print(f"Benchmarking {name} on sorted order...")
|
||||
all_results.extend(measure_operations(ops, sorted_records, 'sorted', repeats=5))
|
||||
|
||||
os.makedirs('docs/data', exist_ok=True)
|
||||
csv_path = 'docs/data/experiment_results.csv'
|
||||
with open(csv_path, '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(f"Experiment finished. Results saved to {csv_path}")
|
||||
|
||||
if __name__ == '__main__':
|
||||
run_full_benchmark()
|
||||
45
KuznetsovYuM/docs/data/1-st-exercise/visualize_results.py
Normal file
45
KuznetsovYuM/docs/data/1-st-exercise/visualize_results.py
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
import pandas as pd
|
||||
import matplotlib.pyplot as plt
|
||||
import numpy as np
|
||||
import os
|
||||
|
||||
csv_path = 'experiment_results.csv'
|
||||
if not os.path.exists(csv_path):
|
||||
print("Run phonebook_structures.py first to generate results.")
|
||||
exit(1)
|
||||
|
||||
df = pd.read_csv(csv_path)
|
||||
|
||||
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()
|
||||
print("Graph saved to performance_comparison.png")
|
||||
505
KuznetsovYuM/docs/data/2-nd-exercise/main.py
Normal file
505
KuznetsovYuM/docs/data/2-nd-exercise/main.py
Normal file
|
|
@ -0,0 +1,505 @@
|
|||
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 col(self):
|
||||
return self._col
|
||||
|
||||
@property
|
||||
def row(self):
|
||||
return self._row
|
||||
|
||||
@property
|
||||
def blocked(self):
|
||||
return self._blocked
|
||||
|
||||
@blocked.setter
|
||||
def blocked(self, value):
|
||||
self._blocked = value
|
||||
|
||||
@property
|
||||
def is_start(self):
|
||||
return self._is_start
|
||||
|
||||
@is_start.setter
|
||||
def is_start(self, value):
|
||||
self._is_start = value
|
||||
|
||||
@property
|
||||
def is_exit(self):
|
||||
return self._is_exit
|
||||
|
||||
@is_exit.setter
|
||||
def is_exit(self, value):
|
||||
self._is_exit = value
|
||||
|
||||
def passable(self):
|
||||
return not self._blocked
|
||||
|
||||
|
||||
class Labyrinth:
|
||||
def __init__(self, width, height):
|
||||
self._width = width
|
||||
self._height = 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._width
|
||||
|
||||
@property
|
||||
def height(self):
|
||||
return self._height
|
||||
|
||||
@property
|
||||
def start_tile(self):
|
||||
return self._start_tile
|
||||
|
||||
@property
|
||||
def exit_tile(self):
|
||||
return self._exit_tile
|
||||
|
||||
def get_tile(self, x, y):
|
||||
if 0 <= x < self._width and 0 <= y < self._height:
|
||||
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 neighbors_of(self, tile):
|
||||
result = []
|
||||
directions = [(0, -1), (0, 1), (-1, 0), (1, 0)]
|
||||
for dx, dy in directions:
|
||||
nx, ny = tile.col + dx, tile.row + dy
|
||||
nb = self.get_tile(nx, ny)
|
||||
if nb and nb.passable():
|
||||
result.append(nb)
|
||||
return result
|
||||
|
||||
|
||||
class LabyrinthLoader:
|
||||
def load(self, filepath):
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class TextFileLoader(LabyrinthLoader):
|
||||
def load(self, filepath):
|
||||
with open(filepath, '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_count = 0
|
||||
exit_count = 0
|
||||
lab = Labyrinth(w, h)
|
||||
|
||||
for row, line in enumerate(lines):
|
||||
for col, ch in enumerate(line):
|
||||
if ch == "#":
|
||||
lab.set_tile_type(col, row, "wall")
|
||||
elif ch == "S":
|
||||
lab.set_tile_type(col, row, "start")
|
||||
start_count += 1
|
||||
elif ch == "E":
|
||||
lab.set_tile_type(col, row, "exit")
|
||||
exit_count += 1
|
||||
else:
|
||||
lab.set_tile_type(col, row, "path")
|
||||
|
||||
if start_count != 1 or exit_count != 1:
|
||||
raise ValueError(f"Maze must have exactly one 'S' and one 'E'. Found: S={start_count}, E={exit_count}")
|
||||
return lab
|
||||
|
||||
|
||||
class SearchAlgorithm:
|
||||
def find_route(self, maze, start, goal):
|
||||
raise NotImplementedError
|
||||
|
||||
def _reconstruct(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 BreadthFirstSearch(SearchAlgorithm):
|
||||
def find_route(self, maze, start, goal):
|
||||
q = deque()
|
||||
q.append(start)
|
||||
parent = {start: None}
|
||||
seen = {start}
|
||||
|
||||
while q:
|
||||
current = q.popleft()
|
||||
if current == goal:
|
||||
self._visited = len(seen)
|
||||
return self._reconstruct(parent, start, goal)
|
||||
for nb in maze.neighbors_of(current):
|
||||
if nb not in seen:
|
||||
seen.add(nb)
|
||||
parent[nb] = current
|
||||
q.append(nb)
|
||||
self._visited = len(seen)
|
||||
return []
|
||||
|
||||
|
||||
class DepthFirstSearch(SearchAlgorithm):
|
||||
def find_route(self, maze, start, goal):
|
||||
stack = [start]
|
||||
parent = {start: None}
|
||||
seen = {start}
|
||||
|
||||
while stack:
|
||||
current = stack.pop()
|
||||
if current == goal:
|
||||
self._visited = len(seen)
|
||||
return self._reconstruct(parent, start, goal)
|
||||
for nb in maze.neighbors_of(current):
|
||||
if nb not in seen:
|
||||
seen.add(nb)
|
||||
parent[nb] = current
|
||||
stack.append(nb)
|
||||
self._visited = len(seen)
|
||||
return []
|
||||
|
||||
|
||||
class AStarSearch(SearchAlgorithm):
|
||||
def _heuristic(self, tile, goal):
|
||||
return abs(tile.col - goal.col) + abs(tile.row - goal.row)
|
||||
|
||||
def find_route(self, maze, 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}
|
||||
closed = set()
|
||||
|
||||
while heap:
|
||||
cur_f, _, cur = heapq.heappop(heap)
|
||||
closed.add(cur)
|
||||
|
||||
if cur == goal:
|
||||
self._visited = len(closed)
|
||||
return self._reconstruct(parent, start, goal)
|
||||
|
||||
if cur_f > f.get(cur, float('inf')):
|
||||
continue
|
||||
|
||||
for nb in maze.neighbors_of(cur):
|
||||
tentative_g = g[cur] + 1
|
||||
if tentative_g < g.get(nb, float('inf')):
|
||||
parent[nb] = cur
|
||||
g[nb] = tentative_g
|
||||
new_f = tentative_g + self._heuristic(nb, goal)
|
||||
f[nb] = new_f
|
||||
heapq.heappush(heap, (new_f, counter, nb))
|
||||
counter += 1
|
||||
|
||||
self._visited = len(closed)
|
||||
return []
|
||||
|
||||
|
||||
class SearchStats:
|
||||
def __init__(self, elapsed_ms, visited, path_len):
|
||||
self.elapsed_ms = elapsed_ms
|
||||
self.visited_cells = visited
|
||||
self.path_length = path_len
|
||||
|
||||
|
||||
class EventListener:
|
||||
def on_event(self, event_type, data):
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class TerminalView(EventListener):
|
||||
def __init__(self, player=None):
|
||||
self._current_path = None
|
||||
self._player = player
|
||||
|
||||
def on_event(self, event_type, data):
|
||||
if event_type == "maze_loaded":
|
||||
self._display_maze(data)
|
||||
elif event_type == "path_found":
|
||||
self._current_path = data
|
||||
self._display_path(data)
|
||||
elif event_type == "player_moved":
|
||||
self._display_maze_with_player(data)
|
||||
|
||||
def _display_maze(self, maze):
|
||||
os.system('cls' if os.name == 'nt' else 'clear')
|
||||
print("=" * (maze.width * 2 + 4))
|
||||
print(" LABYRINTH")
|
||||
print("=" * (maze.width * 2 + 4))
|
||||
|
||||
for y in range(maze.height):
|
||||
print(" ", end='')
|
||||
for x in range(maze.width):
|
||||
cell = maze.get_tile(x, y)
|
||||
if cell == maze.start_tile:
|
||||
print('S', end=' ')
|
||||
elif cell == maze.exit_tile:
|
||||
print('E', end=' ')
|
||||
elif cell.blocked:
|
||||
print('#', end=' ')
|
||||
else:
|
||||
print('.', end=' ')
|
||||
print()
|
||||
print("=" * (maze.width * 2 + 4))
|
||||
print(" S - start E - exit # - wall . - path")
|
||||
|
||||
def _display_maze_with_player(self, maze):
|
||||
os.system('cls' if os.name == 'nt' else 'clear')
|
||||
print("=" * (maze.width * 2 + 4))
|
||||
print(" LABYRINTH (P = player)")
|
||||
print("=" * (maze.width * 2 + 4))
|
||||
|
||||
for y in range(maze.height):
|
||||
print(" ", end='')
|
||||
for x in range(maze.width):
|
||||
cell = maze.get_tile(x, y)
|
||||
if self._player and cell == self._player.position:
|
||||
print('P', end=' ')
|
||||
elif cell == maze.start_tile:
|
||||
print('S', end=' ')
|
||||
elif cell == maze.exit_tile:
|
||||
print('E', end=' ')
|
||||
elif cell.blocked:
|
||||
print('#', end=' ')
|
||||
else:
|
||||
print('.', end=' ')
|
||||
print()
|
||||
print("=" * (maze.width * 2 + 4))
|
||||
print(f" Player at: ({self._player.position.col}, {self._player.position.row})")
|
||||
print(" S - start E - exit # - wall . - path P - player")
|
||||
|
||||
def _display_path(self, path):
|
||||
if not path:
|
||||
print("\n No route found!")
|
||||
else:
|
||||
print(f"\n Path found! Length = {len(path)}")
|
||||
|
||||
|
||||
class Player:
|
||||
def __init__(self, start_tile, labyrinth):
|
||||
self._pos = start_tile
|
||||
self._prev = None
|
||||
self._lab = labyrinth
|
||||
|
||||
@property
|
||||
def position(self):
|
||||
return self._pos
|
||||
|
||||
def move_to(self, new_tile):
|
||||
if new_tile and new_tile.passable():
|
||||
self._prev = self._pos
|
||||
self._pos = new_tile
|
||||
return True
|
||||
return False
|
||||
|
||||
def undo(self):
|
||||
if self._prev:
|
||||
self._pos, self._prev = self._prev, None
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
class Command:
|
||||
def do(self):
|
||||
raise NotImplementedError
|
||||
|
||||
def undo(self):
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class MoveCommand(Command):
|
||||
def __init__(self, player, direction, labyrinth):
|
||||
self._player = player
|
||||
self._dx, self._dy = direction
|
||||
self._lab = labyrinth
|
||||
self._done = False
|
||||
|
||||
def do(self):
|
||||
nx = self._player.position.col + self._dx
|
||||
ny = self._player.position.row + self._dy
|
||||
target = self._lab.get_tile(nx, ny)
|
||||
if target and target.passable():
|
||||
self._player.move_to(target)
|
||||
self._done = True
|
||||
return True
|
||||
return False
|
||||
|
||||
def undo(self):
|
||||
if self._done:
|
||||
self._player.undo()
|
||||
self._done = False
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
|
||||
class MazeSolver:
|
||||
"""Controls the search process and notifies observers."""
|
||||
|
||||
def __init__(self, labyrinth):
|
||||
self._lab = labyrinth
|
||||
self._algorithm = None
|
||||
self._listeners = []
|
||||
|
||||
def add_listener(self, listener):
|
||||
self._listeners.append(listener)
|
||||
|
||||
def notify(self, event, data):
|
||||
for lst in self._listeners:
|
||||
lst.on_event(event, data)
|
||||
|
||||
def set_algorithm(self, algo):
|
||||
self._algorithm = algo
|
||||
|
||||
def solve(self):
|
||||
if self._algorithm is None:
|
||||
return None
|
||||
|
||||
start_time = time.perf_counter()
|
||||
route = self._algorithm.find_route(self._lab, self._lab.start_tile, self._lab.exit_tile)
|
||||
end_time = time.perf_counter()
|
||||
elapsed_ms = (end_time - start_time) * 1000
|
||||
|
||||
self.notify("path_found", route)
|
||||
return SearchStats(elapsed_ms, self._algorithm.visited_cells(), len(route))
|
||||
|
||||
|
||||
def run_experiment(maze_file, algorithm, repetitions=5):
|
||||
loader = TextFileLoader()
|
||||
maze = loader.load(maze_file)
|
||||
|
||||
total_time = 0.0
|
||||
total_visited = 0
|
||||
total_length = 0
|
||||
|
||||
for _ in range(repetitions):
|
||||
solver = MazeSolver(maze)
|
||||
solver.set_algorithm(algorithm)
|
||||
stats = solver.solve()
|
||||
if stats:
|
||||
total_time += stats.elapsed_ms
|
||||
total_visited += stats.visited_cells
|
||||
total_length += stats.path_length
|
||||
|
||||
return {
|
||||
'time_ms': total_time / repetitions,
|
||||
'visited_cells': total_visited / repetitions,
|
||||
'path_length': total_length / repetitions
|
||||
}
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
if len(sys.argv) > 1 and sys.argv[1] == 'experiment':
|
||||
print("Running experiments (use plots.py for full test suite)...")
|
||||
sys.exit(0)
|
||||
|
||||
loader = TextFileLoader()
|
||||
maze = loader.load("maze1.txt")
|
||||
|
||||
player = Player(maze.start_tile, maze)
|
||||
view = TerminalView(player)
|
||||
view.on_event("maze_loaded", maze)
|
||||
|
||||
solver = MazeSolver(maze)
|
||||
solver.add_listener(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(BreadthFirstSearch())
|
||||
stats = solver.solve()
|
||||
print(f"\n BFS: time={stats.elapsed_ms:.3f}ms, visited={stats.visited_cells}, length={stats.path_length}")
|
||||
elif cmd == 'd':
|
||||
solver.set_algorithm(DepthFirstSearch())
|
||||
stats = solver.solve()
|
||||
print(f"\n DFS: time={stats.elapsed_ms:.3f}ms, visited={stats.visited_cells}, length={stats.path_length}")
|
||||
elif cmd == 'a':
|
||||
solver.set_algorithm(AStarSearch())
|
||||
stats = solver.solve()
|
||||
print(f"\n A*: time={stats.elapsed_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)}
|
||||
move = MoveCommand(player, dir_map[cmd], maze)
|
||||
if move.do():
|
||||
history.append(move)
|
||||
view.on_event("player_moved", maze)
|
||||
if player.position == maze.exit_tile:
|
||||
print("\n *** YOU ESCAPED! ***")
|
||||
print(f" Total moves: {len(history)}")
|
||||
break
|
||||
else:
|
||||
print("\n Blocked by a wall!")
|
||||
elif cmd == 'u':
|
||||
if history:
|
||||
last = history.pop()
|
||||
last.undo()
|
||||
view.on_event("player_moved", maze)
|
||||
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!")
|
||||
31
KuznetsovYuM/docs/experiment_results.csv
Normal file
31
KuznetsovYuM/docs/experiment_results.csv
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
Structure,Mode,Repeat,Insert (sec),Search (sec),Delete (sec)
|
||||
LinkedList,random,1,4.391112,0.026905,0.012824
|
||||
LinkedList,random,2,4.845220,0.031560,0.030583
|
||||
LinkedList,random,3,4.461536,0.030224,0.013461
|
||||
LinkedList,random,4,4.562402,0.028962,0.014101
|
||||
LinkedList,random,5,4.491418,0.040197,0.018795
|
||||
LinkedList,sorted,1,3.728189,0.023831,0.010369
|
||||
LinkedList,sorted,2,3.681244,0.023794,0.011584
|
||||
LinkedList,sorted,3,3.710309,0.025346,0.011397
|
||||
LinkedList,sorted,4,3.687962,0.027130,0.010611
|
||||
LinkedList,sorted,5,3.713101,0.026431,0.011425
|
||||
HashTable,random,1,0.056713,0.000387,0.000268
|
||||
HashTable,random,2,0.053692,0.000412,0.000199
|
||||
HashTable,random,3,0.053167,0.001272,0.000238
|
||||
HashTable,random,4,0.059468,0.000414,0.000174
|
||||
HashTable,random,5,0.052122,0.000918,0.000205
|
||||
HashTable,sorted,1,0.054478,0.000406,0.000157
|
||||
HashTable,sorted,2,0.052836,0.000398,0.000190
|
||||
HashTable,sorted,3,0.052295,0.000410,0.000177
|
||||
HashTable,sorted,4,0.053164,0.000447,0.000169
|
||||
HashTable,sorted,5,0.051903,0.000399,0.000179
|
||||
BST,random,1,0.024767,0.000204,0.000125
|
||||
BST,random,2,0.025908,0.000222,0.000119
|
||||
BST,random,3,0.025214,0.000223,0.000113
|
||||
BST,random,4,0.021233,0.000183,0.000111
|
||||
BST,random,5,0.022941,0.000277,0.000140
|
||||
BST,sorted,1,8.967227,0.081463,0.047105
|
||||
BST,sorted,2,8.873885,0.076518,0.042572
|
||||
BST,sorted,3,8.827521,0.066650,0.055038
|
||||
BST,sorted,4,8.722978,0.090392,0.045578
|
||||
BST,sorted,5,9.053348,0.088699,0.054090
|
||||
|
BIN
KuznetsovYuM/docs/performance_comparison.png
Normal file
BIN
KuznetsovYuM/docs/performance_comparison.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 51 KiB |
110
KuznetsovYuM/docs/report-1-st.md
Normal file
110
KuznetsovYuM/docs/report-1-st.md
Normal file
|
|
@ -0,0 +1,110 @@
|
|||
# Отчёт по лабораторной работе
|
||||
## «Сравнение производительности структур данных на примере телефонного справочника»
|
||||
|
||||
**Выполнил:** студент группы ...
|
||||
**Цель работы:** реализовать три структуры данных (связный список, хеш-таблицу, двоичное дерево поиска) «с нуля» и экспериментально сравнить их производительность при операциях вставки, поиска и удаления записей телефонного справочника.
|
||||
|
||||
---
|
||||
|
||||
## 1. Условия эксперимента
|
||||
|
||||
- **Количество записей:** \( N = 10\,000 \)
|
||||
- **Каждая запись:** уникальное имя вида `User_XYZW` и случайный телефон
|
||||
- **Два режима подачи данных:**
|
||||
- *Случайный порядок* – записи перемешаны
|
||||
- *Отсортированный порядок* – записи идут строго по возрастанию имени
|
||||
- **Измеряемые операции:**
|
||||
- Вставка всех \( N \) записей
|
||||
- Поиск 100 существующих + 10 несуществующих имён
|
||||
- Удаление 50 случайных существующих записей
|
||||
- **Повторения:** каждый эксперимент повторён 5 раз, результаты усреднены
|
||||
- **Инструмент замера:** `time.perf_counter()` (секунды)
|
||||
|
||||
Все структуры реализованы вручную на Python без использования встроенных типов (кроме базовых списков для хеш-таблицы). Код находится в файле `phonebook_structures.py`.
|
||||
|
||||
---
|
||||
|
||||
## 2. Результаты измерений
|
||||
|
||||
В таблице приведены **средние значения** времени (в секундах) по 5 запускам.
|
||||
|
||||
| Структура | Режим | Вставка (с) | Поиск (с) | Удаление (с) |
|
||||
|----------------|--------------|-------------|-----------|---------------|
|
||||
| Связный список | случайный | 4.5503 | 0.0316 | 0.0180 |
|
||||
| Связный список | отсортир. | 3.7042 | 0.0253 | 0.0111 |
|
||||
| Хеш-таблица | случайный | 0.0550 | 0.00068 | 0.000217 |
|
||||
| Хеш-таблица | отсортир. | 0.0529 | 0.00041 | 0.000174 |
|
||||
| BST (ДДП) | случайный | 0.0240 | 0.000222 | 0.000122 |
|
||||
| BST (ДДП) | отсортир. | 8.8890 | 0.0807 | 0.0489 |
|
||||
|
||||
*Графическое представление результатов приведено на рисунке 1.*
|
||||
|
||||

|
||||
|
||||
*Рисунок 1 – Время выполнения операций для трёх структур в разных режимах подачи данных (логарифмическая шкала по вертикали для наглядности).*
|
||||
|
||||
---
|
||||
|
||||
## 3. Анализ результатов
|
||||
|
||||
### 3.1. Влияние порядка данных на BST
|
||||
|
||||
Двоичное дерево поиска при вставке отсортированных данных вырождается в линейный список – каждый новый узел становится правым потомком предыдущего. Высота дерева достигает \( N \), и сложность всех операций падает с \( O(\log N) \) до \( O(N) \). Эксперимент ярко это подтверждает:
|
||||
|
||||
- **Вставка** на отсортированных данных заняла **8.889 с** – это в **370 раз** медленнее, чем на случайных (0.024 с).
|
||||
- **Поиск** замедлился в **360 раз** (0.0807 с против 0.000222 с).
|
||||
- **Удаление** замедлилось в **400 раз** (0.0489 с против 0.000122 с).
|
||||
|
||||
Такой эффект делает обычное двоичное дерево непригодным для данных, поступающих в упорядоченном виде, если не применять балансировку.
|
||||
|
||||
### 3.2. Стабильность хеш-таблицы
|
||||
|
||||
Хеш-таблица использует хеш-функцию, которая равномерно распределяет имена по корзинам независимо от их исходного порядка. Поэтому производительность почти не меняется:
|
||||
|
||||
- Вставка: ~0.055 с (случайный) и ~0.053 с (отсортированный) – разница менее 5%.
|
||||
- Поиск: 0.00068 с против 0.00041 с – небольшие колебания связаны со случайными коллизиями.
|
||||
- Удаление: также стабильно.
|
||||
|
||||
Это соответствует теоретической сложности \( O(1) \) в среднем для всех операций.
|
||||
|
||||
### 3.3. Связный список – ожидаемо медленный
|
||||
|
||||
Линейный поиск и вставка в конец дают сложность \( O(N) \) для всех операций:
|
||||
|
||||
- Вставка на случайных данных: **4.55 с** – почти в 200 раз медленнее, чем у хеш-таблицы.
|
||||
- Поиск: **0.0316 с** – на два порядка медленнее, чем у BST на случайных данных.
|
||||
- Отсортированный порядок даёт небольшой выигрыш во вставке (3.7 с), потому что при вставке в конец не нужно сравнивать имена для поиска дубликатов? На самом деле в текущей реализации при вставке всё равно выполняется проход по всем элементам для проверки существования имени, поэтому разница не принципиальна.
|
||||
|
||||
Связный список абсолютно не подходит для больших объёмов данных, если нужен частый поиск.
|
||||
|
||||
### 3.4. Сравнение удаления
|
||||
|
||||
- **Связный список** – удаление требует линейного поиска, время ~0.018 с (сопоставимо с поиском).
|
||||
- **Хеш-таблица** – удаление за \( O(1) \) в среднем: ~0.0002 с.
|
||||
- **BST** на случайных данных – очень быстрое удаление (~0.00012 с), но на отсортированных падает до 0.049 с (из-за вырождения).
|
||||
|
||||
---
|
||||
|
||||
## 4. Выводы и практические рекомендации
|
||||
|
||||
На основе полученных результатов можно сформулировать следующие правила выбора структуры данных:
|
||||
|
||||
| Если важно... | Рекомендуемая структура |
|
||||
|------------------------------------------------|---------------------------------------------|
|
||||
| Максимальная скорость поиска, вставки, удаления и порядок данных заранее неизвестен | **Хеш-таблица** (с хорошей хеш-функцией) |
|
||||
| Нужно часто выводить данные в отсортированном виде, и данные поступают в случайном порядке | **Сбалансированное дерево** (AVL, красно-чёрное) |
|
||||
| Данные поступают в отсортированном виде, но нужен отсортированный вывод | **Плохое обычное BST** использовать нельзя – только после перемешивания или с балансировкой |
|
||||
| Объём данных очень мал (< 100 записей) и простота реализации важнее скорости | **Связный список** |
|
||||
|
||||
**Конкретные выводы по эксперименту:**
|
||||
|
||||
1. **Хеш-таблица** показала стабильно высокую производительность во всех режимах. Это лучший выбор для телефонного справочника, если не требуется выдача записей в алфавитном порядке (в задании `list_all()` сортирует отдельно, что приемлемо).
|
||||
2. **Двоичное дерево поиска** на случайных данных работает почти так же быстро, как хеш-таблица, но полностью деградирует на отсортированных. Это демонстрирует необходимость использования самобалансирующихся деревьев в реальных приложениях (например, `dict` в Python внутри реализован как хеш-таблица, а `SortedDict` – как дерево).
|
||||
3. **Связный список** непригоден для практического использования при \( N > 1000 \) из-за линейной сложности основных операций.
|
||||
|
||||
**Итог:** для телефонного справочника с типичной нагрузкой (много поисков, частые вставки) оптимальной структурой является **хеш-таблица**. Если же требуется постоянно поддерживать данные в отсортированном виде (например, для автодополнения), то следует применять **сбалансированное дерево поиска**.
|
||||
|
||||
---
|
||||
|
||||
*Дата выполнения эксперимента:* 22 мая 2026 г.
|
||||
*Файлы результатов:* `experiment_results.csv`, `performance_comparison.png`
|
||||
Loading…
Reference in New Issue
Block a user