diff --git a/semyanovra/docs/data/1-st/experiment_results.csv b/semyanovra/docs/data/1-st/experiment_results.csv new file mode 100644 index 0000000..ed99866 --- /dev/null +++ b/semyanovra/docs/data/1-st/experiment_results.csv @@ -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 diff --git a/semyanovra/docs/data/1-st/main.py b/semyanovra/docs/data/1-st/main.py new file mode 100644 index 0000000..3d416a2 --- /dev/null +++ b/semyanovra/docs/data/1-st/main.py @@ -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() \ No newline at end of file diff --git a/semyanovra/docs/data/1-st/performance_comparison.png b/semyanovra/docs/data/1-st/performance_comparison.png new file mode 100644 index 0000000..468ba01 Binary files /dev/null and b/semyanovra/docs/data/1-st/performance_comparison.png differ diff --git a/semyanovra/docs/data/2-nd/maze.py b/semyanovra/docs/data/2-nd/maze.py new file mode 100644 index 0000000..5f013ee --- /dev/null +++ b/semyanovra/docs/data/2-nd/maze.py @@ -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() \ No newline at end of file diff --git a/semyanovra/docs/data/2-nd/maze/empty15x15.txt b/semyanovra/docs/data/2-nd/maze/empty15x15.txt new file mode 100644 index 0000000..d35c7ee --- /dev/null +++ b/semyanovra/docs/data/2-nd/maze/empty15x15.txt @@ -0,0 +1,15 @@ +############### +#S # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# E# +############### \ No newline at end of file diff --git a/semyanovra/docs/data/2-nd/maze/large20x20.txt b/semyanovra/docs/data/2-nd/maze/large20x20.txt new file mode 100644 index 0000000..2fb12cc --- /dev/null +++ b/semyanovra/docs/data/2-nd/maze/large20x20.txt @@ -0,0 +1,21 @@ +#################### +#S # +# ### ##### ##### ## +# # # # # # +### # # ### ### # # # +# # # # # # # +# ### ### # # ### ### +# # # # # +##### ### # ####### # +# # # # # +# ### # ### ### # # # +# # # # # # # +# # ### ### # # ### # +# # # # # +# # ######### # ### # +# # # # # # +# ### # ### # # # # # +# # # # # # +### ### # ### # # ### +# E # +#################### \ No newline at end of file diff --git a/semyanovra/docs/data/2-nd/maze/level1.txt b/semyanovra/docs/data/2-nd/maze/level1.txt new file mode 100644 index 0000000..9755a58 --- /dev/null +++ b/semyanovra/docs/data/2-nd/maze/level1.txt @@ -0,0 +1,6 @@ +########## +#S # +# ### #### +# # # +# # E# +########## \ No newline at end of file diff --git a/semyanovra/docs/data/2-nd/maze/medium10x10.txt b/semyanovra/docs/data/2-nd/maze/medium10x10.txt new file mode 100644 index 0000000..44183fd --- /dev/null +++ b/semyanovra/docs/data/2-nd/maze/medium10x10.txt @@ -0,0 +1,10 @@ +########## +#S # +# ### ### # +# # # # +### # ### # +# # # +# ### ### # +# # # +# ### ###E# +########## \ No newline at end of file diff --git a/semyanovra/docs/data/2-nd/maze/no_exit10x10.txt b/semyanovra/docs/data/2-nd/maze/no_exit10x10.txt new file mode 100644 index 0000000..3d7575e --- /dev/null +++ b/semyanovra/docs/data/2-nd/maze/no_exit10x10.txt @@ -0,0 +1,11 @@ +########## +#S # +# ### ### # +# # # # +### # ### # +# # # +# ### ### # +# # # +# ### ### # +########E # +########## \ No newline at end of file diff --git a/semyanovra/docs/maze_benchmark.png b/semyanovra/docs/maze_benchmark.png new file mode 100644 index 0000000..f942095 Binary files /dev/null and b/semyanovra/docs/maze_benchmark.png differ diff --git a/semyanovra/docs/maze_experiment.csv b/semyanovra/docs/maze_experiment.csv new file mode 100644 index 0000000..87de814 --- /dev/null +++ b/semyanovra/docs/maze_experiment.csv @@ -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 diff --git a/semyanovra/docs/performance_comparison.png b/semyanovra/docs/performance_comparison.png new file mode 100644 index 0000000..468ba01 Binary files /dev/null and b/semyanovra/docs/performance_comparison.png differ diff --git a/semyanovra/docs/report1.md b/semyanovra/docs/report1.md new file mode 100644 index 0000000..13b635c --- /dev/null +++ b/semyanovra/docs/report1.md @@ -0,0 +1,66 @@ +# Отчёт по лабораторной работе «Структуры данных» + +## Цель работы +Реализовать три структуры данных (связный список, хеш-таблицу, двоичное дерево поиска) «с нуля» и экспериментально сравнить их производительность на операциях вставки, поиска и удаления записей телефонного справочника. + +## Реализованные структуры +- **Связный список (LinkedList)** – элементы хранятся в узлах со ссылкой на следующий. +- **Хеш-таблица (HashTable)** – массив корзин фиксированного размера (997), каждая корзина – связный список. +- **Двоичное дерево поиска (BST)** – узлы содержат ключ (имя) и ссылки на левое/правое поддеревья. + +Все операции реализованы вручную без использования классов. + +## Методика эксперимента +- **Объём данных**: N = 10 000 записей вида `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)) на 10 000 элементов занимает ~0.027 с, что на два порядка медленнее хеш-таблицы. +Вставка в конец также требует прохода по всему списку (~4 с). + +### Удаление +Наиболее эффективно в хеш-таблице (≈0.00004 с). +В BST на случайных данных удаление быстрое (0.00011 с), но на отсортированных деградирует до 0.05 с. +В списке удаление (0.013 с) сравнимо с поиском. + +## Выводы и рекомендации + +1. **Хеш-таблица** – оптимальный выбор для задач, где нужен быстрый доступ по ключу, а порядок данных не важен. +2. **Двоичное дерево поиска** – подходит, если требуется получать записи в отсортированном порядке **и** данные поступают в случайном порядке. При отсортированных входных данных необходима балансировка (AVL, красно-чёрное дерево). +3. **Связный список** – неэффективен для больших объёмов; может применяться только в учебных целях или при очень маленьких коллекциях. + +В реальных проектах для справочников и словарей следует выбирать хеш-таблицы или сбалансированные деревья в зависимости от необходимости упорядоченного вывода. \ No newline at end of file diff --git a/semyanovra/docs/report2.md b/semyanovra/docs/report2.md new file mode 100644 index 0000000..68702a7 --- /dev/null +++ b/semyanovra/docs/report2.md @@ -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 остается надежным выбором для небольших лабиринтов, где простота реализации важнее производительности. \ No newline at end of file diff --git a/src/bst.py b/src/bst.py new file mode 100644 index 0000000..04ba1d3 --- /dev/null +++ b/src/bst.py @@ -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 \ No newline at end of file diff --git a/src/hash_table.py b/src/hash_table.py new file mode 100644 index 0000000..9f914f6 --- /dev/null +++ b/src/hash_table.py @@ -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 \ No newline at end of file diff --git a/src/linked_list.py b/src/linked_list.py new file mode 100644 index 0000000..fefab0b --- /dev/null +++ b/src/linked_list.py @@ -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 \ No newline at end of file diff --git a/src/measure_time.py b/src/measure_time.py new file mode 100644 index 0000000..f84fe2a --- /dev/null +++ b/src/measure_time.py @@ -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Готово. Замеры можно проводить после реализации структур.") \ No newline at end of file