2026-rff_mp/sorokinfi/427.py

472 lines
19 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import csv
import random
import sys
import time
from collections import defaultdict, deque
import pandas as pd
import matplotlib.pyplot as plt
from abc import ABC, abstractmethod
# увеличиваем лимит рекурсии
sys.setrecursionlimit(25000)
# ЗАДАНИЕ 1: СТРУКТУРЫ ДАННЫХ
# 1. связный список, узел: {'name': 'Имя', 'phone': '123', 'next': None}
# проходит до конца и добавляет в конец
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'] is not None:
current = current['next']
current['next'] = new_node
return head
# ищет узел, возвращает телефон или None
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']
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):
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
# 2. хеш-таблица
# хеш-функция для вычисления бекета
def ht_hash(name, size):
return hash(name) % size
# вычисляет индекс, вызывает ll_insert для соответствующего бакета
def ht_insert(buckets, name, phone):
size = len(buckets)
idx = ht_hash(name, size)
buckets[idx] = ll_insert(buckets[idx], name, phone)
# поиск по хеш-таблице
def ht_find(buckets, name):
size = len(buckets)
idx = ht_hash(name, size)
return ll_find(buckets[idx], name)
# удаление из хеш-таблицы
def ht_delete(buckets, name):
size = len(buckets)
idx = ht_hash(name, size)
buckets[idx] = ll_delete(buckets[idx], name)
# собирает все записи из всех бакетов и сортирует
def ht_list_all(buckets):
all_records = []
for head in buckets:
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
# 3. двоичное дерево поиска
# узел — словарь: {'name': 'Имя', 'phone': '123', 'left': None, 'right': None}
# рекурсивно или итеративно вставляет, возвращает новый корень (если корень меняется)
def bst_insert(root, name, phone):
if root is None:
return {'name': name, 'phone': phone, 'left': None, 'right': None}
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):
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_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']
# две ветви
successor = root['right']
while successor['left'] is not None:
successor = successor['left']
root['name'] = successor['name']
root['phone'] = successor['phone']
root['right'] = bst_delete(root['right'], successor['name'])
return root
# центрированный обход (рекурсивно собирает записи в отсортированном порядке)
def bst_list_all(root):
records = []
def _inorder(node):
if node is not None:
_inorder(node['left'])
records.append((node['name'], node['phone']))
_inorder(node['right'])
_inorder(root)
return records
# 4. ЭКСПЕРИМЕНТАЛЬНАЯ ЧАСТЬ
def run_experiments():
N = 3000
HASH_SIZE = 1007
print(f"генерация тестовых данных для N = {N}...")
records_sorted = [(f"User_{i:05d}", f"+7999123{i:04d}") for i in range(N)]
records_shuffled = records_sorted.copy()
random.seed(42)
random.shuffle(records_shuffled)
# подготовка выборок
existing_sample = [r[0] for r in random.sample(records_sorted, min(100, N))]
non_existing_sample = [f"None_{i}" for i in range(10)]
search_names = existing_sample + non_existing_sample
delete_names = [r[0] for r in random.sample(records_sorted, min(50, N))]
csv_rows = [["структура", "режим", "операция", "повторение", "время (сек)"]]
modes = [("случайный", records_shuffled), ("отсортированный", records_sorted)]
print("запуск экспериментов (5 повторений для каждого режима)")
# ТЕСТ: СВЯЗНЫЙ СПИСОК
for mode_name, data in modes:
for rep in range(1, 6):
head = None
t_start = time.perf_counter()
for name, phone in data:
head = ll_insert(head, name, phone)
t_end = time.perf_counter()
csv_rows.append(["LinkedList", mode_name, "вставка", rep, t_end - t_start])
t_start = time.perf_counter()
for name in search_names:
ll_find(head, name)
t_end = time.perf_counter()
csv_rows.append(["LinkedList", mode_name, "поиск", rep, t_end - t_start])
t_start = time.perf_counter()
for name in delete_names:
head = ll_delete(head, name)
t_end = time.perf_counter()
csv_rows.append(["LinkedList", mode_name, "удаление", rep, t_end - t_start])
# ТЕСТ: ХЕШ-ТАБЛИЦА
for mode_name, data in modes:
for rep in range(1, 6):
buckets = [None] * HASH_SIZE
t_start = time.perf_counter()
for name, phone in data:
ht_insert(buckets, name, phone)
t_end = time.perf_counter()
csv_rows.append(["HashTable", mode_name, "вставка", rep, t_end - t_start])
t_start = time.perf_counter()
for name in search_names:
ht_find(buckets, name)
t_end = time.perf_counter()
csv_rows.append(["HashTable", mode_name, "поиск", rep, t_end - t_start])
t_start = time.perf_counter()
for name in delete_names:
ht_delete(buckets, name)
t_end = time.perf_counter()
csv_rows.append(["HashTable", mode_name, "удаление", rep, t_end - t_start])
# ТЕСТ: ДЕРЕВО ПОИСКА (BST)
for mode_name, data in modes:
for rep in range(1, 6):
root = None
t_start = time.perf_counter()
for name, phone in data:
root = bst_insert(root, name, phone)
t_end = time.perf_counter()
csv_rows.append(["BST", mode_name, "вставка", rep, t_end - t_start])
t_start = time.perf_counter()
for name in search_names:
bst_find(root, name)
t_end = time.perf_counter()
csv_rows.append(["BST", mode_name, "поиск", rep, t_end - t_start])
t_start = time.perf_counter()
for name in delete_names:
root = bst_delete(root, name)
t_end = time.perf_counter()
csv_rows.append(["BST", mode_name, "удаление", rep, t_end - t_start])
# сохранение в csv
with open("results.csv", "w", newline="", encoding="utf-8") as f:
writer = csv.writer(f)
writer.writerows(csv_rows)
print("\nвсе замеры сохранены в файл 'results.csv'.")
show_summary(csv_rows)
# функция для подсчета и вывода среднего времени
def show_summary(rows):
summary = defaultdict(list)
for row in rows[1:]:
struct, mode, op, rep, elapsed = row
summary[(struct, mode, op)].append(elapsed)
print("\nСВОДНЫЕ РЕЗУЛЬТАТЫ (СРЕДНЕЕ ВРЕМЯ ИЗ 5 ЗАПУСКОВ)")
print(f"{'структура':<12} | {'режим данных':<15} | {'операция':<10} | {'время (сек)':<12}")
print("-" * 59)
for (struct, mode, op), times in sorted(summary.items()):
avg_time = sum(times) / len(times)
print(f"{struct:<12} | {mode:<15} | {op:<10} | {avg_time:.6f}")
# 5. АНАЛИЗ РЕЗУЛЬТАТОВ
def plot_results(csv_filename="results.csv"):
print("построение графика")
try:
df = pd.read_csv(csv_filename)
df_insert = df[df["операция"] == "вставка"]
pivot_df = df_insert.pivot_table(
index="структура",
columns="режим",
values="время (сек)",
aggfunc="mean"
)
pivot_df.plot(kind="bar", figsize=(10, 6), color=['#1f77b4', '#ff7f0e'])
plt.title("сравнение времени вставки (N=3000)")
plt.ylabel("среднее время выполнения (сек)")
plt.xlabel("структура данных")
plt.xticks(rotation=0)
plt.grid(axis='y', linestyle='--', alpha=0.7)
plt.savefig("benchmark_chart.png")
print("график сохранен как benchmark_chart.png")
plt.show()
except FileNotFoundError:
print(f"файл {csv_filename} не найден. сначала запустите тесты.")
def print_report():
report = """
5. АНАЛИЗ РЕЗУЛЬТАТОВ ЭКСПЕРИМЕНТОВ
1. Влияние порядка входных данных на скорость вставки в BST:
- На случайных данных BST строится сбалансированным. Высота дерева составляет
примерно O(log N), поэтому вставка происходит почти мгновенно.
- На отсортированных данных происходит ДЕГРАДАЦИЯ дерева. Каждый элемент больше
предыдущего и вставляется строго вправо. Дерево вырождается в связный список.
Сложность возрастает до O(N), что отчетливо видно по гигантскому пику на графике.
2. Чувствительность Хеш-таблицы к порядку:
- Хеш-таблица НЕ ЧУВСТВИТЕЛЬНА к порядку данных. Математическая хеш-функция
превращает любое имя в хаотичный индекс и равномерно распределяет записи
по бакетам. В обоих режимах операции выполняются за константное время O(1).
3. Почему связный список всегда медленен при поиске:
- У связного списка нет индексов для прямого доступа. Поиск всегда линейный O(N)
— алгоритм вынужден последовательно перебирать элементы от головы к хвосту.
4. Как удаление работает в каждой структуре:
- Связный список: O(N) затрачивается на линейный поиск узла, само удаление — O(1).
- Хеш-таблица: O(1) нахождение бакета по хешу, удаление из цепочки коллизий мгновенно.
- BST: В среднем O(log N), в худшем O(N). Требует поиска узла и перестройки связей
(замена удаляемого узла на его потомка или минимальный элемент правого поддерева).
ВЫВОД:
- ДЛЯ ЧАСТЫХ ВСТАВОК И ПОИСКА: Идеально подходит Хеш-таблица благодаря скорости O(1).
- ДЛЯ ПОЛУЧЕНИЯ ДАННЫХ В ПОРЯДКЕ (АЛФАВИТНОМ): Стоит выбирать Двоичное дерево поиска
(BST), так как обход дерева (In-order traversal) сразу выдает отсортированные данные.
"""
print(report)
if __name__ == "__main__":
run_experiments()
plot_results()
print_report()
# ЗАДАНИЕ 2: ПОИСК ВЫХОДА ИЗ ЛАБИРИНТА
# поиск выхода из лабиринта
class Maze:
def __init__(self, grid):
self.grid = grid
self.rows = len(grid)
self.cols = len(grid[0]) if self.rows > 0 else 0
self.start = self._find_point('S')
self.end = self._find_point('E')
def _find_point(self, char):
for r in range(self.rows):
for c in range(self.cols):
if self.grid[r][c] == char: return (r, c)
return None
def is_walkable(self, r, c):
return 0 <= r < self.rows and 0 <= c < self.cols and self.grid[r][c] != '#'
class PathfindingStrategy(ABC):
@abstractmethod
def solve(self, maze: Maze): pass
# стратегия №1: BFS
class BFSStrategy(PathfindingStrategy):
def solve(self, maze: Maze):
# проверка входных данных
if not maze.start or not maze.end:
return None
# инициализация структур данных
queue = deque([(maze.start, [maze.start])])
visited = set([maze.start])
# направление для шагов
directions = [(-1, 0), (1, 0), (0, -1), (0, 1)]
# основной цикл обхода лабиринта
while queue:
current, path = queue.popleft()
if current == maze.end:
return path, len(visited) # возвращаем путь и размер посещенных клеток
r, c = current
for dr, dc in directions:
nr, nc = r + dr, c + dc
if maze.is_walkable(nr, nc) and (nr, nc) not in visited:
visited.add((nr, nc))
queue.append(((nr, nc), path + [(nr, nc)]))
return None, len(visited)
# стратегия №2: DFS
class DFSStrategy(PathfindingStrategy):
def solve(self, maze: Maze):
if not maze.start or not maze.end:
return None, 0
# инициализация структур данных
stack = [(maze.start, [maze.start])]
visited = set()
directions = [(-1, 0), (1, 0), (0, -1), (0, 1)]
# основной цикл обхода лабиринта
while stack:
current, path = stack.pop()
if current in visited:
continue
visited.add(current)
if current == maze.end:
return path, len(visited)
r, c = current
for dr, dc in directions:
nr, nc = r + dr, c + dc
if maze.is_walkable(nr, nc):
stack.append(((nr, nc), path + [(nr, nc)]))
return None, len(visited)
class MazeSolver:
def __init__(self, strategy: PathfindingStrategy): self._strategy = strategy
def set_strategy(self, strategy: PathfindingStrategy): self._strategy = strategy
def solve_maze(self, maze: Maze): return self._strategy.solve(maze)
# подготовка тестовых лабиринтов
class MazeFactory:
@staticmethod
def _generate_random_grid(width, height, wall_chance=0.25, has_exit=True):
# вспомогательный метод генерации сетки лабиринта заданной сложности
grid = [[" " for _ in range(width)] for _ in range(height)]
for r in range(height):
for c in range(width):
if random.random() < wall_chance: grid[r][c] = "#"
grid[0][0] = "S"
if has_exit: grid[height-1][width-1] = "E"
else: grid[height-1][width-1] = "#"
return grid
@staticmethod
def create_maze(maze_type):
random.seed(42)
if maze_type == "маленький (10x10)":
return Maze(MazeFactory._generate_random_grid(10, 10, wall_chance=0.15))
elif maze_type == "средний (50x50)":
return Maze(MazeFactory._generate_random_grid(50, 50, wall_chance=0.25))
elif maze_type == "большой (100x100)":
return Maze(MazeFactory._generate_random_grid(100, 100, wall_chance=0.3))
elif maze_type == "пустой":
grid = [[" " for _ in range(30)] for _ in range(30)]
grid[0][0], grid[29][29] = "S", "E"
return Maze(grid)
elif maze_type == "без выхода":
return Maze(MazeFactory._generate_random_grid(20, 20, wall_chance=0.2, has_exit=False))
return None
# экспериментальная часть лабиринтов
def run_maze_experiments():
maze_types = ["маленький (10x10)", "средний (50x50)", "большой (100x100)", "пустой", "без выхода"]
strategies = [("BFS", BFSStrategy()), ("DFS", DFSStrategy())]