Merge pull request '[2]' (#393) from Ezhovnd/2026-rff_mp:develop into develop

Reviewed-on: #393
This commit is contained in:
git_admin 2026-09-05 08:36:27 +00:00
commit 5a247e8491
3 changed files with 577 additions and 284 deletions

View File

@ -1,285 +1,285 @@
import csv import csv
import time import time
import os import os
import random import random
from collections import deque from collections import deque
import heapq import heapq
import matplotlib.pyplot as plt import matplotlib.pyplot as plt
import pandas as pd import pandas as pd
class Cell: class Cell:
def __init__(self, x, y): def __init__(self, x, y):
self.x = x self.x = x
self.y = y self.y = y
self.is_wall = False self.is_wall = False
self.is_start = False self.is_start = False
self.is_exit = False self.is_exit = False
def isPassable(self): def isPassable(self):
return not self.is_wall return not self.is_wall
class Maze: class Maze:
def __init__(self, width, height): def __init__(self, width, height):
self.width = width self.width = width
self.height = height self.height = height
self.cells = [] self.cells = []
self.start = None self.start = None
self.exit = None self.exit = None
def getCell(self, x, y): def getCell(self, x, y):
if 0 <= x < self.width and 0 <= y < self.height: if 0 <= x < self.width and 0 <= y < self.height:
return self.cells[y][x] return self.cells[y][x]
return None return None
def getNeighbors(self, cell): def getNeighbors(self, cell):
neighbors = [] neighbors = []
for dx, dy in [(-1, 0), (1, 0), (0, -1), (0, 1)]: for dx, dy in [(-1, 0), (1, 0), (0, -1), (0, 1)]:
neighbor = self.getCell(cell.x + dx, cell.y + dy) neighbor = self.getCell(cell.x + dx, cell.y + dy)
if neighbor and neighbor.isPassable(): if neighbor and neighbor.isPassable():
neighbors.append(neighbor) neighbors.append(neighbor)
return neighbors return neighbors
class MazeBuilder: class MazeBuilder:
def buildFromFile(self, filename): def buildFromFile(self, filename):
raise NotImplementedError raise NotImplementedError
class TextFileMazeBuilder(MazeBuilder): class TextFileMazeBuilder(MazeBuilder):
def buildFromFile(self, filename): def buildFromFile(self, filename):
with open(filename, 'r', encoding='utf-8') as f: with open(filename, 'r', encoding='utf-8') as f:
lines = [line.rstrip('\n') for line in f.readlines()] lines = [line.rstrip('\n') for line in f.readlines()]
height = len(lines) height = len(lines)
width = max(len(line) for line in lines) width = max(len(line) for line in lines)
maze = Maze(width, height) maze = Maze(width, height)
maze.cells = [[Cell(x, y) for x in range(width)] for y in range(height)] maze.cells = [[Cell(x, y) for x in range(width)] for y in range(height)]
for y, line in enumerate(lines): for y, line in enumerate(lines):
for x, char in enumerate(line): for x, char in enumerate(line):
cell = maze.cells[y][x] cell = maze.cells[y][x]
if char == '#': if char == '#':
cell.is_wall = True cell.is_wall = True
elif char == 'S': elif char == 'S':
cell.is_start = True cell.is_start = True
maze.start = cell maze.start = cell
elif char == 'E': elif char == 'E':
cell.is_exit = True cell.is_exit = True
maze.exit = cell maze.exit = cell
if maze.start is None or maze.exit is None: if maze.start is None or maze.exit is None:
raise ValueError("В файле должны быть символы S и E") raise ValueError("В файле должны быть символы S и E")
return maze return maze
class PathFindingStrategy: class PathFindingStrategy:
def findPath(self, maze, start, exit): def findPath(self, maze, start, exit):
raise NotImplementedError raise NotImplementedError
class BFSStrategy(PathFindingStrategy): class BFSStrategy(PathFindingStrategy):
def findPath(self, maze, start, exit): def findPath(self, maze, start, exit):
queue = deque([start]) queue = deque([start])
came_from = {start: None} came_from = {start: None}
visited = set([start]) visited = set([start])
while queue: while queue:
current = queue.popleft() current = queue.popleft()
if current == exit: if current == exit:
break break
for neighbor in maze.getNeighbors(current): for neighbor in maze.getNeighbors(current):
if neighbor not in visited: if neighbor not in visited:
visited.add(neighbor) visited.add(neighbor)
queue.append(neighbor) queue.append(neighbor)
came_from[neighbor] = current came_from[neighbor] = current
path = self._reconstruct_path(came_from, exit) path = self._reconstruct_path(came_from, exit)
return path, len(visited) return path, len(visited)
def _reconstruct_path(self, came_from, exit): def _reconstruct_path(self, came_from, exit):
path = [] path = []
current = exit current = exit
while current is not None: while current is not None:
path.append(current) path.append(current)
current = came_from.get(current) current = came_from.get(current)
path.reverse() path.reverse()
return path if path and path[0] == came_from.get(exit) or path[0] == exit else [] return path if path and path[0] == came_from.get(exit) or path[0] == exit else []
class DFSStrategy(PathFindingStrategy): class DFSStrategy(PathFindingStrategy):
def findPath(self, maze, start, exit): def findPath(self, maze, start, exit):
stack = [start] stack = [start]
came_from = {start: None} came_from = {start: None}
visited = set([start]) visited = set([start])
while stack: while stack:
current = stack.pop() current = stack.pop()
if current == exit: if current == exit:
break break
for neighbor in maze.getNeighbors(current): for neighbor in maze.getNeighbors(current):
if neighbor not in visited: if neighbor not in visited:
visited.add(neighbor) visited.add(neighbor)
stack.append(neighbor) stack.append(neighbor)
came_from[neighbor] = current came_from[neighbor] = current
path = self._reconstruct_path(came_from, exit) path = self._reconstruct_path(came_from, exit)
return path, len(visited) return path, len(visited)
def _reconstruct_path(self, came_from, exit): def _reconstruct_path(self, came_from, exit):
path = [] path = []
current = exit current = exit
while current is not None: while current is not None:
path.append(current) path.append(current)
current = came_from.get(current) current = came_from.get(current)
path.reverse() path.reverse()
return path return path
class AStarStrategy(PathFindingStrategy): class AStarStrategy(PathFindingStrategy):
def heuristic(self, a, b): def heuristic(self, a, b):
return abs(a.x - b.x) + abs(a.y - b.y) return abs(a.x - b.x) + abs(a.y - b.y)
def findPath(self, maze, start, exit): def findPath(self, maze, start, exit):
open_set = [] open_set = []
counter = 0 counter = 0
heapq.heappush(open_set, (0, counter, start)) heapq.heappush(open_set, (0, counter, start))
came_from = {start: None} came_from = {start: None}
g_score = {start: 0} g_score = {start: 0}
visited = set() visited = set()
while open_set: while open_set:
_, _, current = heapq.heappop(open_set) _, _, current = heapq.heappop(open_set)
if current in visited: if current in visited:
continue continue
visited.add(current) visited.add(current)
if current == exit: if current == exit:
break break
for neighbor in maze.getNeighbors(current): for neighbor in maze.getNeighbors(current):
tentative_g = g_score[current] + 1 tentative_g = g_score[current] + 1
if neighbor not in g_score or tentative_g < g_score[neighbor]: if neighbor not in g_score or tentative_g < g_score[neighbor]:
came_from[neighbor] = current came_from[neighbor] = current
g_score[neighbor] = tentative_g g_score[neighbor] = tentative_g
f_score = tentative_g + self.heuristic(neighbor, exit) f_score = tentative_g + self.heuristic(neighbor, exit)
counter += 1 counter += 1
heapq.heappush(open_set, (f_score, counter, neighbor)) heapq.heappush(open_set, (f_score, counter, neighbor))
path = self._reconstruct_path(came_from, exit) path = self._reconstruct_path(came_from, exit)
return path, len(visited) return path, len(visited)
def _reconstruct_path(self, came_from, exit): def _reconstruct_path(self, came_from, exit):
path = [] path = []
current = exit current = exit
while current is not None: while current is not None:
path.append(current) path.append(current)
current = came_from.get(current) current = came_from.get(current)
path.reverse() path.reverse()
return path return path
class SearchStats: class SearchStats:
def __init__(self, time_ms, visited_cells, path_length): def __init__(self, time_ms, visited_cells, path_length):
self.time_ms = time_ms self.time_ms = time_ms
self.visited_cells = visited_cells self.visited_cells = visited_cells
self.path_length = path_length self.path_length = path_length
class MazeSolver: class MazeSolver:
def __init__(self, maze=None, strategy=None): def __init__(self, maze=None, strategy=None):
self.maze = maze self.maze = maze
self.strategy = strategy self.strategy = strategy
def setStrategy(self, strategy): def setStrategy(self, strategy):
self.strategy = strategy self.strategy = strategy
def solve(self): def solve(self):
if not self.maze or not self.strategy: if not self.maze or not self.strategy:
return None return None
start_time = time.perf_counter() start_time = time.perf_counter()
path, visited_count = self.strategy.findPath(self.maze, self.maze.start, self.maze.exit) path, visited_count = self.strategy.findPath(self.maze, self.maze.start, self.maze.exit)
end_time = time.perf_counter() end_time = time.perf_counter()
time_ms = (end_time - start_time) * 1000 time_ms = (end_time - start_time) * 1000
path_length = len(path) if path and path[-1] == self.maze.exit else 0 path_length = len(path) if path and path[-1] == self.maze.exit else 0
return SearchStats(round(time_ms, 4), visited_count, path_length) return SearchStats(round(time_ms, 4), visited_count, path_length)
def create_maze_with_walls(size, wall_probability=0.3): def create_maze_with_walls(size, wall_probability=0.3):
maze = Maze(size, size) maze = Maze(size, size)
maze.cells = [[Cell(x, y) for x in range(size)] for y in range(size)] maze.cells = [[Cell(x, y) for x in range(size)] for y in range(size)]
for y in range(size): for y in range(size):
for x in range(size): for x in range(size):
if random.random() < wall_probability: if random.random() < wall_probability:
maze.cells[y][x].is_wall = True maze.cells[y][x].is_wall = True
maze.start = maze.cells[0][0] maze.start = maze.cells[0][0]
maze.exit = maze.cells[size-1][size-1] maze.exit = maze.cells[size-1][size-1]
maze.start.is_start = True maze.start.is_start = True
maze.exit.is_exit = True maze.exit.is_exit = True
maze.start.is_wall = False maze.start.is_wall = False
maze.exit.is_wall = False maze.exit.is_wall = False
return maze return maze
def create_empty_maze(size): def create_empty_maze(size):
maze = Maze(size, size) maze = Maze(size, size)
maze.cells = [[Cell(x, y) for x in range(size)] for y in range(size)] maze.cells = [[Cell(x, y) for x in range(size)] for y in range(size)]
maze.start = maze.cells[0][0] maze.start = maze.cells[0][0]
maze.exit = maze.cells[size-1][size-1] maze.exit = maze.cells[size-1][size-1]
maze.start.is_start = True maze.start.is_start = True
maze.exit.is_exit = True maze.exit.is_exit = True
return maze return maze
def create_no_exit_maze(size, wall_probability=0.3): def create_no_exit_maze(size, wall_probability=0.3):
maze = create_maze_with_walls(size, wall_probability) maze = create_maze_with_walls(size, wall_probability)
maze.exit.is_wall = True maze.exit.is_wall = True
return maze return maze
def run_experiment(): def run_experiment():
maze_configs = { maze_configs = {
"10x10_simple": {"size": 10, "type": "normal", "wall_prob": 0.1}, "10x10_simple": {"size": 10, "type": "normal", "wall_prob": 0.1},
"50x50_with_deadends": {"size": 50, "type": "normal", "wall_prob": 0.3}, "50x50_with_deadends": {"size": 50, "type": "normal", "wall_prob": 0.3},
"100x100_complex": {"size": 100, "type": "normal", "wall_prob": 0.35}, "100x100_complex": {"size": 100, "type": "normal", "wall_prob": 0.35},
"empty": {"size": 30, "type": "empty"}, "empty": {"size": 30, "type": "empty"},
"no_exit": {"size": 30, "type": "no_exit", "wall_prob": 0.3}, "no_exit": {"size": 30, "type": "no_exit", "wall_prob": 0.3},
} }
strategies = { strategies = {
"BFS": BFSStrategy(), "BFS": BFSStrategy(),
"DFS": DFSStrategy(), "DFS": DFSStrategy(),
"AStar": AStarStrategy() "AStar": AStarStrategy()
} }
results = [] results = []
for maze_name, config in maze_configs.items(): for maze_name, config in maze_configs.items():
size = config["size"] size = config["size"]
maze_type = config["type"] maze_type = config["type"]
if maze_type == "empty": if maze_type == "empty":
maze = create_empty_maze(size) maze = create_empty_maze(size)
elif maze_type == "no_exit": elif maze_type == "no_exit":
maze = create_no_exit_maze(size, config.get("wall_prob", 0.3)) maze = create_no_exit_maze(size, config.get("wall_prob", 0.3))
else: else:
maze = create_maze_with_walls(size, config.get("wall_prob", 0.3)) maze = create_maze_with_walls(size, config.get("wall_prob", 0.3))
for strat_name, strategy in strategies.items(): for strat_name, strategy in strategies.items():
solver = MazeSolver(maze, strategy) solver = MazeSolver(maze, strategy)
times, visited_list, lengths = [], [], [] times, visited_list, lengths = [], [], []
for _ in range(7): for _ in range(7):
stats = solver.solve() stats = solver.solve()
times.append(stats.time_ms) times.append(stats.time_ms)
visited_list.append(stats.visited_cells) visited_list.append(stats.visited_cells)
lengths.append(stats.path_length) lengths.append(stats.path_length)
avg_time = sum(times) / len(times) avg_time = sum(times) / len(times)
avg_visited = sum(visited_list) / len(visited_list) avg_visited = sum(visited_list) / len(visited_list)
avg_length = sum(lengths) / len(lengths) avg_length = sum(lengths) / len(lengths)
results.append([ results.append([
maze_name, strat_name, maze_name, strat_name,
round(avg_time, 4), round(avg_time, 4),
int(avg_visited), int(avg_visited),
int(avg_length) int(avg_length)
]) ])
os.makedirs("results", exist_ok=True) os.makedirs("results", exist_ok=True)
csv_path = "results/results.csv" csv_path = "results/results.csv"
with open(csv_path, "w", newline="", encoding="utf-8") as f: with open(csv_path, "w", newline="", encoding="utf-8") as f:
writer = csv.writer(f) writer = csv.writer(f)
writer.writerow(["лабиринт", "стратегия", "время_мс", "посещено_клеток", "длина_пути"]) writer.writerow(["лабиринт", "стратегия", "время_мс", "посещено_клеток", "длина_пути"])
writer.writerows(results) writer.writerows(results)
df = pd.read_csv(csv_path) df = pd.read_csv(csv_path)
plt.figure(figsize=(12, 6)) plt.figure(figsize=(12, 6))
for strat in df["стратегия"].unique(): for strat in df["стратегия"].unique():
subset = df[df["стратегия"] == strat] subset = df[df["стратегия"] == strat]
plt.plot(subset["лабиринт"], subset["время_мс"], marker='o', label=strat) plt.plot(subset["лабиринт"], subset["время_мс"], marker='o', label=strat)
plt.title("Сравнение времени работы алгоритмов") plt.title("Сравнение времени работы алгоритмов")
plt.xlabel("Лабиринт") plt.xlabel("Лабиринт")
plt.ylabel("Время (мс)") plt.ylabel("Время (мс)")
plt.legend() plt.legend()
plt.grid(True) plt.grid(True)
plt.xticks(rotation=45) plt.xticks(rotation=45)
plt.tight_layout() plt.tight_layout()
plt.savefig("results/time_comparison.png") plt.savefig("results/time_comparison.png")
plt.close() plt.close()
plt.figure(figsize=(12, 6)) plt.figure(figsize=(12, 6))
for strat in df["стратегия"].unique(): for strat in df["стратегия"].unique():
subset = df[df["стратегия"] == strat] subset = df[df["стратегия"] == strat]
plt.plot(subset["лабиринт"], subset["посещено_клеток"], marker='o', label=strat) plt.plot(subset["лабиринт"], subset["посещено_клеток"], marker='o', label=strat)
plt.title("Количество посещённых клеток") plt.title("Количество посещённых клеток")
plt.xlabel("Лабиринт") plt.xlabel("Лабиринт")
plt.ylabel("Посещено клеток") plt.ylabel("Посещено клеток")
plt.legend() plt.legend()
plt.grid(True) plt.grid(True)
plt.xticks(rotation=45) plt.xticks(rotation=45)
plt.tight_layout() plt.tight_layout()
plt.savefig("results/visited_comparison.png") plt.savefig("results/visited_comparison.png")
plt.close() plt.close()
if __name__ == "__main__": if __name__ == "__main__":
run_experiment() run_experiment()

View File

@ -0,0 +1,35 @@
ОТЧЁТ ПО ЗАДАНИЮ 1
1. Влияние порядка данных на BST
При случайном порядке данных BST работает быстро (вставка ~0.005 сек).
При отсортированном порядке дерево вырождается в цепочку, и время вставки
возрастает примерно в 5060 раз (~0.31 сек). Сложность деградирует с O(log n) до O(n).
2. Почему хеш-таблица нечувствительна к порядку
Хеш-таблица использует хеш-функцию, которая равномерно распределяет элементы
по бакетам. Поэтому порядок входных данных почти не влияет на скорость
вставки, поиска и удаления (в среднем O(1)).
3. Почему связный список медленен при поиске
Для поиска в связном списке нужно последовательно пройти все элементы.
Поэтому поиск всегда выполняется за O(n), независимо от порядка данных.
Это делает его самым медленным при операциях поиска и удаления.
4. Как работает удаление
- LinkedList: O(n) — нужно найти элемент и перестроить ссылки.
- HashTable: O(1) в среднем — удаление внутри нужного бакета.
- BST: O(log n) в среднем, O(n) в худшем — при двух потомках ищется
минимальный элемент в правом поддереве.
5. Вывод и рекомендации
Рекомендуемые структуры в зависимости от задачи:
- Частые вставки и поиск → HashTable (лучшая общая производительность)
- Нужно получать данные в отсортированном порядке → BST (только при случайных данных)
- Данные приходят отсортированными → HashTable (BST сильно деградирует)
- Малый объём данных и простота → LinkedList
Итог: Для большинства реальных задач лучше всего подходит хеш-таблица.
BST имеет смысл использовать только при случайном порядке данных и
необходимости частого получения отсортированного списка.

258
Ezhovnd/задание 1.py Normal file
View File

@ -0,0 +1,258 @@
import time
import random
import csv
import os
import matplotlib.pyplot as plt
def ll_insert(head, name, phone):
new_node = {'name': name, 'phone': phone, 'next': None}
if head is None:
return new_node
current = head
while current['next']:
current = current['next']
current['next'] = new_node
return head
def ll_find(head, name):
current = head
while current:
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']:
if current['next']['name'] == name:
current['next'] = current['next']['next']
return head
current = current['next']
return head
def ll_list_all(head):
result = []
current = head
while current:
result.append((current['name'], current['phone']))
current = current['next']
return sorted(result)
def create_hash_table(size=200):
return [None] * size
def ht_insert(buckets, name, phone):
index = hash(name) % len(buckets)
buckets[index] = ll_insert(buckets[index], name, phone)
def ht_find(buckets, name):
index = hash(name) % len(buckets)
return ll_find(buckets[index], name)
def ht_delete(buckets, name):
index = hash(name) % len(buckets)
buckets[index] = ll_delete(buckets[index], name)
def ht_list_all(buckets):
result = []
for bucket in buckets:
current = bucket
while current:
result.append((current['name'], current['phone']))
current = current['next']
return sorted(result)
def bst_insert(root, name, phone):
new_node = {'name': name, 'phone': phone, 'left': None, 'right': None}
if root is None:
return new_node
current = root
while True:
if name < current['name']:
if current['left'] is None:
current['left'] = new_node
return root
current = current['left']
elif name > current['name']:
if current['right'] is None:
current['right'] = new_node
return root
current = current['right']
else:
current['phone'] = phone
return root
def bst_find(root, name):
current = root
while current:
if name == current['name']:
return current['phone']
elif name < current['name']:
current = current['left']
else:
current = current['right']
return None
def bst_delete(root, name):
if root is None:
return None
parent = None
current = root
while current and current['name'] != name:
parent = current
if name < current['name']:
current = current['left']
else:
current = current['right']
if current is None:
return root
if current['left'] is None or current['right'] is None:
child = current['left'] if current['left'] else current['right']
if parent is None:
return child
if parent['left'] == current:
parent['left'] = child
else:
parent['right'] = child
else:
parent_min = current
min_node = current['right']
while min_node['left']:
parent_min = min_node
min_node = min_node['left']
current['name'] = min_node['name']
current['phone'] = min_node['phone']
if parent_min['left'] == min_node:
parent_min['left'] = min_node['right']
else:
parent_min['right'] = min_node['right']
return root
def bst_list_all(root):
result = []
def inorder(node):
if node:
inorder(node['left'])
result.append((node['name'], node['phone']))
inorder(node['right'])
inorder(root)
return result
def generate_records(n=10000):
records = [(f"User_{i:05d}", f"8{random.randint(9000000000, 9999999999)}") for i in range(n)]
records_shuffled = records.copy()
random.shuffle(records_shuffled)
records_sorted = sorted(records, key=lambda x: x[0])
return records_shuffled, records_sorted
def run_experiments():
random.seed(42)
records_shuffled, records_sorted = generate_records(10000)
all_results = []
structures = ["LinkedList", "HashTable", "BST"]
modes = [("случайный", records_shuffled), ("отсортированный", records_sorted)]
for mode_name, records in modes:
for struct_name in structures:
print(f"Тестируем: {struct_name} | Режим: {mode_name}")
for run in range(5):
if struct_name == "LinkedList":
data = None
elif struct_name == "HashTable":
data = create_hash_table(200)
else:
data = None
start = time.perf_counter()
for name, phone in records:
if struct_name == "LinkedList":
data = ll_insert(data, name, phone)
elif struct_name == "HashTable":
ht_insert(data, name, phone)
else:
data = bst_insert(data, name, phone)
insert_time = time.perf_counter() - start
test_names = [r[0] for r in random.sample(records, 100)]
test_names += [f"None_{i}" for i in range(10)]
start = time.perf_counter()
for name in test_names:
if struct_name == "LinkedList":
ll_find(data, name)
elif struct_name == "HashTable":
ht_find(data, name)
else:
bst_find(data, name)
find_time = time.perf_counter() - start
delete_names = [r[0] for r in random.sample(records, 50)]
start = time.perf_counter()
for name in delete_names:
if struct_name == "LinkedList":
data = ll_delete(data, name)
elif struct_name == "HashTable":
ht_delete(data, name)
else:
data = bst_delete(data, name)
delete_time = time.perf_counter() - start
all_results.append([struct_name, mode_name, "вставка", run + 1, insert_time])
all_results.append([struct_name, mode_name, "поиск", run + 1, find_time])
all_results.append([struct_name, mode_name, "удаление", run + 1, delete_time])
os.makedirs("docs/data", exist_ok=True)
filepath = "docs/data/results.csv"
with open(filepath, "w", newline="", encoding="utf-8") as f:
writer = csv.writer(f)
writer.writerow(["Структура", "Режим", "Операция", "Запуск", "Время (сек)"])
writer.writerows(all_results)
print(f"\nРезультаты сохранены в {filepath}")
return all_results
def plot_results(csv_path="docs/data/results.csv"):
import pandas as pd
df = pd.read_csv(csv_path)
summary = df.groupby(["Структура", "Режим", "Операция"])["Время (сек)"].mean().reset_index()
for op in ["вставка", "поиск", "удаление"]:
op_data = summary[summary["Операция"] == op]
plt.figure(figsize=(10, 6))
x_labels = []
y_values = []
for _, row in op_data.iterrows():
label = f"{row['Структура']}\n({row['Режим']})"
x_labels.append(label)
y_values.append(row["Время (сек)"])
plt.bar(x_labels, y_values, color=['#4C72B0', '#55A868', '#C44E52'] * 2)
plt.title(f"Среднее время операции: {op}")
plt.ylabel("Время (сек)")
plt.xticks(rotation=45)
plt.tight_layout()
plt.savefig(f"docs/data/graph_{op}.png")
print(f"График сохранён: docs/data/graph_{op}.png")
if __name__ == "__main__":
run_experiments()
plot_results()