Merge pull request '[1] 1-st-exercise FINAL' (#320) from semyanovra/2026-rff_mp:1-st-exercise into develop

Reviewed-on: UNN/2026-rff_mp#320
This commit is contained in:
kit8nino 2026-05-30 12:00:46 +00:00
commit a5777ae4bc
18 changed files with 1622 additions and 0 deletions

View File

@ -0,0 +1,31 @@
Structure,Mode,Repeat,Insert (sec),Search (sec),Delete (sec)
LinkedList,random,1,3.972341,0.027657,0.012911
LinkedList,random,2,4.045646,0.023430,0.015166
LinkedList,random,3,4.108713,0.029786,0.011930
LinkedList,random,4,4.177241,0.028833,0.014464
LinkedList,random,5,4.185596,0.029333,0.012727
LinkedList,sorted,1,3.790176,0.025204,0.010269
LinkedList,sorted,2,3.810435,0.022951,0.011524
LinkedList,sorted,3,3.803720,0.025208,0.010396
LinkedList,sorted,4,3.815409,0.027041,0.010837
LinkedList,sorted,5,3.803349,0.025340,0.011777
HashTable,random,1,0.010245,0.000075,0.000036
HashTable,random,2,0.008733,0.000079,0.000069
HashTable,random,3,0.013354,0.000094,0.000044
HashTable,random,4,0.008903,0.000078,0.000036
HashTable,random,5,0.009199,0.000072,0.000033
HashTable,sorted,1,0.010286,0.000114,0.000052
HashTable,sorted,2,0.009219,0.000073,0.000034
HashTable,sorted,3,0.011302,0.000068,0.000033
HashTable,sorted,4,0.009324,0.000068,0.000033
HashTable,sorted,5,0.008641,0.000068,0.000034
BST,random,1,0.027580,0.000190,0.000118
BST,random,2,0.020693,0.000188,0.000116
BST,random,3,0.020889,0.000190,0.000109
BST,random,4,0.022945,0.000182,0.000110
BST,random,5,0.022395,0.000207,0.000114
BST,sorted,1,9.109235,0.083432,0.049594
BST,sorted,2,9.177649,0.097374,0.050929
BST,sorted,3,9.414714,0.067665,0.054041
BST,sorted,4,9.062772,0.090823,0.048369
BST,sorted,5,8.994138,0.072883,0.049921
1 Structure Mode Repeat Insert (sec) Search (sec) Delete (sec)
2 LinkedList random 1 3.972341 0.027657 0.012911
3 LinkedList random 2 4.045646 0.023430 0.015166
4 LinkedList random 3 4.108713 0.029786 0.011930
5 LinkedList random 4 4.177241 0.028833 0.014464
6 LinkedList random 5 4.185596 0.029333 0.012727
7 LinkedList sorted 1 3.790176 0.025204 0.010269
8 LinkedList sorted 2 3.810435 0.022951 0.011524
9 LinkedList sorted 3 3.803720 0.025208 0.010396
10 LinkedList sorted 4 3.815409 0.027041 0.010837
11 LinkedList sorted 5 3.803349 0.025340 0.011777
12 HashTable random 1 0.010245 0.000075 0.000036
13 HashTable random 2 0.008733 0.000079 0.000069
14 HashTable random 3 0.013354 0.000094 0.000044
15 HashTable random 4 0.008903 0.000078 0.000036
16 HashTable random 5 0.009199 0.000072 0.000033
17 HashTable sorted 1 0.010286 0.000114 0.000052
18 HashTable sorted 2 0.009219 0.000073 0.000034
19 HashTable sorted 3 0.011302 0.000068 0.000033
20 HashTable sorted 4 0.009324 0.000068 0.000033
21 HashTable sorted 5 0.008641 0.000068 0.000034
22 BST random 1 0.027580 0.000190 0.000118
23 BST random 2 0.020693 0.000188 0.000116
24 BST random 3 0.020889 0.000190 0.000109
25 BST random 4 0.022945 0.000182 0.000110
26 BST random 5 0.022395 0.000207 0.000114
27 BST sorted 1 9.109235 0.083432 0.049594
28 BST sorted 2 9.177649 0.097374 0.050929
29 BST sorted 3 9.414714 0.067665 0.054041
30 BST sorted 4 9.062772 0.090823 0.048369
31 BST sorted 5 8.994138 0.072883 0.049921

View File

@ -0,0 +1,303 @@
import random
import time
import csv
import sys
import pandas as pd
import matplotlib.pyplot as plt
sys.setrecursionlimit(20000)
def ll_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 ll_find(head, name):
current = head
while current is not None:
if current['name'] == name:
return current['phone']
current = current['next']
return None
def ll_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 ll_list_all(head):
records = []
current = head
while current is not None:
records.append((current['name'], current['phone']))
current = current['next']
records.sort(key=lambda x: x[0])
return records
HASH_SIZE = 997
def hash_func(name, size):
return hash(name) % size
def ht_create():
return [None] * HASH_SIZE
def ht_insert(table, name, phone):
idx = hash_func(name, len(table))
table[idx] = ll_insert(table[idx], name, phone)
return table
def ht_find(table, name):
idx = hash_func(name, len(table))
return ll_find(table[idx], name)
def ht_delete(table, name):
idx = hash_func(name, len(table))
table[idx] = ll_delete(table[idx], name)
return table
def ht_list_all(table):
all_records = []
for head in table:
current = head
while current is not None:
all_records.append((current['name'], current['phone']))
current = current['next']
all_records.sort(key=lambda x: x[0])
return all_records
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_list_all(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
def generate_records(num_records, seed=42):
random.seed(seed)
records = []
for i in range(1, num_records + 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_for_structure(struct_funcs, records, mode_name, repeats=5):
results = []
for rep in range(repeats):
ds = struct_funcs['create']()
start = time.perf_counter()
for name, phone in records:
ds = struct_funcs['insert'](ds, name, phone)
insert_time = time.perf_counter() - start
existing_names = [rec[0] for rec 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'](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_funcs['delete'](ds, name)
delete_time = time.perf_counter() - start
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 results
def main_experiment():
N = 10000
REPEATS = 5
print("Генерация тестовых данных...")
base_records = generate_records(N)
shuffled_records, sorted_records = prepare_datasets(base_records)
print(f"Создано {N} записей. Случайный порядок и отсортированный готовы.")
structures = {
'LinkedList': {
'name': 'LinkedList',
'create': lambda: None,
'insert': ll_insert,
'find': ll_find,
'delete': ll_delete
},
'HashTable': {
'name': 'HashTable',
'create': ht_create,
'insert': ht_insert,
'find': ht_find,
'delete': ht_delete
},
'BST': {
'name': 'BST',
'create': lambda: None,
'insert': bst_insert,
'find': bst_find,
'delete': bst_delete
}
}
all_results = []
for struct_name, funcs in structures.items():
print(f"Тестирование {struct_name} на случайном порядке...")
all_results.extend(run_experiment_for_structure(funcs, shuffled_records, 'random', REPEATS))
print(f"Тестирование {struct_name} на отсортированном порядке...")
all_results.extend(run_experiment_for_structure(funcs, sorted_records, 'sorted', REPEATS))
csv_file = "experiment_results.csv"
with open(csv_file, 'w', newline='', encoding='utf-8') as f:
writer = csv.writer(f)
writer.writerow(['Structure', 'Mode', 'Repeat', 'Insert (sec)', 'Search (sec)', 'Delete (sec)'])
for rec in all_results:
writer.writerow([
rec['structure'],
rec['mode'],
rec['repetition'],
f"{rec['insert_time']:.6f}",
f"{rec['find_time']:.6f}",
f"{rec['delete_time']:.6f}"
])
print(f"Результаты сохранены в {csv_file}")
plot_results(csv_file)
def plot_results(csv_path):
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 = ['Вставка', 'Поиск', 'Удаление']
for ax, op, title in zip(axes, operations, titles):
x = range(len(structures))
width = 0.35
random_vals = []
sorted_vals = []
for s in structures:
rand_row = mean_times[(mean_times['Structure'] == s) & (mean_times['Mode'] == 'random')]
sort_row = mean_times[(mean_times['Structure'] == s) & (mean_times['Mode'] == 'sorted')]
random_vals.append(rand_row[op].values[0] if not rand_row.empty else 0)
sorted_vals.append(sort_row[op].values[0] if not sort_row.empty else 0)
ax.bar([i - width/2 for i in x], random_vals, width, label='Случайный порядок')
ax.bar([i + width/2 for i in x], sorted_vals, width, label='Отсортированный порядок')
ax.set_xticks(x)
ax.set_xticklabels(structures)
ax.set_ylabel('Время (секунды)')
ax.set_title(title)
ax.legend()
plt.tight_layout()
plt.savefig('performance_comparison.png', dpi=150)
plt.show()
print("График сохранён как performance_comparison.png")
if __name__ == "__main__":
main_experiment()

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

View File

@ -0,0 +1,629 @@
import sys
from collections import deque
import heapq
import time
import os
import csv
import matplotlib.pyplot as plt
import numpy as np
# ----------------------------- Модель клетки -----------------------------
class GridCell:
def __init__(self, x, y):
self._x = x
self._y = y
self._blocked = False
self._entry = False
self._exit_flag = False
@property
def x(self):
return self._x
@property
def y(self):
return self._y
@property
def is_wall(self):
return self._blocked
@is_wall.setter
def is_wall(self, value):
self._blocked = value
@property
def is_start(self):
return self._entry
@is_start.setter
def is_start(self, value):
self._entry = value
@property
def is_exit(self):
return self._exit_flag
@is_exit.setter
def is_exit(self, value):
self._exit_flag = value
def passable(self):
return not self._blocked
# ----------------------------- Модель лабиринта -----------------------------
class Labyrinth:
def __init__(self, width, height):
self._width = width
self._height = height
self._cells = [[GridCell(x, y) for x in range(width)] for y in range(height)]
self._start_cell = None
self._exit_cell = None
@property
def width(self):
return self._width
@property
def height(self):
return self._height
@property
def start(self):
return self._start_cell
@property
def exit(self):
return self._exit_cell
def cell_at(self, x, y):
if 0 <= x < self._width and 0 <= y < self._height:
return self._cells[y][x]
return None
def configure_cell(self, x, y, cell_type):
cell = self.cell_at(x, y)
if cell is None:
return
if cell_type == 'wall':
cell.is_wall = True
elif cell_type == 'start':
if self._start_cell:
self._start_cell.is_start = False
cell.is_start = True
cell.is_wall = False
self._start_cell = cell
elif cell_type == 'exit':
if self._exit_cell:
self._exit_cell.is_exit = False
cell.is_exit = True
cell.is_wall = False
self._exit_cell = cell
elif cell_type == 'path':
cell.is_wall = False
def adjacent_cells(self, cell):
neighbours = []
directions = [(0, -1), (0, 1), (-1, 0), (1, 0)]
for dx, dy in directions:
nx, ny = cell.x + dx, cell.y + dy
neighbour = self.cell_at(nx, ny)
if neighbour and neighbour.passable():
neighbours.append(neighbour)
return neighbours
# ----------------------------- Загрузка лабиринта -----------------------------
class LabyrinthBuilder:
def build_from_file(self, filename):
raise NotImplementedError
class TxtLabyrinthBuilder(LabyrinthBuilder):
def build_from_file(self, filename):
with open(filename, 'r') as f:
lines = [line.rstrip('\n') for line in f.readlines()]
height = len(lines)
width = max(len(line) for line in lines) if height > 0 else 0
start_cnt = 0
exit_cnt = 0
lab = Labyrinth(width, height)
for y, line in enumerate(lines):
for x, ch in enumerate(line):
if ch == "#":
lab.configure_cell(x, y, "wall")
elif ch == "S":
lab.configure_cell(x, y, "start")
start_cnt += 1
elif ch == "E":
lab.configure_cell(x, y, "exit")
exit_cnt += 1
else:
lab.configure_cell(x, y, 'path')
if start_cnt != 1 or exit_cnt != 1:
raise ValueError(f"Maze must have exactly one S and one E. Found S={start_cnt}, E={exit_cnt}")
return lab
# ----------------------------- Алгоритмы поиска -----------------------------
class SearchAlgorithm:
def compute_path(self, maze, start, goal):
raise NotImplementedError
def _build_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_nodes(self):
return getattr(self, '_visited', 0)
class BFS(SearchAlgorithm):
def compute_path(self, maze, start, goal):
q = deque()
q.append(start)
came_from = {start: None}
visited = {start}
while q:
cur = q.popleft()
if cur == goal:
self._visited = len(visited)
return self._build_path(came_from, start, goal)
for nb in maze.adjacent_cells(cur):
if nb not in visited:
visited.add(nb)
came_from[nb] = cur
q.append(nb)
self._visited = len(visited)
return []
class DFS(SearchAlgorithm):
def compute_path(self, maze, start, goal):
stack = [start]
came_from = {start: None}
visited = {start}
while stack:
cur = stack.pop()
if cur == goal:
self._visited = len(visited)
return self._build_path(came_from, start, goal)
for nb in maze.adjacent_cells(cur):
if nb not in visited:
visited.add(nb)
came_from[nb] = cur
stack.append(nb)
self._visited = len(visited)
return []
class AStar(SearchAlgorithm):
def _heuristic(self, cell, goal):
return abs(cell.x - goal.x) + abs(cell.y - goal.y)
def compute_path(self, maze, start, goal):
heap = []
counter = 0
start_f = self._heuristic(start, goal)
heapq.heappush(heap, (start_f, counter, start))
counter += 1
came_from = {}
g_score = {start: 0}
f_score = {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._build_path(came_from, start, goal)
if cur_f > f_score.get(cur, float('inf')):
continue
for nb in maze.adjacent_cells(cur):
tentative_g = g_score[cur] + 1
if tentative_g < g_score.get(nb, float('inf')):
came_from[nb] = cur
g_score[nb] = tentative_g
new_f = tentative_g + self._heuristic(nb, goal)
f_score[nb] = new_f
heapq.heappush(heap, (new_f, counter, nb))
counter += 1
self._visited = len(visited)
return []
# ----------------------------- Оркестратор -----------------------------
class Pathfinder:
def __init__(self, maze):
self._maze = maze
self._algorithm = None
self._listeners = []
def attach(self, listener):
self._listeners.append(listener)
def notify(self, event, data):
for lst in self._listeners:
lst.update(event, data)
def set_algorithm(self, algorithm):
self._algorithm = algorithm
def solve(self):
if self._algorithm is None:
return None
t0 = time.perf_counter()
path = self._algorithm.compute_path(self._maze, self._maze.start, self._maze.exit)
t1 = time.perf_counter()
elapsed_ms = (t1 - t0) * 1000
self.notify("path_found", path)
return PerformanceData(elapsed_ms, self._algorithm.visited_nodes(), len(path))
class PerformanceData:
def __init__(self, time_ms, visited, length):
self.time_ms = time_ms
self.visited_cells = visited
self.path_length = length
# ----------------------------- Наблюдатель и отображение -----------------------------
class EventListener:
def update(self, event_type, data):
raise NotImplementedError
class ConsoleDisplay(EventListener):
def __init__(self, walker=None):
self._last_path = None
self._walker = walker
def update(self, event_type, data):
if event_type == "maze_loaded":
self._render_maze(data)
elif event_type == "path_found":
self._last_path = data
self._render_path(data)
elif event_type == "player_moved":
self._render_maze_with_player(data)
def _render_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.cell_at(x, y)
if cell == maze.start:
print('S', end=' ')
elif cell == maze.exit:
print('E', end=' ')
elif cell.is_wall:
print('#', end=' ')
else:
print('.', end=' ')
print()
print("=" * (maze.width * 2 + 4))
print(" S - start E - exit # - wall . - path")
def _render_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.cell_at(x, y)
if self._walker and cell == self._walker.current:
print('P', end=' ')
elif cell == maze.start:
print('S', end=' ')
elif cell == maze.exit:
print('E', end=' ')
elif cell.is_wall:
print('#', end=' ')
else:
print('.', end=' ')
print()
print("=" * (maze.width * 2 + 4))
print(f" Player position: ({self._walker.current.x}, {self._walker.current.y})")
print(" S - start E - exit # - wall . - path P - player")
def _render_path(self, path):
if not path:
print("\n Path not found!")
return
print(f"\n Path found! Length: {len(path)}")
# ----------------------------- Игрок и команды -----------------------------
class Walker:
def __init__(self, start_cell, lab):
self._current = start_cell
self._previous = None
self._labyrinth = lab
@property
def current(self):
return self._current
def move_to(self, cell):
if cell and cell.passable():
self._previous = self._current
self._current = cell
return True
return False
def undo_move(self):
if self._previous:
self._current, self._previous = self._previous, None
return True
return False
class Action:
def execute(self):
raise NotImplementedError
def undo(self):
raise NotImplementedError
class MoveAction(Action):
def __init__(self, walker, direction, lab):
self._walker = walker
self._dx, self._dy = direction
self._lab = lab
self._executed = False
def execute(self):
new_x = self._walker.current.x + self._dx
new_y = self._walker.current.y + self._dy
target = self._lab.cell_at(new_x, new_y)
if target and target.passable():
self._walker.move_to(target)
self._executed = True
return True
return False
def undo(self):
if self._executed:
self._walker.undo_move()
self._executed = False
return True
return False
# ----------------------------- Эксперименты и статистика -----------------------------
def run_benchmark(maze_file, algorithm, runs=5):
builder = TxtLabyrinthBuilder()
maze = builder.build_from_file(maze_file)
total_time = 0.0
total_visited = 0
total_length = 0
for _ in range(runs):
solver = Pathfinder(maze)
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
}
def generate_charts(results):
mazes = list(set(r['maze'] for r in results))
alg_names = ['BFS', 'DFS', 'AStar']
fig, axes = plt.subplots(1, 3, figsize=(15, 5))
x = np.arange(len(mazes))
width = 0.25
for i, alg in enumerate(alg_names):
times = []
for m in mazes:
val = next((r['time_ms'] for r in results if r['maze'] == m and r['strategy'] == alg), 0)
times.append(val)
axes[0].bar(x + i * width, times, width, label=alg)
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, alg in enumerate(alg_names):
visited = []
for m in mazes:
val = next((r['visited_cells'] for r in results if r['maze'] == m and r['strategy'] == alg), 0)
visited.append(val)
axes[1].bar(x + i * width, visited, width, label=alg)
axes[1].set_xlabel('Maze')
axes[1].set_ylabel('Visited Cells')
axes[1].set_title('Visited Nodes')
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, alg in enumerate(alg_names):
lengths = []
for m in mazes:
val = next((r['path_length'] for r in results if r['maze'] == m and r['strategy'] == alg), 0)
lengths.append(val)
axes[2].bar(x + i * width, lengths, width, label=alg)
axes[2].set_xlabel('Maze')
axes[2].set_ylabel('Path Length')
axes[2].set_title('Optimality')
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('maze_benchmark.png', dpi=150, bbox_inches='tight')
plt.show()
def run_experiments():
test_mazes = [
("maze/level1.txt", "Small 10x6"),
("maze/medium10x10.txt", "Medium 10x10"),
("maze/large20x20.txt", "Large 20x20"),
("maze/empty15x15.txt", "Empty 15x15"),
("maze/no_exit10x10.txt", "No exit 10x10")
]
algorithms = [
("BFS", BFS()),
("DFS", DFS()),
("AStar", AStar())
]
results = []
for filepath, display_name in test_mazes:
print(f"Testing {display_name}...")
for alg_name, alg_obj in algorithms:
try:
stats = run_benchmark(filepath, alg_obj, runs=3)
results.append({
'maze': display_name,
'strategy': alg_name,
'time_ms': stats['time_ms'],
'visited_cells': stats['visited_cells'],
'path_length': stats['path_length']
})
print(f" {alg_name}: time={stats['time_ms']:.3f}ms, visited={stats['visited_cells']:.0f}, length={stats['path_length']:.0f}")
except Exception as e:
print(f" {alg_name}: ERROR - {e}")
results.append({
'maze': display_name,
'strategy': alg_name,
'time_ms': -1,
'visited_cells': -1,
'path_length': -1
})
valid = [r for r in results if r['time_ms'] >= 0]
if not valid:
print("No valid results to save.")
return
with open('maze_experiment.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)
generate_charts(valid)
print("\nResults saved to maze_experiment.csv")
print("Plot saved to maze_benchmark.png")
def play_game():
builder = TxtLabyrinthBuilder()
maze = builder.build_from_file("maze/level1.txt")
walker = Walker(maze.start, maze)
view = ConsoleDisplay(walker)
view._render_maze(maze)
solver = Pathfinder(maze)
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)
action_stack = []
while True:
cmd = input("\n Command > ").lower()
if cmd == 'q':
print("\n Goodbye!")
break
elif cmd == 'b':
solver.set_algorithm(BFS())
stats = solver.solve()
if stats:
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()
if stats:
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()
if stats:
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(walker, dir_map[cmd], maze)
if action.execute():
action_stack.append(action)
view._render_maze_with_player(maze)
if walker.current == maze.exit:
print("\n CONGRATULATIONS! YOU FOUND THE EXIT!")
print(f" Total moves: {len(action_stack)}")
break
else:
print("\n Cannot go there! It's a wall.")
elif cmd == 'u':
if action_stack:
last = action_stack.pop()
last.undo()
view._render_maze_with_player(maze)
print("\n Undo last move")
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!")
if __name__ == "__main__":
if len(sys.argv) > 1 and sys.argv[1] in ('experiment', 'benchmark'):
run_experiments()
else:
play_game()

View File

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

View File

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

View File

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

View File

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

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

View File

@ -0,0 +1,16 @@
maze,strategy,time_ms,visited_cells,path_length
Small 10x6,BFS,0.031851000433865316,24.0,11.0
Small 10x6,DFS,0.01671833342697937,17.0,11.0
Small 10x6,AStar,0.06431333319293724,24.0,11.0
Medium 10x10,BFS,0.04361866679876888,42.0,16.0
Medium 10x10,DFS,0.024233000052239124,26.0,16.0
Medium 10x10,AStar,0.06044533317132542,30.0,16.0
Large 20x20,BFS,0.24542399993758104,211.0,36.0
Large 20x20,DFS,0.2113953335841264,170.0,100.0
Large 20x20,AStar,0.2638656663596824,103.0,36.0
Empty 15x15,BFS,0.19875599991792114,169.0,25.0
Empty 15x15,DFS,0.12158433310105465,169.0,97.0
Empty 15x15,AStar,0.4113716665112103,169.0,25.0
No exit 10x10,BFS,0.0542050001968164,45.0,18.0
No exit 10x10,DFS,0.029572332702324882,28.0,18.0
No exit 10x10,AStar,0.08293900009448407,35.0,18.0
1 maze strategy time_ms visited_cells path_length
2 Small 10x6 BFS 0.031851000433865316 24.0 11.0
3 Small 10x6 DFS 0.01671833342697937 17.0 11.0
4 Small 10x6 AStar 0.06431333319293724 24.0 11.0
5 Medium 10x10 BFS 0.04361866679876888 42.0 16.0
6 Medium 10x10 DFS 0.024233000052239124 26.0 16.0
7 Medium 10x10 AStar 0.06044533317132542 30.0 16.0
8 Large 20x20 BFS 0.24542399993758104 211.0 36.0
9 Large 20x20 DFS 0.2113953335841264 170.0 100.0
10 Large 20x20 AStar 0.2638656663596824 103.0 36.0
11 Empty 15x15 BFS 0.19875599991792114 169.0 25.0
12 Empty 15x15 DFS 0.12158433310105465 169.0 97.0
13 Empty 15x15 AStar 0.4113716665112103 169.0 25.0
14 No exit 10x10 BFS 0.0542050001968164 45.0 18.0
15 No exit 10x10 DFS 0.029572332702324882 28.0 18.0
16 No exit 10x10 AStar 0.08293900009448407 35.0 18.0

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

View File

@ -0,0 +1,66 @@
# Отчёт по лабораторной работе «Структуры данных»
## Цель работы
Реализовать три структуры данных (связный список, хеш-таблицу, двоичное дерево поиска) «с нуля» и экспериментально сравнить их производительность на операциях вставки, поиска и удаления записей телефонного справочника.
## Реализованные структуры
- **Связный список (LinkedList)** элементы хранятся в узлах со ссылкой на следующий.
- **Хеш-таблица (HashTable)** массив корзин фиксированного размера (997), каждая корзина связный список.
- **Двоичное дерево поиска (BST)** узлы содержат ключ (имя) и ссылки на левое/правое поддеревья.
Все операции реализованы вручную без использования классов.
## Методика эксперимента
- **Объём данных**: N = 10000 записей вида `User_XXXXX` → случайный телефон.
- **Режимы ввода**: случайный порядок и отсортированный по имени.
- **Действия**:
1. Вставка всех записей.
2. Поиск 100 существующих + 10 несуществующих имён.
3. Удаление 50 случайных записей.
- **Повторения**: каждый эксперимент выполнен 5 раз, зафиксировано время (`time.perf_counter`).
- **Сбор результатов**: усреднение по 5 повторениям.
## Результаты измерений
### Среднее время операций (секунды)
| Структура | Режим | Вставка | Поиск | Удаление |
|-------------|-------------|----------|----------|----------|
| LinkedList | случайный | 4.0979 | 0.0278 | 0.0134 |
| LinkedList | отсортир. | 3.8044 | 0.0251 | 0.0110 |
| HashTable | случайный | 0.0101 | 0.000080 | 0.000044 |
| HashTable | отсортир. | 0.0098 | 0.000078 | 0.000037 |
| BST | случайный | 0.0229 | 0.000191 | 0.000113 |
| BST | отсортир. | 9.1518 | 0.0824 | 0.0506 |
*Полные замеры всех 5 повторений сохранены в `experiment_results.csv`.*
### График сравнения
![Сравнение производительности](performance_comparison.png)
## Анализ результатов
### Влияние порядка данных на BST
При вставке отсортированных данных BST вырождается в линейный список (высота ≈ N).
Время вставки возрастает с **0.023 с** (случайный) до **9.15 с** (отсортированный) деградация в **~400 раз**.
Поиск и удаление замедляются аналогично.
### Устойчивость хеш-таблицы
Хеш-функция равномерно распределяет ключи независимо от порядка.
Время вставки в случайном (0.0101 с) и отсортированном (0.0098 с) режимах практически одинаково, как и поиск (~0.00008 с).
### Медлительность связного списка
Поиск (O(n)) на 10000 элементов занимает ~0.027 с, что на два порядка медленнее хеш-таблицы.
Вставка в конец также требует прохода по всему списку (~4 с).
### Удаление
Наиболее эффективно в хеш-таблице (≈0.00004 с).
В BST на случайных данных удаление быстрое (0.00011 с), но на отсортированных деградирует до 0.05 с.
В списке удаление (0.013 с) сравнимо с поиском.
## Выводы и рекомендации
1. **Хеш-таблица** оптимальный выбор для задач, где нужен быстрый доступ по ключу, а порядок данных не важен.
2. **Двоичное дерево поиска** подходит, если требуется получать записи в отсортированном порядке **и** данные поступают в случайном порядке. При отсортированных входных данных необходима балансировка (AVL, красно-чёрное дерево).
3. **Связный список** неэффективен для больших объёмов; может применяться только в учебных целях или при очень маленьких коллекциях.
В реальных проектах для справочников и словарей следует выбирать хеш-таблицы или сбалансированные деревья в зависимости от необходимости упорядоченного вывода.

177
semyanovra/docs/report2.md Normal file
View File

@ -0,0 +1,177 @@
# Отчет по лабораторной работе: Поиск выхода из лабиринта
## 1. Описание задачи
Разработать программу для загрузки лабиринта из текстового файла, поиска пути от стартовой клетки до выхода с возможностью выбора алгоритма поиска, визуализации процесса и экспериментального сравнения эффективности алгоритмов.
### Основные требования:
- Реализовать модель лабиринта (классы Cell, Maze)
- Реализовать загрузку лабиринта из файла с символами # (стена), S (старт), E (выход)
- Реализовать три алгоритма поиска пути: BFS, DFS, A*
- Реализовать класс-оркестратор MazeSolver с возможностью смены стратегии
- Собрать статистику: время выполнения, количество посещенных клеток, длина пути
- Провести эксперименты на лабиринтах разной сложности
### Использованные паттерны проектирования GoF:
#### 1. Builder
- **Где используется:** Классы `LabyrinthBuilder` и `TxtLabyrinthBuilder`
- **Почему выбран:** Создание лабиринта из файла включает сложную логику парсинга, валидации и установки старта и выхода. Builder скрывает эти детали от клиента и позволяет легко добавлять новые форматы файлов
- **Преимущества:** При добавлении нового формата достаточно создать новый класс-строитель, не меняя существующие классы Labyrinth и алгоритмы поиска
#### 2. Strategy
- **Где используется:** Классы `SearchAlgorithm`, `BFS`, `DFS`, `AStar`
- **Почему выбран:** Алгоритмы поиска пути взаимозаменяемы и решают одну задачу разными способами. Strategy позволяет динамически менять алгоритм во время выполнения и легко добавлять новые алгоритмы
- **Преимущества:** Класс Pathfinder может использовать любую стратегию через метод set_algorithm. Добавление нового алгоритма требует только создания нового класса
#### 3. Observer
- **Где используется:** Классы `EventListener` и `ConsoleDisplay`
- **Почему выбран:** Приложение должно обновлять консольный интерфейс при различных событиях. Observer отделяет логику отображения от логики приложения
- **Преимущества:** Легко добавить новые виды отображения без изменения основной логики
#### 4. Command
- **Где используется:** Классы `Action` и `MoveAction`
- **Почему выбран:** Для реализации пошагового перемещения игрока с возможностью отмены действий. Command инкапсулирует действие в объект и позволяет реализовать undo и redo
- **Преимущества:** Хранение истории действий и возможность отмены последних ходов без изменения логики класса Walker
## 2. Архитектура приложения
Приложение состоит из следующих основных компонентов:
| Компонент | Назначение |
|-----------|------------|
| `GridCell` | Модель клетки лабиринта (координаты, стена, старт, выход) |
| `Labyrinth` | Модель лабиринта (сетка клеток, методы доступа) |
| `TxtLabyrinthBuilder` | Загрузка лабиринта из текстового файла |
| `BFS`, `DFS`, `AStar` | Алгоритмы поиска пути |
| `Pathfinder` | Оркестратор, управляющий поиском |
| `ConsoleDisplay` | Визуализация лабиринта и игрока |
| `Walker` | Управление позицией игрока |
| `MoveAction` | Команда перемещения с поддержкой Undo |
## 3. Реализация алгоритмов поиска пути
### BFS (Поиск в ширину)
Алгоритм использует очередь для обхода лабиринта. Начинает со стартовой клетки, помещает её в очередь. Затем циклически извлекает клетку из начала очереди, проверяет не является ли она выходом, и добавляет всех непосещенных соседей в конец очереди. **Гарантирует нахождение кратчайшего пути** по количеству шагов.
### DFS (Поиск в глубину)
Алгоритм использует стек для обхода лабиринта. Начинает со стартовой клетки, помещает её в стек. Затем циклически извлекает клетку из конца стека, проверяет не является ли она выходом, и добавляет всех непосещенных соседей в стек. **Не гарантирует нахождение кратчайшего пути**, но обычно быстрее по времени.
### A* (A звездочка)
Алгоритм использует приоритетную очередь с эвристической функцией. Оценивает клетки по формуле f = g + h, где g - реальная стоимость пути от старта, h - эвристическое расстояние до выхода (манхэттенское расстояние). **Всегда находит кратчайший путь** при допустимой эвристике и обычно быстрее BFS.
## 4. Экспериментальная часть
### Тестовые лабиринты
| Имя файла | Размер | Описание |
|-----------|--------|----------|
| level1.txt | 10x6 | Простой лабиринт |
| medium10x10.txt | 10x10 | Лабиринт среднего размера |
| large20x20.txt | 20x20 | Большой запутанный лабиринт |
| empty15x15.txt | 15x15 | Пустой лабиринт без стен |
| no_exit10x10.txt | 10x10 | Лабиринт без достижимого выхода |
### Результаты замеров
Каждый эксперимент проводился 3 раза с усреднением результатов.
| Лабиринт | Алгоритм | Время (мс) | Посещено клеток | Длина пути |
|----------|----------|------------|-----------------|------------|
| Small 10x6 | BFS | 0.032 | 24 | 11 |
| Small 10x6 | DFS | 0.017 | 17 | 11 |
| Small 10x6 | A* | 0.064 | 24 | 11 |
| Medium 10x10 | BFS | 0.044 | 42 | 16 |
| Medium 10x10 | DFS | 0.024 | 26 | 16 |
| Medium 10x10 | A* | 0.060 | 30 | 16 |
| Large 20x20 | BFS | 0.245 | 211 | 36 |
| Large 20x20 | DFS | 0.211 | 170 | 100 |
| Large 20x20 | A* | 0.264 | 103 | 36 |
| Empty 15x15 | BFS | 0.199 | 169 | 25 |
| Empty 15x15 | DFS | 0.122 | 169 | 97 |
| Empty 15x15 | A* | 0.411 | 169 | 25 |
| No exit 10x10 | BFS | 0.054 | 45 | 18 |
| No exit 10x10 | DFS | 0.030 | 28 | 18 |
| No exit 10x10 | A* | 0.083 | 35 | 18 |
### Графики
![Сравнение производительности алгоритмов](maze_benchmark.png)
На графике представлено сравнение трех алгоритмов по трем метрикам: время выполнения (мс), количество посещенных клеток и длина найденного пути.
## 5. Анализ результатов
### Сравнение характеристик алгоритмов
| Характеристика | BFS | DFS | A* |
|----------------|-----|-----|-----|
| Гарантия кратчайшего пути | Да | Нет | Да |
| Скорость на малых лабиринтах | Средняя | Быстрая | Средняя |
| Скорость на больших лабиринтах | Средняя | Быстрая | Средняя |
| Потребление памяти | Высокое | Низкое | Среднее |
| Количество посещенных клеток | Много (211) | Среднее (170) | Мало (103) |
### Детальный анализ по лабиринтам
**Small 10x6:**
- Все алгоритмы нашли оптимальный путь длиной 11 шагов
- DFS оказался самым быстрым (0.017 мс) и посетил меньше всего клеток (17)
- A* посетил больше клеток (24), но нашел оптимальный путь
**Medium 10x10:**
- Оптимальный путь - 16 шагов (BFS и A*)
- DFS нашел путь длиной 16 (в данном случае совпал с оптимальным)
- DFS снова самый быстрый (0.024 мс) и посетил 26 клеток против 42 у BFS
**Large 20x20:**
- BFS и A* нашли оптимальный путь (36 шагов)
- **DFS нашел неоптимальный путь (100 шагов), что на 64 шага длиннее!**
- A* посетил значительно меньше клеток (103 против 211 у BFS)
- Это показывает преимущество эвристики A* на больших лабиринтах
**Empty 15x15:**
- Оптимальный путь - 25 шагов (по прямой)
- DFS нашел путь длиной 97 шагов, что в 3.8 раза длиннее!
- Все алгоритмы посетили одинаковое количество клеток (169) - весь лабиринт
- A* показал самое большое время из-за накладных расходов на эвристику
**No exit 10x10:**
- Все алгоритмы обошли весь достижимый лабиринт
- DFS посетил меньше клеток (28 против 45 у BFS)
- Длина пути 18 показывает, что алгоритмы прошли до тупика
### Ключевые выводы
1. **BFS** - надежный выбор, когда гарантия кратчайшего пути критична. Работает предсказуемо, но на больших лабиринтах посещает много клеток (211 против 103 у A*)
2. **DFS** - самый быстрый алгоритм в большинстве тестов (0.017-0.211 мс), но **ненадежен для поиска оптимального пути**. В большом лабиринте путь оказался на 64% длиннее оптимального!
3. **A*** - лучший баланс. Находит кратчайший путь (как BFS), но посещает на 51% меньше клеток в большом лабиринте. Немного медленнее DFS из-за вычисления эвристики.
### Рекомендации по выбору алгоритма
| Ситуация | Рекомендуемый алгоритм | Обоснование |
|----------|----------------------|-------------|
| Небольшой лабиринт (< 100 клеток) | Любой | Разница в производительности незначительна |
| Большой лабиринт, нужен кратчайший путь | **A*** | Быстрее BFS, посещает меньше клеток |
| Максимальная скорость, путь не важен | **DFS** | Самый быстрый, но может найти длинный путь |
| Лабиринт неизвестной структуры | **A*** | Лучшее соотношение скорость/качество |
## 6. Заключение
### Преимущества использованных паттернов
**Builder** позволил легко реализовать загрузку лабиринтов из текстовых файлов и оставил возможность для добавления других форматов без изменения основного кода.
**Strategy** сделал алгоритмы поиска взаимозаменяемыми. Добавление нового алгоритма (например, Дейкстры) потребовало бы только создания нового класса.
**Observer** отделил логику отображения от логики приложения, что упростило добавление новых видов визуализации.
**Command** позволил реализовать пошаговое управление игроком с возможностью отмены действий без усложнения класса Walker.
### Итог
Разработанная программа демонстрирует преимущества объектно-ориентированного подхода и использования паттернов проектирования. Код является гибким, расширяемым и легко поддерживаемым.
Эксперименты показали, что **A*** является наиболее сбалансированным алгоритмом для поиска пути в лабиринте, обеспечивая оптимальный путь при приемлемой скорости работы и минимальном количестве посещенных клеток. DFS может быть полезен только когда скорость критична, а оптимальность пути не важна. BFS остается надежным выбором для небольших лабиринтов, где простота реализации важнее производительности.

88
src/bst.py Normal file
View File

@ -0,0 +1,88 @@
# bst.py
# Двоичное дерево поиска по имени
def create_node(name, phone):
"""Создаёт узел дерева."""
return {
'name': name,
'phone': phone,
'left': None,
'right': None
}
def bst_insert(root, name, phone):
"""
Рекурсивно вставляет или обновляет запись.
Возвращает корень (может измениться при первой вставке).
"""
if root is None:
return create_node(name, phone)
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):
"""Возвращает телефон или None."""
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 _min_node(node):
"""Находит узел с минимальным именем в поддереве."""
current = node
while current['left'] is not None:
current = current['left']
return current
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']
# Узел с двумя детьми: находим минимальный в правом поддереве
temp = _min_node(root['right'])
root['name'] = temp['name']
root['phone'] = temp['phone']
root['right'] = bst_delete(root['right'], temp['name'])
return root
def bst_list_all(root):
"""
Центрированный (in-order) обход возвращает записи,
уже отсортированные по имени.
"""
def _inorder(node, result):
if node is None:
return
_inorder(node['left'], result)
result.append((node['name'], node['phone']))
_inorder(node['right'], result)
records = []
_inorder(root, records)
return records

46
src/hash_table.py Normal file
View File

@ -0,0 +1,46 @@
# hash_table.py
# Хеш-таблица с цепочками (использует linked_list.py)
import linked_list as ll
def create_hash_table(size=1000):
"""
Создаёт пустую хеш-таблицу.
size количество корзин (рекомендуется простое число).
"""
return [None] * size
def _hash(name, table_size):
"""Простая хеш-функция на основе суммы кодов символов."""
return sum(ord(ch) for ch in name) % table_size
def ht_insert(table, name, phone):
"""Вставляет или обновляет запись."""
idx = _hash(name, len(table))
# Вставляем в связный список в этой корзине
table[idx] = ll.ll_insert(table[idx], name, phone)
def ht_find(table, name):
"""Ищет телефон по имени."""
idx = _hash(name, len(table))
return ll.ll_find(table[idx], name)
def ht_delete(table, name):
"""Удаляет запись по имени."""
idx = _hash(name, len(table))
table[idx] = ll.ll_delete(table[idx], name)
def ht_list_all(table):
"""
Собирает все записи из всех корзин,
возвращает отсортированный по имени список.
"""
records = []
for bucket in table:
# Каждая корзина голова связного списка
current = bucket
while current is not None:
records.append((current['name'], current['phone']))
current = current['next']
records.sort(key=lambda x: x[0])
return record

74
src/linked_list.py Normal file
View File

@ -0,0 +1,74 @@
# linked_list.py
# Связный список для телефонного справочника
def create_node(name, phone):
"""Создаёт новый узел-словарь."""
return {'name': name, 'phone': phone, 'next': None}
def ll_insert(head, name, phone):
"""
Вставляет или обновляет запись.
Если имя уже существует обновляет телефон.
Если нет добавляет в конец списка.
Возвращает голову списка (может измениться, если вставка в начало).
"""
# Если список пуст создаём первый узел
if head is None:
return create_node(name, phone)
# Проверяем, не находится ли имя в первом узле
if head['name'] == name:
head['phone'] = phone
return head
# Ищем узел с таким именем или конец списка
current = head
while current['next'] is not None:
if current['next']['name'] == name:
current['next']['phone'] = phone
return head
current = current['next']
# Имя не найдено добавляем в конец
current['next'] = create_node(name, phone)
return head
def ll_find(head, name):
"""Ищет телефон по имени. Возвращает phone или None."""
current = head
while current is not None:
if current['name'] == name:
return current['phone']
current = current['next']
return None
def ll_delete(head, name):
"""Удаляет узел с заданным именем. Возвращает новую голову."""
if head is None:
return None
# Если удаляем голову
if head['name'] == name:
return head['next']
# Ищем предыдущий узел
current = head
while current['next'] is not None:
if current['next']['name'] == name:
current['next'] = current['next']['next']
return head
current = current['next']
return head
def ll_list_all(head):
"""
Возвращает список всех записей в виде [(name, phone), ...],
отсортированный по имени. Сама структура не сортируется.
"""
records = []
current = head
while current is not None:
records.append((current['name'], current['phone']))
current = current['next']
records.sort(key=lambda x: x[0]) # сортировка по имени
return record

129
src/measure_time.py Normal file
View File

@ -0,0 +1,129 @@
"""
Экспериментальная часть. Пункт 2: Инструменты замера времени.
Цель: предоставить функции для многократного измерения времени выполнения
операций со структурами данных (LinkedList, HashTable, BST).
Особенности:
- Используется time.perf_counter() для высокой точности.
- Каждый эксперимент повторяется min_runs раз (по умолчанию 5), результаты сохраняются.
- Вычисляется среднее арифметическое и список всех замеров.
- Результаты можно напрямую сохранить в CSV.
"""
import time
from typing import List, Tuple, Callable, Any
import random
# Предполагается, что generate_test_data из пункта 1 уже определена
# from experimental_part1 import generate_test_data # если код в другом файле
# ========== 1. Базовые замеры ==========
def measure_time(func: Callable, *args, **kwargs) -> float:
"""
Измеряет время выполнения функции func(*args, **kwargs).
Возвращает время в секундах (float).
"""
start = time.perf_counter()
result = func(*args, **kwargs)
end = time.perf_counter()
return end - start, result
# ========== 2. Многократные замеры с усреднением ==========
def run_experiment(func: Callable, args: Tuple, min_runs: int = 5) -> Tuple[float, List[float]]:
"""
Повторяет замер функции func(*args) минимум min_runs раз.
Возвращает (среднееремя, список_всехамеров).
"""
times = []
for _ in range(min_runs):
elapsed, _ = measure_time(func, *args)
times.append(elapsed)
avg_time = sum(times) / len(times)
return avg_time, times
# ========== 3. Тестовые сценарии (заглушки для демонстрации) ==========
# Ниже приведены примеры-заглушки для структур данных.
# В реальной работе их нужно заменить на реализованные функции.
def stub_insert(structure, name, phone):
"""Заглушка для вставки."""
pass
def stub_find(structure, name):
"""Заглушка для поиска."""
return None
def stub_delete(structure, name):
"""Заглушка для удаления."""
pass
def stub_list_all(structure):
"""Заглушка для получения всех записей."""
return []
# Пример функции, которая вставляет все записи из списка в структуру
def insert_all(structure, records, insert_func):
"""
Выполняет вставку всех записей (name, phone) в structure,
используя функцию insert_func(structure, name, phone).
"""
for name, phone in records:
insert_func(structure, name, phone)
# Пример замера вставки для конкретной структуры
def benchmark_insert(structure_creator, records, insert_func, runs=5):
"""
Создаёт новую структуру через structure_creator(),
затем измеряет время вставки всех записей.
"""
def _insert_all():
structure = structure_creator()
insert_all(structure, records, insert_func)
return structure
avg_time, all_times = run_experiment(_insert_all, args=(), min_runs=runs)
return avg_time, all_times
# ========== 4. Пример использования (демонстрация) ==========
if __name__ == "__main__":
# Фиксируем seed для воспроизводимости
random.seed(42)
# Генерируем тестовые данные (пункт 1)
N = 10000
records_shuffled, records_sorted = generate_test_data(N, duplicate_names_ratio=0.1)
# Выбираем 100 случайных имён для поиска (существующих) и 10 несуществующих
existing_names = [name for name, _ in records_shuffled[:100]] # первые 100 имён
nonexisting_names = [f"None_{i}" for i in range(10)]
# Для демонстрации используем заглушки
def dummy_creator():
return "dummy_structure"
print("=== Демонстрация замера времени (заглушки) ===")
avg, times = benchmark_insert(dummy_creator, records_shuffled, stub_insert, runs=3)
print(f"Среднее время вставки (заглушка): {avg:.6f} сек")
print(f"Все замеры: {times}")
# Пример сбора результатов для CSV
results = [
["Структура", "Режим", "Операция", "Время (сек)"],
["LinkedList", "случайный", "вставка", 0.123],
# ... реальные данные появятся после реализации структур
]
# Сохранение в CS
V (раскомментировать при необходимости)
# import csv
# with open("docs/data/results.csv", "w", newline="") as f:
# writer = csv.writer(f)
# writer.writerows(results)
print("\nГотово. Замеры можно проводить после реализации структур.")