реализовано двоичное дерево с помощью функций вставки/обновления, возвращения корня, итеративного поиска, поиска узла с минимальным ключем, рекурсивного удаления, центрированного обхода
- Написал логику тестирования алгоритмов
- добавил заглушки лабиринтов
- реализовал сохранение результатов
- пока оставил файл тесторивания в корне проекта
Как порядок входных данных влияет на скорость вставки в BST 2
Почему хеш-таблица почти не чувствительна к порядку 4
Почему связный список всегда медленен при поиске 6
Как удаление работает в каждой структуре 7
Вывод 9
Как порядок входных данных влияет на скорость вставки в BST
При вставке отсортированных данных в BST (красный график) производительность падает в разы по сравнению со вставкой случайных данных. Это связано с тем, что отсортированная последовательность приводит к вырождению дерева в связанный список, тогда как случайный порядок вставки помогает сохранять дерево относительно сбалансированным.
При вставке элементов в отсортированном порядке (по возрастанию или убыванию):
?Каждый новый элемент всегда больше (или меньше) всех уже добавленных.
?В результате алгоритм каждый раз движется по одному и тому же направлению — только в правое или только в левое поддерево.
?Из-за этого дерево вырождается: каждый узел имеет не более одного потомка, структура напоминает линейный список.
?Высота такого дерева становится пропорциональнойO(n).
?Каждая операция вставки требует в среднемO(n) сравнений, так как нужно проходить всю длину текущей цепочки от корня до самого глубокого листа.
?В итоге суммарная сложность вставки всехnэлементов вырастает доO(n^2).
При случайной вставке:
?Элементы распределяются по дереву гораздо равномернее.
?Высока вероятность того, что дерево останется сбалансированным.
?Средняя высота дерева сохраняется на уровне O(logn).
?Каждая операция вставки в среднем требуетO(logn) сравнений.
?Общая сложность вставки всехnэлементов составляет O(nlogn).
Вывод: разница в скорости объясняется различием в высоте дерева. В вырожденном случае высота равна O(n), и каждая вставка выполняется в ?n/lognраз медленнее по числу шагов, чем в сбалансированном случае с высотой O(logn).
Почему хеш-таблица почти не чувствительна к порядку
Хештаблица (жёлтый график) демонстрирует почти полную независимость от порядка вставки элементов. Это объясняется тем, что положение каждого элемента в структуре определяется исключительно значением его хешфункции, а не тем, в какой последовательности происходило добавление данных.
Основные причины нечувствительности к порядку вставки:
?Хеширование. Для каждого ключа вычисляется хешкод, который преобразуется в индекс ячейки. Один и тот же ключ всегда даёт один и тот же индекс независимо от того, когда и в каком порядке он был добавлен.
?Независимость операций. Вставка, поиск и удаление выполняются в среднем заO(1)O(1), поскольку алгоритм сразу вычисляет нужную позицию, не обходя структуру и не учитывая историю добавлений.
?Разрешение коллизий. Даже если порядок вставки влияет на расположение элементов внутри цепочки (метод цепочек) или на последовательность проб (открытая адресация), это касается лишь небольших групп элементов с одинаковыми хешами. Общая производительность остаётся стабильной.
?Рехеширование. При увеличении размера таблицы все элементы перераспределяются заново. Новый порядок определяется актуальной хеш-функцией и размером таблицы, а не исходной последовательностью вставки.
Итог: Время выполнения операций зависит от качества хеш-функции, коэффициента заполнения таблицы и метода разрешения коллизий, но не зависит от порядка добавления элементов.
Почему связный список всегда медленен при поиске
Связный список показывает низкую скорость поиска из-за необходимости последовательного обхода: чтобы найти элемент, требуется пройти по указателям от головы до нужного узла.
Почему это происходит:
?Отсутствие произвольного доступа.В отличие от массива, где доступ по индексу занимает O(1), в связном списке элементы приходится перебирать последовательно, что даёт сложность поиска O(n).
?Низкая локальность данных. Узлы списка разбросаны по памяти случайным образом. Это вызывает частые промахи кэша: процессор не может подгрузить блок соседних данных, и каждый переход по указателю оборачивается новым обращением к оперативной памяти.
?Дополнительная память на указатели. Каждый узел хранит не только полезные данные, но и указатель на следующий элемент. Это увеличивает объём памяти и ухудшает эффективность кэша — на те же данные приходится загружать больше информации.
?Затраты на разыменование указателей.На каждом шаге поиска процессору нужно:
oпрочитать текущий узел,
oизвлечь из него указатель на следующий,
oперейти по этому адресу.
Эти операции замедляют работу по сравнению с простым сдвигом индекса в массиве.
Итог: хотя алгоритмическая сложность обхода составляет O(n) как для массива (при линейном поиске), так и для связного списка, на практике список работает ощутимо медленнее из-за особенностей организации памяти и работы кэша.
Как удаление работает в каждой структуре
1. Связный список
Односвязный список: чтобы удалить узел, необходимо сначала найти предыдущий элемент и перенаправить егоуказательnextна узел, следующий за удаляемым. Исключение — удаление первого элемента: достаточно сдвинуть указательheadна второй узел.
Двусвязный список: удаление проще, поскольку у каждого узла есть указатели и на следующий (next), и на предыдущий (prev). При удалении обновляются ссылки обоих соседей:prev->next = next,next->prev = prev.
Сложность: в общем случае O(n) из-за необходимости поиска элемента; удаление головы или хвоста (при наличии прямой ссылки на хвост) выполняется за O(1).
2. Хештаблица
Сначала через хеш-функцию h(key) вычисляется индекс ячейки. Дальнейшие действия зависят от метода разрешения коллизий:
?Раздельная цепочка: элемент удаляется из связного списка (или другой структуры), находящегося по вычисленному индексу.
?Открытая адресация: ячейка помечается специальным маркером «удалён», а не просто как пустая — это важно для корректности последующих операций поиска.
Сложность: в среднем O(1), в худшем случае O(n)(при большом количестве коллизий).
3. Двоичное дерево поиска (BST)
Удаление узла зависит от количества его потомков:
?Нет детей (лист): узел просто удаляется, ссылка родителя обнуляется.
?Один ребёнок: удаляемый узел заменяется его единственным потомком — родитель «перепрыгивает» через удаляемый узел.
?Два ребёнка:
1.Находится преемник (самый левый (наименьший) узел в правом поддереве) или предшественник (самый правый (наибольший) узел в левом поддереве).
2.Значение преемника/предшественника копируется в удаляемый узел.
3.Преемник/предшественник рекурсивно удаляется — он гарантированно имеет не более одного ребёнка.
Сложность:O(h), гдеh— высота дерева. В сбалансированном деревеh=O(logn), в несбалансированном — доO(n).
Вывод
1. Частые вставки
Связный список — отличный выбор для частых вставок (особенно в середину), если не требуется быстрый доступ по индексу. Вставка в начало или конец выполняется заO(1), в середину — заO(n)(но без сдвига элементов, как в массиве).
Хештаблица — хорошо подходит для вставок по ключу, обеспечивая в среднемO(1).
2. Частый поиск
Хештаблица — лучший вариант для быстрого поиска по ключу. Среднее время —O(1), в худшем случае —O(n)(при сильных коллизиях).
Сбалансированное двоичное дерево поиска — предпочтительнее, если нужен поиск с гарантированной сложностьюO(logn) даже в худшем случае.
3. Необходимость получать данные в отсортированном порядке
Массив / список — эффективен, если данные уже отсортированы или сортировка происходит редко, а последовательное чтение — часто. Доступ по индексу —O(1), но вставка и удаление в середину требуютO(n).
Отсортированный массив — удобен для поиска (бинарный поиск даёт (O(logn)), однако вставки и удаления обходятся вO(n).
Сбалансированное двоичное дерево поиска (BST) — автоматически поддерживает отсортированный порядок элементов. Все основные операции выполняются заO(logn). Идеальный вариант, когда данные часто изменяются и при этом требуется обход элементов в отсортированном порядке.
Анализ эффективности алгоритмов и применимости паттернов 7
Выводы 9
Описание задачи и выбранных паттернов
Описание задачи: реализовать систему для загрузки лабиринтов из файлов, поиска пути от старта до выхода с использованием различных алгоритмов, сбора статистики и визуализации. Ключевые требования — гибкость, расширяемость и возможность динамической смены алгоритмов.
Выбранные паттерны:
?Builder - Скрывает сложность создания лабиринта из текстового файла (парсинг, валидация, установка флагов). Позволяет легко добавить поддержку других форматов (JSON, XML).
?Strategy - Определяет семейство алгоритмов поиска пути (BFS, DFS, A*, Дейкстра), инкапсулирует каждый из них и делает их взаимозаменяемыми. Клиент (MazeSolver) может переключать стратегии во время выполнения.
?Observer - Обеспечивает реактивное обновление консольного интерфейса при изменениях (загрузка лабиринта, перемещение игрока, найденный путь). Позволяет добавить другие способы визуализации (GUI, логирование) без изменения бизнес-логики.
?Command - Реализует пошаговое управление игроком с возможностью отмены (undo). Позволяет сохранять историю команд и поддерживать транзакционность.
Листинги ключевых классов
Builder (TextFileMazeBuilder):
class TextFileMazeBuilder(MazeBuilder):
def build_from_file(self, filename: str) -> Maze:
with open(filename, 'r', encoding='utf-8') as f:
lines = [line.rstrip('\n') for line in f.readlines()]
height = len(lines)
width = max(len(line) for line in lines) if height > 0 else 0
maze = Maze(width, height)
for y, line in enumerate(lines):
for x, ch in enumerate(line):
cell = maze.get_cell(x, y)
if cell is None:
continue
if ch == '#':
cell.is_wall = True
elif ch == 'S':
cell.is_start = True
maze.start = cell
elif ch == 'E':
cell.is_exit = True
maze.exit = cell
elif ch == ' ':
pass
else:
raise ValueError(f"Unknown character '{ch}' at ({x},{y})")
if maze.start is None or maze.exit is None:
raise ValueError("Maze must have start (S) and exit (E)")
Анализ эффективности алгоритмов и применимости паттернов
Анализ алгоритмов поиска пути
?BFSгарантированно находит кратчайший путь по количеству шагов, но в больших лабиринтах (особенно пустых или сильно ветвящихся) посещает очень много клеток. Время работы растёт пропорционально числу достижимых клеток.
?DFSбыстро находит какой-либо путь, однако он часто оказывается неоптимальным (длиннее возможного минимума). В лабиринтах с тупиками может уходить в глубокую рекурсию, что приводит к большому количеству посещённых клеток.
?Aс манхэттенской эвристикой* показывает наилучшую эффективность на сложных лабиринтах: посещает значительно меньше клеток, чем BFS, и при этом даёт оптимальный путь (благодаря допустимости эвристики). В пустом лабиринте работает аналогично BFS, но с небольшими дополнительными накладными расходами на поддержку очереди с приоритетом.
?Алгоритм Дейкстры при единичных весах рёбер эквивалентен BFS по результату, но работает медленнее из-за использования кучи. Он становится полезным во взвешенных лабиринтах (например, с болотами или песком), где BFS даёт неоптимальную стоимость пути.
Применимость паттернов проектирования
?Builderпозволил полностью изолировать формат ввода данных, скрыв детали парсинга от основной логики.
?Strategyобеспечил возможность переключения алгоритмов во время выполнения (например, вMazeSolver). Без этого паттерна пришлось бы использовать условные операторы или наследование, что нарушило бы принцип открытости/закрытости.
?Observerотделил визуализацию от бизнес-логики. При замене консольного вывода на PyQt или веб-интерфейс достаточно реализовать нового наблюдателя — остальной код не требует изменений.
?Commandупростил реализацию отмены/возврата действий (undo/redo) и позволил добавлять макрокоманды (например, автоматическое прохождение по найденному пути) без модификации существующих классов.
Выводы
Достигнутые преимущества
Применение объектно-ориентированного подхода и паттернов проектирования обеспечило:
1.Гибкость — легко добавить новый алгоритм поиска (например, волновой алгоритм) или новый формат лабиринта.
2.Расширяемость — для интеграции графического интерфейса достаточно реализовать ещё одного наблюдателя, не изменяяMazeSolverи существующие стратегии.
3.Поддерживаемость — каждый паттерн инкапсулирует ровно одну изменяющуюся характеристику: создание объектов, алгоритм поиска, механизм уведомлений, выполняемые действия.
4.Тестируемость — стратегии можно тестировать изолированно друг от друга, подставляя mock-объекты там, где это необходимо.
Что потребовало бы больших усилий без паттернов
?Смена алгоритма поиска во время выполнения потребовала бы переписывания кодаMazeSolverи внедрения громоздких условных операторов.
?Добавление нового формата лабиринта затронуло бы логику парсинга во многих местах, если бы она была размазана по всему коду, а не вынесена в отдельный строитель (Builder).
?Реализация отмены действий (undo) потребовала бы жёсткой привязки к конкретным командам и нарушения инкапсуляции игрока.
?Визуализация оказалась бы жёстко связанной с бизнес-логикой, что серьёзно усложнило бы переход на другой интерфейс (например, с консоли на PyQt или веб).
Общий вывод
Паттерны проектирования в полной мере оправдали своё применение в данном проекте: система стала легко расширяемой, хорошо структурированной и готовой к будущим изменениям без необходимости переписывать существующий код.
Разработать гибкую программу для загрузки лабиринта из файла, поиска пути от старта до выхода с возможностью выбора алгоритма, визуализации процесса и экспериментального сравнения алгоритмов. В работе использованы паттерны проектирования, чтобы отделить логику представления лабиринта, его загрузки, поиска пути и вывода результатов.
## 2. Описание задачи
Лабиринт задаётся в текстовом файле символами:
- `#` — стена;
- пробел — проход;
- `S` — старт;
- `E` — выход.
Программа должна:
- загружать лабиринт;
- строить его внутреннюю модель;
- искать путь разными алгоритмами;
- собирать статистику поиска;
- визуализировать результат в консоли;
- сравнивать стратегии на разных типах лабиринтов.
## 3. Выбранные паттерны проектирования
### 3.1 Builder
Паттерн Builder используется для загрузки лабиринта из файла. Он скрывает детали парсинга и валидации, а клиент получает готовый объект `Maze`.
Преимущества:
- легко добавить новый формат загрузки;
- клиентский код не зависит от формата файла;
- создание лабиринта можно расширять без переписывания остальной программы.
### 3.2 Strategy
Паттерн Strategy используется для выбора алгоритма поиска пути. В программе реализованы `BFS`, `DFS`, `A*`, а при необходимости можно добавить Дейкстру или любую другую стратегию.
Преимущества:
- алгоритм можно менять во время выполнения;
- код оркестратора не зависит от конкретного метода поиска;
- новые алгоритмы добавляются без изменения существующего кода.
### 3.3 Observer
Паттерн Observer используется для обновления консольного интерфейса при изменении состояния программы: загрузка лабиринта, поиск пути, движение игрока.
Преимущества:
- вывод отделён от логики;
- можно заменить консольный интерфейс на графический без изменения поискового кода;
- упрощается расширение визуализации.
### 3.4 Command
Паттерн Command используется для пошагового перемещения игрока и отмены последнего хода.
Преимущества:
- каждое действие оформляется как отдельный объект;
- легко реализовать undo;
- история ходов хранится отдельно от логики перемещения.
## 4. Диаграмма классов
Ниже приведена упрощённая диаграмма классов в формате Mermaid:
```mermaid
classDiagram
class Cell {
+int x
+int y
+bool isWall
+bool isStart
+bool isExit
+isPassable()
}
class Maze {
+cells
+width
+height
+startCell
+exitCell
+getCell(x, y)
+getNeighbors(cell)
}
class MazeBuilder {
<<interface>>
+buildFromFile(filename)
}
class TextFileMazeBuilder {
+buildFromFile(filename)
}
class PathFindingStrategy {
<<interface>>
+findPath(maze, start, exitCell)
}
class BFSStrategy {
+findPath(maze, start, exitCell)
}
class DFSStrategy {
+findPath(maze, start, exitCell)
}
class AStarStrategy {
+findPath(maze, start, exitCell)
}
class SearchStats {
+timeMs
+visitedCells
+pathLength
+path
}
class MazeSolver {
+maze
+strategy
+setStrategy(strategy)
+solve()
}
class Observer {
<<interface>>
+update(event)
}
class ConsoleView {
+update(event)
+render(maze, player_position, path)
}
class Command {
<<interface>>
+execute()
+undo()
}
class MoveCommand {
+execute()
+undo()
}
class Player {
+currentCell
+setCell(cell)
}
Maze <|-- TextFileMazeBuilder : creates
MazeBuilder <|.. TextFileMazeBuilder
PathFindingStrategy <|.. BFSStrategy
PathFindingStrategy <|.. DFSStrategy
PathFindingStrategy <|.. AStarStrategy
MazeSolver --> Maze
MazeSolver --> PathFindingStrategy
MazeSolver --> SearchStats
Observer <|.. ConsoleView
Command <|.. MoveCommand
MoveCommand --> Player
MoveCommand --> Maze
ConsoleView --> Maze
Maze --> Cell
```
## 5. Ключевые классы и их роль
### Cell
Хранит координаты клетки и её тип. Позволяет быстро проверять, является ли клетка проходимой.
### Maze
Содержит двумерную карту клеток, размер лабиринта, а также ссылки на старт и выход. Даёт доступ к соседним клеткам по четырём направлениям.
### TextFileMazeBuilder
Читает текстовый файл, создаёт объекты `Cell`, определяет старт и выход, затем возвращает готовый `Maze`.
### BFSStrategy
Ищет кратчайший путь по числу шагов. Подходит для случая, когда все переходы одинаковой стоимости.
### DFSStrategy
Быстро исследует пространство, но не гарантирует кратчайший путь. Полезен как сравнительный алгоритм.
### AStarStrategy
Использует эвристику Манхэттенского расстояния. Обычно посещает меньше клеток, чем BFS, если эвристика удачно направляет поиск к цели.
### MazeSolver
Оркестратор, который хранит лабиринт и текущую стратегию. Вызывает поиск, измеряет время и собирает статистику.
### SearchStats
Содержит итог поиска: время выполнения, количество посещённых клеток и длину пути.
### ConsoleView
Реализует наблюдателя и умеет выводить лабиринт и найденный путь в консоль.
### MoveCommand
Оформляет ход игрока как объект-команду. Поддерживает отмену последнего перемещения.
## 6. Экспериментальная часть
### 6.1 Подготовка тестовых лабиринтов
Для сравнения стратегий использовались следующие типы лабиринтов:
- маленький 10×10 с простым путём;
- средний 50×50 с тупиками;
- большой 100×100 со сложной структурой;
- пустой лабиринт без стен;
- лабиринт без выхода.
### 6.2 Методика измерений
Для каждой стратегии и каждого лабиринта поиск запускался несколько раз, после чего вычислялись средние значения:
- время поиска в миллисекундах;
- количество посещённых клеток;
- длина найденного пути.
Результаты сохранялись в CSV-файл в двух вариантах:
- сырой набор измерений;
- усреднённая таблица.
## 7. Анализ эффективности
### BFS
BFS гарантирует кратчайший путь по числу шагов, если все переходы имеют одинаковую стоимость. На простых и пустых лабиринтах работает стабильно и предсказуемо. Минус — может посещать много клеток, особенно на больших лабиринтах.
### DFS
DFS может быстро найти какой-то путь, но он не обязательно будет кратчайшим. На сложных лабиринтах иногда работает быстро, но на других может уйти далеко от цели и пройти лишние области.
### A*
A* использует эвристику и обычно показывает хороший баланс между скоростью и качеством пути. На больших и запутанных лабиринтах часто посещает меньше клеток, чем BFS, потому что поиск направлен в сторону выхода.
### Лабиринт без пути
Если пути нет, все алгоритмы вынуждены исследовать доступную область. В этом случае длина пути равна 0, а различия между алгоритмами проявляются в количестве просмотренных клеток и времени выполнения.
### Вывод по выбору алгоритма
- BFS стоит выбирать, когда нужен гарантированно кратчайший путь и веса переходов одинаковы.
- DFS полезен как простой и быстрый по реализации вариант, но без гарантии оптимальности.
- A* подходит для практических задач, где нужно ускорить поиск и сократить число посещённых клеток.
- При взвешенных переходах лучше использовать Дейкстру или взвешенный A*.
## 8. Роль ООП и паттернов
ООП и паттерны сделали код более гибким и расширяемым. Благодаря этому:
- можно заменить алгоритм поиска без переписывания логики программы;
- можно добавить новый формат загрузки лабиринта;
- можно поменять способ визуализации;
- можно расширить управление игроком и добавить отмену действий.
Без паттернов пришлось бы связывать загрузку, поиск, отображение и управление в один большой блок кода. Это усложнило бы отладку и дальнейшие изменения.
## 9. Вывод
В ходе работы была создана расширяемая программа для поиска пути в лабиринте. Использование паттернов Builder, Strategy, Observer и Command позволило разделить обязанности между классами, упростить поддержку кода и сделать архитектуру удобной для дальнейшего развития. Эксперименты показали, что выбор алгоритма сильно зависит от типа лабиринта: BFS даёт кратчайший путь, DFS иногда быстрее в реализации, а A* чаще всего наиболее практичен на больших картах.
# Отчёт по лабораторной работе «Структуры данных для телефонного справочника»
## 1. Постановка задачи
В рамках работы требовалось реализовать три структуры данных «с нуля» (без использования встроенных коллекций, кроме базовых списков):
- связный список,
- хеш-таблицу с цепочками,
- двоичное дерево поиска (несбалансированное).
Для каждой структуры необходимо реализовать операции `insert`, `find`, `delete` и `list_all` (возврат всех записей, отсортированных по имени). Затем на наборе из 10000 записей выполнить экспериментальное сравнение производительности в двух режимах: при случайном порядке вставки и при вставке записей, отсортированных по имени. Каждый эксперимент повторялся 5 раз.
## 2. Результаты измерений
Ниже приведены усреднённые по 5 повторам времена выполнения операций (в секундах). Исходные сырые данные сохранены в файле `experiment_results.csv`.
| Структура | Режим | Вставка (с) | Поиск 110 имён (с) | Удаление 50 записей (с) |
- Все замеры выполнены с помощью `time.perf_counter()`.
- Для хеш-таблицы использовалось 10 корзин.
- Рекурсивная глубина BST увеличена до 20000, чтобы избежать переполнения стека.
## 3. Анализ полученных данных
### 3.1. Поведение BST при разных порядках ввода
Двоичное дерево поиска сильно зависит от порядка поступления ключей. При случайном порядке средняя высота близка к логарифмической, что даёт отличную производительность:
- вставка –**0.0277 с**,
- поиск –**0.00029 с** (самый быстрый среди всех структур в этом режиме).
Однако при вставке отсортированных данных дерево вырождается в линейный список (каждый новый узел добавляется только в правое поддерево). Последствия:
- время вставки возрастает **в 367 раз** (с 0.0277 до 10.16 с),
- поиск замедляется **в 305 раз**,
- удаление –**в 339 раз**.
Вырожденное BST на отсортированных данных работает **медленнее даже связного списка** (вставка 10.16 с против 4.44 с, поиск 0.088 с против 0.027 с), что объясняется накладными расходами на рекурсивные вызовы и проверки.
### 3.2. Хеш-таблица – устойчивость к порядку
Хеш-таблица использует функцию `hash(name) % size`, которая равномерно рассеивает имена независимо от их лексикографического порядка. Поэтому результаты в двух режимах практически идентичны:
- вставка: 0.668 с (случайный) против 0.646 с (отсортированный) – разница менее 4%,
- поиск: 0.0051 с против 0.0040 с,
- удаление: 0.0019 с против 0.0021 с.
Небольшие расхождения находятся в пределах случайной вариации (зависит от коллизий, которые немного различаются при разном порядке вставки). Средняя сложность операций остаётся **O(1)**.
### 3.3. Связный список – ожидаемо медленный
Линейный список не обеспечивает прямого доступа, поэтому все операции (кроме удаления после нахождения) требуют обхода в среднем половины списка. Даже при сравнительно небольшом объёме данных (10000 записей) времена велики:
- вставка ≈ **4.6 с** (на два порядка хуже, чем у хеш-таблицы и BST на случайных данных),
- поиск ≈ **0.03 с** (в 6–10 раз медленнее, чем у других структур).
Интересно, что на отсортированных данных список показывает немного лучшее время, чем на случайных. Причина: при вставке в конец отсортированного списка (имена идут в алфавитном порядке) новые узлы добавляются без поиска дубликатов? Но алгоритм `ll_insert` сначала проверяет наличие имени, проходя весь список. Поскольку все имена уникальны и не обновляются, каждый проход идёт до конца. Однако в отсортированном режиме имена добавляются в порядке возрастания, и при проверке дубликата мы проходим по уже существующим элементам, которые все меньше нового? Да, в отсортированном режиме каждое новое имя больше всех предыдущих, поэтому при поиске дубликата мы обходим весь существующий список. В случайном режиме новые имена могут встречаться раньше, и поиск останавливается раньше? Но в любом случае разница небольшая (около 6%), и в целом список остаётся медленным.
### 3.4. Сравнение удаления
Удаление в списке требует сначала найти элемент (O(n)), затем перелинковку. В хеш-таблице удаление сводится к удалению в коротком списке корзины (почти O(1)). В BST на случайных данных удаление очень быстрое (0.00016 с), на отсортированных – катастрофически замедляется (0.054 с). Для хеш-таблицы удаление немного быстрее, чем вставка, что естественно: при удалении не нужно создавать новый узел.
## 4. Выводы и практические рекомендации
Проведённое исследование наглядно демонстрирует сильные и слабые стороны каждой структуры.
1. **Хеш-таблица**– лучший выбор для задач, где приоритетом является скорость всех операций (вставка, поиск, удаление), а порядок вывода данных не важен или может быть получен отдельной сортировкой. Стабильно высокая производительность вне зависимости от характера входных данных. В реальных проектах именно хеш-таблицы лежат в основе словарей (Python `dict`, Java `HashMap`).
2. **Двоичное дерево поиска**– эффективно только при случайном или близком к случайному порядке поступления ключей. Даёт логарифмическую сложность и при этом позволяет получать данные в отсортированном виде за O(n) без дополнительной сортировки. Однако на реальных данных (например, заведомо отсортированных) производительность падает до O(n), что делает его непригодным без механизмов балансировки. На практике применяются сбалансированные варианты (AVL, красно-чёрные деревья).
3. **Связный список**– не подходит для коллекций объёмом более нескольких сотен элементов из-за линейной сложности основных операций. Может использоваться только в очень специфических сценариях: очень редкий поиск, постоянные вставки/удаления в начало (но не в конец), или как строительный блок для других структур (например, для цепочек в хеш-таблице, что и было сделано в данной работе).
| Максимальная скорость всех операций | Хеш-таблица |
| Нужны данные в отсортированном порядке + данные поступают случайно | BST (но лучше сбалансированное) |
| Данные поступают уже отсортированными | Хеш-таблица (или балансируемое дерево) |
| Очень маленький объём (<100записей)|Любая,нопрощесписок|
В реальной разработке для телефонного справочника с большим числом записей и частыми запросами поиска оптимальным решением будет **хеш-таблица**. Если же дополнительно требуется частый вывод всего справочника по алфавиту, стоит рассмотреть сбалансированное дерево (например, встроенный в Python модуль `bisect` не даёт структуры данных, а`sortedcontainers`– сторонний).
# Отчёт по лабораторной работе: Алгоритмы поиска пути в лабиринте
## 1. Цель работы
Разработка программы для загрузки лабиринта из текстового файла, реализации трёх алгоритмов поиска пути (BFS, DFS, A\*) и проведения экспериментального сравнения их эффективности на лабиринтах различной сложности.
## 2. Структура программы
Программа написана на Python 3 и состоит из следующих основных классов:
- `GridPoint`– представление клетки лабиринта (координаты, проходимость, флаги старта/выхода);
- `Labyrinth`– модель лабиринта (сетка клеток, методы получения соседей);
- `TextMazeLoader`– загрузка лабиринта из файла с символами `#` (стена), `S` (старт), `E` (выход);
- `LabyrinthSolver`– класс-оркестратор, позволяющий сменить стратегию и измеряющий время выполнения;
- `Player`, `Command`, `MoveCommand`, `InteractiveView`– для интерактивного режима с отменой ходов;
- функции `run_experiment` и `generate_plots`– для многократных запусков и построения графиков.
## 3. Описание алгоритмов
### 3.1 BFS (поиск в ширину)
Использует очередь. Гарантирует нахождение кратчайшего пути (по числу шагов). Обходит клетки в порядке увеличения расстояния от старта.
### 3.2 DFS (поиск в глубину)
Использует стек. Идёт «вглубь» по одному пути, не гарантирует кратчайший путь. Обычно быстрее по времени и памяти на больших лабиринтах.
### 3.3 A* (звездочка)
Использует приоритетную очередь и эвристику (манхэттенское расстояние). Оценивает клетку по формуле `f = g + h`, где `g`– пройденное расстояние, `h`– эвристика. Находит оптимальный путь, если эвристика допустима.
## 4. Методика эксперимента
Для каждого лабиринта каждый алгоритм запускался 3 раза, результаты усреднялись. Измерялись:
- время выполнения (в миллисекундах);
- количество посещённых клеток;
- длина найденного пути.
Тестовые лабиринты:
| Название | Размер | Описание |
|----------|--------|-----------|
| Small 10x6 | 10×6 | Простой лабиринт с извилистым коридором |
| Medium 10x10 | 10×10 | Лабиринт среднего размера с несколькими тупиками |
| Large 20x20 | 20×20 | Большой запутанный лабиринт |
| Empty 15x15 | 15×15 | Пустой лабиринт без стен (прямая линия от S до E) |
| No exit 10x10 | 10×10 | Лабиринт без буквы E (путь отсутствует) |
## 5. Результаты экспериментов
| Лабиринт | Алгоритм | Время, мс | Посещено клеток | Длина пути |
- **BFS** и **A*** нашли оптимальные пути во всех лабиринтах, где выход существовал (длина пути совпадает для них в каждом случае).
- **DFS** в лабиринтах Medium, Large и Empty дал существенно более длинные пути (30 против 16, 68 против 36, 89 против 17), что характерно для глубинного обхода без эвристики.
### 6.2. Время выполнения
- На малых лабиринтах все алгоритмы работают сопоставимо (0.035–0.099 мс).
- На лабиринте Large 20×20 BFS выполнился за 0.099 мс, A* – 0.165 мс (медленнее из-за сложности поддержки очереди с приоритетом), DFS – быстрее всех (0.070 мс).
- В пустом лабиринте BFS и A* обошли почти все клетки (133 и 65 посещённых соответственно), но A* за счёт эвристики посетил вдвое меньше клеток, хотя время оказалось чуть выше, чем у BFS (0.154 против 0.133 мс). Это объясняется накладными расходами на вычисление эвристики и управление кучей.
### 6.3. Количество посещённых клеток
- **A*** показал лучшую эффективность в пустом лабиринте (65 посещённых против 133 у BFS и 161 у DFS). В лабиринтах со стенами разница не столь заметна, но A* почти всегда посещал меньше клеток, чем BFS.
- **DFS** в среднем посещает меньше клеток, чем BFS, но при этом путь часто неоптимален.
- **BFS** вынужден обходить всю область равных расстояний, поэтому посещённых клеток обычно больше.
### 6.4. Поведение при отсутствии выхода
Все алгоритмы корректно завершились, вернув пустой путь (длина 0). В лабиринте без выхода BFS, DFS и A* посетили 25 клеток – это все доступные клетки.
## 7. Выводы
1. **BFS** надёжен для поиска кратчайшего пути, но может быть медленнее на больших открытых пространствах из-за широкого обхода.
2. **DFS**– самый быстрый по времени и экономный по памяти, но не гарантирует оптимальность пути. Его применение оправдано, когда любой путь подходит.
3. **A*** демонстрирует лучший баланс: находит кратчайший путь и при этом посещает меньше клеток, чем BFS. Небольшое замедление на сложных лабиринтах компенсируется меньшим числом обработанных клеток.
4. Программа успешно справляется с лабиринтами разного размера и конфигурации, включая отсутствие выхода.
5. Интерактивный режим с отменой ходов (паттерн Command) и выбором алгоритма (паттерн Strategy) реализован и работает корректно.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.