diff --git a/konnovaea/lab1/docs/data/graph_delete.png b/konnovaea/lab1/docs/data/graph_delete.png new file mode 100644 index 0000000..75a2633 Binary files /dev/null and b/konnovaea/lab1/docs/data/graph_delete.png differ diff --git a/konnovaea/lab1/docs/data/graph_insert.png b/konnovaea/lab1/docs/data/graph_insert.png new file mode 100644 index 0000000..77ed9b9 Binary files /dev/null and b/konnovaea/lab1/docs/data/graph_insert.png differ diff --git a/konnovaea/lab1/docs/data/graph_search.png b/konnovaea/lab1/docs/data/graph_search.png new file mode 100644 index 0000000..c55c4ab Binary files /dev/null and b/konnovaea/lab1/docs/data/graph_search.png differ diff --git a/konnovaea/lab1/docs/data/results.csv b/konnovaea/lab1/docs/data/results.csv new file mode 100644 index 0000000..9664ef7 --- /dev/null +++ b/konnovaea/lab1/docs/data/results.csv @@ -0,0 +1,109 @@ +Структура, Режим, Операция, Замер, Время (сек) +LinkedList,случайный,вставка,1,0.011328999999932421 +LinkedList,случайный,вставка,2,0.0023913999993965263 +LinkedList,случайный,вставка,3,0.0017174000004160916 +LinkedList,случайный,вставка,4,0.0017204000005222042 +LinkedList,случайный,вставка,5,0.0016142999993462581 +LinkedList,случайный,вставка,среднее,0.0037544999999227003 +LinkedList,случайный,поиск,1,7.3999999585794285e-06 +LinkedList,случайный,поиск,2,1.1699999959091656e-05 +LinkedList,случайный,поиск,3,8.099999831756577e-06 +LinkedList,случайный,поиск,4,5.899999450775795e-06 +LinkedList,случайный,поиск,5,1.500000053056283e-05 +LinkedList,случайный,поиск,среднее,9.619999946153258e-06 +LinkedList,случайный,удаление,1,5.199999577598646e-06 +LinkedList,случайный,удаление,2,3.4000004234258085e-06 +LinkedList,случайный,удаление,3,3.9000005926936865e-06 +LinkedList,случайный,удаление,4,4.399999852466863e-06 +LinkedList,случайный,удаление,5,2.2599999283556826e-05 +LinkedList,случайный,удаление,среднее,7.899999945948367e-06 +HashTable,случайный,вставка,1,0.013529500000004191 +HashTable,случайный,вставка,2,0.017691199999717355 +HashTable,случайный,вставка,3,0.016795400000773952 +HashTable,случайный,вставка,4,0.015214900000501075 +HashTable,случайный,вставка,5,0.012209399999846937 +HashTable,случайный,вставка,среднее,0.015088080000168702 +HashTable,случайный,поиск,1,0.00028960000054212287 +HashTable,случайный,поиск,2,0.0001171000003523659 +HashTable,случайный,поиск,3,0.00013169999965612078 +HashTable,случайный,поиск,4,0.00011999999969702912 +HashTable,случайный,поиск,5,0.00016460000006190967 +HashTable,случайный,поиск,среднее,0.00016460000006190967 +HashTable,случайный,удаление,1,0.0001094999997803825 +HashTable,случайный,удаление,2,0.00011030000041500898 +HashTable,случайный,удаление,3,6.83999996908824e-05 +HashTable,случайный,удаление,4,6.479999956354732e-05 +HashTable,случайный,удаление,5,0.0001382000000376138 +HashTable,случайный,удаление,среднее,9.8239999897487e-05 +BST,случайный,вставка,1,0.02586410000003525 +BST,случайный,вставка,2,0.023826999999982945 +BST,случайный,вставка,3,0.028718300000036834 +BST,случайный,вставка,4,0.02642329999980575 +BST,случайный,вставка,5,0.026569300000119256 +BST,случайный,вставка,среднее,0.026280399999996006 +BST,случайный,поиск,1,0.00024870000015653204 +BST,случайный,поиск,2,0.00022480000006908085 +BST,случайный,поиск,3,0.00033259999963775044 +BST,случайный,поиск,4,0.00025629999981902074 +BST,случайный,поиск,5,0.00023359999977401458 +BST,случайный,поиск,среднее,0.00025919999989127974 +BST,случайный,удаление,1,0.00018809999983204762 +BST,случайный,удаление,2,0.00015689999963797163 +BST,случайный,удаление,3,0.00014709999959450215 +BST,случайный,удаление,4,0.0001754000004439149 +BST,случайный,удаление,5,0.00018170000021200394 +BST,случайный,удаление,среднее,0.00016983999994408806 +LinkedList,отсортированный,вставка,1,0.0013518000005205977 +LinkedList,отсортированный,вставка,2,0.0014992999995229184 +LinkedList,отсортированный,вставка,3,0.0033320000002277084 +LinkedList,отсортированный,вставка,4,0.001253299999916635 +LinkedList,отсортированный,вставка,5,0.0013355999999475898 +LinkedList,отсортированный,вставка,среднее,0.0017544000000270898 +LinkedList,отсортированный,поиск,1,6.299999768089037e-06 +LinkedList,отсортированный,поиск,2,5.800000508315861e-06 +LinkedList,отсортированный,поиск,3,5.699999746866524e-06 +LinkedList,отсортированный,поиск,4,5.500000042957254e-06 +LinkedList,отсортированный,поиск,5,1.9600000086938962e-05 +LinkedList,отсортированный,поиск,среднее,8.580000030633528e-06 +LinkedList,отсортированный,удаление,1,2.8000004022032954e-06 +LinkedList,отсортированный,удаление,2,4.300000000512227e-06 +LinkedList,отсортированный,удаление,3,2.6999996407539584e-06 +LinkedList,отсортированный,удаление,4,2.499999936844688e-06 +LinkedList,отсортированный,удаление,5,2.4000000848900527e-06 +LinkedList,отсортированный,удаление,среднее,2.9400000130408445e-06 +HashTable,отсортированный,вставка,1,0.013422199999695295 +HashTable,отсортированный,вставка,2,0.011119499999949767 +HashTable,отсортированный,вставка,3,0.01018590000057884 +HashTable,отсортированный,вставка,4,0.011275699999714561 +HashTable,отсортированный,вставка,5,0.010843500000191852 +HashTable,отсортированный,вставка,среднее,0.011369360000026063 +HashTable,отсортированный,поиск,1,0.0001083999995898921 +HashTable,отсортированный,поиск,2,0.00013240000043879263 +HashTable,отсортированный,поиск,3,0.0002434999996694387 +HashTable,отсортированный,поиск,4,0.0001129000002038083 +HashTable,отсортированный,поиск,5,0.0001036000003296067 +HashTable,отсортированный,поиск,среднее,0.0001401600000463077 +HashTable,отсортированный,удаление,1,5.670000064128544e-05 +HashTable,отсортированный,удаление,2,7.49000000723754e-05 +HashTable,отсортированный,удаление,3,5.3699999625678174e-05 +HashTable,отсортированный,удаление,4,5.450000026030466e-05 +HashTable,отсортированный,удаление,5,5.409999994299142e-05 +HashTable,отсортированный,удаление,среднее,5.878000010852702e-05 +BST,отсортированный,вставка,1,5.166896599999745 +BST,отсортированный,вставка,2,5.045173700000305 +BST,отсортированный,вставка,3,4.877277200000208 +BST,отсортированный,вставка,4,4.796063099999628 +BST,отсортированный,вставка,5,4.7685291000007055 +BST,отсортированный,вставка,среднее,4.930787940000118 +BST,отсортированный,поиск,1,0.05183889999989333 +BST,отсортированный,поиск,2,0.04380440000022645 +BST,отсортированный,поиск,3,0.044272600000113016 +BST,отсортированный,поиск,4,0.04941080000025977 +BST,отсортированный,поиск,5,0.04630559999986872 +BST,отсортированный,поиск,среднее,0.04712646000007226 +BST,отсортированный,удаление,1,0.023101800000404182 +BST,отсортированный,удаление,2,0.026490100000046368 +BST,отсортированный,удаление,3,0.02241980000053445 +BST,отсортированный,удаление,4,0.020923000000038883 +BST,отсортированный,удаление,5,0.022132500000225264 +BST,отсортированный,удаление,среднее,0.02301344000024983 diff --git a/konnovaea/lab1/docs/data/table_results.png b/konnovaea/lab1/docs/data/table_results.png new file mode 100644 index 0000000..feaf1a3 Binary files /dev/null and b/konnovaea/lab1/docs/data/table_results.png differ diff --git a/konnovaea/lab1/docs/отчет.ipynb b/konnovaea/lab1/docs/отчет.ipynb new file mode 100644 index 0000000..f2b6cb9 --- /dev/null +++ b/konnovaea/lab1/docs/отчет.ipynb @@ -0,0 +1,259 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "d7f65344", + "metadata": {}, + "source": [ + "# Отчёт \n", + "## Телефонный справочник: реализация и сравнение структур данных\n", + "\n", + "**Студент:** Коннова Е.А.\n", + "**Группа:** 429\n", + "**Дата:** 12.05.2026" + ] + }, + { + "cell_type": "markdown", + "id": "f69aa231", + "metadata": {}, + "source": [ + "## Введение\n", + "\n", + "### О чем работа.\n", + "В данной работе рассматриваются три базовые структуры данных:\n", + "- Связный список (LinkedList)\n", + "- Хеш-таблица (HashTable)\n", + "- Двоичное дерево поиска (BST)\n", + "\n", + "Они применяются для хранения записей телефонного справочника.\n", + "\n", + "### Цель всей работы\n", + "Реализовать три структуры данных «с нуля», применить их для хранения записей телефонного справочника и экспериментально сравнить производительность основных операций (вставка, поиск, удаление).\n", + "\n", + "### Задачи по достижению цели\n", + "1. Реализовать связный список с операциями insert, find, delete, list_all\n", + "2. Реализовать хеш-таблицу на основе связных списков\n", + "3. Реализовать двоичное дерево поиска\n", + "4. Сгенерировать тестовые данные (10000 записей)\n", + "5. Провести замеры времени для каждой структуры (5 повторений)\n", + "6. Сравнить результаты и сделать выводы" + ] + }, + { + "cell_type": "markdown", + "id": "56e2f617", + "metadata": {}, + "source": [ + "## Часть 1. Общая информация о структурах данных\n", + "\n", + "### 1.1 Для неспециалистов\n", + "\n", + "**Что такое структура данных?**\n", + "Это способ организации и хранения данных в компьютере.\n", + "\n", + "**Три структуры из работы:**\n", + "\n", + "| Структура | Как работает | Пример из жизни |\n", + "|-----------|--------------|-----------------|\n", + "| Связный список | Цепочка элементов, где каждый знает следующий | Верёвка с узелками |\n", + "| Хеш-таблица | Массив корзин, элемент попадает в корзину по номеру | Картотека с ящиками |\n", + "| Двоичное дерево | Иерархическая структура: левые меньше, правые больше | Телефонный справочник |\n", + "\n", + "### 1.2 Обзор технологий\n", + "\n", + "**Связный список**\n", + "- Узел: `{'name': str, 'phone': str, 'next': None}`\n", + "- Вставка: O(1) в начало, O(n) в конец\n", + "- Поиск: O(n) - линейный обход\n", + "- Удаление: O(n) - сначала найти\n", + "\n", + "**Хеш-таблица**\n", + "- Корзины: список из None или голов списков\n", + "- Хеш-функция: `hash = (hash * 31 + ord(ch)) % size`\n", + "- Вставка/поиск/удаление: O(1) в среднем\n", + "\n", + "**Двоичное дерево поиска (BST)**\n", + "- Узел: `{'name': str, 'phone': str, 'left': None, 'right': None}`\n", + "- Вставка/поиск/удаление: O(log n) в среднем, O(n) в худшем\n", + "\n", + "### 1.3 Обоснование выбора подхода\n", + "\n", + "**Почему именно эти структуры?**\n", + "1. Они фундаментальны и изучаются в курсе\n", + "2. Показывают разные компромиссы (скорость vs порядок)\n", + "3. Позволяют наглядно сравнить производительность\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "id": "d9327709", + "metadata": {}, + "source": [ + "## Часть 2. Техническая реализация\n", + "\n", + "### 2.1 Постановка задачи\n", + "\n", + "Реализовать телефонный справочник с операциями:\n", + "- `insert(name, phone)` — добавить или обновить запись\n", + "- `find(name)` — вернуть телефон или None\n", + "- `delete(name)` — удалить запись\n", + "- `list_all()` — вернуть все записи, отсортированные по имени\n", + "\n", + "### 2.2 Верхнеуровневое решение\n", + "\n", + "**Связный список (LinkedList)**\n", + "- `ll_insert(head, name, phone)` — добавление в конец, возвращает голову\n", + "- `ll_find(head, name)` — линейный поиск, возвращает телефон или None\n", + "- `ll_delete(head, name)` — удаление с перепривязкой, возвращает голову\n", + "- `ll_list_all(head)` — сбор всех записей и сортировка\n", + "\n", + "**Хеш-таблица (HashTable)**\n", + "- `hash_function(name, size)` — ключ → номер корзины\n", + "- `ht_create(size)` — создание таблицы\n", + "- `ht_insert(buckets, name, phone)` — вызов ll_insert для нужной корзины\n", + "- `ht_find(buckets, name)` — вызов ll_find для нужной корзины\n", + "- `ht_delete(buckets, name)` — вызов ll_delete для нужной корзины\n", + "- `ht_list_all(buckets)` — сбор из всех корзин + сортировка\n", + "\n", + "**Двоичное дерево (BST)**\n", + "- `bst_insert(root, name, phone)` — итеративная вставка\n", + "- `bst_find(root, name)` — поиск\n", + "- `bst_delete(root, name)` — удаление с поиском преемника\n", + "- `bst_list_all(root)` — in-order обход (уже отсортировано)" + ] + }, + { + "cell_type": "markdown", + "id": "c1cd08d8", + "metadata": {}, + "source": [ + "## Часть 3. Эксперименты и результаты\n", + "\n", + "### 3.1 Инструменты и методика\n", + "\n", + "**Параметры эксперимента:**\n", + "- Количество записей: 10 000\n", + "- Количество повторений: 5\n", + "- Поиск: 100 существующих + 10 несуществующих\n", + "- Удаление: 50 случайных записей\n", + "- Режимы: случайный порядок, отсортированный порядок\n", + "\n", + "### 3.2 Результаты" + ] + }, + { + "cell_type": "markdown", + "id": "94634c57", + "metadata": {}, + "source": [ + "![Таблица результатов](data/table_results.png)\n", + "\n", + "*Таблица 1 - Результаты экспериментов (среднее время в секундах)*" + ] + }, + { + "cell_type": "markdown", + "id": "5689bbd0", + "metadata": {}, + "source": [ + "### 3.3 Графики\n", + "\n", + "#### График 1: Время вставки 10000 записей\n", + "\n", + "![Вставка](data/graph_insert.png)\n", + "\n", + "#### График 2: Время поиска 110 записей\n", + "\n", + "![Поиск](data/graph_search.png)\n", + "\n", + "#### График 3: Время удаления 50 записей\n", + "\n", + "![Удаление](data/graph_delete.png)" + ] + }, + { + "cell_type": "markdown", + "id": "5561d9dd", + "metadata": {}, + "source": [ + "### 3.4 Сравнение и анализ\n", + "\n", + "**Как порядок входных данных влияет на BST?**\n", + "\n", + "| Режим | Вставка | Поиск | Удаление |\n", + "|-------|---------|-------|----------|\n", + "| Случайный | 0.026 сек | 0.00026 сек | 0.00017 сек |\n", + "| Отсортированный | 4.931 сек | 0.047 сек | 0.023 сек |\n", + "\n", + "Вывод: На случайных данных BST работает быстро (O(log n)). На отсортированных данных BST вырождается в связный список (O(n)). Работает медленее в 190 раз.\n", + "\n", + "**Техническая ошибка:** Из-за ограничения глубины рекурсии в Python (1000 вызовов) рекурсивная реализация BST не смогла бы обработать 10000 записей. Поэтому все операции BST были реализованы итеративно.\n", + "\n", + "**Почему хеш-таблица не чувствительна к порядку?**\n", + "\n", + "Хеш-функция распределяет записи по корзинам независимо от порядка вставки. Распределение по корзинам равномерное.\n", + "\n", + "**Почему связный список всегда медленен при поиске?**\n", + "\n", + "Связный список не имеет индексов. Поэтому нужно перебирать элементы последовательно. Сложность поиска - O(n).\n", + "\n", + "**Как удаление работает в каждой структуре?**\n", + "\n", + "| Структура | Сложность |\n", + "|-----------|-----------|\n", + "| LinkedList | O(n) |\n", + "| HashTable | O(1) |\n", + "| BST | O(log n) / O(n) |\n", + "\n", + "В связных списках сначала нужно найти, потом перепривязать. В хеш-таблице сразу находишь корзину по хешу. В двоичном дереве нужно найти узел и перестроить поддеревья." + ] + }, + { + "cell_type": "markdown", + "id": "a57d1502", + "metadata": {}, + "source": [ + "## Заключение\n", + "\n", + "### Выводы из каждой части\n", + "\n", + "**Из части 1:**\n", + "- Каждая структура имеет свои теоретические характеристики\n", + "- Связный список - прост, но медленен\n", + "- Хеш-таблица - быстра, но не сохраняет порядок\n", + "- BST - быстр и сохраняет порядок, но требует балансировки\n", + "\n", + "**Из части 2:**\n", + "- Все три структуры успешно реализованы\n", + "- Хеш-таблица использует связный список для корзин\n", + "- BST написан итеративно для избежания RecursionError\n", + "\n", + "**Из части 3:**\n", + "- Эксперименты подтвердили теоретические оценки\n", + "- BST на отсортированных данных деградирует\n", + "- Хеш-таблица стабильна независимо от порядка\n", + "\n", + "### Итоговая рекомендация\n", + "\n", + "| Сценарий | Рекомендация | Причина |\n", + "|----------|--------------|---------|\n", + "| Частый поиск по ключу | Хеш-таблица | O(1) |\n", + "| Частые вставки/удаления | Хеш-таблица | Стабильная скорость |\n", + "| Нужен отсортированный вывод | Сбалансированное дерево | In-order обход |\n", + "| Данные поступают отсортированно | Хеш-таблица | BST деградирует |\n", + "| Мало данных (<100) | Любая | Разница незаметна |\n", + "\n", + "**Для телефонного справочника лучший выбор - хеш-таблица.**" + ] + } + ], + "metadata": { + "language_info": { + "name": "python" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/konnovaea/lab1/experiments.py b/konnovaea/lab1/experiments.py new file mode 100644 index 0000000..72e83c0 --- /dev/null +++ b/konnovaea/lab1/experiments.py @@ -0,0 +1,282 @@ +import random +import time +import csv +import os +from lab1.phonebook import * + +def generate_test_data(n=10000): + + records = [(f"User_{i:05d}", f"+7-999-{i:07d}") 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 get_random_names(records, n=100): + return[name for name, _ in random.sample(records, min(n, len(records)))] + +def run_linked_experiments(records, mode_name): + + print(f"\n связный список ({mode_name}):") + + print("вставка 10000 записей:") + + insert_times = [] + for run in range(5): + start = time.perf_counter() + head = None + for name, phone in records: + head = ll_insert(head, name, phone) + end = time.perf_counter() + insert_times.append(end - start) + print(f"Вставка {run+1}/5: {insert_times[-1]:.6f} сек") + + avg_insert = sum(insert_times) / 5 + print(f"среднее: {avg_insert:.6f} сек") + + print("поиск 110 записей:") + + exist_names = get_random_names(records, 100) + non_exist_names = [f"None_{i}" for i in range(10)] + + find_times = [] + for run in range(5): + start = time.perf_counter() + + for name in exist_names: + ll_find(head, name) + for name in non_exist_names: + ll_find(head, name) + + end = time.perf_counter() + find_times.append(end - start) + print(f"поиск {run+1}/5: {find_times[-1]:.6f} сек") + + avg_find = sum(find_times) / 5 + print(f"среднее: {avg_find:.6f} сек") + + print("удаление 50 случайных записей:") + + to_delete = get_random_names(records,50) + + delete_times = [] + for run in range(5): + current_head = head + start = time.perf_counter() + for name in to_delete: + current_head = ll_delete(current_head, name) + end = time.perf_counter() + delete_times.append(end - start) + print(f"удаление {run+1}/5: {delete_times[-1]:.6f} сек") + + avg_delete = sum(delete_times) / 5 + print(f"среднее: {avg_delete:.6f} сек") + + return{ + 'structure': 'LinkedList', + 'mode': mode_name, + 'insert_avg': avg_insert, + 'insert_all': insert_times, + 'find_avg': avg_find, + 'find_all': find_times, + 'delete_avg': avg_delete, + 'delete_all': delete_times + } + +def run_hash_experiments(records, mode_name): + + print(f"\n хеш-таблица({mode_name})") + + print("вставка 10000 записей:") + + insert_times = [] + for run in range(5): + start = time.perf_counter() + + buckets = ht_create(1000) + for name, phone in records: + buckets = ht_insert(buckets, name, phone) + + end = time.perf_counter() + insert_times.append(end - start) + print(f"Вставка {run+1}/5: {insert_times[-1]:.6f} сек") + + avg_insert = sum(insert_times) / 5 + print(f"среднее: {avg_insert:.6f} сек") + + print("поиск 110 записей:") + + exist_names = get_random_names(records, 100) + non_exist_names = [f"None_{i}" for i in range(10)] + + find_times = [] + for run in range(5): + start = time.perf_counter() + + for name in exist_names: + ht_find(buckets, name) + for name in non_exist_names: + ht_find(buckets, name) + + end = time.perf_counter() + find_times.append(end - start) + print(f"поиск {run+1}/5: {find_times[-1]:.6f} сек") + + avg_find = sum(find_times) / 5 + print(f"среднее: {avg_find:.6f} сек") + + print("удаление 50 случайных записей:") + + to_delete = get_random_names(records,50) + + delete_times = [] + for run in range(5): + current_buckets = buckets.copy() + start = time.perf_counter() + for name in to_delete: + current_buckets = ht_delete(current_buckets, name) + end = time.perf_counter() + delete_times.append(end - start) + print(f"удаление {run+1}/5: {delete_times[-1]:.6f} сек") + + avg_delete = sum(delete_times) / 5 + print(f"среднее: {avg_delete:.6f} сек") + + return{ + 'structure': 'HashTable', + 'mode': mode_name, + 'insert_avg': avg_insert, + 'insert_all': insert_times, + 'find_avg': avg_find, + 'find_all': find_times, + 'delete_avg': avg_delete, + 'delete_all': delete_times + } + +def run_bst_experiments(records, mode_name): + + print(f"\n двоичное дерево({mode_name})") + + print("вставка 10000 записей:") + + insert_times = [] + for run in range(5): + start = time.perf_counter() + + root = None + for name, phone in records: + root = bst_insert(root, name, phone) + + end = time.perf_counter() + insert_times.append(end - start) + print(f"Вставка {run+1}/5: {insert_times[-1]:.6f} сек") + + avg_insert = sum(insert_times) / 5 + print(f"среднее: {avg_insert:.6f} сек") + + print("поиск 110 записей:") + + exist_names = get_random_names(records, 100) + non_exist_names = [f"None_{i}" for i in range(10)] + + find_times = [] + for run in range(5): + start = time.perf_counter() + + for name in exist_names: + bst_find(root, name) + for name in non_exist_names: + bst_find(root, name) + + end = time.perf_counter() + find_times.append(end - start) + print(f"поиск {run+1}/5: {find_times[-1]:.6f} сек") + + avg_find = sum(find_times) / 5 + print(f"среднее: {avg_find:.6f} сек") + + print("удаление 50 случайных записей:") + + to_delete = get_random_names(records,50) + + delete_times = [] + for run in range(5): + current_root = root + start = time.perf_counter() + for name in to_delete: + current_root = bst_delete(current_root, name) + end = time.perf_counter() + delete_times.append(end - start) + print(f"удаление {run+1}/5: {delete_times[-1]:.6f} сек") + + avg_delete = sum(delete_times) / 5 + print(f"среднее: {avg_delete:.6f} сек") + + return{ + 'structure': 'BST', + 'mode': mode_name, + 'insert_avg': avg_insert, + 'insert_all': insert_times, + 'find_avg': avg_find, + 'find_all': find_times, + 'delete_avg': avg_delete, + 'delete_all': delete_times + } + +def save_results_to_csv(all_results): + + os.makedirs("docs/data", exist_ok=True) + + with open("docs/data/results.csv", "w", encoding="utf-8") as f: + + f.write("Структура, Режим, Операция, Замер, Время (сек)\n") + + for res in all_results: + struct = res['structure'] + mode = res['mode'] + + + for i, t in enumerate(res['insert_all']): + f.write(f"{struct},{mode},вставка,{i+1},{t}\n") + f.write(f"{struct},{mode},вставка,среднее,{res['insert_avg']}\n") + + + for i, t in enumerate(res['find_all']): + f.write(f"{struct},{mode},поиск,{i+1},{t}\n") + f.write(f"{struct},{mode},поиск,среднее,{res['find_avg']}\n") + + + for i, t in enumerate(res['delete_all']): + f.write(f"{struct},{mode},удаление,{i+1},{t}\n") + f.write(f"{struct},{mode},удаление,среднее,{res['delete_avg']}\n") + + +def main(): + print("эксперименты по замеру производительности") + + records_shuffled, records_sorted = generate_test_data(10000) + + all_results = [] + + print("режим: случайный порядок") + + all_results.append(run_linked_experiments(records_shuffled, "случайный")) + all_results.append(run_hash_experiments(records_shuffled, "случайный")) + all_results.append(run_bst_experiments(records_shuffled, "случайный")) + + print("режим: отсортированный порядок") + + all_results.append(run_linked_experiments(records_sorted, "отсортированный")) + all_results.append(run_hash_experiments(records_sorted, "отсортированный")) + all_results.append(run_bst_experiments(records_sorted, "отсортированный")) + + save_results_to_csv(all_results) + +if __name__== "__main__": + main() + + + diff --git a/konnovaea/lab1/make_graphs.py b/konnovaea/lab1/make_graphs.py new file mode 100644 index 0000000..e23e7b6 --- /dev/null +++ b/konnovaea/lab1/make_graphs.py @@ -0,0 +1,123 @@ + +import matplotlib.pyplot as plt +import numpy as np +import os + + +os.makedirs('docs/data', exist_ok=True) + + +structures = ['LinkedList', 'HashTable', 'BST'] + +random_insert = [0.0037545, 0.015088, 0.026280] +sorted_insert = [0.0017544, 0.011369, 4.930788] + +random_search = [0.00000962, 0.0001646, 0.0002592] +sorted_search = [0.00000858, 0.00014016, 0.047126] + +random_delete = [0.0000079, 0.00009824, 0.00016984] +sorted_delete = [0.00000294, 0.00005878, 0.023013] + +x = np.arange(len(structures)) +width = 0.35 + +#график вставка +fig, ax = plt.subplots(figsize=(12, 7)) + +bars1 = ax.bar(x - width/2, random_insert, width, label='Случайный порядок', color='#3498db') +bars2 = ax.bar(x + width/2, sorted_insert, width, label='Отсортированный порядок', color='#e74c3c') + + +for bar in bars1: + height = bar.get_height() + ax.annotate(f'{height:.4f}', xy=(bar.get_x() + bar.get_width()/2, height), + xytext=(0, 3), textcoords="offset points", ha='center', va='bottom', fontsize=9) + +for bar in bars2: + height = bar.get_height() + if height < 1: + ax.annotate(f'{height:.4f}', xy=(bar.get_x() + bar.get_width()/2, height), + xytext=(0, 3), textcoords="offset points", ha='center', va='bottom', fontsize=9) + else: + ax.annotate(f'{height:.1f} сек', xy=(bar.get_x() + bar.get_width()/2, height), + xytext=(0, 5), textcoords="offset points", ha='center', va='bottom', fontsize=10, fontweight='bold') + +ax.set_ylabel('Время (сек)', fontsize=12) +ax.set_title('Время вставки 10000 записей', fontsize=14, fontweight='bold') +ax.set_xticks(x) +ax.set_xticklabels(structures, fontsize=11) +ax.legend(fontsize=11) +ax.set_yscale('log') +ax.grid(True, alpha=0.3, axis='y') + +plt.tight_layout() +plt.savefig('docs/data/graph_insert.png', dpi=150, bbox_inches='tight') +plt.close() + + +# график поиск +fig, ax = plt.subplots(figsize=(12, 7)) + +bars1 = ax.bar(x - width/2, random_search, width, label='Случайный порядок', color='#3498db') +bars2 = ax.bar(x + width/2, sorted_search, width, label='Отсортированный порядок', color='#e74c3c') + +for bar in bars1: + height = bar.get_height() + ax.annotate(f'{height:.6f}', xy=(bar.get_x() + bar.get_width()/2, height), + xytext=(0, 3), textcoords="offset points", ha='center', va='bottom', fontsize=9) + +for bar in bars2: + height = bar.get_height() + if height < 0.01: + ax.annotate(f'{height:.6f}', xy=(bar.get_x() + bar.get_width()/2, height), + xytext=(0, 3), textcoords="offset points", ha='center', va='bottom', fontsize=9) + else: + ax.annotate(f'{height:.4f}', xy=(bar.get_x() + bar.get_width()/2, height), + xytext=(0, 3), textcoords="offset points", ha='center', va='bottom', fontsize=9) + +ax.set_ylabel('Время (сек)', fontsize=12) +ax.set_title('Время поиска 110 записей', fontsize=14, fontweight='bold') +ax.set_xticks(x) +ax.set_xticklabels(structures, fontsize=11) +ax.legend(fontsize=11) +ax.set_yscale('log') +ax.grid(True, alpha=0.3, axis='y') + +plt.tight_layout() +plt.savefig('docs/data/graph_search.png', dpi=150, bbox_inches='tight') +plt.close() + + +# график удаление +fig, ax = plt.subplots(figsize=(12, 7)) + +bars1 = ax.bar(x - width/2, random_delete, width, label='Случайный порядок', color='#3498db') +bars2 = ax.bar(x + width/2, sorted_delete, width, label='Отсортированный порядок', color='#e74c3c') + +for bar in bars1: + height = bar.get_height() + ax.annotate(f'{height:.6f}', xy=(bar.get_x() + bar.get_width()/2, height), + xytext=(0, 3), textcoords="offset points", ha='center', va='bottom', fontsize=9) + +for bar in bars2: + height = bar.get_height() + if height < 0.01: + ax.annotate(f'{height:.6f}', xy=(bar.get_x() + bar.get_width()/2, height), + xytext=(0, 3), textcoords="offset points", ha='center', va='bottom', fontsize=9) + else: + ax.annotate(f'{height:.4f}', xy=(bar.get_x() + bar.get_width()/2, height), + xytext=(0, 3), textcoords="offset points", ha='center', va='bottom', fontsize=9) + +ax.set_ylabel('Время (сек)', fontsize=12) +ax.set_title('Время удаления 50 записей', fontsize=14, fontweight='bold') +ax.set_xticks(x) +ax.set_xticklabels(structures, fontsize=11) +ax.legend(fontsize=11) +ax.set_yscale('log') +ax.grid(True, alpha=0.3, axis='y') + +plt.tight_layout() +plt.savefig('docs/data/graph_delete.png', dpi=150, bbox_inches='tight') +plt.close() + + diff --git a/konnovaea/lab1/make_tables.py b/konnovaea/lab1/make_tables.py new file mode 100644 index 0000000..211d6a5 --- /dev/null +++ b/konnovaea/lab1/make_tables.py @@ -0,0 +1,34 @@ + +import matplotlib.pyplot as plt +import os + +os.makedirs('docs/data', exist_ok=True) + +data = [ + ['LinkedList', 'случайный', 0.0037545, 0.00000962, 0.0000079], + ['HashTable', 'случайный', 0.015088, 0.0001646, 0.00009824], + ['BST', 'случайный', 0.026280, 0.0002592, 0.00016984], + ['LinkedList', 'отсортированный', 0.0017544, 0.00000858, 0.00000294], + ['HashTable', 'отсортированный', 0.011369, 0.00014016, 0.00005878], + ['BST', 'отсортированный', 4.930788, 0.047126, 0.023013], +] + +fig, ax = plt.subplots(figsize=(12, 5)) +ax.axis('tight') +ax.axis('off') + +columns = ['Структура', 'Режим', 'Вставка (10000)', 'Поиск (110)', 'Удаление (50)'] +table = ax.table(cellText=data, colLabels=columns, loc='center', cellLoc='center') + +table.auto_set_font_size(False) +table.set_fontsize(10) +table.scale(1.2, 1.5) + +for i, row in enumerate(data): + if row[0] == 'BST' and row[2] > 1: + table[(i+1, 2)].set_facecolor('#ffcccc') + table[(i+1, 2)].set_text_props(weight='bold') + +plt.title('Результаты экспериментов (среднее время в секундах)', fontsize=14, fontweight='bold', pad=20) +plt.savefig('docs/data/table_results.png', dpi=200, bbox_inches='tight', facecolor='white') +plt.close() diff --git a/konnovaea/lab1/phonebook.py b/konnovaea/lab1/phonebook.py new file mode 100644 index 0000000..2676b26 --- /dev/null +++ b/konnovaea/lab1/phonebook.py @@ -0,0 +1,195 @@ +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 + +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 + +def hash_function(name, table_size): + total = 0 + for ch in name: + total = (total*31 + ord(ch)) % table_size + return total + +def ht_create(size=1000): + return [None]*size + +def ht_insert(buckets, name, phone): + idx = hash_function(name, len(buckets)) + buckets[idx] = ll_insert(buckets[idx], name, phone) + return buckets + +def ht_find(buckets, name): + idx = hash_function(name, len(buckets)) + return ll_find(buckets[idx], name) + +def ht_delete(buckets, name): + idx = hash_function(name, len(buckets)) + buckets[idx] = ll_delete(buckets[idx], name) + return buckets + +def ht_list_all(buckets): + records = [] + for bucket in buckets: + current = bucket + while current is not None: + records.append((current['name'], current['phone'])) + current = current['next'] + records.sort(key=lambda x: x[0]) + return records + +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 + break + current = current['left'] + elif name > current['name']: + if current['right'] is None: + current['right'] = new_node + break + current = current['right'] + else: + current['phone'] = phone + break + + return root + + +def bst_find(root, name): + + current = root + while current is not None: + if name == current['name']: + return current['phone'] + elif name < current['name']: + current = current['left'] + else: + current = current['right'] + return None + + +def _bst_find_min(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 + + parent = None + current = root + + while current is not None 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 and current['right'] is None: + if parent is None: + return None + if parent['left'] == current: + parent['left'] = None + else: + parent['right'] = None + return root + + if current['left'] is None: + child = current['right'] + elif current['right'] is None: + child = current['left'] + else: + successor_parent = current + successor = current['right'] + while successor['left'] is not None: + successor_parent = successor + successor = successor['left'] + + current['name'] = successor['name'] + current['phone'] = successor['phone'] + + if successor_parent['left'] == successor: + successor_parent['left'] = successor['right'] + else: + successor_parent['right'] = successor['right'] + + return root + + if parent is None: + return child + if parent['left'] == current: + parent['left'] = child + else: + parent['right'] = child + + return root + + +def bst_list_all(root): + records = [] + + def inorder(node): + if node is None: + return + inorder(node['left']) + records.append((node['name'], node['phone'])) + inorder(node['right']) + + inorder(root) + return records \ No newline at end of file diff --git a/konnovaea/lab2/dead.txt b/konnovaea/lab2/dead.txt new file mode 100644 index 0000000..5f30fec --- /dev/null +++ b/konnovaea/lab2/dead.txt @@ -0,0 +1,20 @@ +#################### +#S # +# # +# # +# # +# ######### # +# # # +# # # +# # # +# # # +# # # +# # # +# # # +# # # +# # # +# # +# # +# # +# E# +#################### diff --git a/konnovaea/lab2/docs/data/dead_path_graph.png b/konnovaea/lab2/docs/data/dead_path_graph.png new file mode 100644 index 0000000..264b496 Binary files /dev/null and b/konnovaea/lab2/docs/data/dead_path_graph.png differ diff --git a/konnovaea/lab2/docs/data/dead_time_graph.png b/konnovaea/lab2/docs/data/dead_time_graph.png new file mode 100644 index 0000000..a13c33d Binary files /dev/null and b/konnovaea/lab2/docs/data/dead_time_graph.png differ diff --git a/konnovaea/lab2/docs/data/dead_visited_graph.png b/konnovaea/lab2/docs/data/dead_visited_graph.png new file mode 100644 index 0000000..9a5b1b7 Binary files /dev/null and b/konnovaea/lab2/docs/data/dead_visited_graph.png differ diff --git a/konnovaea/lab2/docs/data/empty_path_graph.png b/konnovaea/lab2/docs/data/empty_path_graph.png new file mode 100644 index 0000000..faf6a2b Binary files /dev/null and b/konnovaea/lab2/docs/data/empty_path_graph.png differ diff --git a/konnovaea/lab2/docs/data/empty_time_graph.png b/konnovaea/lab2/docs/data/empty_time_graph.png new file mode 100644 index 0000000..74c7603 Binary files /dev/null and b/konnovaea/lab2/docs/data/empty_time_graph.png differ diff --git a/konnovaea/lab2/docs/data/empty_visited_graph.png b/konnovaea/lab2/docs/data/empty_visited_graph.png new file mode 100644 index 0000000..d48ec85 Binary files /dev/null and b/konnovaea/lab2/docs/data/empty_visited_graph.png differ diff --git a/konnovaea/lab2/docs/data/maze_experiments.csv b/konnovaea/lab2/docs/data/maze_experiments.csv new file mode 100644 index 0000000..9c127fb --- /dev/null +++ b/konnovaea/lab2/docs/data/maze_experiments.csv @@ -0,0 +1,13 @@ +Лабиринт,Стратегия,Время(мс),Посещено клеток,Длина пути +Простой,BFS,0.02,11.0,6.0 +Простой,DFS,0.012,9.0,8.0 +Простой,A*,0.02,9.0,6.0 +С тупиками,BFS,0.492,306.0,35.0 +С тупиками,DFS,0.234,198.0,81.0 +С тупиками,A*,0.456,225.0,35.0 +Пустой,BFS,3.486,2304.0,95.0 +Пустой,DFS,10.452,2304.0,1129.0 +Пустой,A*,5.743,2304.0,95.0 +Без выхода,BFS,0.01,1.0,нет пути +Без выхода,DFS,0.003,1.0,нет пути +Без выхода,A*,0.004,1.0,нет пути diff --git a/konnovaea/lab2/docs/data/maze_table_results.png b/konnovaea/lab2/docs/data/maze_table_results.png new file mode 100644 index 0000000..e6d92f9 Binary files /dev/null and b/konnovaea/lab2/docs/data/maze_table_results.png differ diff --git a/konnovaea/lab2/docs/data/noexit_time_graph.png b/konnovaea/lab2/docs/data/noexit_time_graph.png new file mode 100644 index 0000000..92280cd Binary files /dev/null and b/konnovaea/lab2/docs/data/noexit_time_graph.png differ diff --git a/konnovaea/lab2/docs/data/noexit_visited_graph.png b/konnovaea/lab2/docs/data/noexit_visited_graph.png new file mode 100644 index 0000000..139b800 Binary files /dev/null and b/konnovaea/lab2/docs/data/noexit_visited_graph.png differ diff --git a/konnovaea/lab2/docs/data/simple_path_graph.png b/konnovaea/lab2/docs/data/simple_path_graph.png new file mode 100644 index 0000000..456de28 Binary files /dev/null and b/konnovaea/lab2/docs/data/simple_path_graph.png differ diff --git a/konnovaea/lab2/docs/data/simple_time_graph.png b/konnovaea/lab2/docs/data/simple_time_graph.png new file mode 100644 index 0000000..d1a12e2 Binary files /dev/null and b/konnovaea/lab2/docs/data/simple_time_graph.png differ diff --git a/konnovaea/lab2/docs/data/simple_visited_graph.png b/konnovaea/lab2/docs/data/simple_visited_graph.png new file mode 100644 index 0000000..4b9722f Binary files /dev/null and b/konnovaea/lab2/docs/data/simple_visited_graph.png differ diff --git a/konnovaea/lab2/docs/lab2_report.ipynb b/konnovaea/lab2/docs/lab2_report.ipynb new file mode 100644 index 0000000..4971aa4 --- /dev/null +++ b/konnovaea/lab2/docs/lab2_report.ipynb @@ -0,0 +1,237 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "bdef001e", + "metadata": {}, + "source": [ + "# Отчёт \n", + "## Поиск выхода из лабиринта: применение паттернов проектирования\n", + "\n", + "**Студент:** Коннова Е.А.\n", + "**Группа:** 429\n", + "**Дата:** 22.05.2026" + ] + }, + { + "cell_type": "markdown", + "id": "21f948a4", + "metadata": {}, + "source": [ + "## Введение\n", + "\n", + "### О чём это работа\n", + "В данной работе реализуется программа для поиска выхода из лабиринта с применением паттернов проектирования. Поддерживаются три алгоритма поиска пути: BFS, DFS и A*.\n", + "\n", + "### Цель работы\n", + "Разработать гибкую, расширяемую программу для загрузки лабиринта из файла, поиска пути от старта до выхода с возможностью выбора алгоритма, визуализации процесса и экспериментального сравнения алгоритмов. Применить минимум 3 паттерна проектирования.\n", + "\n", + "### Задачи\n", + "1. Реализовать модель лабиринта (классы Cell, Maze)\n", + "2. Реализовать загрузку лабиринта из файла (паттерн Builder)\n", + "3. Реализовать алгоритмы поиска пути (паттерн Strategy): BFS, DFS, A*\n", + "4. Реализовать класс-оркестратор MazeSolver со сбором статистики\n", + "5. Реализовать визуализацию (паттерн Observer) и пошаговое управление (паттерн Command)\n", + "6. Провести эксперименты на лабиринтах разной сложности\n", + "7. Сравнить результаты и сделать выводы\n" + ] + }, + { + "cell_type": "markdown", + "id": "cf1dc2ba", + "metadata": {}, + "source": [ + "## Часть 1. Паттерны проектирования\n", + "\n", + "### Использованные паттерны\n", + "\n", + "| Паттерн | Назначение | Реализация |\n", + "|---------|------------|------------|\n", + "| Builder | Создание лабиринта из файла | TextFileMazeBuilder |\n", + "| Strategy | Семейство алгоритмов поиска | BFSStrategy, DFSStrategy, AStarStrategy |\n", + "| Observer | Уведомление о событиях | ConsoleView |\n", + "| Command | Отмена ходов | MoveCommand |\n" + ] + }, + { + "cell_type": "markdown", + "id": "55cef4b9", + "metadata": {}, + "source": [ + "## Часть 2. Реализация\n", + "\n", + "### 2.1 Модель лабиринта\n", + "\n", + "**Класс Cell** - клетка лабиринта\n", + "- Поля: x, y, is_wall, is_start, is_exit\n", + "- Метод: is_passable() - возвращает True, если не стена\n", + "\n", + "**Класс Maze** - лабиринт\n", + "- Поля: width, height, cells[][], start, exit\n", + "- Методы: get_cell(x, y), get_neighbors(cell)\n", + "\n", + "### 2.2 Загрузка лабиринта (Builder)\n", + "\n", + "**TextFileMazeBuilder**\n", + "- Читает файл с символами (# - стена, пробел - проход, S - старт, E - выход)\n", + "- Создаёт клетки с нужными флагами\n", + "- Возвращает готовый Maze\n", + "\n", + "### 2.3 Алгоритмы поиска (Strategy)\n", + "\n", + "**Интерфейс PathFindingStrategy**\n", + "- Метод: find_path(maze, start, exit) возвращает (путь, количество_посещённых)\n", + "\n", + "**BFSStrategy** - поиск в ширину (очередь)\n", + "- Гарантирует кратчайший путь\n", + "\n", + "**DFSStrategy** - поиск в глубину (стек)\n", + "- Быстрый, но не гарантирует кратчайший путь\n", + "\n", + "**AStarStrategy** - A* (приоритетная очередь)\n", + "- Использует эвристику (манхэттенское расстояние)\n", + "\n", + "### 2.4 Оркестратор\n", + "\n", + "**MazeSolver**\n", + "- Поля: maze, strategy\n", + "- Методы: set_strategy(), solve() → SearchStats\n", + "\n", + "**SearchStats**\n", + "- Поля: path, time_ms, visited_count, path_length" + ] + }, + { + "cell_type": "markdown", + "id": "5c9bd0d2", + "metadata": {}, + "source": [ + "## Часть 3. Эксперименты\n", + "\n", + "### 3.1 Условия\n", + "\n", + "| Параметр | Значение |\n", + "|----------|----------|\n", + "| Повторений | 5 |\n", + "| Алгоритмы | BFS, DFS, A* |\n", + "| Лабиринты | Простой (10x10), С тупиками (50x50), Пустой (100x100), Без выхода |\n", + "\n", + "### 3.2 Тестовые лабиринты\n", + "\n", + "| Лабиринт | Размер | Характеристика |\n", + "|----------|--------|----------------|\n", + "| Простой | 10x10 | Прямой путь от старта к выходу |\n", + "| С тупиками | 20x20 | Много тупиков, запутанный |\n", + "| Пустой | 50x50 | Без стен (максимальная производительность) |\n", + "| Без выхода | 10x10 | Выход отгорожен стенами |\n", + "\n", + "### 3.3 Результаты экспериментов\n", + "\n", + "| Лабиринт | Стратегия | Время (мс) | Посещено клеток | Длина пути |\n", + "|----------|-----------|------------|-----------------|------------|\n", + "| Простой (10x10) | BFS | 0.020 | 11 | 6 |\n", + "| Простой (10x10) | DFS | 0.012 | 9 | 8 |\n", + "| Простой (10x10) | A* | 0.020 | 9 | 6 |\n", + "| С тупиками (20x20) | BFS | 0.492 | 306 | 35 |\n", + "| С тупиками (20x20) | DFS | 0.234 | 198 | 81 |\n", + "| С тупиками (20x20) | A* | 0.456 | 225 | 35 |\n", + "| Пустой (50x50) | BFS | 3.486 | 2304 | 95 |\n", + "| Пустой (50x50) | DFS | 10.452 | 2304 | 1129 |\n", + "| Пустой (50x50) | A* | 5.743 | 2304 | 95 |\n", + "| Без выхода | BFS | 0.010 | 1 | нет пути |\n", + "| Без выхода | DFS | 0.003 | 1 | нет пути |\n", + "| Без выхода | A* | 0.004 | 1 | нет пути |\n", + "\n", + "### 3.4 Графики\n", + "\n", + "#### Простой лабиринт (10x10)\n", + "\n", + "![Время](data/simple_time_graph.png)\n", + "![Посещено](data/simple_visited_graph.png)\n", + "![Длина пути](data/simple_path_graph.png)\n", + "\n", + "#### Лабиринт с тупиками (20x20)\n", + "\n", + "![Время](data/dead_time_graph.png)\n", + "![Посещено](data/dead_visited_graph.png)\n", + "![Длина пути](data/dead_path_graph.png)\n", + "\n", + "#### Пустой лабиринт (50x50)\n", + "\n", + "![Время](data/empty_time_graph.png)\n", + "![Посещено](data/empty_visited_graph.png)\n", + "![Длина пути](data/empty_path_graph.png)\n", + "\n", + "#### Лабиринт без выхода\n", + "\n", + "![Время](data/noexit_time_graph.png)\n", + "![Посещено](data/noexit_visited_graph.png)\n", + "\n", + "### 3.5 Общая таблица результатов\n", + "\n", + "![Таблица](data/maze_table_results.png)\n", + "\n", + "### 3.6 Анализ результатов\n", + "\n", + "**Простой лабиринт (10x10):**\n", + "- BFS и A* нашли кратчайший путь (6 шагов)\n", + "- DFS нашёл более длинный путь (8 шагов), но был быстрее всех\n", + "\n", + "**Лабиринт с тупиками (20x20):**\n", + "- BFS и A* нашли кратчайший путь (35 шагов)\n", + "- DFS нашёл очень длинный путь (81 шаг), так как ушёл в глубину по тупикам\n", + "\n", + "**Пустой лабиринт (50x50):**\n", + "- BFS и A* нашли кратчайший путь (95 шагов)\n", + "- DFS нашёл очень длинный путь (1129 шагов)\n", + "\n", + "**Лабиринт без выхода:**\n", + "- Все алгоритмы посетили только стартовую клетку (1) и вернули \"нет пути\"\n", + "\n", + "### 3.7 Сравнение алгоритмов\n", + "\n", + "| Алгоритм | Кратчайший путь | Скорость | Память | Когда использовать |\n", + "|----------|-----------------|----------|--------|-------------------|\n", + "| BFS | Да | Средняя | Много | Нужен гарантированно кратчайший путь |\n", + "| DFS | Нет | Быстрая | Мало | Важна скорость, не важна длина пути |\n", + "| A* | Да | Быстрая | Средне | Большие лабиринты, есть эвристика |\n", + "\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "id": "e687a8ee", + "metadata": {}, + "source": [ + "## Заключение\n", + "\n", + "### Выводы\n", + "\n", + "1. **BFS** гарантирует кратчайший путь, но медленнее на больших лабиринтах\n", + "2. **DFS** самый быстрый, но путь может быть очень длинным\n", + "3. **A*** - лучший компромисс: находит кратчайший путь и работает быстро\n", + "\n", + "### Рекомендация\n", + "\n", + "Для поиска выхода из лабиринта рекомендуется использовать **A*** - он сочетает скорость и оптимальность.\n", + "\n", + "### Как паттерны помогли\n", + "\n", + "| Изменение | Без паттернов | С паттернами |\n", + "|-----------|---------------|--------------|\n", + "| Добавить новый алгоритм | Изменить MazeSolver | Создать новую стратегию |\n", + "| Сменить визуализацию | Переписать MazeSolver | Добавить новый Observer |\n", + "\n", + "**Итог:** Паттерны сделали код гибким и расширяемым." + ] + } + ], + "metadata": { + "language_info": { + "name": "python" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/konnovaea/lab2/empty.txt b/konnovaea/lab2/empty.txt new file mode 100644 index 0000000..c83b6cd --- /dev/null +++ b/konnovaea/lab2/empty.txt @@ -0,0 +1,50 @@ +################################################## +#S # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# E# +################################################## diff --git a/konnovaea/lab2/make_lab2_plots.py b/konnovaea/lab2/make_lab2_plots.py new file mode 100644 index 0000000..6b96ed1 --- /dev/null +++ b/konnovaea/lab2/make_lab2_plots.py @@ -0,0 +1,162 @@ +import matplotlib.pyplot as plt +import os + +os.makedirs('lab2/docs/data', exist_ok=True) + +algorithms = ['BFS', 'DFS', 'A*'] + + +simple_time = [0.020, 0.012, 0.020] +simple_visited = [11, 9, 9] +simple_path = [6, 8, 6] + +fig, ax = plt.subplots(figsize=(8, 5)) +bars = ax.bar(algorithms, simple_time, color=['#3498db', '#e74c3c', '#2ecc71']) +ax.set_ylabel('Время (мс)') +ax.set_title('Время выполнения (простой лабиринт 10x10)') +for bar, val in zip(bars, simple_time): + ax.text(bar.get_x() + bar.get_width()/2, bar.get_height() + 0.001, f'{val:.3f}', ha='center', va='bottom') +plt.savefig('docs/data/simple_time_graph.png', dpi=150, bbox_inches='tight') +plt.close() + +fig, ax = plt.subplots(figsize=(8, 5)) +bars = ax.bar(algorithms, simple_visited, color=['#3498db', '#e74c3c', '#2ecc71']) +ax.set_ylabel('Количество клеток') +ax.set_title('Посещённые клетки (простой лабиринт)') +for bar, val in zip(bars, simple_visited): + ax.text(bar.get_x() + bar.get_width()/2, bar.get_height() + 0.3, str(val), ha='center', va='bottom') +plt.savefig('docs/data/simple_visited_graph.png', dpi=150, bbox_inches='tight') +plt.close() + +fig, ax = plt.subplots(figsize=(8, 5)) +bars = ax.bar(algorithms, simple_path, color=['#3498db', '#e74c3c', '#2ecc71']) +ax.set_ylabel('Длина пути (шагов)') +ax.set_title('Длина найденного пути (простой лабиринт)') +for bar, val in zip(bars, simple_path): + ax.text(bar.get_x() + bar.get_width()/2, bar.get_height() + 0.3, str(val), ha='center', va='bottom') +plt.savefig('docs/data/simple_path_graph.png', dpi=150, bbox_inches='tight') +plt.close() + + +dead_time = [0.492, 0.234, 0.456] +dead_visited = [306, 198, 225] +dead_path = [35, 81, 35] + +fig, ax = plt.subplots(figsize=(8, 5)) +bars = ax.bar(algorithms, dead_time, color=['#3498db', '#e74c3c', '#2ecc71']) +ax.set_ylabel('Время (мс)') +ax.set_title('Время выполнения (лабиринт с тупиками)') +for bar, val in zip(bars, dead_time): + ax.text(bar.get_x() + bar.get_width()/2, bar.get_height() + 0.01, f'{val:.3f}', ha='center', va='bottom') +plt.savefig('docs/data/dead_time_graph.png', dpi=150, bbox_inches='tight') +plt.close() + +fig, ax = plt.subplots(figsize=(8, 5)) +bars = ax.bar(algorithms, dead_visited, color=['#3498db', '#e74c3c', '#2ecc71']) +ax.set_ylabel('Количество клеток') +ax.set_title('Посещённые клетки (лабиринт с тупиками)') +for bar, val in zip(bars, dead_visited): + ax.text(bar.get_x() + bar.get_width()/2, bar.get_height() + 10, str(val), ha='center', va='bottom') +plt.savefig('docs/data/dead_visited_graph.png', dpi=150, bbox_inches='tight') +plt.close() + +fig, ax = plt.subplots(figsize=(8, 5)) +bars = ax.bar(algorithms, dead_path, color=['#3498db', '#e74c3c', '#2ecc71']) +ax.set_ylabel('Длина пути (шагов)') +ax.set_title('Длина найденного пути (лабиринт с тупиками)') +for bar, val in zip(bars, dead_path): + ax.text(bar.get_x() + bar.get_width()/2, bar.get_height() + 3, str(val), ha='center', va='bottom') +plt.savefig('docs/data/dead_path_graph.png', dpi=150, bbox_inches='tight') +plt.close() + + +empty_time = [3.486, 10.452, 5.743] +empty_visited = [2304, 2304, 2304] +empty_path = [95, 1129, 95] + +fig, ax = plt.subplots(figsize=(8, 5)) +bars = ax.bar(algorithms, empty_time, color=['#3498db', '#e74c3c', '#2ecc71']) +ax.set_ylabel('Время (мс)') +ax.set_title('Время выполнения (пустой лабиринт 50x50)') +for bar, val in zip(bars, empty_time): + ax.text(bar.get_x() + bar.get_width()/2, bar.get_height() + 0.3, f'{val:.3f}', ha='center', va='bottom') +plt.savefig('docs/data/empty_time_graph.png', dpi=150, bbox_inches='tight') +plt.close() + +fig, ax = plt.subplots(figsize=(8, 5)) +bars = ax.bar(algorithms, empty_visited, color=['#3498db', '#e74c3c', '#2ecc71']) +ax.set_ylabel('Количество клеток') +ax.set_title('Посещённые клетки (пустой лабиринт)') +for bar, val in zip(bars, empty_visited): + ax.text(bar.get_x() + bar.get_width()/2, bar.get_height() + 50, str(val), ha='center', va='bottom') +plt.savefig('docs/data/empty_visited_graph.png', dpi=150, bbox_inches='tight') +plt.close() + +fig, ax = plt.subplots(figsize=(8, 5)) +bars = ax.bar(algorithms, empty_path, color=['#3498db', '#e74c3c', '#2ecc71']) +ax.set_ylabel('Длина пути (шагов)') +ax.set_title('Длина найденного пути (пустой лабиринт)') +for bar, val in zip(bars, empty_path): + ax.text(bar.get_x() + bar.get_width()/2, bar.get_height() + 50, str(val), ha='center', va='bottom') +plt.savefig('docs/data/empty_path_graph.png', dpi=150, bbox_inches='tight') +plt.close() + + +noexit_time = [0.010, 0.003, 0.004] +noexit_visited = [1, 1, 1] + +fig, ax = plt.subplots(figsize=(8, 5)) +bars = ax.bar(algorithms, noexit_time, color=['#3498db', '#e74c3c', '#2ecc71']) +ax.set_ylabel('Время (мс)') +ax.set_title('Время выполнения (лабиринт без выхода)') +for bar, val in zip(bars, noexit_time): + ax.text(bar.get_x() + bar.get_width()/2, bar.get_height() + 0.0005, f'{val:.3f}', ha='center', va='bottom') +plt.savefig('docs/data/noexit_time_graph.png', dpi=150, bbox_inches='tight') +plt.close() + +fig, ax = plt.subplots(figsize=(8, 5)) +bars = ax.bar(algorithms, noexit_visited, color=['#3498db', '#e74c3c', '#2ecc71']) +ax.set_ylabel('Количество клеток') +ax.set_title('Посещённые клетки (лабиринт без выхода)') +for bar, val in zip(bars, noexit_visited): + ax.text(bar.get_x() + bar.get_width()/2, bar.get_height() + 0.05, str(val), ha='center', va='bottom') +plt.savefig('docs/data/noexit_visited_graph.png', dpi=150, bbox_inches='tight') +plt.close() + + +fig, ax = plt.subplots(figsize=(14, 8)) +ax.axis('off') + +table_data = [ + ['Лабиринт', 'Стратегия', 'Время (мс)', 'Посещено', 'Длина пути'], + ['Простой (10x10)', 'BFS', '0.020', '11', '6'], + ['Простой (10x10)', 'DFS', '0.012', '9', '8'], + ['Простой (10x10)', 'A*', '0.020', '9', '6'], + ['С тупиками (20x20)', 'BFS', '0.492', '306', '35'], + ['С тупиками (20x20)', 'DFS', '0.234', '198', '81'], + ['С тупиками (20x20)', 'A*', '0.456', '225', '35'], + ['Пустой (50x50)', 'BFS', '3.486', '2304', '95'], + ['Пустой (50x50)', 'DFS', '10.452', '2304', '1129'], + ['Пустой (50x50)', 'A*', '5.743', '2304', '95'], + ['Без выхода', 'BFS', '0.010', '1', 'нет пути'], + ['Без выхода', 'DFS', '0.003', '1', 'нет пути'], + ['Без выхода', 'A*', '0.004', '1', 'нет пути'], +] + +table = ax.table(cellText=table_data, loc='center', cellLoc='center', colWidths=[0.2, 0.13, 0.13, 0.13, 0.13]) +table.auto_set_font_size(False) +table.set_fontsize(10) +table.scale(1, 1.8) + +for i in range(5): + table[(0, i)].set_facecolor('#4472C4') + table[(0, i)].set_text_props(weight='bold', color='white') + +for i in range(1, len(table_data)): + if i % 2 == 1: + for j in range(5): + table[(i, j)].set_facecolor('#E8F0FE') + +plt.title('Результаты экспериментов по поиску пути в лабиринте', fontsize=14, fontweight='bold', pad=20) +plt.savefig('docs/data/maze_table_results.png', dpi=200, bbox_inches='tight', facecolor='white') +plt.close() diff --git a/konnovaea/lab2/maze_experiments.py b/konnovaea/lab2/maze_experiments.py new file mode 100644 index 0000000..3de62c6 --- /dev/null +++ b/konnovaea/lab2/maze_experiments.py @@ -0,0 +1,154 @@ +import time +import csv +import os +from lab2.maze_solver import TextFileMazeBuilder, BFSStrategy, DFSStrategy, AStarStrategy, MazeSolver + +def save_maze_to_file(maze, filename): + with open(filename, 'w') as f: + for row in maze: + f.write(''.join(row) + '\n') + +def run_test(maze_file, strategy_class): + builder = TextFileMazeBuilder() + maze = builder.build_from_file(maze_file) + solver = MazeSolver(maze, strategy_class) + + times = [] + visited = [] + path_len = [] + + for i in range(5): + stats = solver.solve() + times.append(stats.time_ms) + visited.append(stats.visited_count) + path_len.append(stats.path_length) + + return { + 'time': sum(times) / 5, + 'visited': sum(visited) / 5, + 'path': sum(path_len) / 5, + 'path_found': max(path_len) > 0 + } + +def main(): + + print("Эксперименты по поиску пути в лабиринте") + + + results = [] + + + print("\n1. Простой лабиринт (10x10)") + + + + simple = [ + "#######", + "#S #", + "# ### #", + "# E #", + "#######" + ] + with open('simple.txt', 'w') as f: + for line in simple: + f.write(line + '\n') + + for name, strategy in [('BFS', BFSStrategy()), ('DFS', DFSStrategy()), ('A*', AStarStrategy())]: + res = run_test('simple.txt', strategy) + print(f"{name}: время={res['time']:.3f}мс, посещено={res['visited']:.0f}, путь={res['path']:.0f}") + results.append(['Простой', name, round(res['time'], 3), round(res['visited'], 0), round(res['path'], 0)]) + + + print("\n2. Лабиринт с тупиками (20x20)") + + dead = [] + for y in range(20): + row = [] + for x in range(20): + if x == 0 or y == 0 or x == 19 or y == 19: + row.append('#') + elif (x == 5 and y > 5 and y < 15) or (y == 5 and x > 5 and x < 15): + row.append('#') + else: + row.append(' ') + dead.append(row) + dead[1][1] = 'S' + dead[18][18] = 'E' + + with open('dead.txt', 'w') as f: + for row in dead: + f.write(''.join(row) + '\n') + + for name, strategy in [('BFS', BFSStrategy()), ('DFS', DFSStrategy()), ('A*', AStarStrategy())]: + res = run_test('dead.txt', strategy) + print(f"{name}: время={res['time']:.3f}мс, посещено={res['visited']:.0f}, путь={res['path']:.0f}") + results.append(['С тупиками', name, round(res['time'], 3), round(res['visited'], 0), round(res['path'], 0)]) + + + print("\n3. Пустой лабиринт (50x50)") + + + empty = [] + for y in range(50): + row = [] + for x in range(50): + if x == 0 or y == 0 or x == 49 or y == 49: + row.append('#') + else: + row.append(' ') + empty.append(row) + empty[1][1] = 'S' + empty[48][48] = 'E' + + with open('empty.txt', 'w') as f: + for row in empty: + f.write(''.join(row) + '\n') + + for name, strategy in [('BFS', BFSStrategy()), ('DFS', DFSStrategy()), ('A*', AStarStrategy())]: + res = run_test('empty.txt', strategy) + print(f"{name}: время={res['time']:.3f}мс, посещено={res['visited']:.0f}, путь={res['path']:.0f}") + results.append(['Пустой', name, round(res['time'], 3), round(res['visited'], 0), round(res['path'], 0)]) + + + print("\n4. Лабиринт без выхода (10x10)") + + + noexit = [] + for y in range(10): + row = [] + for x in range(10): + if x == 0 or y == 0 or x == 9 or y == 9: + row.append('#') + else: + row.append('#') + noexit.append(row) + noexit[1][1] = 'S' + noexit[8][8] = 'E' + + with open('noexit.txt', 'w') as f: + for row in noexit: + f.write(''.join(row) + '\n') + + for name, strategy in [('BFS', BFSStrategy()), ('DFS', DFSStrategy()), ('A*', AStarStrategy())]: + try: + res = run_test('noexit.txt', strategy) + if res['path_found']: + print(f"{name}: путь найден! длина={res['path']:.0f}") + results.append(['Без выхода', name, round(res['time'], 3), round(res['visited'], 0), round(res['path'], 0)]) + else: + print(f"{name}: путь не найден (корректно)") + results.append(['Без выхода', name, round(res['time'], 3), round(res['visited'], 0), 'нет пути']) + except Exception as e: + print(f"{name}: ошибка - {e}") + results.append(['Без выхода', name, 0, 0, 'ошибка']) + + + os.makedirs('docs/data', exist_ok=True) + with open('docs/data/maze_experiments.csv', 'w', newline='', encoding='utf-8') as f: + writer = csv.writer(f) + writer.writerow(['Лабиринт', 'Стратегия', 'Время(мс)', 'Посещено клеток', 'Длина пути']) + writer.writerows(results) + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/konnovaea/lab2/maze_solver.py b/konnovaea/lab2/maze_solver.py new file mode 100644 index 0000000..c22b789 --- /dev/null +++ b/konnovaea/lab2/maze_solver.py @@ -0,0 +1,367 @@ +from abc import ABC, abstractmethod +from collections import deque +import heapq +import time +import os + +class Cell: + def __init__(self, x, y): + self.x = x + self.y = y + self.is_wall = False + self.is_start = False + self.is_exit = False + + def is_passable(self): + return not self.is_wall + + def __repr__(self): + return f"Cell({self.x},{self.y})" + + +class Maze: + def __init__(self, width, height): + self.width = width + self.height = height + self.cells = [] + self.start = None + self.exit = None + + for y in range(height): + row = [] + for x in range(width): + row.append(Cell(x, y)) + self.cells.append(row) + + def get_cell(self, x, y): + if 0 <= x < self.width and 0 <= y < self.height: + return self.cells[y][x] + return None + + def get_neighbors(self, cell): + neighbors = [] + for dx, dy in [(0, -1), (0, 1), (-1, 0), (1, 0)]: + nx, ny = cell.x + dx, cell.y + dy + neighbor = self.get_cell(nx, ny) + if neighbor and neighbor.is_passable(): + neighbors.append(neighbor) + return neighbors + + +class TextFileMazeBuilder: + def build_from_file(self, filename): + with open(filename, 'r') as f: + lines = [line.rstrip() for line in f.readlines()] + + height = len(lines) + width = len(lines[0]) + maze = Maze(width, height) + + for y, line in enumerate(lines): + for x, ch in enumerate(line): + cell = maze.get_cell(x, y) + if ch == '#': + cell.is_wall = True + elif ch == 'S': + maze.start = cell + cell.is_start = True + elif ch == 'E': + maze.exit = cell + cell.is_exit = True + + return maze + + +class PathFindingStrategy(ABC): + @abstractmethod + def find_path(self, maze, start, exit): + pass + + +class BFSStrategy(PathFindingStrategy): + def find_path(self, maze, start, exit): + if not start or not exit: + return [], 0 + + queue = deque([(start, [start])]) + visited = {start} + + while queue: + current, path = queue.popleft() + if current == exit: + return path, len(visited) + + for neighbor in maze.get_neighbors(current): + if neighbor not in visited: + visited.add(neighbor) + queue.append((neighbor, path + [neighbor])) + + return [], len(visited) + + +class DFSStrategy(PathFindingStrategy): + def find_path(self, maze, start, exit): + if not start or not exit: + return [], 0 + + stack = [(start, [start])] + visited = {start} + + while stack: + current, path = stack.pop() + if current == exit: + return path, len(visited) + + for neighbor in maze.get_neighbors(current): + if neighbor not in visited: + visited.add(neighbor) + stack.append((neighbor, path + [neighbor])) + + return [], len(visited) + + +class AStarStrategy(PathFindingStrategy): + def _heuristic(self, a, b): + return abs(a.x - b.x) + abs(a.y - b.y) + + def find_path(self, maze, start, exit): + if not start or not exit: + return [], 0 + + heap = [(self._heuristic(start, exit), 0, start, [start])] + g_score = {start: 0} + visited = set() + counter = 1 + + while heap: + _, _, current, path = heapq.heappop(heap) + + if current in visited: + continue + + visited.add(current) + + if current == exit: + return path, len(visited) + + for neighbor in maze.get_neighbors(current): + tentative_g = g_score[current] + 1 + if neighbor not in g_score or tentative_g < g_score[neighbor]: + g_score[neighbor] = tentative_g + f = tentative_g + self._heuristic(neighbor, exit) + heapq.heappush(heap, (f, counter, neighbor, path + [neighbor])) + counter += 1 + + return [], len(visited) + + +class SearchStats: + def __init__(self, path, time_ms, visited_count): + self.path = path + self.time_ms = time_ms + self.visited_count = visited_count + self.path_length = len(path) if path else 0 + + +class MazeSolver: + def __init__(self, maze, strategy=None): + self.maze = maze + self.strategy = strategy + self.observers = [] + + def attach(self, observer): + self.observers.append(observer) + + def detach(self, observer): + self.observers.remove(observer) + + def notify(self, event, data=None): + for observer in self.observers: + observer.update(event, data) + + def set_strategy(self, strategy): + self.strategy = strategy + + def solve(self): + if self.strategy is None: + raise ValueError("Стратегия не установлена") + self.notify("search_started") + + start_time = time.perf_counter() + path, visited_count = self.strategy.find_path(self.maze, self.maze.start, self.maze.exit) + end_time = time.perf_counter() + time_ms = (end_time - start_time) * 1000 + + self.notify("search_finished", time_ms) + self.notify("path_found", path) + + return SearchStats(path, time_ms, visited_count) + +class Observer(ABC): + + @abstractmethod + def update(self, event, data=None): + pass + +class ConsoleView(Observer): + + def __init__(self): + self.events = [] + + def update(self, event, data=None): + self.events.append((event, data)) + + if event == "maze_loaded": + print("[Событие] Лфбирин загружен") + elif event == "path_found": + print(f"[Событие] Путь найден! Длина: {len(data) if data else 0}") + elif event == "search_started": + print(f"[Событие] Поиск завершён. Время: {data:.3f}мс" if data else "[Событие] Поиск завершён") + elif event == "mpve": + print(f"[Событие] Игрок переместился в {data}") + elif event == "undo": + print("[Событие] Отмена последнего хода") + + def render(self,maze, player=None, path=None): + + os.system('cls' if os.name == 'nt' else 'clear') + + print("Лабиринт") + + for y in range(maze.height): + row = "" + for x in range(maze.width): + cell = maze.get_cell(x,y) + + if player and cell == player.current_cell: + row += "p " #игрок + elif path and cell in path: + row += "* " #путь + elif cell.is_wall: + row += "# " #стена + elif cell.is_start: + row += "S " #старт + elif cell.is_exit: + row += "E " #выход + else: + row += ". " #прозод + print(row) + + print("Управление: W/A/S/D - движение, U - отмена, Q - выход") + +class Command(ABC): + + @abstractmethod + def execute(self): + pass + + @abstractmethod + def undo(self): + pass + +class Player: + + def __init__(self, start_cell): + self.current_cell = start_cell + self.start_cell = start_cell + + def move_to(self, cell): + self.current_cell = cell + + def resent(self): + self.current_cell = self.start_cell + + def __repr__(self): + return f"Player at ({self.current_cell.x}, {self.current_cell.y})" + +class MoveCommand(Command): + + def __init__(self, player, new_cell, view): + self.player = player + self.new_cell = new_cell + self.old_cell = player.current_cell + self.view = view + + def execute(self): + self.player.move_to(self.new_cell) + self.view.update("undo", None) + + def undo(self): + self.player.move_to(self.old_cell) + self.view.update("undo",None) + +class GameController: + + def __init__(self, maze, view): + self.maze = maze + self.view = view + self.player = Player(maze.start) + self.command_history = [] + + def get_cell_in_direction(self, direction): + + x, y = self.player.current_cell.x, self.player.current_cell.y + + if direction == 'w': + y -= 1 + elif direction == 's': + y += 1 + elif direction == 'a': + x -= 1 + elif direction == 'd': + x += 1 + else: + return None + + return self.maze.get_cell(x, y) + + def try_move(self, direction): + + new_cell = self.get_cell_in_direction(direction) + + if new_cell and new_cell.is_passable(): + command = MoveCommand(self.player, new_cell, self.view) + command.execute() + self.command_history.append(command) + + if new_cell.is_exit: + self.view.update("path_found", []) + print("Вы нашли выход.") + return True + else: + print("Невозможно пройти - стена") + return False + + def undo(self): + + if self.command_history: + command = self.command_history.pop() + command.undo() + else: + print("Нечего отменять") + + def visualize_path(self, path): + self.view.render(self.maze, self.player, path) + + def run_manual_mode(self): + + while True: + self.view.render(self.maze, self.player) + + command = input("Введите команду: ").lower().strip() + + if command in ['w', 'a', 's', 'd']: + self.try_move(command) + elif command == 'u': + self.undo() + elif command == 'q': + print('Выход из игры') + break + else: + print("Неизвестная команда. Используйте: W/A/S/D - движение, U - отмена, Q - выход") + + + + + + diff --git a/konnovaea/lab2/noexit.txt b/konnovaea/lab2/noexit.txt new file mode 100644 index 0000000..0d9e9c5 --- /dev/null +++ b/konnovaea/lab2/noexit.txt @@ -0,0 +1,10 @@ +########## +#S######## +########## +########## +########## +########## +########## +########## +########E# +########## diff --git a/konnovaea/lab2/simple.txt b/konnovaea/lab2/simple.txt new file mode 100644 index 0000000..b7edabb --- /dev/null +++ b/konnovaea/lab2/simple.txt @@ -0,0 +1,5 @@ +####### +#S # +# ### # +# E # +#######