diff --git a/sorokinfi/427.py b/sorokinfi/427.py new file mode 100644 index 0000000..e46818f --- /dev/null +++ b/sorokinfi/427.py @@ -0,0 +1,379 @@ +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: ПОИСК ВЫХОДА ИЗ ЛАБИРИНТА + +# стратегия №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 + 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