Compare commits

..

62 Commits

Author SHA1 Message Date
d8b9205806 Merge pull request '[1][2] lab1 + lab2' (#408) from KuznetsovMA/2026-rff_mp:lab2 into develop
Reviewed-on: UNN/2026-rff_mp#408
2026-09-23 14:41:00 +00:00
0920ef78c2 [2] Maze solver lab 2026-09-23 12:27:43 +03:00
030469c8f9 [1] Data structures lab 2026-09-23 12:22:47 +03:00
1dfec663e1 Merge pull request '[1][2] Data Structures Lab and Lab for finding a way out of the maze' (#407) from sorokinad/2026-rff_mp:SorokinAD into develop
Reviewed-on: UNN/2026-rff_mp#407
2026-09-22 18:20:49 +00:00
7465167739 Merge pull request 'Отчеты' (#403) from starikovta/2026-rff_mp:task1-data-structures into develop
Reviewed-on: UNN/2026-rff_mp#403
2026-09-08 07:48:06 +00:00
657f864ed6 Merge pull request 'Добавить tsareveo' (#402) from tsareveo/2026-rff_mp:tsareveo-patch-1 into develop
Reviewed-on: UNN/2026-rff_mp#402
2026-09-08 07:47:44 +00:00
faf54114a0 Merge pull request 'PaulVA-lab2' (#405) from PaulVA/2026-rff_mp:PaulVA-lab2 into develop
Reviewed-on: UNN/2026-rff_mp#405
2026-09-08 07:47:05 +00:00
f1d9a64911 Перемещены лабораторные работы в папку PaulVA 2026-09-05 19:26:22 +03:00
c12f9ef1bf Добавлены лабораторные работы 1 и 2 2026-09-05 19:09:46 +03:00
972243d118 Отчеты 2026-09-05 17:16:35 +03:00
4fecfc8b0c Merge branch 'develop' into tsareveo-patch-1 2026-09-05 13:41:06 +00:00
115d6ac18d Добавить tsareveo 2026-09-05 13:19:10 +00:00
7eb345fedc Merge pull request 'nikitovie' (#401) from nikitovie/2026-rff_mp:nikitovie into develop
Reviewed-on: UNN/2026-rff_mp#401
2026-09-05 12:47:33 +00:00
571bde5320 Merge pull request 'LebedevES' (#397) from LebedevES/2026-rff_mp:LebedevES into develop
Reviewed-on: UNN/2026-rff_mp#397
2026-09-05 12:46:58 +00:00
182e97e0be Merge pull request 'FirsovAV' (#400) from LebedevES/2026-rff_mp:FirsovAV into develop
Reviewed-on: UNN/2026-rff_mp#400
2026-09-05 12:46:07 +00:00
7cf11cb3ef Загрузить файлы в «nikitovie/docs:» 2026-09-05 12:35:17 +00:00
3ec4ea8e0f Удалить nikitovie/docs:/gitkeep 2026-09-05 12:34:13 +00:00
471618449c Загрузить файлы в «nikitovie/docs:/data/zadanie2» 2026-09-05 12:33:02 +00:00
1549daa6a9 Загрузить файлы в «nikitovie/docs:/data/zadanie2» 2026-09-05 12:32:19 +00:00
49bbdda926 Загрузить файлы в «nikitovie/docs:/data/zadanie2» 2026-09-05 12:31:52 +00:00
8774dfb38c Загрузить файлы в «nikitovie/docs:/data/zadanie2» 2026-09-05 12:31:32 +00:00
634a882918 Удалить nikitovie/docs:/data/zadanie2/.gitkeep 2026-09-05 12:31:15 +00:00
865cdc7a4e Загрузить файлы в «nikitovie/docs:/data/zadanie2» 2026-09-05 12:31:04 +00:00
8ebedbaf2a Удалить nikitovie/docs:/data/.gitkeep 2026-09-05 12:28:04 +00:00
f1bb79cd58 Удалить nikitovie/docs:/data/zadanie_1/.gitkeep 2026-09-05 12:27:48 +00:00
80948d5363 Загрузить файлы в «nikitovie/docs:/data/zadanie_1» 2026-09-05 12:26:45 +00:00
f34137546b Добавить nikitovie/docs:/data/zadanie2/.gitkeep 2026-09-05 12:26:09 +00:00
e86eb5766a Удалить nikitovie/docs:/data/zadanie_2 2026-09-05 12:25:53 +00:00
e84264cf8b Добавить nikitovie/docs:/data/zadanie_2 2026-09-05 12:25:28 +00:00
cdd44303e4 Добавить nikitovie/docs:/data/zadanie_1/.gitkeep 2026-09-05 12:25:04 +00:00
59ff0051d9 Добавить nikitovie/docs:/data/.gitkeep 2026-09-05 12:24:25 +00:00
64c96253ba Добавить nikitovie/docs:/gitkeep 2026-09-05 12:22:49 +00:00
7fc4e1ab10 [1] zad 1, zad 2 2026-09-05 14:48:28 +03:00
8f9da375c1 [0] initial commit 2026-09-05 14:44:06 +03:00
58d0447aae [0] initial commit 2026-09-05 13:51:05 +03:00
672af89f8c [3] finale 2026-09-05 13:33:00 +03:00
ba9eaf7570 [2] zad 2 2026-09-05 13:23:06 +03:00
bb1eebe572 [1] zad 1 2026-09-05 13:21:50 +03:00
8184739210 [2] zadanie 2 2026-09-05 13:15:01 +03:00
9caaeb37a9 [1] zadanie 1 2026-09-05 13:10:55 +03:00
9c5302e8ff [0] initial commit 2026-09-05 13:06:43 +03:00
f95819cb5b [0] initial commit 2026-09-05 12:59:41 +03:00
6ee3ec91a1 Merge pull request 'victorovaas-lab2' (#396) from victorovaas/2026-rff_mp:victorovaas-lab2 into develop
Reviewed-on: UNN/2026-rff_mp#396
2026-09-05 09:11:31 +00:00
6ad28df31e Merge pull request 'victorovaas-lab1' (#395) from victorovaas/2026-rff_mp:victorovaas-lab1 into develop
Reviewed-on: UNN/2026-rff_mp#395
2026-09-05 09:11:06 +00:00
1d6032f5a1 собрала отдельные папки для лаб 2026-09-05 12:05:00 +03:00
0b2b19a2ca собрала отдельные папки для лаб 2026-09-05 11:58:05 +03:00
69d425a34e добавила вторую лабу 2026-09-05 11:53:01 +03:00
106ea57b17 добавила первую лабу 2026-09-05 11:45:48 +03:00
5a247e8491 Merge pull request '[2]' (#393) from Ezhovnd/2026-rff_mp:develop into develop
Reviewed-on: UNN/2026-rff_mp#393
2026-09-05 08:36:27 +00:00
c1236a618f Upload files to "Ezhovnd" 2026-09-05 08:34:29 +00:00
46fd403eb8 Update Ezhovnd/2 задание.py 2026-09-05 08:32:56 +00:00
48b8937bf3 Merge pull request '[1]' (#392) from Ezhovnd/2026-rff_mp:develop into develop
Reviewed-on: UNN/2026-rff_mp#392
2026-09-05 08:29:09 +00:00
82c6dd2718 Загрузить файлы в «victorovaas» 2026-09-05 08:19:47 +00:00
56e881192a Добавить victorovaas/lab1 2026-09-05 08:18:52 +00:00
ac36eb6c81 Удалить victorovaas/lab1 2026-09-05 08:17:21 +00:00
1f488408aa Добавить victorovaas/lab1 2026-09-05 08:15:51 +00:00
78222d064e Удалить victorovaas/experiments.py 2026-09-05 08:14:34 +00:00
de3a8108fd Удалить victorovaas/make_graphs.py 2026-09-05 08:14:28 +00:00
091488afe9 Загрузить файлы в «victorovaas» 2026-09-05 08:13:44 +00:00
fd367d2c1a Upload files to "Ezhovnd" 2026-09-05 08:03:31 +00:00
sorokinad
4950eb4f0d [1][2] Data Structures Lab and Lab for finding a way out of the maze 2026-05-26 23:26:20 +03:00
52ab70ee71 Задание 1 2026-05-21 22:00:40 +03:00
166 changed files with 18668 additions and 0 deletions

1
2026-rff_mp Submodule

@ -0,0 +1 @@
Subproject commit 52c001a380431727397e4275c2be9d94fe5fcc8d

0
BobrovKN/425.txt Normal file
View File

292
BobrovKN/zadanie/zad 1/1.py Normal file
View File

@ -0,0 +1,292 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import time
import random
import csv
import sys
sys.setrecursionlimit(30000)
def ll_create_node(name, phone):
return {'name': name, 'phone': phone, 'next': None}
def ll_insert(head, name, phone):
if head is None:
return ll_create_node(name, phone)
if head['name'] == name:
head['phone'] = phone
return head
current = head
while current['next'] is not None:
if current['next']['name'] == name:
current['next']['phone'] = phone
return head
current = current['next']
current['next'] = ll_create_node(name, phone)
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):
return sum(ord(c) for c in name) % table_size
def ht_create_table(size=2000):
return [None] * size
def ht_insert(table, name, phone):
index = hash_function(name, len(table))
table[index] = ll_insert(table[index], name, phone)
def ht_find(table, name):
index = hash_function(name, len(table))
return ll_find(table[index], name)
def ht_delete(table, name):
index = hash_function(name, len(table))
table[index] = ll_delete(table[index], name)
def ht_list_all(table):
all_records = []
for bucket in table:
if bucket is not None:
current = bucket
while current is not None:
all_records.append((current['name'], current['phone']))
current = current['next']
all_records.sort(key=lambda x: x[0])
return all_records
def bst_create_node(name, phone):
return {'name': name, 'phone': phone, 'left': None, 'right': None}
def bst_insert(root, name, phone):
if root is None:
return bst_create_node(name, phone)
current = root
while True:
if name < current['name']:
if current['left'] is None:
current['left'] = bst_create_node(name, phone)
break
else:
current = current['left']
elif name > current['name']:
if current['right'] is None:
current['right'] = bst_create_node(name, phone)
break
else:
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']:
current = current['left']
elif name > current['name']:
current = current['right']
else:
return current['phone']
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 or current['right'] is None:
if current['left'] is not None:
child = current['left']
else:
child = current['right']
if parent is None:
return child
if parent['left'] == current:
parent['left'] = child
else:
parent['right'] = child
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
def bst_list_all(root):
records = []
stack = []
current = root
while stack or current is not None:
while current is not None:
stack.append(current)
current = current['left']
current = stack.pop()
records.append((current['name'], current['phone']))
current = current['right']
return records
def generate_data(n=10000):
records = [(f"User_{i:05d}", f"+7-999-{i:06d}") for i in range(n)]
records_shuffled = records.copy()
random.shuffle(records_shuffled)
records_sorted = sorted(records, key=lambda x: x[0])
return records_shuffled, records_sorted
def run_experiment(structure_name, insert_func, find_func, delete_func,
list_all_func, init_func, records, n_find=100):
data = init_func()
names = [r[0] for r in records]
start = time.perf_counter()
for name, phone in records:
if structure_name == "HashTable":
insert_func(data, name, phone)
else:
data = insert_func(data, name, phone)
insert_time = time.perf_counter() - start
find_names = random.sample(names, min(n_find, len(names)))
missing_names = [f"None_{i}" for i in range(10)]
all_find_names = find_names + missing_names
start = time.perf_counter()
for name in all_find_names:
if structure_name == "HashTable":
find_func(data, name)
else:
find_func(data, name)
find_time = time.perf_counter() - start
delete_names = random.sample(names, min(50, len(names)))
start = time.perf_counter()
for name in delete_names:
if structure_name == "HashTable":
delete_func(data, name)
else:
data = delete_func(data, name)
delete_time = time.perf_counter() - start
return insert_time, find_time, delete_time
def main():
print("Generating test data...")
records_shuffled, records_sorted = generate_data(10000)
results = []
structures = [
("LinkedList", ll_insert, ll_find, ll_delete, ll_list_all, lambda: None),
("HashTable", ht_insert, ht_find, ht_delete, ht_list_all, lambda: ht_create_table(2000)),
("BST", bst_insert, bst_find, bst_delete, bst_list_all, lambda: None)
]
for mode_name, records in [("random", records_shuffled), ("sorted", records_sorted)]:
print(f"\nMode: {mode_name}")
for struct_name, insert_f, find_f, delete_f, list_f, init_f in structures:
print(f" Testing {struct_name}...")
times = []
for run in range(5):
insert_t, find_t, delete_t = run_experiment(
struct_name, insert_f, find_f, delete_f, list_f, init_f, records
)
times.append((insert_t, find_t, delete_t))
print(f" Run {run+1}: insert={insert_t:.4f}s, find={find_t:.4f}s, delete={delete_t:.4f}s")
avg_insert = sum(t[0] for t in times) / 5
avg_find = sum(t[1] for t in times) / 5
avg_delete = sum(t[2] for t in times) / 5
results.append([struct_name, mode_name, "insert", avg_insert])
results.append([struct_name, mode_name, "find", avg_find])
results.append([struct_name, mode_name, "delete", avg_delete])
with open("results.csv", "w", newline="", encoding="utf-8") as f:
writer = csv.writer(f)
writer.writerow(["Structure", "Mode", "Operation", "Time_seconds"])
writer.writerows(results)
print("\n" + "="*60)
print("RESULTS (average over 5 runs):")
print("="*60)
for row in results:
print(f"{row[0]:12} | {row[1]:8} | {row[2]:8} | {row[3]:.6f} sec")
print("\nResults saved to results.csv")
if __name__ == "__main__":
main()

View File

@ -0,0 +1,122 @@
Методы Программирования
Структуры данных,
анализ 1 задания
Бобров К. Н.
425 группа
Содержание
Как порядок входных данных влияет на скорость вставки в 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). Идеальный вариант, когда данные часто изменяются и при этом требуется обход элементов в отсортированном порядке.

View File

@ -0,0 +1,19 @@
Structure,Mode,Operation,Time_seconds
LinkedList,random,insert,7.967956480104476
LinkedList,random,find,0.05891917999833822
LinkedList,random,delete,0.03816298004239797
HashTable,random,insert,0.39825033992528913
HashTable,random,find,0.002917400002479553
HashTable,random,delete,0.0021501399576663973
BST,random,insert,0.02822491992264986
BST,random,find,0.00023473985493183136
BST,random,delete,0.00016456004232168198
LinkedList,sorted,insert,8.014810599852353
LinkedList,sorted,find,0.058480959851294756
LinkedList,sorted,delete,0.04817821998149156
HashTable,sorted,insert,0.3703480200842023
HashTable,sorted,find,0.002751259971410036
HashTable,sorted,delete,0.0018340200185775757
BST,sorted,insert,7.301413399912417
BST,sorted,find,0.06847236007452011
BST,sorted,delete,0.03443789994344115
1 Structure Mode Operation Time_seconds
2 LinkedList random insert 7.967956480104476
3 LinkedList random find 0.05891917999833822
4 LinkedList random delete 0.03816298004239797
5 HashTable random insert 0.39825033992528913
6 HashTable random find 0.002917400002479553
7 HashTable random delete 0.0021501399576663973
8 BST random insert 0.02822491992264986
9 BST random find 0.00023473985493183136
10 BST random delete 0.00016456004232168198
11 LinkedList sorted insert 8.014810599852353
12 LinkedList sorted find 0.058480959851294756
13 LinkedList sorted delete 0.04817821998149156
14 HashTable sorted insert 0.3703480200842023
15 HashTable sorted find 0.002751259971410036
16 HashTable sorted delete 0.0018340200185775757
17 BST sorted insert 7.301413399912417
18 BST sorted find 0.06847236007452011
19 BST sorted delete 0.03443789994344115

589
BobrovKN/zadanie/zad 2/2.py Normal file
View File

@ -0,0 +1,589 @@
import time
import heapq
from collections import deque
from typing import List, Optional, Dict, Tuple
from abc import ABC, abstractmethod
import csv
import random
class Cell:
def __init__(self, x: int, y: int):
self.x = x
self.y = y
self.is_wall = False
self.is_start = False
self.is_exit = False
def is_passable(self) -> bool:
return not self.is_wall
class Maze:
def __init__(self, width: int, height: int):
self.width = width
self.height = height
self.cells = [[Cell(x, y) for y in range(height)] for x in range(width)]
self.start: Optional[Cell] = None
self.exit: Optional[Cell] = None
def get_cell(self, x: int, y: int) -> Optional[Cell]:
if 0 <= x < self.width and 0 <= y < self.height:
return self.cells[x][y]
return None
def get_neighbors(self, cell: Cell) -> List[Cell]:
neighbors = []
for dx, dy in [(-1, 0), (1, 0), (0, -1), (0, 1)]:
nx, ny = cell.x + dx, cell.y + dy
nb = self.get_cell(nx, ny)
if nb and nb.is_passable():
neighbors.append(nb)
return neighbors
class MazeBuilder(ABC):
@abstractmethod
def build_from_file(self, filename: str) -> Maze:
pass
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)")
return maze
class PathFindingStrategy(ABC):
@abstractmethod
def find_path(self, maze: Maze, start: Cell, exit: Cell) -> List[Cell]:
pass
@abstractmethod
def get_name(self) -> str:
pass
class BFSStrategy(PathFindingStrategy):
def find_path(self, maze: Maze, start: Cell, exit: Cell) -> List[Cell]:
queue = deque([start])
came_from = {start: None}
while queue:
current = queue.popleft()
if current == exit:
break
for nb in maze.get_neighbors(current):
if nb not in came_from:
came_from[nb] = current
queue.append(nb)
if exit not in came_from:
return []
path = []
cur = exit
while cur:
path.append(cur)
cur = came_from[cur]
path.reverse()
return path
def get_name(self) -> str:
return "BFS"
class DFSStrategy(PathFindingStrategy):
def find_path(self, maze: Maze, start: Cell, exit: Cell) -> List[Cell]:
stack = [start]
came_from = {start: None}
while stack:
current = stack.pop()
if current == exit:
break
for nb in maze.get_neighbors(current):
if nb not in came_from:
came_from[nb] = current
stack.append(nb)
if exit not in came_from:
return []
path = []
cur = exit
while cur:
path.append(cur)
cur = came_from[cur]
path.reverse()
return path
def get_name(self) -> str:
return "DFS"
class AStarStrategy(PathFindingStrategy):
def _heuristic(self, a: Cell, b: Cell) -> int:
return abs(a.x - b.x) + abs(a.y - b.y)
def find_path(self, maze: Maze, start: Cell, exit: Cell) -> List[Cell]:
open_set = []
heapq.heappush(open_set, (0, id(start), start))
came_from = {}
g_score = {start: 0}
f_score = {start: self._heuristic(start, exit)}
while open_set:
_, _, current = heapq.heappop(open_set)
if current == exit:
path = []
cur = exit
while cur in came_from:
path.append(cur)
cur = came_from[cur]
path.append(start)
path.reverse()
return path
for neighbor in maze.get_neighbors(current):
tentative_g = g_score[current] + 1
if tentative_g < g_score.get(neighbor, float('inf')):
came_from[neighbor] = current
g_score[neighbor] = tentative_g
f_score[neighbor] = tentative_g + self._heuristic(neighbor, exit)
heapq.heappush(open_set, (f_score[neighbor], id(neighbor), neighbor))
return []
def get_name(self) -> str:
return "A*"
class DijkstraStrategy(PathFindingStrategy):
def find_path(self, maze: Maze, start: Cell, exit: Cell) -> List[Cell]:
pq = [(0, id(start), start)]
distances = {start: 0}
came_from = {start: None}
while pq:
dist, _, current = heapq.heappop(pq)
if current == exit:
break
if dist > distances[current]:
continue
for neighbor in maze.get_neighbors(current):
new_dist = dist + 1
if new_dist < distances.get(neighbor, float('inf')):
distances[neighbor] = new_dist
came_from[neighbor] = current
heapq.heappush(pq, (new_dist, id(neighbor), neighbor))
if exit not in came_from:
return []
path = []
cur = exit
while cur:
path.append(cur)
cur = came_from[cur]
path.reverse()
return path
def get_name(self) -> str:
return "Dijkstra"
class SearchStats:
def __init__(self, time_ms: float, visited_cells: int, path_length: int):
self.time_ms = time_ms
self.visited_cells = visited_cells
self.path_length = path_length
def __str__(self):
return f"Time: {self.time_ms:.2f}ms, Visited: {self.visited_cells}, Path: {self.path_length}"
class MazeSolver:
def __init__(self, maze: Maze, strategy: PathFindingStrategy):
self.maze = maze
self.strategy = strategy
def set_strategy(self, strategy: PathFindingStrategy):
self.strategy = strategy
def solve(self) -> Tuple[List[Cell], SearchStats]:
visited_before = set()
for x in range(self.maze.width):
for y in range(self.maze.height):
cell = self.maze.get_cell(x, y)
if cell and cell.is_passable():
visited_before.add(cell)
start_time = time.perf_counter()
path = self.strategy.find_path(self.maze, self.maze.start, self.maze.exit)
end_time = time.perf_counter()
visited_after = set()
for x in range(self.maze.width):
for y in range(self.maze.height):
cell = self.maze.get_cell(x, y)
if cell and cell.is_passable():
visited_after.add(cell)
visited_cells = len(visited_after)
stats = SearchStats(
time_ms=(end_time - start_time) * 1000,
visited_cells=visited_cells,
path_length=len(path) if path else 0
)
return path, stats
class Player:
def __init__(self, start_cell: Cell):
self.current_cell = start_cell
self.previous_cell = None
def move_to(self, cell: Cell) -> bool:
if cell.is_passable():
self.previous_cell = self.current_cell
self.current_cell = cell
return True
return False
def undo(self):
if self.previous_cell:
self.current_cell, self.previous_cell = self.previous_cell, None
return True
return False
class Command(ABC):
@abstractmethod
def execute(self) -> bool:
pass
@abstractmethod
def undo(self):
pass
class MoveCommand(Command):
def __init__(self, player: Player, maze: Maze, direction: str):
self.player = player
self.maze = maze
self.direction = direction
self.executed = False
def execute(self) -> bool:
dx, dy = 0, 0
if self.direction == 'W' or self.direction == 'w':
dy = -1
elif self.direction == 'S' or self.direction == 's':
dy = 1
elif self.direction == 'A' or self.direction == 'a':
dx = -1
elif self.direction == 'D' or self.direction == 'd':
dx = 1
new_x = self.player.current_cell.x + dx
new_y = self.player.current_cell.y + dy
new_cell = self.maze.get_cell(new_x, new_y)
if new_cell and new_cell.is_passable():
self.executed = self.player.move_to(new_cell)
return self.executed
return False
def undo(self):
if self.executed:
self.player.undo()
self.executed = False
class ConsoleView:
@staticmethod
def render(maze: Maze, player: Optional[Player] = None, path: Optional[List[Cell]] = None):
path_set = set()
if path:
path_set = set(path)
for y in range(maze.height):
line = ""
for x in range(maze.width):
cell = maze.get_cell(x, y)
if not cell:
line += " "
elif player and player.current_cell == cell:
line += "P"
elif cell.is_start:
line += "S"
elif cell.is_exit:
line += "E"
elif cell.is_wall:
line += "#"
elif path and cell in path_set:
line += "."
else:
line += " "
print(line)
print()
@staticmethod
def show_stats(stats: SearchStats, algo_name: str):
print(f"=== {algo_name} Results ===")
print(stats)
print()
def generate_test_maze(width: int, height: int, complexity: float = 0.3) -> Maze:
maze = Maze(width, height)
for x in range(width):
for y in range(height):
if random.random() < complexity:
maze.cells[x][y].is_wall = True
maze.start = maze.get_cell(0, 0)
if maze.start:
maze.start.is_start = True
maze.start.is_wall = False
maze.exit = maze.get_cell(width - 1, height - 1)
if maze.exit:
maze.exit.is_exit = True
maze.exit.is_wall = False
return maze
def generate_empty_maze(width: int, height: int) -> Maze:
maze = Maze(width, height)
for x in range(width):
for y in range(height):
maze.cells[x][y].is_wall = False
maze.start = maze.get_cell(0, 0)
if maze.start:
maze.start.is_start = True
maze.exit = maze.get_cell(width - 1, height - 1)
if maze.exit:
maze.exit.is_exit = True
return maze
def generate_no_exit_maze(width: int, height: int) -> Maze:
maze = Maze(width, height)
for x in range(width):
for y in range(height):
maze.cells[x][y].is_wall = False
for x in range(width):
maze.cells[x][height // 2].is_wall = True
maze.start = maze.get_cell(0, 0)
if maze.start:
maze.start.is_start = True
maze.exit = maze.get_cell(width - 1, height - 1)
if maze.exit:
maze.exit.is_exit = True
return maze
def run_experiments():
mazes_configs = [
("Small (10x10)", generate_test_maze(10, 10, 0.2)),
("Medium (50x50)", generate_test_maze(50, 50, 0.25)),
("Large (100x100)", generate_test_maze(100, 100, 0.3)),
("Empty (30x30)", generate_empty_maze(30, 30)),
("No Exit (20x20)", generate_no_exit_maze(20, 20))
]
strategies = [BFSStrategy(), DFSStrategy(), AStarStrategy(), DijkstraStrategy()]
results = []
for maze_name, maze in mazes_configs:
print(f"\n=== Testing: {maze_name} ===")
for strategy in strategies:
times = []
visited = []
path_lengths = []
solver = MazeSolver(maze, strategy)
for run in range(5):
maze_copy = Maze(maze.width, maze.height)
for x in range(maze.width):
for y in range(maze.height):
orig = maze.get_cell(x, y)
copy = maze_copy.get_cell(x, y)
if orig:
copy.is_wall = orig.is_wall
copy.is_start = orig.is_start
copy.is_exit = orig.is_exit
maze_copy.start = maze_copy.get_cell(maze.start.x, maze.start.y) if maze.start else None
maze_copy.exit = maze_copy.get_cell(maze.exit.x, maze.exit.y) if maze.exit else None
solver.maze = maze_copy
solver.set_strategy(strategy)
path, stats = solver.solve()
times.append(stats.time_ms)
visited.append(stats.visited_cells)
path_lengths.append(stats.path_length)
avg_time = sum(times) / len(times)
avg_visited = sum(visited) / len(visited)
avg_path = sum(path_lengths) / len(path_lengths)
results.append({
'maze': maze_name,
'algorithm': strategy.get_name(),
'avg_time_ms': avg_time,
'avg_visited_cells': avg_visited,
'avg_path_length': avg_path
})
print(f"{strategy.get_name()}: {avg_time:.2f}ms, {avg_visited:.0f} cells, path={avg_path:.0f}")
with open('experiment_results.csv', 'w', newline='', encoding='utf-8') as f:
writer = csv.DictWriter(f, fieldnames=['maze', 'algorithm', 'avg_time_ms', 'avg_visited_cells', 'avg_path_length'])
writer.writeheader()
writer.writerows(results)
print("\nResults saved to experiment_results.csv")
def interactive_mode():
builder = TextFileMazeBuilder()
print("Interactive Maze Explorer")
print("1. Load maze from file")
print("2. Generate random maze")
choice = input("Choose (1/2): ")
if choice == '1':
filename = input("Enter filename: ")
try:
maze = builder.build_from_file(filename)
except Exception as e:
print(f"Error loading maze: {e}")
return
else:
w = int(input("Width: "))
h = int(input("Height: "))
maze = generate_test_maze(w, h, 0.3)
player = Player(maze.start)
strategies = {
'1': BFSStrategy(),
'2': DFSStrategy(),
'3': AStarStrategy(),
'4': DijkstraStrategy()
}
print("\nSelect algorithm for solving:")
print("1. BFS (shortest path)")
print("2. DFS (fast, not optimal)")
print("3. A* (heuristic)")
print("4. Dijkstra")
algo_choice = input("Choose: ")
solver = MazeSolver(maze, strategies.get(algo_choice, BFSStrategy()))
path, stats = solver.solve()
view = ConsoleView()
if path:
print(f"\nPath found! Length: {len(path)}")
view.show_stats(stats, solver.strategy.get_name())
else:
print("\nNo path found!")
while True:
view.render(maze, player, path if path else None)
if player.current_cell == maze.exit:
print("Congratulations! You reached the exit!")
break
cmd = input("Move (W/A/S/D) | U=undo | Q=quit | S=solve: ").upper()
if cmd == 'Q':
break
elif cmd == 'U':
player.undo()
print("Undo last move")
elif cmd == 'S' and path:
for cell in path:
if cell == player.current_cell:
continue
player.move_to(cell)
view.render(maze, player, path)
input("Press Enter to continue...")
if player.current_cell == maze.exit:
print("You reached the exit!")
break
elif cmd in ['W', 'A', 'S', 'D']:
move_cmd = MoveCommand(player, maze, cmd)
if move_cmd.execute():
print("Moved")
else:
print("Can't move there!")
def main():
print("Maze Solver with Design Patterns")
print("1. Run experiments")
print("2. Interactive mode")
choice = input("Choose (1/2): ")
if choice == '1':
run_experiments()
else:
interactive_mode()
if __name__ == "__main__":
main()

View File

@ -0,0 +1,21 @@
maze,algorithm,avg_time_ms,avg_visited_cells,avg_path_length
Small (10x10),BFS,0.006740167737007141,80.0,0.0
Small (10x10),DFS,0.00408003106713295,80.0,0.0
Small (10x10),A*,0.005039852112531662,80.0,0.0
Small (10x10),Dijkstra,0.0031800009310245514,80.0,0.0
Medium (50x50),BFS,3.44578018411994,1890.0,99.0
Medium (50x50),DFS,1.3188599608838558,1890.0,341.0
Medium (50x50),A*,2.061920054256916,1890.0,99.0
Medium (50x50),Dijkstra,4.679400008171797,1890.0,99.0
Large (100x100),BFS,0.025319866836071014,6998.0,0.0
Large (100x100),DFS,0.019940081983804703,6998.0,0.0
Large (100x100),A*,0.035060010850429535,6998.0,0.0
Large (100x100),Dijkstra,0.02901991829276085,6998.0,0.0
Empty (30x30),BFS,1.2404202483594418,900.0,59.0
Empty (30x30),DFS,0.8887200616300106,900.0,465.0
Empty (30x30),A*,0.9468601085245609,900.0,59.0
Empty (30x30),Dijkstra,2.678940072655678,900.0,59.0
No Exit (20x20),BFS,0.27012014761567116,380.0,0.0
No Exit (20x20),DFS,0.3163599409162998,380.0,0.0
No Exit (20x20),A*,0.5885399878025055,380.0,0.0
No Exit (20x20),Dijkstra,0.5776201374828815,380.0,0.0
1 maze algorithm avg_time_ms avg_visited_cells avg_path_length
2 Small (10x10) BFS 0.006740167737007141 80.0 0.0
3 Small (10x10) DFS 0.00408003106713295 80.0 0.0
4 Small (10x10) A* 0.005039852112531662 80.0 0.0
5 Small (10x10) Dijkstra 0.0031800009310245514 80.0 0.0
6 Medium (50x50) BFS 3.44578018411994 1890.0 99.0
7 Medium (50x50) DFS 1.3188599608838558 1890.0 341.0
8 Medium (50x50) A* 2.061920054256916 1890.0 99.0
9 Medium (50x50) Dijkstra 4.679400008171797 1890.0 99.0
10 Large (100x100) BFS 0.025319866836071014 6998.0 0.0
11 Large (100x100) DFS 0.019940081983804703 6998.0 0.0
12 Large (100x100) A* 0.035060010850429535 6998.0 0.0
13 Large (100x100) Dijkstra 0.02901991829276085 6998.0 0.0
14 Empty (30x30) BFS 1.2404202483594418 900.0 59.0
15 Empty (30x30) DFS 0.8887200616300106 900.0 465.0
16 Empty (30x30) A* 0.9468601085245609 900.0 59.0
17 Empty (30x30) Dijkstra 2.678940072655678 900.0 59.0
18 No Exit (20x20) BFS 0.27012014761567116 380.0 0.0
19 No Exit (20x20) DFS 0.3163599409162998 380.0 0.0
20 No Exit (20x20) A* 0.5885399878025055 380.0 0.0
21 No Exit (20x20) Dijkstra 0.5776201374828815 380.0 0.0

View File

@ -0,0 +1,196 @@
Методы программирования
Поиск выхода из лабиринта.
Анализ 2 задания
Бобров К. Н.
425 группа
Содержание
Описание задачи и выбранных паттернов 2
Листинги ключевых классов 4
Результаты экспериментов 6
Анализ эффективности алгоритмов и применимости паттернов 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)")
return maze
Strategy (пример BFS):
class BFSStrategy(PathFindingStrategy):
def find_path(self, maze: Maze, start: Cell, exit: Cell) -> List[Cell]:
queue = deque([start])
came_from = {start: None}
while queue:
current = queue.popleft()
if current == exit:
break
for nb in maze.get_neighbors(current):
if nb not in came_from:
came_from[nb] = current
queue.append(nb)
if exit not in came_from:
return []
path = []
cur = exit
while cur:
path.append(cur)
cur = came_from[cur]
path.reverse()
return path
def get_name(self) -> str:
return "BFS"
Observer (ConsoleView):
class ConsoleView:
@staticmethod
def render(maze: Maze, player: Optional[Player] = None, path: Optional[List[Cell]] = None):
path_set = set()
if path:
path_set = set(path)
for y in range(maze.height):
line = ""
for x in range(maze.width):
cell = maze.get_cell(x, y)
if not cell:
line += " "
elif player and player.current_cell == cell:
line += "P"
elif cell.is_start:
line += "S"
elif cell.is_exit:
line += "E"
elif cell.is_wall:
line += "#"
elif path and cell in path_set:
line += "."
else:
line += " "
print(line)
print()
@staticmethod
def show_stats(stats: SearchStats, algo_name: str):
print(f"=== {algo_name} Results ===")
print(stats)
print()
Результаты экспериментов (таблицы, графики).
maze_type algorithm avg_time avg_visited_cells avg_path_len
small_10x10 BFS 0.08572000006097369 79.0 19.0
small_10x10 DFS 0.039739999920129776 79.0 31.0
small_10x10_ A* 0.13467999997374136 79.0 19.0
small_10x10 Dijkstra 0.11474000057205558 79.0 19.0
medium_50x50 BFS 1.8074600004183594 1874.0 99.0
medium_50x50 DFS 0.5937599995377241 1874.0 429.0
medium_50x50 A* 1.6300600003887666 1874.0 99.0
medium_50x50 Dijkstra 3.1870400001935195 1874.0 99.0
large_100x100 BFS 0.014439999722526409 7033.0 0.0
large_100x100 DFS 0.014839999857940711 7033.0 0.0
large_100x100 A* 0.02542000001994893 7033.0 0.0
large_100x100 Dijkstra 0.02548000011302065 7033.0 0.0
empty_30x30 BFS 0.784620000194991 900.0 59.0
empty_30x30 DFS 0.5252399994787993 900.0 465.
empty_30x30 A* 1.150900000357069 900.0 59.0
empty_30x30 Dijkstra 1.564640000287909 900.0 59.0
no_exit_20x20 BFS 0.2002399993216386 380. 0.0
no_exit_20x20 DFS 0.2512400002160575 380.0 0.0
no_exit_20x20 A* 0.5590400000073714 380.0 0.
no_exit_20x20 Dijkstra 0.35640000060084276 380.0 0.0
Графики построены кодом из файла RESULT22.
Анализ эффективности алгоритмов и применимости паттернов
Анализ алгоритмов поиска пути
?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 или веб).
Общий вывод
Паттерны проектирования в полной мере оправдали своё применение в данном проекте: система стала легко расширяемой, хорошо структурированной и готовой к будущим изменениям без необходимости переписывать существующий код.

View File

285
Ezhovnd/2 задание.py Normal file
View File

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

View File

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

View File

@ -0,0 +1,169 @@
Отчёт ко 2 заданию
1. Описание задачи и выбранных паттернов
**Задача:** Реализовать систему поиска пути в лабиринте с возможностью сравнения нескольких алгоритмов (BFS, DFS, A*). Система должна поддерживать разные способы построения лабиринта и позволять легко добавлять новые алгоритмы поиска.
Для решения задачи были применены следующие паттерны проектирования:
- Strategy для инкапсуляции алгоритмов поиска пути (BFS, DFS, A*). Позволяет динамически менять стратегию поиска.
- Builder для построения лабиринта из файла. Отделяет процесс создания лабиринта от его представления.
Эти паттерны обеспечивают гибкость и расширяемость системы.
Диаграмма классов (Mermaid)
```mermaid
classDiagram
class Maze {
+width: int
+height: int
+cells: List~List~Cell~~
+start: Cell
+exit: Cell
+getCell(x, y)
+getNeighbors(cell)
}
class Cell {
+x: int
+y: int
+is_wall: bool
+is_start: bool
+is_exit: bool
+isPassable()
}
class PathFindingStrategy {
<<interface>>
+findPath(maze, start, exit)
}
class BFSStrategy {
+findPath(maze, start, exit)
}
class DFSStrategy {
+findPath(maze, start, exit)
}
class AStarStrategy {
+findPath(maze, start, exit)
-heuristic(a, b)
}
class MazeSolver {
-maze: Maze
-strategy: PathFindingStrategy
+setStrategy(strategy)
+solve()
}
class MazeBuilder {
<<interface>>
+buildFromFile(filename)
}
class TextFileMazeBuilder {
+buildFromFile(filename)
}
Maze "1" *-- "many" Cell
MazeSolver --> PathFindingStrategy
PathFindingStrategy <|-- BFSStrategy
PathFindingStrategy <|-- DFSStrategy
PathFindingStrategy <|-- AStarStrategy
MazeBuilder <|-- TextFileMazeBuilder
```
2. Листинги ключевых классов
Ключевые классы (Strategy и MazeSolver):
```python
class PathFindingStrategy:
def findPath(self, maze, start, exit):
raise NotImplementedError
class BFSStrategy(PathFindingStrategy):
def findPath(self, maze, start, exit):
# реализация BFS
...
class DFSStrategy(PathFindingStrategy):
def findPath(self, maze, start, exit):
# реализация DFS
...
class AStarStrategy(PathFindingStrategy):
def findPath(self, maze, start, exit):
# реализация A*
...
```
```python
class MazeSolver:
def __init__(self, maze=None, strategy=None):
self.maze = maze
self.strategy = strategy
def setStrategy(self, strategy):
self.strategy = strategy
def solve(self):
if not self.maze or not self.strategy:
return None
# замер времени и вызов стратегии
...
```
Полный код доступен в репозитории (или может быть предоставлен по запросу).
3. Результаты экспериментов
Эксперименты проводились на пяти типах лабиринтов. Ниже представлены ключевые результаты.
Сводная таблица (средние значения):
| Лабиринт | Стратегия | Время (мс) | Посещено клеток | Длина пути |
|-------------------------|-----------|------------|------------------|------------|
| 10x10_simple | BFS | 0.07 | 90 | 37 |
| 10x10_simple | DFS | 0.03 | 67 | 37 |
| 10x10_simple | A* | 0.09 | 76 | 19 |
| 50x50_with_deadends | BFS | 1.29 | 1657 | 0 |
| 50x50_with_deadends | DFS | 0.64 | 993 | 243 |
| 50x50_with_deadends | A* | 0.56 | 440 | 101 |
| 100x100_complex | BFS | 4.40 | 5735 | 1 |
| 100x100_complex | DFS | 4.33 | 5735 | 1 |
| 100x100_complex | A* | 7.03 | 5735 | 1 |
| empty | BFS | 0.68 | 900 | 0 |
| empty | DFS | 0.39 | 900 | 465 |
| empty | A* | 1.04 | 900 | 59 |
Графики (сохранены в папке `results/`):
- `time_comparison.png` сравнение времени работы алгоритмов
- `visited_comparison.png` сравнение количества посещённых клеток
4. Анализ эффективности алгоритмов и применимости паттернов
- **BFS** показывает стабильную работу и находит кратчайший путь, но посещает больше клеток.
- **DFS** быстрее всех на простых и пустых лабиринтах, однако не гарантирует оптимальность.
- **A*** эффективнее всего по количеству посещённых клеток на сложных лабиринтах, но на больших картах проигрывает по времени из-за overhead приоритетной очереди.
Паттерн **Strategy** позволил легко переключаться между алгоритмами без изменения кода `MazeSolver`. Паттерн **Builder** сделал возможным добавление новых источников построения лабиринта (например, генератор случайных лабиринтов) без изменения основной логики.
5. Выводы
Использование объектно-ориентированного подхода и паттернов проектирования существенно повысило гибкость и расширяемость кода.
Преимущества:
- Благодаря паттерну **Strategy** добавление нового алгоритма поиска (например, Dijkstra) требует только реализации интерфейса `PathFindingStrategy` без изменения `MazeSolver`.
- Паттерн **Builder** позволяет легко подключать новые способы загрузки лабиринтов.
- Код стал более читаемым и поддерживаемым.
Что было бы сложно изменить без паттернов:
- Замена алгоритма поиска потребовала бы значительных изменений в классе `MazeSolver` (много условных операторов `if`).
- Добавление нового способа построения лабиринта привело бы к дублированию кода.
- Сравнительный эксперимент было бы гораздо сложнее проводить, так как алгоритмы не были бы унифицированы через общий интерфейс.
Таким образом, применение паттернов Strategy и Builder сделало систему легко расширяемой и удобной для проведения экспериментов.

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

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

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

16
Ezhovnd/табл.csv Normal file
View File

@ -0,0 +1,16 @@
лабиринт,стратегия,время_мс,посещено_клеток,длина_пути
10x10_simple,BFS,0.0646,82,1
10x10_simple,DFS,0.0633,82,1
10x10_simple,AStar,0.0929,82,1
50x50_with_deadends,BFS,1.3632,1687,0
50x50_with_deadends,DFS,0.1943,400,205
50x50_with_deadends,AStar,0.6863,562,101
100x100_complex,BFS,4.8617,6060,1
100x100_complex,DFS,4.6471,6060,1
100x100_complex,AStar,7.4691,6060,1
empty,BFS,0.6954,900,0
empty,DFS,0.4106,900,465
empty,AStar,1.0604,900,59
no_exit,BFS,0.0017,1,1
no_exit,DFS,0.0009,1,1
no_exit,AStar,0.001,1,1
1 лабиринт стратегия время_мс посещено_клеток длина_пути
2 10x10_simple BFS 0.0646 82 1
3 10x10_simple DFS 0.0633 82 1
4 10x10_simple AStar 0.0929 82 1
5 50x50_with_deadends BFS 1.3632 1687 0
6 50x50_with_deadends DFS 0.1943 400 205
7 50x50_with_deadends AStar 0.6863 562 101
8 100x100_complex BFS 4.8617 6060 1
9 100x100_complex DFS 4.6471 6060 1
10 100x100_complex AStar 7.4691 6060 1
11 empty BFS 0.6954 900 0
12 empty DFS 0.4106 900 465
13 empty AStar 1.0604 900 59
14 no_exit BFS 0.0017 1 1
15 no_exit DFS 0.0009 1 1
16 no_exit AStar 0.001 1 1

0
FirsovAV/425.txt Normal file
View File

View File

@ -0,0 +1,292 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import time
import random
import csv
import sys
sys.setrecursionlimit(30000)
def ll_create_node(name, phone):
return {'name': name, 'phone': phone, 'next': None}
def ll_insert(head, name, phone):
if head is None:
return ll_create_node(name, phone)
if head['name'] == name:
head['phone'] = phone
return head
current = head
while current['next'] is not None:
if current['next']['name'] == name:
current['next']['phone'] = phone
return head
current = current['next']
current['next'] = ll_create_node(name, phone)
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):
return sum(ord(c) for c in name) % table_size
def ht_create_table(size=2000):
return [None] * size
def ht_insert(table, name, phone):
index = hash_function(name, len(table))
table[index] = ll_insert(table[index], name, phone)
def ht_find(table, name):
index = hash_function(name, len(table))
return ll_find(table[index], name)
def ht_delete(table, name):
index = hash_function(name, len(table))
table[index] = ll_delete(table[index], name)
def ht_list_all(table):
all_records = []
for bucket in table:
if bucket is not None:
current = bucket
while current is not None:
all_records.append((current['name'], current['phone']))
current = current['next']
all_records.sort(key=lambda x: x[0])
return all_records
def bst_create_node(name, phone):
return {'name': name, 'phone': phone, 'left': None, 'right': None}
def bst_insert(root, name, phone):
if root is None:
return bst_create_node(name, phone)
current = root
while True:
if name < current['name']:
if current['left'] is None:
current['left'] = bst_create_node(name, phone)
break
else:
current = current['left']
elif name > current['name']:
if current['right'] is None:
current['right'] = bst_create_node(name, phone)
break
else:
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']:
current = current['left']
elif name > current['name']:
current = current['right']
else:
return current['phone']
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 or current['right'] is None:
if current['left'] is not None:
child = current['left']
else:
child = current['right']
if parent is None:
return child
if parent['left'] == current:
parent['left'] = child
else:
parent['right'] = child
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
def bst_list_all(root):
records = []
stack = []
current = root
while stack or current is not None:
while current is not None:
stack.append(current)
current = current['left']
current = stack.pop()
records.append((current['name'], current['phone']))
current = current['right']
return records
def generate_data(n=10000):
records = [(f"User_{i:05d}", f"+7-999-{i:06d}") for i in range(n)]
records_shuffled = records.copy()
random.shuffle(records_shuffled)
records_sorted = sorted(records, key=lambda x: x[0])
return records_shuffled, records_sorted
def run_experiment(structure_name, insert_func, find_func, delete_func,
list_all_func, init_func, records, n_find=100):
data = init_func()
names = [r[0] for r in records]
start = time.perf_counter()
for name, phone in records:
if structure_name == "HashTable":
insert_func(data, name, phone)
else:
data = insert_func(data, name, phone)
insert_time = time.perf_counter() - start
find_names = random.sample(names, min(n_find, len(names)))
missing_names = [f"None_{i}" for i in range(10)]
all_find_names = find_names + missing_names
start = time.perf_counter()
for name in all_find_names:
if structure_name == "HashTable":
find_func(data, name)
else:
find_func(data, name)
find_time = time.perf_counter() - start
delete_names = random.sample(names, min(50, len(names)))
start = time.perf_counter()
for name in delete_names:
if structure_name == "HashTable":
delete_func(data, name)
else:
data = delete_func(data, name)
delete_time = time.perf_counter() - start
return insert_time, find_time, delete_time
def main():
print("Generating test data...")
records_shuffled, records_sorted = generate_data(10000)
results = []
structures = [
("LinkedList", ll_insert, ll_find, ll_delete, ll_list_all, lambda: None),
("HashTable", ht_insert, ht_find, ht_delete, ht_list_all, lambda: ht_create_table(2000)),
("BST", bst_insert, bst_find, bst_delete, bst_list_all, lambda: None)
]
for mode_name, records in [("random", records_shuffled), ("sorted", records_sorted)]:
print(f"\nMode: {mode_name}")
for struct_name, insert_f, find_f, delete_f, list_f, init_f in structures:
print(f" Testing {struct_name}...")
times = []
for run in range(5):
insert_t, find_t, delete_t = run_experiment(
struct_name, insert_f, find_f, delete_f, list_f, init_f, records
)
times.append((insert_t, find_t, delete_t))
print(f" Run {run+1}: insert={insert_t:.4f}s, find={find_t:.4f}s, delete={delete_t:.4f}s")
avg_insert = sum(t[0] for t in times) / 5
avg_find = sum(t[1] for t in times) / 5
avg_delete = sum(t[2] for t in times) / 5
results.append([struct_name, mode_name, "insert", avg_insert])
results.append([struct_name, mode_name, "find", avg_find])
results.append([struct_name, mode_name, "delete", avg_delete])
with open("results.csv", "w", newline="", encoding="utf-8") as f:
writer = csv.writer(f)
writer.writerow(["Structure", "Mode", "Operation", "Time_seconds"])
writer.writerows(results)
print("\n" + "="*60)
print("RESULTS (average over 5 runs):")
print("="*60)
for row in results:
print(f"{row[0]:12} | {row[1]:8} | {row[2]:8} | {row[3]:.6f} sec")
print("\nResults saved to results.csv")
if __name__ == "__main__":
main()

View File

@ -0,0 +1,122 @@
Методы Программирования
Структуры данных,
анализ 1 задания
Бобров К. Н.
425 группа
Содержание
Как порядок входных данных влияет на скорость вставки в 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). Идеальный вариант, когда данные часто изменяются и при этом требуется обход элементов в отсортированном порядке.

View File

@ -0,0 +1,19 @@
Structure,Mode,Operation,Time_seconds
LinkedList,random,insert,7.967956480104476
LinkedList,random,find,0.05891917999833822
LinkedList,random,delete,0.03816298004239797
HashTable,random,insert,0.39825033992528913
HashTable,random,find,0.002917400002479553
HashTable,random,delete,0.0021501399576663973
BST,random,insert,0.02822491992264986
BST,random,find,0.00023473985493183136
BST,random,delete,0.00016456004232168198
LinkedList,sorted,insert,8.014810599852353
LinkedList,sorted,find,0.058480959851294756
LinkedList,sorted,delete,0.04817821998149156
HashTable,sorted,insert,0.3703480200842023
HashTable,sorted,find,0.002751259971410036
HashTable,sorted,delete,0.0018340200185775757
BST,sorted,insert,7.301413399912417
BST,sorted,find,0.06847236007452011
BST,sorted,delete,0.03443789994344115
1 Structure Mode Operation Time_seconds
2 LinkedList random insert 7.967956480104476
3 LinkedList random find 0.05891917999833822
4 LinkedList random delete 0.03816298004239797
5 HashTable random insert 0.39825033992528913
6 HashTable random find 0.002917400002479553
7 HashTable random delete 0.0021501399576663973
8 BST random insert 0.02822491992264986
9 BST random find 0.00023473985493183136
10 BST random delete 0.00016456004232168198
11 LinkedList sorted insert 8.014810599852353
12 LinkedList sorted find 0.058480959851294756
13 LinkedList sorted delete 0.04817821998149156
14 HashTable sorted insert 0.3703480200842023
15 HashTable sorted find 0.002751259971410036
16 HashTable sorted delete 0.0018340200185775757
17 BST sorted insert 7.301413399912417
18 BST sorted find 0.06847236007452011
19 BST sorted delete 0.03443789994344115

View File

@ -0,0 +1,589 @@
import time
import heapq
from collections import deque
from typing import List, Optional, Dict, Tuple
from abc import ABC, abstractmethod
import csv
import random
class Cell:
def __init__(self, x: int, y: int):
self.x = x
self.y = y
self.is_wall = False
self.is_start = False
self.is_exit = False
def is_passable(self) -> bool:
return not self.is_wall
class Maze:
def __init__(self, width: int, height: int):
self.width = width
self.height = height
self.cells = [[Cell(x, y) for y in range(height)] for x in range(width)]
self.start: Optional[Cell] = None
self.exit: Optional[Cell] = None
def get_cell(self, x: int, y: int) -> Optional[Cell]:
if 0 <= x < self.width and 0 <= y < self.height:
return self.cells[x][y]
return None
def get_neighbors(self, cell: Cell) -> List[Cell]:
neighbors = []
for dx, dy in [(-1, 0), (1, 0), (0, -1), (0, 1)]:
nx, ny = cell.x + dx, cell.y + dy
nb = self.get_cell(nx, ny)
if nb and nb.is_passable():
neighbors.append(nb)
return neighbors
class MazeBuilder(ABC):
@abstractmethod
def build_from_file(self, filename: str) -> Maze:
pass
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)")
return maze
class PathFindingStrategy(ABC):
@abstractmethod
def find_path(self, maze: Maze, start: Cell, exit: Cell) -> List[Cell]:
pass
@abstractmethod
def get_name(self) -> str:
pass
class BFSStrategy(PathFindingStrategy):
def find_path(self, maze: Maze, start: Cell, exit: Cell) -> List[Cell]:
queue = deque([start])
came_from = {start: None}
while queue:
current = queue.popleft()
if current == exit:
break
for nb in maze.get_neighbors(current):
if nb not in came_from:
came_from[nb] = current
queue.append(nb)
if exit not in came_from:
return []
path = []
cur = exit
while cur:
path.append(cur)
cur = came_from[cur]
path.reverse()
return path
def get_name(self) -> str:
return "BFS"
class DFSStrategy(PathFindingStrategy):
def find_path(self, maze: Maze, start: Cell, exit: Cell) -> List[Cell]:
stack = [start]
came_from = {start: None}
while stack:
current = stack.pop()
if current == exit:
break
for nb in maze.get_neighbors(current):
if nb not in came_from:
came_from[nb] = current
stack.append(nb)
if exit not in came_from:
return []
path = []
cur = exit
while cur:
path.append(cur)
cur = came_from[cur]
path.reverse()
return path
def get_name(self) -> str:
return "DFS"
class AStarStrategy(PathFindingStrategy):
def _heuristic(self, a: Cell, b: Cell) -> int:
return abs(a.x - b.x) + abs(a.y - b.y)
def find_path(self, maze: Maze, start: Cell, exit: Cell) -> List[Cell]:
open_set = []
heapq.heappush(open_set, (0, id(start), start))
came_from = {}
g_score = {start: 0}
f_score = {start: self._heuristic(start, exit)}
while open_set:
_, _, current = heapq.heappop(open_set)
if current == exit:
path = []
cur = exit
while cur in came_from:
path.append(cur)
cur = came_from[cur]
path.append(start)
path.reverse()
return path
for neighbor in maze.get_neighbors(current):
tentative_g = g_score[current] + 1
if tentative_g < g_score.get(neighbor, float('inf')):
came_from[neighbor] = current
g_score[neighbor] = tentative_g
f_score[neighbor] = tentative_g + self._heuristic(neighbor, exit)
heapq.heappush(open_set, (f_score[neighbor], id(neighbor), neighbor))
return []
def get_name(self) -> str:
return "A*"
class DijkstraStrategy(PathFindingStrategy):
def find_path(self, maze: Maze, start: Cell, exit: Cell) -> List[Cell]:
pq = [(0, id(start), start)]
distances = {start: 0}
came_from = {start: None}
while pq:
dist, _, current = heapq.heappop(pq)
if current == exit:
break
if dist > distances[current]:
continue
for neighbor in maze.get_neighbors(current):
new_dist = dist + 1
if new_dist < distances.get(neighbor, float('inf')):
distances[neighbor] = new_dist
came_from[neighbor] = current
heapq.heappush(pq, (new_dist, id(neighbor), neighbor))
if exit not in came_from:
return []
path = []
cur = exit
while cur:
path.append(cur)
cur = came_from[cur]
path.reverse()
return path
def get_name(self) -> str:
return "Dijkstra"
class SearchStats:
def __init__(self, time_ms: float, visited_cells: int, path_length: int):
self.time_ms = time_ms
self.visited_cells = visited_cells
self.path_length = path_length
def __str__(self):
return f"Time: {self.time_ms:.2f}ms, Visited: {self.visited_cells}, Path: {self.path_length}"
class MazeSolver:
def __init__(self, maze: Maze, strategy: PathFindingStrategy):
self.maze = maze
self.strategy = strategy
def set_strategy(self, strategy: PathFindingStrategy):
self.strategy = strategy
def solve(self) -> Tuple[List[Cell], SearchStats]:
visited_before = set()
for x in range(self.maze.width):
for y in range(self.maze.height):
cell = self.maze.get_cell(x, y)
if cell and cell.is_passable():
visited_before.add(cell)
start_time = time.perf_counter()
path = self.strategy.find_path(self.maze, self.maze.start, self.maze.exit)
end_time = time.perf_counter()
visited_after = set()
for x in range(self.maze.width):
for y in range(self.maze.height):
cell = self.maze.get_cell(x, y)
if cell and cell.is_passable():
visited_after.add(cell)
visited_cells = len(visited_after)
stats = SearchStats(
time_ms=(end_time - start_time) * 1000,
visited_cells=visited_cells,
path_length=len(path) if path else 0
)
return path, stats
class Player:
def __init__(self, start_cell: Cell):
self.current_cell = start_cell
self.previous_cell = None
def move_to(self, cell: Cell) -> bool:
if cell.is_passable():
self.previous_cell = self.current_cell
self.current_cell = cell
return True
return False
def undo(self):
if self.previous_cell:
self.current_cell, self.previous_cell = self.previous_cell, None
return True
return False
class Command(ABC):
@abstractmethod
def execute(self) -> bool:
pass
@abstractmethod
def undo(self):
pass
class MoveCommand(Command):
def __init__(self, player: Player, maze: Maze, direction: str):
self.player = player
self.maze = maze
self.direction = direction
self.executed = False
def execute(self) -> bool:
dx, dy = 0, 0
if self.direction == 'W' or self.direction == 'w':
dy = -1
elif self.direction == 'S' or self.direction == 's':
dy = 1
elif self.direction == 'A' or self.direction == 'a':
dx = -1
elif self.direction == 'D' or self.direction == 'd':
dx = 1
new_x = self.player.current_cell.x + dx
new_y = self.player.current_cell.y + dy
new_cell = self.maze.get_cell(new_x, new_y)
if new_cell and new_cell.is_passable():
self.executed = self.player.move_to(new_cell)
return self.executed
return False
def undo(self):
if self.executed:
self.player.undo()
self.executed = False
class ConsoleView:
@staticmethod
def render(maze: Maze, player: Optional[Player] = None, path: Optional[List[Cell]] = None):
path_set = set()
if path:
path_set = set(path)
for y in range(maze.height):
line = ""
for x in range(maze.width):
cell = maze.get_cell(x, y)
if not cell:
line += " "
elif player and player.current_cell == cell:
line += "P"
elif cell.is_start:
line += "S"
elif cell.is_exit:
line += "E"
elif cell.is_wall:
line += "#"
elif path and cell in path_set:
line += "."
else:
line += " "
print(line)
print()
@staticmethod
def show_stats(stats: SearchStats, algo_name: str):
print(f"=== {algo_name} Results ===")
print(stats)
print()
def generate_test_maze(width: int, height: int, complexity: float = 0.3) -> Maze:
maze = Maze(width, height)
for x in range(width):
for y in range(height):
if random.random() < complexity:
maze.cells[x][y].is_wall = True
maze.start = maze.get_cell(0, 0)
if maze.start:
maze.start.is_start = True
maze.start.is_wall = False
maze.exit = maze.get_cell(width - 1, height - 1)
if maze.exit:
maze.exit.is_exit = True
maze.exit.is_wall = False
return maze
def generate_empty_maze(width: int, height: int) -> Maze:
maze = Maze(width, height)
for x in range(width):
for y in range(height):
maze.cells[x][y].is_wall = False
maze.start = maze.get_cell(0, 0)
if maze.start:
maze.start.is_start = True
maze.exit = maze.get_cell(width - 1, height - 1)
if maze.exit:
maze.exit.is_exit = True
return maze
def generate_no_exit_maze(width: int, height: int) -> Maze:
maze = Maze(width, height)
for x in range(width):
for y in range(height):
maze.cells[x][y].is_wall = False
for x in range(width):
maze.cells[x][height // 2].is_wall = True
maze.start = maze.get_cell(0, 0)
if maze.start:
maze.start.is_start = True
maze.exit = maze.get_cell(width - 1, height - 1)
if maze.exit:
maze.exit.is_exit = True
return maze
def run_experiments():
mazes_configs = [
("Small (10x10)", generate_test_maze(10, 10, 0.2)),
("Medium (50x50)", generate_test_maze(50, 50, 0.25)),
("Large (100x100)", generate_test_maze(100, 100, 0.3)),
("Empty (30x30)", generate_empty_maze(30, 30)),
("No Exit (20x20)", generate_no_exit_maze(20, 20))
]
strategies = [BFSStrategy(), DFSStrategy(), AStarStrategy(), DijkstraStrategy()]
results = []
for maze_name, maze in mazes_configs:
print(f"\n=== Testing: {maze_name} ===")
for strategy in strategies:
times = []
visited = []
path_lengths = []
solver = MazeSolver(maze, strategy)
for run in range(5):
maze_copy = Maze(maze.width, maze.height)
for x in range(maze.width):
for y in range(maze.height):
orig = maze.get_cell(x, y)
copy = maze_copy.get_cell(x, y)
if orig:
copy.is_wall = orig.is_wall
copy.is_start = orig.is_start
copy.is_exit = orig.is_exit
maze_copy.start = maze_copy.get_cell(maze.start.x, maze.start.y) if maze.start else None
maze_copy.exit = maze_copy.get_cell(maze.exit.x, maze.exit.y) if maze.exit else None
solver.maze = maze_copy
solver.set_strategy(strategy)
path, stats = solver.solve()
times.append(stats.time_ms)
visited.append(stats.visited_cells)
path_lengths.append(stats.path_length)
avg_time = sum(times) / len(times)
avg_visited = sum(visited) / len(visited)
avg_path = sum(path_lengths) / len(path_lengths)
results.append({
'maze': maze_name,
'algorithm': strategy.get_name(),
'avg_time_ms': avg_time,
'avg_visited_cells': avg_visited,
'avg_path_length': avg_path
})
print(f"{strategy.get_name()}: {avg_time:.2f}ms, {avg_visited:.0f} cells, path={avg_path:.0f}")
with open('experiment_results.csv', 'w', newline='', encoding='utf-8') as f:
writer = csv.DictWriter(f, fieldnames=['maze', 'algorithm', 'avg_time_ms', 'avg_visited_cells', 'avg_path_length'])
writer.writeheader()
writer.writerows(results)
print("\nResults saved to experiment_results.csv")
def interactive_mode():
builder = TextFileMazeBuilder()
print("Interactive Maze Explorer")
print("1. Load maze from file")
print("2. Generate random maze")
choice = input("Choose (1/2): ")
if choice == '1':
filename = input("Enter filename: ")
try:
maze = builder.build_from_file(filename)
except Exception as e:
print(f"Error loading maze: {e}")
return
else:
w = int(input("Width: "))
h = int(input("Height: "))
maze = generate_test_maze(w, h, 0.3)
player = Player(maze.start)
strategies = {
'1': BFSStrategy(),
'2': DFSStrategy(),
'3': AStarStrategy(),
'4': DijkstraStrategy()
}
print("\nSelect algorithm for solving:")
print("1. BFS (shortest path)")
print("2. DFS (fast, not optimal)")
print("3. A* (heuristic)")
print("4. Dijkstra")
algo_choice = input("Choose: ")
solver = MazeSolver(maze, strategies.get(algo_choice, BFSStrategy()))
path, stats = solver.solve()
view = ConsoleView()
if path:
print(f"\nPath found! Length: {len(path)}")
view.show_stats(stats, solver.strategy.get_name())
else:
print("\nNo path found!")
while True:
view.render(maze, player, path if path else None)
if player.current_cell == maze.exit:
print("Congratulations! You reached the exit!")
break
cmd = input("Move (W/A/S/D) | U=undo | Q=quit | S=solve: ").upper()
if cmd == 'Q':
break
elif cmd == 'U':
player.undo()
print("Undo last move")
elif cmd == 'S' and path:
for cell in path:
if cell == player.current_cell:
continue
player.move_to(cell)
view.render(maze, player, path)
input("Press Enter to continue...")
if player.current_cell == maze.exit:
print("You reached the exit!")
break
elif cmd in ['W', 'A', 'S', 'D']:
move_cmd = MoveCommand(player, maze, cmd)
if move_cmd.execute():
print("Moved")
else:
print("Can't move there!")
def main():
print("Maze Solver with Design Patterns")
print("1. Run experiments")
print("2. Interactive mode")
choice = input("Choose (1/2): ")
if choice == '1':
run_experiments()
else:
interactive_mode()
if __name__ == "__main__":
main()

View File

@ -0,0 +1,21 @@
maze,algorithm,avg_time_ms,avg_visited_cells,avg_path_length
Small (10x10),BFS,0.006740167737007141,80.0,0.0
Small (10x10),DFS,0.00408003106713295,80.0,0.0
Small (10x10),A*,0.005039852112531662,80.0,0.0
Small (10x10),Dijkstra,0.0031800009310245514,80.0,0.0
Medium (50x50),BFS,3.44578018411994,1890.0,99.0
Medium (50x50),DFS,1.3188599608838558,1890.0,341.0
Medium (50x50),A*,2.061920054256916,1890.0,99.0
Medium (50x50),Dijkstra,4.679400008171797,1890.0,99.0
Large (100x100),BFS,0.025319866836071014,6998.0,0.0
Large (100x100),DFS,0.019940081983804703,6998.0,0.0
Large (100x100),A*,0.035060010850429535,6998.0,0.0
Large (100x100),Dijkstra,0.02901991829276085,6998.0,0.0
Empty (30x30),BFS,1.2404202483594418,900.0,59.0
Empty (30x30),DFS,0.8887200616300106,900.0,465.0
Empty (30x30),A*,0.9468601085245609,900.0,59.0
Empty (30x30),Dijkstra,2.678940072655678,900.0,59.0
No Exit (20x20),BFS,0.27012014761567116,380.0,0.0
No Exit (20x20),DFS,0.3163599409162998,380.0,0.0
No Exit (20x20),A*,0.5885399878025055,380.0,0.0
No Exit (20x20),Dijkstra,0.5776201374828815,380.0,0.0
1 maze algorithm avg_time_ms avg_visited_cells avg_path_length
2 Small (10x10) BFS 0.006740167737007141 80.0 0.0
3 Small (10x10) DFS 0.00408003106713295 80.0 0.0
4 Small (10x10) A* 0.005039852112531662 80.0 0.0
5 Small (10x10) Dijkstra 0.0031800009310245514 80.0 0.0
6 Medium (50x50) BFS 3.44578018411994 1890.0 99.0
7 Medium (50x50) DFS 1.3188599608838558 1890.0 341.0
8 Medium (50x50) A* 2.061920054256916 1890.0 99.0
9 Medium (50x50) Dijkstra 4.679400008171797 1890.0 99.0
10 Large (100x100) BFS 0.025319866836071014 6998.0 0.0
11 Large (100x100) DFS 0.019940081983804703 6998.0 0.0
12 Large (100x100) A* 0.035060010850429535 6998.0 0.0
13 Large (100x100) Dijkstra 0.02901991829276085 6998.0 0.0
14 Empty (30x30) BFS 1.2404202483594418 900.0 59.0
15 Empty (30x30) DFS 0.8887200616300106 900.0 465.0
16 Empty (30x30) A* 0.9468601085245609 900.0 59.0
17 Empty (30x30) Dijkstra 2.678940072655678 900.0 59.0
18 No Exit (20x20) BFS 0.27012014761567116 380.0 0.0
19 No Exit (20x20) DFS 0.3163599409162998 380.0 0.0
20 No Exit (20x20) A* 0.5885399878025055 380.0 0.0
21 No Exit (20x20) Dijkstra 0.5776201374828815 380.0 0.0

View File

@ -0,0 +1,196 @@
Методы программирования
Поиск выхода из лабиринта.
Анализ 2 задания
Бобров К. Н.
425 группа
Содержание
Описание задачи и выбранных паттернов 2
Листинги ключевых классов 4
Результаты экспериментов 6
Анализ эффективности алгоритмов и применимости паттернов 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)")
return maze
Strategy (пример BFS):
class BFSStrategy(PathFindingStrategy):
def find_path(self, maze: Maze, start: Cell, exit: Cell) -> List[Cell]:
queue = deque([start])
came_from = {start: None}
while queue:
current = queue.popleft()
if current == exit:
break
for nb in maze.get_neighbors(current):
if nb not in came_from:
came_from[nb] = current
queue.append(nb)
if exit not in came_from:
return []
path = []
cur = exit
while cur:
path.append(cur)
cur = came_from[cur]
path.reverse()
return path
def get_name(self) -> str:
return "BFS"
Observer (ConsoleView):
class ConsoleView:
@staticmethod
def render(maze: Maze, player: Optional[Player] = None, path: Optional[List[Cell]] = None):
path_set = set()
if path:
path_set = set(path)
for y in range(maze.height):
line = ""
for x in range(maze.width):
cell = maze.get_cell(x, y)
if not cell:
line += " "
elif player and player.current_cell == cell:
line += "P"
elif cell.is_start:
line += "S"
elif cell.is_exit:
line += "E"
elif cell.is_wall:
line += "#"
elif path and cell in path_set:
line += "."
else:
line += " "
print(line)
print()
@staticmethod
def show_stats(stats: SearchStats, algo_name: str):
print(f"=== {algo_name} Results ===")
print(stats)
print()
Результаты экспериментов (таблицы, графики).
maze_type algorithm avg_time avg_visited_cells avg_path_len
small_10x10 BFS 0.08572000006097369 79.0 19.0
small_10x10 DFS 0.039739999920129776 79.0 31.0
small_10x10_ A* 0.13467999997374136 79.0 19.0
small_10x10 Dijkstra 0.11474000057205558 79.0 19.0
medium_50x50 BFS 1.8074600004183594 1874.0 99.0
medium_50x50 DFS 0.5937599995377241 1874.0 429.0
medium_50x50 A* 1.6300600003887666 1874.0 99.0
medium_50x50 Dijkstra 3.1870400001935195 1874.0 99.0
large_100x100 BFS 0.014439999722526409 7033.0 0.0
large_100x100 DFS 0.014839999857940711 7033.0 0.0
large_100x100 A* 0.02542000001994893 7033.0 0.0
large_100x100 Dijkstra 0.02548000011302065 7033.0 0.0
empty_30x30 BFS 0.784620000194991 900.0 59.0
empty_30x30 DFS 0.5252399994787993 900.0 465.
empty_30x30 A* 1.150900000357069 900.0 59.0
empty_30x30 Dijkstra 1.564640000287909 900.0 59.0
no_exit_20x20 BFS 0.2002399993216386 380. 0.0
no_exit_20x20 DFS 0.2512400002160575 380.0 0.0
no_exit_20x20 A* 0.5590400000073714 380.0 0.
no_exit_20x20 Dijkstra 0.35640000060084276 380.0 0.0
Графики построены кодом из файла RESULT22.
Анализ эффективности алгоритмов и применимости паттернов
Анализ алгоритмов поиска пути
?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 или веб).
Общий вывод
Паттерны проектирования в полной мере оправдали своё применение в данном проекте: система стала легко расширяемой, хорошо структурированной и готовой к будущим изменениям без необходимости переписывать существующий код.

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

View File

@ -0,0 +1,19 @@
structure,order,operation,run1,run2,run3,run4,run5,average
LinkedList,random,insert,3.000600399999712,3.022712899999533,2.9421689999999217,2.9075659000000087,3.0319512999994913,2.980999899999733
LinkedList,random,find,0.031094500000108383,0.02800200000001496,0.034349299999121286,0.029372199999670556,0.03242119999958959,0.031047839999700955
LinkedList,random,delete,0.017322699999567703,0.0368361000000732,0.04029200000059063,0.03775789999963308,0.03554420000000391,0.033550579999973705
HashTable,random,insert,0.011551699999472476,0.012756400000398571,0.011765299999751733,0.011679000000185624,0.011983400000644906,0.011947160000090662
HashTable,random,find,0.00012409999999363208,0.00011009999980160501,0.0001415999995515449,0.00010400000064691994,0.00010089999977935804,0.000116139999954612
HashTable,random,delete,6.38999999864609e-05,6.779999966965988e-05,6.0600000324484427e-05,6.070000017643906e-05,6.0600000324484427e-05,6.272000009630574e-05
BST,random,insert,0.014788199999202334,0.014159299999846553,0.013975800000480376,0.014118900000539725,0.013331299999663315,0.01407469999994646
BST,random,find,0.00013829999988956843,0.00011389999963284936,0.00011369999992894009,0.00011379999978089472,0.00011439999980211724,0.00011881999980687397
BST,random,delete,8.690000049682567e-05,6.450000000768341e-05,6.2199999774748e-05,6.209999992279336e-05,6.229999962670263e-05,6.759999996575061e-05
LinkedList,sorted,insert,2.4411346000006233,2.36463619999995,2.2797248999995645,2.2860746000005747,2.2526011999998445,2.3248343000001115
LinkedList,sorted,find,0.024703000000044995,0.02455259999987902,0.02468479999970441,0.02444869999999355,0.02606350000041857,0.02489052000000811
LinkedList,sorted,delete,0.012835599999561964,0.027673999999933585,0.027570299999752024,0.02708100000018021,0.02999909999925876,0.02503199999973731
HashTable,sorted,insert,0.011780100000578386,0.010850699999537028,0.010314100000869075,0.010621500000524975,0.011015500000212342,0.010916380000344362
HashTable,sorted,find,0.0001464000006308197,0.00017980000029638177,0.00016909999976633117,0.00012620000052265823,0.00023630000032426324,0.0001715600003080908
HashTable,sorted,delete,0.00016370000048482325,0.00018089999957737746,0.0001443999999537482,7.579999964946182e-05,6.469999971159268e-05,0.0001258999998754007
BST,sorted,insert,3.5400651999998445,3.5145174999997835,3.5583661999999094,3.5149656000003233,3.481246600000304,3.521832220000033
BST,sorted,find,0.03275260000009439,0.030442500000390282,0.02994349999971746,0.030269500000031258,0.030329999999594293,0.030747619999965538
BST,sorted,delete,0.012705400000413647,0.01333390000036161,0.013192000000344706,0.013699000000087835,0.013079800000014075,0.013202020000244374
1 structure order operation run1 run2 run3 run4 run5 average
2 LinkedList random insert 3.000600399999712 3.022712899999533 2.9421689999999217 2.9075659000000087 3.0319512999994913 2.980999899999733
3 LinkedList random find 0.031094500000108383 0.02800200000001496 0.034349299999121286 0.029372199999670556 0.03242119999958959 0.031047839999700955
4 LinkedList random delete 0.017322699999567703 0.0368361000000732 0.04029200000059063 0.03775789999963308 0.03554420000000391 0.033550579999973705
5 HashTable random insert 0.011551699999472476 0.012756400000398571 0.011765299999751733 0.011679000000185624 0.011983400000644906 0.011947160000090662
6 HashTable random find 0.00012409999999363208 0.00011009999980160501 0.0001415999995515449 0.00010400000064691994 0.00010089999977935804 0.000116139999954612
7 HashTable random delete 6.38999999864609e-05 6.779999966965988e-05 6.0600000324484427e-05 6.070000017643906e-05 6.0600000324484427e-05 6.272000009630574e-05
8 BST random insert 0.014788199999202334 0.014159299999846553 0.013975800000480376 0.014118900000539725 0.013331299999663315 0.01407469999994646
9 BST random find 0.00013829999988956843 0.00011389999963284936 0.00011369999992894009 0.00011379999978089472 0.00011439999980211724 0.00011881999980687397
10 BST random delete 8.690000049682567e-05 6.450000000768341e-05 6.2199999774748e-05 6.209999992279336e-05 6.229999962670263e-05 6.759999996575061e-05
11 LinkedList sorted insert 2.4411346000006233 2.36463619999995 2.2797248999995645 2.2860746000005747 2.2526011999998445 2.3248343000001115
12 LinkedList sorted find 0.024703000000044995 0.02455259999987902 0.02468479999970441 0.02444869999999355 0.02606350000041857 0.02489052000000811
13 LinkedList sorted delete 0.012835599999561964 0.027673999999933585 0.027570299999752024 0.02708100000018021 0.02999909999925876 0.02503199999973731
14 HashTable sorted insert 0.011780100000578386 0.010850699999537028 0.010314100000869075 0.010621500000524975 0.011015500000212342 0.010916380000344362
15 HashTable sorted find 0.0001464000006308197 0.00017980000029638177 0.00016909999976633117 0.00012620000052265823 0.00023630000032426324 0.0001715600003080908
16 HashTable sorted delete 0.00016370000048482325 0.00018089999957737746 0.0001443999999537482 7.579999964946182e-05 6.469999971159268e-05 0.0001258999998754007
17 BST sorted insert 3.5400651999998445 3.5145174999997835 3.5583661999999094 3.5149656000003233 3.481246600000304 3.521832220000033
18 BST sorted find 0.03275260000009439 0.030442500000390282 0.02994349999971746 0.030269500000031258 0.030329999999594293 0.030747619999965538
19 BST sorted delete 0.012705400000413647 0.01333390000036161 0.013192000000344706 0.013699000000087835 0.013079800000014075 0.013202020000244374

View File

@ -0,0 +1,34 @@
Лабораторная работа 1
Цель работы
Нужно было сделать три структуры данных и проверить как они работают на телефонном справочнике.
Ход работы
Сделал связный список хеш таблицу и двоичное дерево поиска. Для всех структур сделал добавление поиск удаление и вывод записей. Для проверки создал 10000 записей с именами User\_00000 и т.д. Потом проверил работу со случайным порядком и с отсортированным порядком. Каждый эксперимент повторял 5 раз.
Результаты
Результаты сохранились в results.csv. Также сделал графики для добавления поиска и удаления. По результатам видно что связный список медленно ищет записи потому что нужно идти по элементам. Хеш таблица работает примерно одинаково при разном порядке записей. У двоичного дерева порядок записей влияет намного сильнее. Если добавлять записи по порядку дерево становится похожим на обычный список и работает медленнее.
Вывод
В работе я сделал три структуры данных и проверил их работу. Самой удобной для телефонного справочника получилась хеш таблица. Связный список проще но поиск медленный. Двоичное дерево может работать быстро но сильно зависит от порядка добавления данных.

View File

@ -0,0 +1,185 @@
import random
import time
import csv
import os
from phonebook import *
N = 10000
REPEATS = 5
def generate_test_data():
records = [
(f"User_{i:05d}", f"+7900000{i:04d}")
for i in range(N)
]
records_shuffled = records.copy()
random.shuffle(records_shuffled)
records_sorted = records.copy()
return records_shuffled, records_sorted
def measure_experiment(insert_function, find_function, delete_function, records):
insert_times = []
find_times = []
delete_times = []
for _ in range(REPEATS):
structure = None
start = time.perf_counter()
for name, phone in records:
structure = insert_function(structure, name, phone)
insert_times.append(time.perf_counter() - start)
structure_for_find = structure
names = [name for name, phone in records]
search_names = random.sample(names, 100) + [
"NotFound_001",
"NotFound_002",
"NotFound_003",
"NotFound_004",
"NotFound_005",
"NotFound_006",
"NotFound_007",
"NotFound_008",
"NotFound_009",
"NotFound_010"
]
for _ in range(REPEATS):
start = time.perf_counter()
for name in search_names:
find_function(structure_for_find, name)
find_times.append(time.perf_counter() - start)
delete_names = random.sample(names, 50)
for _ in range(REPEATS):
structure = structure_for_find
start = time.perf_counter()
for name in delete_names:
structure = delete_function(structure, name)
delete_times.append(time.perf_counter() - start)
return insert_times, find_times, delete_times
def measure_hash(records):
insert_times = []
find_times = []
delete_times = []
names = [name for name, phone in records]
search_names = random.sample(names, 100) + [
f"NotFound_{i:03d}" for i in range(10)
]
delete_names = random.sample(names, 50)
for _ in range(REPEATS):
buckets = ht_create()
start = time.perf_counter()
for name, phone in records:
ht_insert(buckets, name, phone)
insert_times.append(time.perf_counter() - start)
structure_for_find = buckets
for _ in range(REPEATS):
start = time.perf_counter()
for name in search_names:
ht_find(structure_for_find, name)
find_times.append(time.perf_counter() - start)
for _ in range(REPEATS):
buckets = structure_for_find.copy()
start = time.perf_counter()
for name in delete_names:
ht_delete(buckets, name)
delete_times.append(time.perf_counter() - start)
return insert_times, find_times, delete_times
def average(values):
return sum(values) / len(values)
def run():
records_shuffled, records_sorted = generate_test_data()
results = []
for order_name, records in [
("random", records_shuffled),
("sorted", records_sorted)
]:
print("Order:", order_name)
ll = measure_experiment(
ll_insert,
ll_find,
ll_delete,
records
)
results.append(["LinkedList", order_name, "insert", *ll[0]])
results.append(["LinkedList", order_name, "find", *ll[1]])
results.append(["LinkedList", order_name, "delete", *ll[2]])
ht = measure_hash(records)
results.append(["HashTable", order_name, "insert", *ht[0]])
results.append(["HashTable", order_name, "find", *ht[1]])
results.append(["HashTable", order_name, "delete", *ht[2]])
bst = measure_experiment(
bst_insert,
bst_find,
bst_delete,
records
)
results.append(["BST", order_name, "insert", *bst[0]])
results.append(["BST", order_name, "find", *bst[1]])
results.append(["BST", order_name, "delete", *bst[2]])
os.makedirs("docs/data", exist_ok=True)
with open("docs/data/results.csv", "w", newline="", encoding="utf-8") as file:
writer = csv.writer(file)
writer.writerow([
"structure",
"order",
"operation",
"run1",
"run2",
"run3",
"run4",
"run5",
"average"
])
for row in results:
writer.writerow(row + [average(row[3:])])
print("Results saved to docs/data/results.csv")
if __name__ == "__main__":
run()

View File

@ -0,0 +1,56 @@
import csv
import os
import matplotlib.pyplot as plt
data = []
with open("docs/data/results.csv", "r", encoding="utf-8") as file:
reader = csv.DictReader(file)
for row in reader:
data.append(row)
def get_average(structure, order, operation):
for row in data:
if (
row["structure"] == structure
and row["order"] == order
and row["operation"] == operation
):
return float(row["average"])
return 0
structures = ["LinkedList", "HashTable", "BST"]
orders = ["random", "sorted"]
os.makedirs("docs/data", exist_ok=True)
for operation in ["insert", "find", "delete"]:
random_values = [
get_average(s, "random", operation)
for s in structures
]
sorted_values = [
get_average(s, "sorted", operation)
for s in structures
]
x = range(len(structures))
plt.figure()
plt.bar([i - 0.2 for i in x], random_values, width=0.4, label="random")
plt.bar([i + 0.2 for i in x], sorted_values, width=0.4, label="sorted")
plt.xticks(list(x), structures)
plt.ylabel("Time, seconds")
plt.title(operation.capitalize() + " time")
plt.yscale("log")
plt.legend()
plt.tight_layout()
plt.savefig("docs/data/graph_" + operation + ".png")
plt.close()
print("Graphs saved to docs/data/")

View File

@ -0,0 +1,211 @@
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:
if current['name'] == name:
current['phone'] = phone
return head
current = current['next']
if current['name'] == name:
current['phone'] = phone
else:
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):
index = hash_function(name, len(buckets))
buckets[index] = ll_insert(buckets[index], name, phone)
return buckets
def ht_find(buckets, name):
index = hash_function(name, len(buckets))
return ll_find(buckets[index], name)
def ht_delete(buckets, name):
index = hash_function(name, len(buckets))
buckets[index] = ll_delete(buckets[index], 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']
if name < current['name']:
current = current['left']
else:
current = current['right']
return None
def bst_delete(root, name):
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:
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

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

View File

@ -0,0 +1,16 @@
maze,strategy,time_ms,visited_cells,path_length
simple.txt,BFS,0.01464000015403144,11.0,6.0
simple.txt,DFS,0.010180000390391797,9.0,8.0
simple.txt,A*,0.017740000475896522,9.0,6.0
dead.txt,BFS,0.3642999996372964,307.0,35.0
dead.txt,DFS,0.23493999906349927,279.0,151.0
dead.txt,A*,0.38374000068870373,235.0,35.0
large.txt,BFS,23.894459999428364,6812.0,2329.0
large.txt,DFS,84.77875999960816,6796.0,4537.0
large.txt,A*,28.69542000044021,6791.0,2329.0
empty.txt,BFS,1.2770400004228577,1176.0,48.0
empty.txt,DFS,7.602279999264283,2304.0,1176.0
empty.txt,A*,0.10093999881064519,48.0,48.0
noexit.txt,BFS,0.003699999797390774,1.0,0.0
noexit.txt,DFS,0.0032000003557186574,1.0,0.0
noexit.txt,A*,0.004120000085094944,1.0,0.0
1 maze strategy time_ms visited_cells path_length
2 simple.txt BFS 0.01464000015403144 11.0 6.0
3 simple.txt DFS 0.010180000390391797 9.0 8.0
4 simple.txt A* 0.017740000475896522 9.0 6.0
5 dead.txt BFS 0.3642999996372964 307.0 35.0
6 dead.txt DFS 0.23493999906349927 279.0 151.0
7 dead.txt A* 0.38374000068870373 235.0 35.0
8 large.txt BFS 23.894459999428364 6812.0 2329.0
9 large.txt DFS 84.77875999960816 6796.0 4537.0
10 large.txt A* 28.69542000044021 6791.0 2329.0
11 empty.txt BFS 1.2770400004228577 1176.0 48.0
12 empty.txt DFS 7.602279999264283 2304.0 1176.0
13 empty.txt A* 0.10093999881064519 48.0 48.0
14 noexit.txt BFS 0.003699999797390774 1.0 0.0
15 noexit.txt DFS 0.0032000003557186574 1.0 0.0
16 noexit.txt A* 0.004120000085094944 1.0 0.0

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

View File

@ -0,0 +1,212 @@
Лабораторная работа 2
Поиск выхода из лабиринта
Цель работы
-----------
Цель работы состоит в реализации программы для поиска выхода из лабиринта с использованием объектно ориентированного подхода и паттернов проектирования
В программе реализована загрузка лабиринта из файла несколько алгоритмов поиска и сравнение их работы
Структура программы
-------------------
В программе используются классы Cell для отдельной клетки лабиринта и Maze для самого лабиринта
Для загрузки используется MazeBuilder и его реализация TextFileMazeBuilder
Для поиска пути используется общий класс PathFindingStrategy и три алгоритма BFSStrategy DFSStrategy и AStarStrategy
За хранение результатов отвечает SearchStats а запуск поиска выполняет MazeSolver
Для вывода информации используются Observer и ConsoleView
Использованные паттерны
-----------------------
В работе использованы три паттерна Builder Strategy и Observer
Builder используется для загрузки лабиринта из текстового файла
TextFileMazeBuilder читает файл и создаёт объект Maze
В файле символ # обозначает стену пробел обозначает свободную клетку S является началом а E выходом
Использование Builder позволяет отдельно реализовать загрузку лабиринта и сам класс лабиринта
Strategy используется для выбора алгоритма поиска
В программе реализованы BFS DFS и A*
Все алгоритмы имеют общий интерфейс PathFindingStrategy поэтому в MazeSolver можно менять алгоритм без изменения самого решателя
Observer используется для вывода информации о поиске
MazeSolver отправляет события а ConsoleView получает их и выводит информацию в консоль
Таким образом вывод отделён от основной логики поиска
Алгоритмы поиска
----------------
BFS использует очередь и при обычных условиях находит кратчайший путь в лабиринте без весов
DFS использует стек и может найти путь быстрее но найденный путь не обязательно будет кратчайшим
A* использует очередь с приоритетом и манхэттенскую эвристику поэтому старается в первую очередь проверять клетки которые находятся ближе к выходу
Схема классов
-------------
classDiagram
class Cell {
x
y
is_wall
is_start
is_exit
is_passable()
}
class Maze {
width
height
cells
start
exit
get_cell()
get_neighbors()
}
class MazeBuilder {
build_from_file()
}
class TextFileMazeBuilder {
build_from_file()
}
class PathFindingStrategy {
find_path()
}
class BFSStrategy {
find_path()
}
class DFSStrategy {
find_path()
}
class AStarStrategy {
find_path()
}
class SearchStats {
path
time_ms
visited_count
path_length
}
class MazeSolver {
maze
strategy
set_strategy()
solve()
}
class Observer {
update()
}
class ConsoleView {
update()
}
MazeBuilder <|-- TextFileMazeBuilder
PathFindingStrategy <|-- BFSStrategy
PathFindingStrategy <|-- DFSStrategy
PathFindingStrategy <|-- AStarStrategy
Observer <|-- ConsoleView
MazeSolver --> Maze
MazeSolver --> PathFindingStrategy
MazeSolver --> Observer
Maze --> Cell
Тестирование
------------
Для проверки использовалось пять разных лабиринтов
simple.txt представляет простой лабиринт dead.txt содержит тупики large.txt является большим запутанным лабиринтом empty.txt не содержит стен а в noexit.txt выход недостижим
Каждый алгоритм запускался пять раз
Во время эксперимента измерялось время поиска количество посещённых клеток и длина найденного пути
Результаты сохранялись в файл results.csv
Результаты
----------
simple.txt
Алгоритм Время мс Посещено Путь
BFS 0.01464 11 6
DFS 0.01018 9 8
A* 0.01774 9 6
Все алгоритмы работают быстро
BFS и A* нашли более короткий путь чем DFS
dead.txt
Алгоритм Время мс Посещено Путь
BFS 0.36430 307 35
DFS 0.23494 279 151
A* 0.38374 235 35
DFS работал немного быстрее но нашёл более длинный путь
BFS и A* нашли короткий путь
large.txt
Алгоритм Время мс Посещено Путь
BFS 23.89446 6812 2329
DFS 84.77876 6796 4537
A* 28.69542 6791 2329
На большом лабиринте DFS показал худшее время и самый длинный путь
BFS и A* нашли одинаковый путь
empty.txt
Алгоритм Время мс Посещено Путь
BFS 1.277
04 1176 48
DFS 7.60228 2304 1176
A* 0.10094 48 48
В лабиринте без стен лучше всего показал себя A*
Он посетил меньше всего клеток и работал быстрее
noexit.txt
Алгоритм Время мс Посещено Путь
BFS 0.00370 1 0
DFS 0.00320 1 0
A* 0.00412 1 0
В этом лабиринте выход недостижим поэтому все алгоритмы быстро закончили поиск
Графики
-------
Для сравнения времени работы были построены графики для каждого лабиринта
Графики находятся в папке docs/data
simple_time.png dead_time.png large_time.png empty_time.png и noexit_time.png
Вывод
-----
В работе была создана программа для поиска выхода из лабиринта
Были реализованы BFS DFS и A* а также использованы паттерны Builder Strategy и Observer
По результатам эксперимента BFS хорошо подходит для поиска кратчайшего пути DFS может найти путь быстрее но он не всегда получается коротким A* хорошо показывает себя на больших и открытых лабиринтах

View File

@ -0,0 +1,93 @@
import csv
import os
from maze_solver import (
TextFileMazeBuilder,
MazeSolver,
BFSStrategy,
DFSStrategy,
AStarStrategy
)
REPEATS = 5
MAZES = [
"simple.txt",
"dead.txt",
"large.txt",
"empty.txt",
"noexit.txt"
]
STRATEGIES = [
("BFS", BFSStrategy()),
("DFS", DFSStrategy()),
("A*", AStarStrategy())
]
def average(values):
return sum(values) / len(values)
def run():
builder = TextFileMazeBuilder()
results = []
for maze_name in MAZES:
filename = os.path.join("lab2", "mazes", maze_name)
print("Maze:", maze_name)
for strategy_name, strategy in STRATEGIES:
times = []
visited = []
path_lengths = []
for _ in range(REPEATS):
maze = builder.build_from_file(filename)
solver = MazeSolver(maze, strategy)
stats = solver.solve()
times.append(stats.time_ms)
visited.append(stats.visited_count)
path_lengths.append(stats.path_length)
results.append([
maze_name,
strategy_name,
average(times),
average(visited),
average(path_lengths)
])
print(
strategy_name,
"time =", average(times),
"visited =", average(visited),
"path =", average(path_lengths)
)
os.makedirs("lab2/docs/data", exist_ok=True)
with open(
"lab2/docs/data/results.csv",
"w",
newline="",
encoding="utf-8"
) as file:
writer = csv.writer(file)
writer.writerow([
"maze",
"strategy",
"time_ms",
"visited_cells",
"path_length"
])
writer.writerows(results)
print("Results saved to lab2/docs/data/results.csv")
if __name__ == "__main__":
run()

View File

@ -0,0 +1,23 @@
import csv
import matplotlib.pyplot as plt
with open("lab2/docs/data/results.csv", encoding="utf-8") as file:
rows = list(csv.DictReader(file))
mazes = ["simple.txt", "dead.txt", "large.txt", "empty.txt", "noexit.txt"]
strategies = ["BFS", "DFS", "A*"]
for maze in mazes:
values = []
for strategy in strategies:
for row in rows:
if row["maze"] == maze and row["strategy"] == strategy:
values.append(float(row["time_ms"]))
plt.bar(strategies, values)
plt.title("Время поиска: " + maze)
plt.xlabel("Стратегия")
plt.ylabel("Время, мс")
plt.savefig("lab2/docs/data/" + maze.replace(".txt", "_time.png"))
plt.close()

View File

@ -0,0 +1,26 @@
lines = []
for y in range(100):
row = [" "] * 100
if y == 0 or y == 99:
row = ["#"] * 100
else:
row[0] = "#"
row[99] = "#"
lines.append(row)
lines[1][1] = "S"
lines[98][98] = "E"
for x in range(4, 96, 4):
gap = 1 if (x // 4) % 2 == 0 else 98
for y in range(1, 99):
if y != gap:
lines[y][x] = "#"
with open("lab2/mazes/large.txt", "w", encoding="utf-8") as file:
for row in lines:
file.write("".join(row) + "\n")

View File

@ -0,0 +1,284 @@
from abc import ABC, abstractmethod
from collections import deque
import heapq
import time
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
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 = []
directions = [
(0, -1),
(0, 1),
(-1, 0),
(1, 0)
]
for dx, dy in directions:
neighbor = self.get_cell(
cell.x + dx,
cell.y + dy
)
if neighbor and neighbor.is_passable():
neighbors.append(neighbor)
return neighbors
class MazeBuilder(ABC):
@abstractmethod
def build_from_file(self, filename):
pass
class TextFileMazeBuilder(MazeBuilder):
def build_from_file(self, filename):
with open(filename, "r", encoding="utf-8") as file:
lines = [line.rstrip("\n") for line in file]
if not lines:
raise ValueError("Файл лабиринта пустой")
width = len(lines[0])
for line in lines:
if len(line) != width:
raise ValueError("Строки лабиринта имеют разную длину")
maze = Maze(width, len(lines))
for y, line in enumerate(lines):
for x, symbol in enumerate(line):
cell = maze.get_cell(x, y)
if symbol == "#":
cell.is_wall = True
elif symbol == "S":
if maze.start is not None:
raise ValueError("В лабиринте несколько стартов")
maze.start = cell
cell.is_start = True
elif symbol == "E":
if maze.exit is not None:
raise ValueError("В лабиринте несколько выходов")
maze.exit = cell
cell.is_exit = True
elif symbol == " ":
pass
else:
raise ValueError("Неизвестный символ в лабиринте")
if maze.start is None:
raise ValueError("В лабиринте нет старта")
if maze.exit is None:
raise ValueError("В лабиринте нет выхода")
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 start is None or exit is None:
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 start is None or exit is None:
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 start is None or exit is None:
return [], 0
heap = []
counter = 0
heapq.heappush(
heap,
(self.heuristic(start, exit), counter, start, [start])
)
g_score = {start: 0}
visited = set()
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):
new_cost = g_score[current] + 1
if neighbor not in g_score or new_cost < g_score[neighbor]:
g_score[neighbor] = new_cost
counter += 1
priority = new_cost + self.heuristic(
neighbor,
exit
)
heapq.heappush(
heap,
(
priority,
counter,
neighbor,
path + [neighbor]
)
)
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 update(self, event, data=None):
if event == "search_started":
print("Поиск начат")
elif event == "search_finished":
print(f"Поиск завершен за {data:.3f} мс")
elif event == "path_found":
print(f"Длина пути: {len(data)}")

View File

@ -0,0 +1,20 @@
####################
#S #
# #
# #
# #
# #
# ######### #
# # #
# # #
# # #
# # #
# # #
# # #
# # #
# # #
# #
# #
# #
# E#
####################

View File

@ -0,0 +1,50 @@
##################################################
#S #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# #
#E #
##################################################

View File

@ -0,0 +1,100 @@
####################################################################################################
#S # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # E#
####################################################################################################

View File

@ -0,0 +1,10 @@
##########
#S########
##########
##########
##########
##########
##########
##########
########E#
##########

View File

@ -0,0 +1,5 @@
#######
#S #
# ### #
# E #
#######

0
LebedevES/425.txt Normal file
View File

View File

@ -0,0 +1,292 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import time
import random
import csv
import sys
sys.setrecursionlimit(30000)
def ll_create_node(name, phone):
return {'name': name, 'phone': phone, 'next': None}
def ll_insert(head, name, phone):
if head is None:
return ll_create_node(name, phone)
if head['name'] == name:
head['phone'] = phone
return head
current = head
while current['next'] is not None:
if current['next']['name'] == name:
current['next']['phone'] = phone
return head
current = current['next']
current['next'] = ll_create_node(name, phone)
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):
return sum(ord(c) for c in name) % table_size
def ht_create_table(size=2000):
return [None] * size
def ht_insert(table, name, phone):
index = hash_function(name, len(table))
table[index] = ll_insert(table[index], name, phone)
def ht_find(table, name):
index = hash_function(name, len(table))
return ll_find(table[index], name)
def ht_delete(table, name):
index = hash_function(name, len(table))
table[index] = ll_delete(table[index], name)
def ht_list_all(table):
all_records = []
for bucket in table:
if bucket is not None:
current = bucket
while current is not None:
all_records.append((current['name'], current['phone']))
current = current['next']
all_records.sort(key=lambda x: x[0])
return all_records
def bst_create_node(name, phone):
return {'name': name, 'phone': phone, 'left': None, 'right': None}
def bst_insert(root, name, phone):
if root is None:
return bst_create_node(name, phone)
current = root
while True:
if name < current['name']:
if current['left'] is None:
current['left'] = bst_create_node(name, phone)
break
else:
current = current['left']
elif name > current['name']:
if current['right'] is None:
current['right'] = bst_create_node(name, phone)
break
else:
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']:
current = current['left']
elif name > current['name']:
current = current['right']
else:
return current['phone']
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 or current['right'] is None:
if current['left'] is not None:
child = current['left']
else:
child = current['right']
if parent is None:
return child
if parent['left'] == current:
parent['left'] = child
else:
parent['right'] = child
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
def bst_list_all(root):
records = []
stack = []
current = root
while stack or current is not None:
while current is not None:
stack.append(current)
current = current['left']
current = stack.pop()
records.append((current['name'], current['phone']))
current = current['right']
return records
def generate_data(n=10000):
records = [(f"User_{i:05d}", f"+7-999-{i:06d}") for i in range(n)]
records_shuffled = records.copy()
random.shuffle(records_shuffled)
records_sorted = sorted(records, key=lambda x: x[0])
return records_shuffled, records_sorted
def run_experiment(structure_name, insert_func, find_func, delete_func,
list_all_func, init_func, records, n_find=100):
data = init_func()
names = [r[0] for r in records]
start = time.perf_counter()
for name, phone in records:
if structure_name == "HashTable":
insert_func(data, name, phone)
else:
data = insert_func(data, name, phone)
insert_time = time.perf_counter() - start
find_names = random.sample(names, min(n_find, len(names)))
missing_names = [f"None_{i}" for i in range(10)]
all_find_names = find_names + missing_names
start = time.perf_counter()
for name in all_find_names:
if structure_name == "HashTable":
find_func(data, name)
else:
find_func(data, name)
find_time = time.perf_counter() - start
delete_names = random.sample(names, min(50, len(names)))
start = time.perf_counter()
for name in delete_names:
if structure_name == "HashTable":
delete_func(data, name)
else:
data = delete_func(data, name)
delete_time = time.perf_counter() - start
return insert_time, find_time, delete_time
def main():
print("Generating test data...")
records_shuffled, records_sorted = generate_data(10000)
results = []
structures = [
("LinkedList", ll_insert, ll_find, ll_delete, ll_list_all, lambda: None),
("HashTable", ht_insert, ht_find, ht_delete, ht_list_all, lambda: ht_create_table(2000)),
("BST", bst_insert, bst_find, bst_delete, bst_list_all, lambda: None)
]
for mode_name, records in [("random", records_shuffled), ("sorted", records_sorted)]:
print(f"\nMode: {mode_name}")
for struct_name, insert_f, find_f, delete_f, list_f, init_f in structures:
print(f" Testing {struct_name}...")
times = []
for run in range(5):
insert_t, find_t, delete_t = run_experiment(
struct_name, insert_f, find_f, delete_f, list_f, init_f, records
)
times.append((insert_t, find_t, delete_t))
print(f" Run {run+1}: insert={insert_t:.4f}s, find={find_t:.4f}s, delete={delete_t:.4f}s")
avg_insert = sum(t[0] for t in times) / 5
avg_find = sum(t[1] for t in times) / 5
avg_delete = sum(t[2] for t in times) / 5
results.append([struct_name, mode_name, "insert", avg_insert])
results.append([struct_name, mode_name, "find", avg_find])
results.append([struct_name, mode_name, "delete", avg_delete])
with open("results.csv", "w", newline="", encoding="utf-8") as f:
writer = csv.writer(f)
writer.writerow(["Structure", "Mode", "Operation", "Time_seconds"])
writer.writerows(results)
print("\n" + "="*60)
print("RESULTS (average over 5 runs):")
print("="*60)
for row in results:
print(f"{row[0]:12} | {row[1]:8} | {row[2]:8} | {row[3]:.6f} sec")
print("\nResults saved to results.csv")
if __name__ == "__main__":
main()

View File

@ -0,0 +1,122 @@
Методы Программирования
Структуры данных,
анализ 1 задания
Бобров К. Н.
425 группа
Содержание
Как порядок входных данных влияет на скорость вставки в 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). Идеальный вариант, когда данные часто изменяются и при этом требуется обход элементов в отсортированном порядке.

View File

@ -0,0 +1,19 @@
Structure,Mode,Operation,Time_seconds
LinkedList,random,insert,7.967956480104476
LinkedList,random,find,0.05891917999833822
LinkedList,random,delete,0.03816298004239797
HashTable,random,insert,0.39825033992528913
HashTable,random,find,0.002917400002479553
HashTable,random,delete,0.0021501399576663973
BST,random,insert,0.02822491992264986
BST,random,find,0.00023473985493183136
BST,random,delete,0.00016456004232168198
LinkedList,sorted,insert,8.014810599852353
LinkedList,sorted,find,0.058480959851294756
LinkedList,sorted,delete,0.04817821998149156
HashTable,sorted,insert,0.3703480200842023
HashTable,sorted,find,0.002751259971410036
HashTable,sorted,delete,0.0018340200185775757
BST,sorted,insert,7.301413399912417
BST,sorted,find,0.06847236007452011
BST,sorted,delete,0.03443789994344115
1 Structure Mode Operation Time_seconds
2 LinkedList random insert 7.967956480104476
3 LinkedList random find 0.05891917999833822
4 LinkedList random delete 0.03816298004239797
5 HashTable random insert 0.39825033992528913
6 HashTable random find 0.002917400002479553
7 HashTable random delete 0.0021501399576663973
8 BST random insert 0.02822491992264986
9 BST random find 0.00023473985493183136
10 BST random delete 0.00016456004232168198
11 LinkedList sorted insert 8.014810599852353
12 LinkedList sorted find 0.058480959851294756
13 LinkedList sorted delete 0.04817821998149156
14 HashTable sorted insert 0.3703480200842023
15 HashTable sorted find 0.002751259971410036
16 HashTable sorted delete 0.0018340200185775757
17 BST sorted insert 7.301413399912417
18 BST sorted find 0.06847236007452011
19 BST sorted delete 0.03443789994344115

View File

@ -0,0 +1,589 @@
import time
import heapq
from collections import deque
from typing import List, Optional, Dict, Tuple
from abc import ABC, abstractmethod
import csv
import random
class Cell:
def __init__(self, x: int, y: int):
self.x = x
self.y = y
self.is_wall = False
self.is_start = False
self.is_exit = False
def is_passable(self) -> bool:
return not self.is_wall
class Maze:
def __init__(self, width: int, height: int):
self.width = width
self.height = height
self.cells = [[Cell(x, y) for y in range(height)] for x in range(width)]
self.start: Optional[Cell] = None
self.exit: Optional[Cell] = None
def get_cell(self, x: int, y: int) -> Optional[Cell]:
if 0 <= x < self.width and 0 <= y < self.height:
return self.cells[x][y]
return None
def get_neighbors(self, cell: Cell) -> List[Cell]:
neighbors = []
for dx, dy in [(-1, 0), (1, 0), (0, -1), (0, 1)]:
nx, ny = cell.x + dx, cell.y + dy
nb = self.get_cell(nx, ny)
if nb and nb.is_passable():
neighbors.append(nb)
return neighbors
class MazeBuilder(ABC):
@abstractmethod
def build_from_file(self, filename: str) -> Maze:
pass
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)")
return maze
class PathFindingStrategy(ABC):
@abstractmethod
def find_path(self, maze: Maze, start: Cell, exit: Cell) -> List[Cell]:
pass
@abstractmethod
def get_name(self) -> str:
pass
class BFSStrategy(PathFindingStrategy):
def find_path(self, maze: Maze, start: Cell, exit: Cell) -> List[Cell]:
queue = deque([start])
came_from = {start: None}
while queue:
current = queue.popleft()
if current == exit:
break
for nb in maze.get_neighbors(current):
if nb not in came_from:
came_from[nb] = current
queue.append(nb)
if exit not in came_from:
return []
path = []
cur = exit
while cur:
path.append(cur)
cur = came_from[cur]
path.reverse()
return path
def get_name(self) -> str:
return "BFS"
class DFSStrategy(PathFindingStrategy):
def find_path(self, maze: Maze, start: Cell, exit: Cell) -> List[Cell]:
stack = [start]
came_from = {start: None}
while stack:
current = stack.pop()
if current == exit:
break
for nb in maze.get_neighbors(current):
if nb not in came_from:
came_from[nb] = current
stack.append(nb)
if exit not in came_from:
return []
path = []
cur = exit
while cur:
path.append(cur)
cur = came_from[cur]
path.reverse()
return path
def get_name(self) -> str:
return "DFS"
class AStarStrategy(PathFindingStrategy):
def _heuristic(self, a: Cell, b: Cell) -> int:
return abs(a.x - b.x) + abs(a.y - b.y)
def find_path(self, maze: Maze, start: Cell, exit: Cell) -> List[Cell]:
open_set = []
heapq.heappush(open_set, (0, id(start), start))
came_from = {}
g_score = {start: 0}
f_score = {start: self._heuristic(start, exit)}
while open_set:
_, _, current = heapq.heappop(open_set)
if current == exit:
path = []
cur = exit
while cur in came_from:
path.append(cur)
cur = came_from[cur]
path.append(start)
path.reverse()
return path
for neighbor in maze.get_neighbors(current):
tentative_g = g_score[current] + 1
if tentative_g < g_score.get(neighbor, float('inf')):
came_from[neighbor] = current
g_score[neighbor] = tentative_g
f_score[neighbor] = tentative_g + self._heuristic(neighbor, exit)
heapq.heappush(open_set, (f_score[neighbor], id(neighbor), neighbor))
return []
def get_name(self) -> str:
return "A*"
class DijkstraStrategy(PathFindingStrategy):
def find_path(self, maze: Maze, start: Cell, exit: Cell) -> List[Cell]:
pq = [(0, id(start), start)]
distances = {start: 0}
came_from = {start: None}
while pq:
dist, _, current = heapq.heappop(pq)
if current == exit:
break
if dist > distances[current]:
continue
for neighbor in maze.get_neighbors(current):
new_dist = dist + 1
if new_dist < distances.get(neighbor, float('inf')):
distances[neighbor] = new_dist
came_from[neighbor] = current
heapq.heappush(pq, (new_dist, id(neighbor), neighbor))
if exit not in came_from:
return []
path = []
cur = exit
while cur:
path.append(cur)
cur = came_from[cur]
path.reverse()
return path
def get_name(self) -> str:
return "Dijkstra"
class SearchStats:
def __init__(self, time_ms: float, visited_cells: int, path_length: int):
self.time_ms = time_ms
self.visited_cells = visited_cells
self.path_length = path_length
def __str__(self):
return f"Time: {self.time_ms:.2f}ms, Visited: {self.visited_cells}, Path: {self.path_length}"
class MazeSolver:
def __init__(self, maze: Maze, strategy: PathFindingStrategy):
self.maze = maze
self.strategy = strategy
def set_strategy(self, strategy: PathFindingStrategy):
self.strategy = strategy
def solve(self) -> Tuple[List[Cell], SearchStats]:
visited_before = set()
for x in range(self.maze.width):
for y in range(self.maze.height):
cell = self.maze.get_cell(x, y)
if cell and cell.is_passable():
visited_before.add(cell)
start_time = time.perf_counter()
path = self.strategy.find_path(self.maze, self.maze.start, self.maze.exit)
end_time = time.perf_counter()
visited_after = set()
for x in range(self.maze.width):
for y in range(self.maze.height):
cell = self.maze.get_cell(x, y)
if cell and cell.is_passable():
visited_after.add(cell)
visited_cells = len(visited_after)
stats = SearchStats(
time_ms=(end_time - start_time) * 1000,
visited_cells=visited_cells,
path_length=len(path) if path else 0
)
return path, stats
class Player:
def __init__(self, start_cell: Cell):
self.current_cell = start_cell
self.previous_cell = None
def move_to(self, cell: Cell) -> bool:
if cell.is_passable():
self.previous_cell = self.current_cell
self.current_cell = cell
return True
return False
def undo(self):
if self.previous_cell:
self.current_cell, self.previous_cell = self.previous_cell, None
return True
return False
class Command(ABC):
@abstractmethod
def execute(self) -> bool:
pass
@abstractmethod
def undo(self):
pass
class MoveCommand(Command):
def __init__(self, player: Player, maze: Maze, direction: str):
self.player = player
self.maze = maze
self.direction = direction
self.executed = False
def execute(self) -> bool:
dx, dy = 0, 0
if self.direction == 'W' or self.direction == 'w':
dy = -1
elif self.direction == 'S' or self.direction == 's':
dy = 1
elif self.direction == 'A' or self.direction == 'a':
dx = -1
elif self.direction == 'D' or self.direction == 'd':
dx = 1
new_x = self.player.current_cell.x + dx
new_y = self.player.current_cell.y + dy
new_cell = self.maze.get_cell(new_x, new_y)
if new_cell and new_cell.is_passable():
self.executed = self.player.move_to(new_cell)
return self.executed
return False
def undo(self):
if self.executed:
self.player.undo()
self.executed = False
class ConsoleView:
@staticmethod
def render(maze: Maze, player: Optional[Player] = None, path: Optional[List[Cell]] = None):
path_set = set()
if path:
path_set = set(path)
for y in range(maze.height):
line = ""
for x in range(maze.width):
cell = maze.get_cell(x, y)
if not cell:
line += " "
elif player and player.current_cell == cell:
line += "P"
elif cell.is_start:
line += "S"
elif cell.is_exit:
line += "E"
elif cell.is_wall:
line += "#"
elif path and cell in path_set:
line += "."
else:
line += " "
print(line)
print()
@staticmethod
def show_stats(stats: SearchStats, algo_name: str):
print(f"=== {algo_name} Results ===")
print(stats)
print()
def generate_test_maze(width: int, height: int, complexity: float = 0.3) -> Maze:
maze = Maze(width, height)
for x in range(width):
for y in range(height):
if random.random() < complexity:
maze.cells[x][y].is_wall = True
maze.start = maze.get_cell(0, 0)
if maze.start:
maze.start.is_start = True
maze.start.is_wall = False
maze.exit = maze.get_cell(width - 1, height - 1)
if maze.exit:
maze.exit.is_exit = True
maze.exit.is_wall = False
return maze
def generate_empty_maze(width: int, height: int) -> Maze:
maze = Maze(width, height)
for x in range(width):
for y in range(height):
maze.cells[x][y].is_wall = False
maze.start = maze.get_cell(0, 0)
if maze.start:
maze.start.is_start = True
maze.exit = maze.get_cell(width - 1, height - 1)
if maze.exit:
maze.exit.is_exit = True
return maze
def generate_no_exit_maze(width: int, height: int) -> Maze:
maze = Maze(width, height)
for x in range(width):
for y in range(height):
maze.cells[x][y].is_wall = False
for x in range(width):
maze.cells[x][height // 2].is_wall = True
maze.start = maze.get_cell(0, 0)
if maze.start:
maze.start.is_start = True
maze.exit = maze.get_cell(width - 1, height - 1)
if maze.exit:
maze.exit.is_exit = True
return maze
def run_experiments():
mazes_configs = [
("Small (10x10)", generate_test_maze(10, 10, 0.2)),
("Medium (50x50)", generate_test_maze(50, 50, 0.25)),
("Large (100x100)", generate_test_maze(100, 100, 0.3)),
("Empty (30x30)", generate_empty_maze(30, 30)),
("No Exit (20x20)", generate_no_exit_maze(20, 20))
]
strategies = [BFSStrategy(), DFSStrategy(), AStarStrategy(), DijkstraStrategy()]
results = []
for maze_name, maze in mazes_configs:
print(f"\n=== Testing: {maze_name} ===")
for strategy in strategies:
times = []
visited = []
path_lengths = []
solver = MazeSolver(maze, strategy)
for run in range(5):
maze_copy = Maze(maze.width, maze.height)
for x in range(maze.width):
for y in range(maze.height):
orig = maze.get_cell(x, y)
copy = maze_copy.get_cell(x, y)
if orig:
copy.is_wall = orig.is_wall
copy.is_start = orig.is_start
copy.is_exit = orig.is_exit
maze_copy.start = maze_copy.get_cell(maze.start.x, maze.start.y) if maze.start else None
maze_copy.exit = maze_copy.get_cell(maze.exit.x, maze.exit.y) if maze.exit else None
solver.maze = maze_copy
solver.set_strategy(strategy)
path, stats = solver.solve()
times.append(stats.time_ms)
visited.append(stats.visited_cells)
path_lengths.append(stats.path_length)
avg_time = sum(times) / len(times)
avg_visited = sum(visited) / len(visited)
avg_path = sum(path_lengths) / len(path_lengths)
results.append({
'maze': maze_name,
'algorithm': strategy.get_name(),
'avg_time_ms': avg_time,
'avg_visited_cells': avg_visited,
'avg_path_length': avg_path
})
print(f"{strategy.get_name()}: {avg_time:.2f}ms, {avg_visited:.0f} cells, path={avg_path:.0f}")
with open('experiment_results.csv', 'w', newline='', encoding='utf-8') as f:
writer = csv.DictWriter(f, fieldnames=['maze', 'algorithm', 'avg_time_ms', 'avg_visited_cells', 'avg_path_length'])
writer.writeheader()
writer.writerows(results)
print("\nResults saved to experiment_results.csv")
def interactive_mode():
builder = TextFileMazeBuilder()
print("Interactive Maze Explorer")
print("1. Load maze from file")
print("2. Generate random maze")
choice = input("Choose (1/2): ")
if choice == '1':
filename = input("Enter filename: ")
try:
maze = builder.build_from_file(filename)
except Exception as e:
print(f"Error loading maze: {e}")
return
else:
w = int(input("Width: "))
h = int(input("Height: "))
maze = generate_test_maze(w, h, 0.3)
player = Player(maze.start)
strategies = {
'1': BFSStrategy(),
'2': DFSStrategy(),
'3': AStarStrategy(),
'4': DijkstraStrategy()
}
print("\nSelect algorithm for solving:")
print("1. BFS (shortest path)")
print("2. DFS (fast, not optimal)")
print("3. A* (heuristic)")
print("4. Dijkstra")
algo_choice = input("Choose: ")
solver = MazeSolver(maze, strategies.get(algo_choice, BFSStrategy()))
path, stats = solver.solve()
view = ConsoleView()
if path:
print(f"\nPath found! Length: {len(path)}")
view.show_stats(stats, solver.strategy.get_name())
else:
print("\nNo path found!")
while True:
view.render(maze, player, path if path else None)
if player.current_cell == maze.exit:
print("Congratulations! You reached the exit!")
break
cmd = input("Move (W/A/S/D) | U=undo | Q=quit | S=solve: ").upper()
if cmd == 'Q':
break
elif cmd == 'U':
player.undo()
print("Undo last move")
elif cmd == 'S' and path:
for cell in path:
if cell == player.current_cell:
continue
player.move_to(cell)
view.render(maze, player, path)
input("Press Enter to continue...")
if player.current_cell == maze.exit:
print("You reached the exit!")
break
elif cmd in ['W', 'A', 'S', 'D']:
move_cmd = MoveCommand(player, maze, cmd)
if move_cmd.execute():
print("Moved")
else:
print("Can't move there!")
def main():
print("Maze Solver with Design Patterns")
print("1. Run experiments")
print("2. Interactive mode")
choice = input("Choose (1/2): ")
if choice == '1':
run_experiments()
else:
interactive_mode()
if __name__ == "__main__":
main()

View File

@ -0,0 +1,21 @@
maze,algorithm,avg_time_ms,avg_visited_cells,avg_path_length
Small (10x10),BFS,0.006740167737007141,80.0,0.0
Small (10x10),DFS,0.00408003106713295,80.0,0.0
Small (10x10),A*,0.005039852112531662,80.0,0.0
Small (10x10),Dijkstra,0.0031800009310245514,80.0,0.0
Medium (50x50),BFS,3.44578018411994,1890.0,99.0
Medium (50x50),DFS,1.3188599608838558,1890.0,341.0
Medium (50x50),A*,2.061920054256916,1890.0,99.0
Medium (50x50),Dijkstra,4.679400008171797,1890.0,99.0
Large (100x100),BFS,0.025319866836071014,6998.0,0.0
Large (100x100),DFS,0.019940081983804703,6998.0,0.0
Large (100x100),A*,0.035060010850429535,6998.0,0.0
Large (100x100),Dijkstra,0.02901991829276085,6998.0,0.0
Empty (30x30),BFS,1.2404202483594418,900.0,59.0
Empty (30x30),DFS,0.8887200616300106,900.0,465.0
Empty (30x30),A*,0.9468601085245609,900.0,59.0
Empty (30x30),Dijkstra,2.678940072655678,900.0,59.0
No Exit (20x20),BFS,0.27012014761567116,380.0,0.0
No Exit (20x20),DFS,0.3163599409162998,380.0,0.0
No Exit (20x20),A*,0.5885399878025055,380.0,0.0
No Exit (20x20),Dijkstra,0.5776201374828815,380.0,0.0
1 maze algorithm avg_time_ms avg_visited_cells avg_path_length
2 Small (10x10) BFS 0.006740167737007141 80.0 0.0
3 Small (10x10) DFS 0.00408003106713295 80.0 0.0
4 Small (10x10) A* 0.005039852112531662 80.0 0.0
5 Small (10x10) Dijkstra 0.0031800009310245514 80.0 0.0
6 Medium (50x50) BFS 3.44578018411994 1890.0 99.0
7 Medium (50x50) DFS 1.3188599608838558 1890.0 341.0
8 Medium (50x50) A* 2.061920054256916 1890.0 99.0
9 Medium (50x50) Dijkstra 4.679400008171797 1890.0 99.0
10 Large (100x100) BFS 0.025319866836071014 6998.0 0.0
11 Large (100x100) DFS 0.019940081983804703 6998.0 0.0
12 Large (100x100) A* 0.035060010850429535 6998.0 0.0
13 Large (100x100) Dijkstra 0.02901991829276085 6998.0 0.0
14 Empty (30x30) BFS 1.2404202483594418 900.0 59.0
15 Empty (30x30) DFS 0.8887200616300106 900.0 465.0
16 Empty (30x30) A* 0.9468601085245609 900.0 59.0
17 Empty (30x30) Dijkstra 2.678940072655678 900.0 59.0
18 No Exit (20x20) BFS 0.27012014761567116 380.0 0.0
19 No Exit (20x20) DFS 0.3163599409162998 380.0 0.0
20 No Exit (20x20) A* 0.5885399878025055 380.0 0.0
21 No Exit (20x20) Dijkstra 0.5776201374828815 380.0 0.0

View File

@ -0,0 +1,196 @@
Методы программирования
Поиск выхода из лабиринта.
Анализ 2 задания
Бобров К. Н.
425 группа
Содержание
Описание задачи и выбранных паттернов 2
Листинги ключевых классов 4
Результаты экспериментов 6
Анализ эффективности алгоритмов и применимости паттернов 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)")
return maze
Strategy (пример BFS):
class BFSStrategy(PathFindingStrategy):
def find_path(self, maze: Maze, start: Cell, exit: Cell) -> List[Cell]:
queue = deque([start])
came_from = {start: None}
while queue:
current = queue.popleft()
if current == exit:
break
for nb in maze.get_neighbors(current):
if nb not in came_from:
came_from[nb] = current
queue.append(nb)
if exit not in came_from:
return []
path = []
cur = exit
while cur:
path.append(cur)
cur = came_from[cur]
path.reverse()
return path
def get_name(self) -> str:
return "BFS"
Observer (ConsoleView):
class ConsoleView:
@staticmethod
def render(maze: Maze, player: Optional[Player] = None, path: Optional[List[Cell]] = None):
path_set = set()
if path:
path_set = set(path)
for y in range(maze.height):
line = ""
for x in range(maze.width):
cell = maze.get_cell(x, y)
if not cell:
line += " "
elif player and player.current_cell == cell:
line += "P"
elif cell.is_start:
line += "S"
elif cell.is_exit:
line += "E"
elif cell.is_wall:
line += "#"
elif path and cell in path_set:
line += "."
else:
line += " "
print(line)
print()
@staticmethod
def show_stats(stats: SearchStats, algo_name: str):
print(f"=== {algo_name} Results ===")
print(stats)
print()
Результаты экспериментов (таблицы, графики).
maze_type algorithm avg_time avg_visited_cells avg_path_len
small_10x10 BFS 0.08572000006097369 79.0 19.0
small_10x10 DFS 0.039739999920129776 79.0 31.0
small_10x10_ A* 0.13467999997374136 79.0 19.0
small_10x10 Dijkstra 0.11474000057205558 79.0 19.0
medium_50x50 BFS 1.8074600004183594 1874.0 99.0
medium_50x50 DFS 0.5937599995377241 1874.0 429.0
medium_50x50 A* 1.6300600003887666 1874.0 99.0
medium_50x50 Dijkstra 3.1870400001935195 1874.0 99.0
large_100x100 BFS 0.014439999722526409 7033.0 0.0
large_100x100 DFS 0.014839999857940711 7033.0 0.0
large_100x100 A* 0.02542000001994893 7033.0 0.0
large_100x100 Dijkstra 0.02548000011302065 7033.0 0.0
empty_30x30 BFS 0.784620000194991 900.0 59.0
empty_30x30 DFS 0.5252399994787993 900.0 465.
empty_30x30 A* 1.150900000357069 900.0 59.0
empty_30x30 Dijkstra 1.564640000287909 900.0 59.0
no_exit_20x20 BFS 0.2002399993216386 380. 0.0
no_exit_20x20 DFS 0.2512400002160575 380.0 0.0
no_exit_20x20 A* 0.5590400000073714 380.0 0.
no_exit_20x20 Dijkstra 0.35640000060084276 380.0 0.0
Графики построены кодом из файла RESULT22.
Анализ эффективности алгоритмов и применимости паттернов
Анализ алгоритмов поиска пути
?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 или веб).
Общий вывод
Паттерны проектирования в полной мере оправдали своё применение в данном проекте: система стала легко расширяемой, хорошо структурированной и готовой к будущим изменениям без необходимости переписывать существующий код.

0
PaulVA/429 Normal file
View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

View File

@ -0,0 +1,19 @@
structure,order,operation,run1,run2,run3,run4,run5,average
LinkedList,random,insert,3.000600399999712,3.022712899999533,2.9421689999999217,2.9075659000000087,3.0319512999994913,2.980999899999733
LinkedList,random,find,0.031094500000108383,0.02800200000001496,0.034349299999121286,0.029372199999670556,0.03242119999958959,0.031047839999700955
LinkedList,random,delete,0.017322699999567703,0.0368361000000732,0.04029200000059063,0.03775789999963308,0.03554420000000391,0.033550579999973705
HashTable,random,insert,0.011551699999472476,0.012756400000398571,0.011765299999751733,0.011679000000185624,0.011983400000644906,0.011947160000090662
HashTable,random,find,0.00012409999999363208,0.00011009999980160501,0.0001415999995515449,0.00010400000064691994,0.00010089999977935804,0.000116139999954612
HashTable,random,delete,6.38999999864609e-05,6.779999966965988e-05,6.0600000324484427e-05,6.070000017643906e-05,6.0600000324484427e-05,6.272000009630574e-05
BST,random,insert,0.014788199999202334,0.014159299999846553,0.013975800000480376,0.014118900000539725,0.013331299999663315,0.01407469999994646
BST,random,find,0.00013829999988956843,0.00011389999963284936,0.00011369999992894009,0.00011379999978089472,0.00011439999980211724,0.00011881999980687397
BST,random,delete,8.690000049682567e-05,6.450000000768341e-05,6.2199999774748e-05,6.209999992279336e-05,6.229999962670263e-05,6.759999996575061e-05
LinkedList,sorted,insert,2.4411346000006233,2.36463619999995,2.2797248999995645,2.2860746000005747,2.2526011999998445,2.3248343000001115
LinkedList,sorted,find,0.024703000000044995,0.02455259999987902,0.02468479999970441,0.02444869999999355,0.02606350000041857,0.02489052000000811
LinkedList,sorted,delete,0.012835599999561964,0.027673999999933585,0.027570299999752024,0.02708100000018021,0.02999909999925876,0.02503199999973731
HashTable,sorted,insert,0.011780100000578386,0.010850699999537028,0.010314100000869075,0.010621500000524975,0.011015500000212342,0.010916380000344362
HashTable,sorted,find,0.0001464000006308197,0.00017980000029638177,0.00016909999976633117,0.00012620000052265823,0.00023630000032426324,0.0001715600003080908
HashTable,sorted,delete,0.00016370000048482325,0.00018089999957737746,0.0001443999999537482,7.579999964946182e-05,6.469999971159268e-05,0.0001258999998754007
BST,sorted,insert,3.5400651999998445,3.5145174999997835,3.5583661999999094,3.5149656000003233,3.481246600000304,3.521832220000033
BST,sorted,find,0.03275260000009439,0.030442500000390282,0.02994349999971746,0.030269500000031258,0.030329999999594293,0.030747619999965538
BST,sorted,delete,0.012705400000413647,0.01333390000036161,0.013192000000344706,0.013699000000087835,0.013079800000014075,0.013202020000244374
1 structure order operation run1 run2 run3 run4 run5 average
2 LinkedList random insert 3.000600399999712 3.022712899999533 2.9421689999999217 2.9075659000000087 3.0319512999994913 2.980999899999733
3 LinkedList random find 0.031094500000108383 0.02800200000001496 0.034349299999121286 0.029372199999670556 0.03242119999958959 0.031047839999700955
4 LinkedList random delete 0.017322699999567703 0.0368361000000732 0.04029200000059063 0.03775789999963308 0.03554420000000391 0.033550579999973705
5 HashTable random insert 0.011551699999472476 0.012756400000398571 0.011765299999751733 0.011679000000185624 0.011983400000644906 0.011947160000090662
6 HashTable random find 0.00012409999999363208 0.00011009999980160501 0.0001415999995515449 0.00010400000064691994 0.00010089999977935804 0.000116139999954612
7 HashTable random delete 6.38999999864609e-05 6.779999966965988e-05 6.0600000324484427e-05 6.070000017643906e-05 6.0600000324484427e-05 6.272000009630574e-05
8 BST random insert 0.014788199999202334 0.014159299999846553 0.013975800000480376 0.014118900000539725 0.013331299999663315 0.01407469999994646
9 BST random find 0.00013829999988956843 0.00011389999963284936 0.00011369999992894009 0.00011379999978089472 0.00011439999980211724 0.00011881999980687397
10 BST random delete 8.690000049682567e-05 6.450000000768341e-05 6.2199999774748e-05 6.209999992279336e-05 6.229999962670263e-05 6.759999996575061e-05
11 LinkedList sorted insert 2.4411346000006233 2.36463619999995 2.2797248999995645 2.2860746000005747 2.2526011999998445 2.3248343000001115
12 LinkedList sorted find 0.024703000000044995 0.02455259999987902 0.02468479999970441 0.02444869999999355 0.02606350000041857 0.02489052000000811
13 LinkedList sorted delete 0.012835599999561964 0.027673999999933585 0.027570299999752024 0.02708100000018021 0.02999909999925876 0.02503199999973731
14 HashTable sorted insert 0.011780100000578386 0.010850699999537028 0.010314100000869075 0.010621500000524975 0.011015500000212342 0.010916380000344362
15 HashTable sorted find 0.0001464000006308197 0.00017980000029638177 0.00016909999976633117 0.00012620000052265823 0.00023630000032426324 0.0001715600003080908
16 HashTable sorted delete 0.00016370000048482325 0.00018089999957737746 0.0001443999999537482 7.579999964946182e-05 6.469999971159268e-05 0.0001258999998754007
17 BST sorted insert 3.5400651999998445 3.5145174999997835 3.5583661999999094 3.5149656000003233 3.481246600000304 3.521832220000033
18 BST sorted find 0.03275260000009439 0.030442500000390282 0.02994349999971746 0.030269500000031258 0.030329999999594293 0.030747619999965538
19 BST sorted delete 0.012705400000413647 0.01333390000036161 0.013192000000344706 0.013699000000087835 0.013079800000014075 0.013202020000244374

View File

@ -0,0 +1,34 @@
Лабораторная работа 1
Цель работы
Нужно было сделать три структуры данных и проверить как они работают на телефонном справочнике.
Ход работы
Сделал связный список хеш таблицу и двоичное дерево поиска. Для всех структур сделал добавление поиск удаление и вывод записей. Для проверки создал 10000 записей с именами User\_00000 и т.д. Потом проверил работу со случайным порядком и с отсортированным порядком. Каждый эксперимент повторял 5 раз.
Результаты
Результаты сохранились в results.csv. Также сделал графики для добавления поиска и удаления. По результатам видно что связный список медленно ищет записи потому что нужно идти по элементам. Хеш таблица работает примерно одинаково при разном порядке записей. У двоичного дерева порядок записей влияет намного сильнее. Если добавлять записи по порядку дерево становится похожим на обычный список и работает медленнее.
Вывод
В работе я сделал три структуры данных и проверил их работу. Самой удобной для телефонного справочника получилась хеш таблица. Связный список проще но поиск медленный. Двоичное дерево может работать быстро но сильно зависит от порядка добавления данных.

185
PaulVA/lab1/experiments.py Normal file
View File

@ -0,0 +1,185 @@
import random
import time
import csv
import os
from phonebook import *
N = 10000
REPEATS = 5
def generate_test_data():
records = [
(f"User_{i:05d}", f"+7900000{i:04d}")
for i in range(N)
]
records_shuffled = records.copy()
random.shuffle(records_shuffled)
records_sorted = records.copy()
return records_shuffled, records_sorted
def measure_experiment(insert_function, find_function, delete_function, records):
insert_times = []
find_times = []
delete_times = []
for _ in range(REPEATS):
structure = None
start = time.perf_counter()
for name, phone in records:
structure = insert_function(structure, name, phone)
insert_times.append(time.perf_counter() - start)
structure_for_find = structure
names = [name for name, phone in records]
search_names = random.sample(names, 100) + [
"NotFound_001",
"NotFound_002",
"NotFound_003",
"NotFound_004",
"NotFound_005",
"NotFound_006",
"NotFound_007",
"NotFound_008",
"NotFound_009",
"NotFound_010"
]
for _ in range(REPEATS):
start = time.perf_counter()
for name in search_names:
find_function(structure_for_find, name)
find_times.append(time.perf_counter() - start)
delete_names = random.sample(names, 50)
for _ in range(REPEATS):
structure = structure_for_find
start = time.perf_counter()
for name in delete_names:
structure = delete_function(structure, name)
delete_times.append(time.perf_counter() - start)
return insert_times, find_times, delete_times
def measure_hash(records):
insert_times = []
find_times = []
delete_times = []
names = [name for name, phone in records]
search_names = random.sample(names, 100) + [
f"NotFound_{i:03d}" for i in range(10)
]
delete_names = random.sample(names, 50)
for _ in range(REPEATS):
buckets = ht_create()
start = time.perf_counter()
for name, phone in records:
ht_insert(buckets, name, phone)
insert_times.append(time.perf_counter() - start)
structure_for_find = buckets
for _ in range(REPEATS):
start = time.perf_counter()
for name in search_names:
ht_find(structure_for_find, name)
find_times.append(time.perf_counter() - start)
for _ in range(REPEATS):
buckets = structure_for_find.copy()
start = time.perf_counter()
for name in delete_names:
ht_delete(buckets, name)
delete_times.append(time.perf_counter() - start)
return insert_times, find_times, delete_times
def average(values):
return sum(values) / len(values)
def run():
records_shuffled, records_sorted = generate_test_data()
results = []
for order_name, records in [
("random", records_shuffled),
("sorted", records_sorted)
]:
print("Order:", order_name)
ll = measure_experiment(
ll_insert,
ll_find,
ll_delete,
records
)
results.append(["LinkedList", order_name, "insert", *ll[0]])
results.append(["LinkedList", order_name, "find", *ll[1]])
results.append(["LinkedList", order_name, "delete", *ll[2]])
ht = measure_hash(records)
results.append(["HashTable", order_name, "insert", *ht[0]])
results.append(["HashTable", order_name, "find", *ht[1]])
results.append(["HashTable", order_name, "delete", *ht[2]])
bst = measure_experiment(
bst_insert,
bst_find,
bst_delete,
records
)
results.append(["BST", order_name, "insert", *bst[0]])
results.append(["BST", order_name, "find", *bst[1]])
results.append(["BST", order_name, "delete", *bst[2]])
os.makedirs("docs/data", exist_ok=True)
with open("docs/data/results.csv", "w", newline="", encoding="utf-8") as file:
writer = csv.writer(file)
writer.writerow([
"structure",
"order",
"operation",
"run1",
"run2",
"run3",
"run4",
"run5",
"average"
])
for row in results:
writer.writerow(row + [average(row[3:])])
print("Results saved to docs/data/results.csv")
if __name__ == "__main__":
run()

56
PaulVA/lab1/graphs.py Normal file
View File

@ -0,0 +1,56 @@
import csv
import os
import matplotlib.pyplot as plt
data = []
with open("docs/data/results.csv", "r", encoding="utf-8") as file:
reader = csv.DictReader(file)
for row in reader:
data.append(row)
def get_average(structure, order, operation):
for row in data:
if (
row["structure"] == structure
and row["order"] == order
and row["operation"] == operation
):
return float(row["average"])
return 0
structures = ["LinkedList", "HashTable", "BST"]
orders = ["random", "sorted"]
os.makedirs("docs/data", exist_ok=True)
for operation in ["insert", "find", "delete"]:
random_values = [
get_average(s, "random", operation)
for s in structures
]
sorted_values = [
get_average(s, "sorted", operation)
for s in structures
]
x = range(len(structures))
plt.figure()
plt.bar([i - 0.2 for i in x], random_values, width=0.4, label="random")
plt.bar([i + 0.2 for i in x], sorted_values, width=0.4, label="sorted")
plt.xticks(list(x), structures)
plt.ylabel("Time, seconds")
plt.title(operation.capitalize() + " time")
plt.yscale("log")
plt.legend()
plt.tight_layout()
plt.savefig("docs/data/graph_" + operation + ".png")
plt.close()
print("Graphs saved to docs/data/")

211
PaulVA/lab1/phonebook.py Normal file
View File

@ -0,0 +1,211 @@
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:
if current['name'] == name:
current['phone'] = phone
return head
current = current['next']
if current['name'] == name:
current['phone'] = phone
else:
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):
index = hash_function(name, len(buckets))
buckets[index] = ll_insert(buckets[index], name, phone)
return buckets
def ht_find(buckets, name):
index = hash_function(name, len(buckets))
return ll_find(buckets[index], name)
def ht_delete(buckets, name):
index = hash_function(name, len(buckets))
buckets[index] = ll_delete(buckets[index], 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']
if name < current['name']:
current = current['left']
else:
current = current['right']
return None
def bst_delete(root, name):
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:
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

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

View File

@ -0,0 +1,16 @@
maze,strategy,time_ms,visited_cells,path_length
simple.txt,BFS,0.01464000015403144,11.0,6.0
simple.txt,DFS,0.010180000390391797,9.0,8.0
simple.txt,A*,0.017740000475896522,9.0,6.0
dead.txt,BFS,0.3642999996372964,307.0,35.0
dead.txt,DFS,0.23493999906349927,279.0,151.0
dead.txt,A*,0.38374000068870373,235.0,35.0
large.txt,BFS,23.894459999428364,6812.0,2329.0
large.txt,DFS,84.77875999960816,6796.0,4537.0
large.txt,A*,28.69542000044021,6791.0,2329.0
empty.txt,BFS,1.2770400004228577,1176.0,48.0
empty.txt,DFS,7.602279999264283,2304.0,1176.0
empty.txt,A*,0.10093999881064519,48.0,48.0
noexit.txt,BFS,0.003699999797390774,1.0,0.0
noexit.txt,DFS,0.0032000003557186574,1.0,0.0
noexit.txt,A*,0.004120000085094944,1.0,0.0
1 maze strategy time_ms visited_cells path_length
2 simple.txt BFS 0.01464000015403144 11.0 6.0
3 simple.txt DFS 0.010180000390391797 9.0 8.0
4 simple.txt A* 0.017740000475896522 9.0 6.0
5 dead.txt BFS 0.3642999996372964 307.0 35.0
6 dead.txt DFS 0.23493999906349927 279.0 151.0
7 dead.txt A* 0.38374000068870373 235.0 35.0
8 large.txt BFS 23.894459999428364 6812.0 2329.0
9 large.txt DFS 84.77875999960816 6796.0 4537.0
10 large.txt A* 28.69542000044021 6791.0 2329.0
11 empty.txt BFS 1.2770400004228577 1176.0 48.0
12 empty.txt DFS 7.602279999264283 2304.0 1176.0
13 empty.txt A* 0.10093999881064519 48.0 48.0
14 noexit.txt BFS 0.003699999797390774 1.0 0.0
15 noexit.txt DFS 0.0032000003557186574 1.0 0.0
16 noexit.txt A* 0.004120000085094944 1.0 0.0

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

212
PaulVA/lab2/docs/report.md Normal file
View File

@ -0,0 +1,212 @@
Лабораторная работа 2
Поиск выхода из лабиринта
Цель работы
-----------
Цель работы состоит в реализации программы для поиска выхода из лабиринта с использованием объектно ориентированного подхода и паттернов проектирования
В программе реализована загрузка лабиринта из файла несколько алгоритмов поиска и сравнение их работы
Структура программы
-------------------
В программе используются классы Cell для отдельной клетки лабиринта и Maze для самого лабиринта
Для загрузки используется MazeBuilder и его реализация TextFileMazeBuilder
Для поиска пути используется общий класс PathFindingStrategy и три алгоритма BFSStrategy DFSStrategy и AStarStrategy
За хранение результатов отвечает SearchStats а запуск поиска выполняет MazeSolver
Для вывода информации используются Observer и ConsoleView
Использованные паттерны
-----------------------
В работе использованы три паттерна Builder Strategy и Observer
Builder используется для загрузки лабиринта из текстового файла
TextFileMazeBuilder читает файл и создаёт объект Maze
В файле символ # обозначает стену пробел обозначает свободную клетку S является началом а E выходом
Использование Builder позволяет отдельно реализовать загрузку лабиринта и сам класс лабиринта
Strategy используется для выбора алгоритма поиска
В программе реализованы BFS DFS и A*
Все алгоритмы имеют общий интерфейс PathFindingStrategy поэтому в MazeSolver можно менять алгоритм без изменения самого решателя
Observer используется для вывода информации о поиске
MazeSolver отправляет события а ConsoleView получает их и выводит информацию в консоль
Таким образом вывод отделён от основной логики поиска
Алгоритмы поиска
----------------
BFS использует очередь и при обычных условиях находит кратчайший путь в лабиринте без весов
DFS использует стек и может найти путь быстрее но найденный путь не обязательно будет кратчайшим
A* использует очередь с приоритетом и манхэттенскую эвристику поэтому старается в первую очередь проверять клетки которые находятся ближе к выходу
Схема классов
-------------
classDiagram
class Cell {
x
y
is_wall
is_start
is_exit
is_passable()
}
class Maze {
width
height
cells
start
exit
get_cell()
get_neighbors()
}
class MazeBuilder {
build_from_file()
}
class TextFileMazeBuilder {
build_from_file()
}
class PathFindingStrategy {
find_path()
}
class BFSStrategy {
find_path()
}
class DFSStrategy {
find_path()
}
class AStarStrategy {
find_path()
}
class SearchStats {
path
time_ms
visited_count
path_length
}
class MazeSolver {
maze
strategy
set_strategy()
solve()
}
class Observer {
update()
}
class ConsoleView {
update()
}
MazeBuilder <|-- TextFileMazeBuilder
PathFindingStrategy <|-- BFSStrategy
PathFindingStrategy <|-- DFSStrategy
PathFindingStrategy <|-- AStarStrategy
Observer <|-- ConsoleView
MazeSolver --> Maze
MazeSolver --> PathFindingStrategy
MazeSolver --> Observer
Maze --> Cell
Тестирование
------------
Для проверки использовалось пять разных лабиринтов
simple.txt представляет простой лабиринт dead.txt содержит тупики large.txt является большим запутанным лабиринтом empty.txt не содержит стен а в noexit.txt выход недостижим
Каждый алгоритм запускался пять раз
Во время эксперимента измерялось время поиска количество посещённых клеток и длина найденного пути
Результаты сохранялись в файл results.csv
Результаты
----------
simple.txt
Алгоритм Время мс Посещено Путь
BFS 0.01464 11 6
DFS 0.01018 9 8
A* 0.01774 9 6
Все алгоритмы работают быстро
BFS и A* нашли более короткий путь чем DFS
dead.txt
Алгоритм Время мс Посещено Путь
BFS 0.36430 307 35
DFS 0.23494 279 151
A* 0.38374 235 35
DFS работал немного быстрее но нашёл более длинный путь
BFS и A* нашли короткий путь
large.txt
Алгоритм Время мс Посещено Путь
BFS 23.89446 6812 2329
DFS 84.77876 6796 4537
A* 28.69542 6791 2329
На большом лабиринте DFS показал худшее время и самый длинный путь
BFS и A* нашли одинаковый путь
empty.txt
Алгоритм Время мс Посещено Путь
BFS 1.277
04 1176 48
DFS 7.60228 2304 1176
A* 0.10094 48 48
В лабиринте без стен лучше всего показал себя A*
Он посетил меньше всего клеток и работал быстрее
noexit.txt
Алгоритм Время мс Посещено Путь
BFS 0.00370 1 0
DFS 0.00320 1 0
A* 0.00412 1 0
В этом лабиринте выход недостижим поэтому все алгоритмы быстро закончили поиск
Графики
-------
Для сравнения времени работы были построены графики для каждого лабиринта
Графики находятся в папке docs/data
simple_time.png dead_time.png large_time.png empty_time.png и noexit_time.png
Вывод
-----
В работе была создана программа для поиска выхода из лабиринта
Были реализованы BFS DFS и A* а также использованы паттерны Builder Strategy и Observer
По результатам эксперимента BFS хорошо подходит для поиска кратчайшего пути DFS может найти путь быстрее но он не всегда получается коротким A* хорошо показывает себя на больших и открытых лабиринтах

View File

@ -0,0 +1,93 @@
import csv
import os
from maze_solver import (
TextFileMazeBuilder,
MazeSolver,
BFSStrategy,
DFSStrategy,
AStarStrategy
)
REPEATS = 5
MAZES = [
"simple.txt",
"dead.txt",
"large.txt",
"empty.txt",
"noexit.txt"
]
STRATEGIES = [
("BFS", BFSStrategy()),
("DFS", DFSStrategy()),
("A*", AStarStrategy())
]
def average(values):
return sum(values) / len(values)
def run():
builder = TextFileMazeBuilder()
results = []
for maze_name in MAZES:
filename = os.path.join("lab2", "mazes", maze_name)
print("Maze:", maze_name)
for strategy_name, strategy in STRATEGIES:
times = []
visited = []
path_lengths = []
for _ in range(REPEATS):
maze = builder.build_from_file(filename)
solver = MazeSolver(maze, strategy)
stats = solver.solve()
times.append(stats.time_ms)
visited.append(stats.visited_count)
path_lengths.append(stats.path_length)
results.append([
maze_name,
strategy_name,
average(times),
average(visited),
average(path_lengths)
])
print(
strategy_name,
"time =", average(times),
"visited =", average(visited),
"path =", average(path_lengths)
)
os.makedirs("lab2/docs/data", exist_ok=True)
with open(
"lab2/docs/data/results.csv",
"w",
newline="",
encoding="utf-8"
) as file:
writer = csv.writer(file)
writer.writerow([
"maze",
"strategy",
"time_ms",
"visited_cells",
"path_length"
])
writer.writerows(results)
print("Results saved to lab2/docs/data/results.csv")
if __name__ == "__main__":
run()

23
PaulVA/lab2/graphs.py Normal file
View File

@ -0,0 +1,23 @@
import csv
import matplotlib.pyplot as plt
with open("lab2/docs/data/results.csv", encoding="utf-8") as file:
rows = list(csv.DictReader(file))
mazes = ["simple.txt", "dead.txt", "large.txt", "empty.txt", "noexit.txt"]
strategies = ["BFS", "DFS", "A*"]
for maze in mazes:
values = []
for strategy in strategies:
for row in rows:
if row["maze"] == maze and row["strategy"] == strategy:
values.append(float(row["time_ms"]))
plt.bar(strategies, values)
plt.title("Время поиска: " + maze)
plt.xlabel("Стратегия")
plt.ylabel("Время, мс")
plt.savefig("lab2/docs/data/" + maze.replace(".txt", "_time.png"))
plt.close()

26
PaulVA/lab2/make_large.py Normal file
View File

@ -0,0 +1,26 @@
lines = []
for y in range(100):
row = [" "] * 100
if y == 0 or y == 99:
row = ["#"] * 100
else:
row[0] = "#"
row[99] = "#"
lines.append(row)
lines[1][1] = "S"
lines[98][98] = "E"
for x in range(4, 96, 4):
gap = 1 if (x // 4) % 2 == 0 else 98
for y in range(1, 99):
if y != gap:
lines[y][x] = "#"
with open("lab2/mazes/large.txt", "w", encoding="utf-8") as file:
for row in lines:
file.write("".join(row) + "\n")

284
PaulVA/lab2/maze_solver.py Normal file
View File

@ -0,0 +1,284 @@
from abc import ABC, abstractmethod
from collections import deque
import heapq
import time
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
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 = []
directions = [
(0, -1),
(0, 1),
(-1, 0),
(1, 0)
]
for dx, dy in directions:
neighbor = self.get_cell(
cell.x + dx,
cell.y + dy
)
if neighbor and neighbor.is_passable():
neighbors.append(neighbor)
return neighbors
class MazeBuilder(ABC):
@abstractmethod
def build_from_file(self, filename):
pass
class TextFileMazeBuilder(MazeBuilder):
def build_from_file(self, filename):
with open(filename, "r", encoding="utf-8") as file:
lines = [line.rstrip("\n") for line in file]
if not lines:
raise ValueError("Файл лабиринта пустой")
width = len(lines[0])
for line in lines:
if len(line) != width:
raise ValueError("Строки лабиринта имеют разную длину")
maze = Maze(width, len(lines))
for y, line in enumerate(lines):
for x, symbol in enumerate(line):
cell = maze.get_cell(x, y)
if symbol == "#":
cell.is_wall = True
elif symbol == "S":
if maze.start is not None:
raise ValueError("В лабиринте несколько стартов")
maze.start = cell
cell.is_start = True
elif symbol == "E":
if maze.exit is not None:
raise ValueError("В лабиринте несколько выходов")
maze.exit = cell
cell.is_exit = True
elif symbol == " ":
pass
else:
raise ValueError("Неизвестный символ в лабиринте")
if maze.start is None:
raise ValueError("В лабиринте нет старта")
if maze.exit is None:
raise ValueError("В лабиринте нет выхода")
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 start is None or exit is None:
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 start is None or exit is None:
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 start is None or exit is None:
return [], 0
heap = []
counter = 0
heapq.heappush(
heap,
(self.heuristic(start, exit), counter, start, [start])
)
g_score = {start: 0}
visited = set()
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):
new_cost = g_score[current] + 1
if neighbor not in g_score or new_cost < g_score[neighbor]:
g_score[neighbor] = new_cost
counter += 1
priority = new_cost + self.heuristic(
neighbor,
exit
)
heapq.heappush(
heap,
(
priority,
counter,
neighbor,
path + [neighbor]
)
)
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 update(self, event, data=None):
if event == "search_started":
print("Поиск начат")
elif event == "search_finished":
print(f"Поиск завершен за {data:.3f} мс")
elif event == "path_found":
print(f"Длина пути: {len(data)}")

View File

@ -0,0 +1,20 @@
####################
#S #
# #
# #
# #
# #
# ######### #
# # #
# # #
# # #
# # #
# # #
# # #
# # #
# # #
# #
# #
# #
# E#
####################

View File

@ -0,0 +1,50 @@
##################################################
#S #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# #
#E #
##################################################

100
PaulVA/lab2/mazes/large.txt Normal file
View File

@ -0,0 +1,100 @@
####################################################################################################
#S # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # E#
####################################################################################################

View File

@ -0,0 +1,10 @@
##########
#S########
##########
##########
##########
##########
##########
##########
########E#
##########

View File

@ -0,0 +1,5 @@
#######
#S #
# ### #
# E #
#######

View File

@ -0,0 +1,252 @@
from MP_records import records
import random as rd
import time
import csv
import codecs
import sys
sys.setrecursionlimit(15000)
# ---------- Binary Search Tree ----------
# Узел:
# {
# "name": name,
# "phone": phone,
# "left": None,
# "right": None
# }
def bst_insert(root, name, phone):
"""
Вставляет новую запись или обновляет телефон по имени.
Возвращает корень дерева.
"""
if root is None:
return {
"name": name,
"phone": phone,
"left": None,
"right": None
}
if name == root["name"]:
root["phone"] = phone
elif name < root["name"]:
root["left"] = bst_insert(root["left"], name, phone)
else:
root["right"] = bst_insert(root["right"], name, phone)
return root
def bst_find(root, name):
"""
Поиск телефона по имени.
"""
if root is None:
return None
if name == root["name"]:
return root["phone"]
if name < root["name"]:
return bst_find(root["left"], name)
return bst_find(root["right"], name)
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
if name < root["name"]:
root["left"] = bst_delete(root["left"], name)
elif name > root["name"]:
root["right"] = bst_delete(root["right"], name)
else:
# Узел без левого потомка
if root["left"] is None:
return root["right"]
# Узел без правого потомка
if root["right"] is None:
return root["left"]
# Узел с двумя потомками
successor = bst_find_min(root["right"])
root["name"] = successor["name"]
root["phone"] = successor["phone"]
root["right"] = bst_delete(root["right"], successor["name"])
return root
def bst_inorder(root, result):
"""
Центрированный обход дерева.
"""
if root is None:
return
bst_inorder(root["left"], result)
result.append((root["name"], root["phone"]))
bst_inorder(root["right"], result)
def bst_list_all(root):
"""
Возвращает список записей в отсортированном порядке.
"""
result = []
bst_inorder(root, result)
return result
# ---------- Benchmark helpers ----------
def build_bst(records_list):
root = None
for name, phone in records_list:
root = bst_insert(root, name, phone)
return root
def measure_bst(records_list, mode_name, repeats=5):
rows = []
insertion_times = []
finding_times = []
deletion_times = []
for run_number in range(1, repeats + 1):
data = records_list[:]
if mode_name == "случайный":
rd.shuffle(data)
# А. Вставка
root = None
start = time.perf_counter()
for name, phone in data:
root = bst_insert(root, name, phone)
end = time.perf_counter()
insertion_time = end - start
insertion_times.append(insertion_time)
# Б. Поиск
existing_names = [name for name, phone in rd.sample(data, 100)]
missing_names = [f"None_{i}" for i in range(10)]
search_names = existing_names + missing_names
rd.shuffle(search_names)
start = time.perf_counter()
for name in search_names:
bst_find(root, name)
end = time.perf_counter()
finding_time = end - start
finding_times.append(finding_time)
# В. Удаление
delete_names = rd.sample(existing_names, 50)
start = time.perf_counter()
for name in delete_names:
root = bst_delete(root, name)
end = time.perf_counter()
deletion_time = end - start
deletion_times.append(deletion_time)
rows.append(["BinarySearchTree", mode_name, "вставка", run_number, insertion_time])
rows.append(["BinarySearchTree", mode_name, "поиск", run_number, finding_time])
rows.append(["BinarySearchTree", mode_name, "удаление", run_number, deletion_time])
rows.append(["BinarySearchTree", mode_name, "вставка", "среднее", sum(insertion_times) / repeats])
rows.append(["BinarySearchTree", mode_name, "поиск", "среднее", sum(finding_times) / repeats])
rows.append(["BinarySearchTree", mode_name, "удаление", "среднее", sum(deletion_times) / repeats])
return rows
def save_results(rows, filename="results.csv"):
with codecs.open(filename, "a+", "utf-16") as file:
writer = csv.writer(file)
writer.writerows(rows)
def run_shuffled(records_shuffled):
rows = measure_bst(records_shuffled, "случайный")
save_results(rows)
return rows
def run_sorted(records_sorted):
rows = measure_bst(records_sorted, "отсортированный")
save_results(rows)
return rows
# ---------- Manual tests ----------
def test():
root = None
root = bst_insert(root, "Ivan", "111")
root = bst_insert(root, "Anna", "222")
root = bst_insert(root, "Petr", "333")
root = bst_insert(root, "Maria", "444")
print(bst_find(root, "Anna")) # 222
print(bst_find(root, "Unknown")) # None
root = bst_insert(root, "Anna", "999")
print(bst_find(root, "Anna")) # 999
root = bst_delete(root, "Ivan")
print(bst_list_all(root))
if __name__ == "__main__":
records_shuffled, records_sorted = records()
run_shuffled(records_shuffled)
run_sorted(records_sorted)

View File

@ -0,0 +1,456 @@
from MP_records import records
import string
import random as rd
import time
import csv
import codecs
def polynomial_hash(word):
p=11111
m=(10**9)+9
hashh=0
for i in range(len(word)):
hashh+=ord(word[i])*(p**i)
hashh=hashh%m
return hashh
def hash_to_index(hashh,length):
#print(hashh)
#if len(str(hashh))>4:
#hashh=int(str(hashh)[3:])
while hashh>length:
hashh=hashh%(length)
return hashh
def ll_insert(table,name,phone,index):
if table[index]==None:
entry={"name":name,"phone":phone,"next":None}
table[index]=entry
return table
else:
entry={"name":name,"phone":phone,"next":None}
if table[index]["phone"]==phone:
table[index]["name"]=name
return table
if table[index]["next"]==None:
table[index]["next"]=entry
return table
else:
nexxt=table[index]["next"]
if nexxt["phone"]==phone:
nexxt["name"]=name
return table
while nexxt["next"]!=None:
nexxt=nexxt["next"]
if nexxt["phone"]==phone:
nexxt["name"]=name
return table
nexxt["next"]=entry
return table
def ht_insert(table,name,phone):
index=hash_to_index(polynomial_hash(name), len(table))
ll_insert(table,name,phone,index)
return table
def ht_find(table, name):
index=hash_to_index(polynomial_hash(name), len(table))
if table[index]!=None:
if table[index]["name"]==name:
return table[index]["phone"]
elif table[index]["next"]!=None:
if table[index]["next"]["name"]==name:
return table[index]["next"]["phone"]
else:
nexxt=table[index]["next"]
while nexxt["next"]!=None:
nexxt=nexxt["next"]
if nexxt["name"]==name:
return nexxt["phone"]
return None
def ht_delete(table,name):
index=hash_to_index(polynomial_hash(name), len(table))
if len(table)>0:
if table[index]["name"]==name:
if table[index]["next"]!=None:
table[index]=table[index]["next"]
return table
else:
table[index]=None
return table
elif table[index]["next"]!=None:
if table[index]["next"]["name"]==name:
if table[index]["next"]["next"]!=None:
table[index]["next"]=table[index]["next"]["next"]
return table
else:
table[index]["next"]=None
return table
elif table[index]["next"]["next"]!=None:
nexxt1=table[index]["next"]
nexxt2=nexxt1["next"]
if nexxt2["name"]==name:
if nexxt2["next"]!=None:
nexxt1["next"]=nexxt2["next"]
return table
else:
nexxt1["next"]=None
return table
while nexxt2["next"]!=None:
nexxt1=nexxt2
nexxt2=nexxt1["next"]
if nexxt2["name"]==name:
if nexxt2["next"]!=None:
nexxt1["next"]=nexxt2["next"]
return table
else:
nexxt1["next"]=None
return table
def bad_sort(names,phones):
names1=[]
phones1=[]
while len(names)>0:
min_=names[0].encode()
ph=phones[0]
for i in range(len(names)):
nm=names[i].encode()
if nm<min_:
min_=nm
ph=phones[i]
#print(min_.decode()," - ",ph)
names1.append(min_.decode())
phones1.append(ph)
names.remove(min_.decode())
phones.remove(ph)
#print(names1,"\n",phones1)
return names1, phones1
def Shell(names,phones):
N = len(names)
n = N // 2
while n>0:
for i in range (0,N-n):
j=i
while j+n<N:
if (names[j].encode())>(names[j+n].encode()):
t=names[j]
t1=phones[j]
names[j]=names[j+n]
phones[j]=phones[j+n]
names[j+n]=t
phones[j+n]=t1
j=i
else:
j+=n
n=n//2
return names,phones
def ht_listall(table):
names=[]
phones=[]
pointer=0
while pointer<len(table):
if table[pointer]!=None:
names.append(table[pointer]["name"])
phones.append(table[pointer]["phone"])
if table[pointer]["next"]!=None:
names.append(table[pointer]["next"]["name"])
phones.append(table[pointer]["next"]["phone"])
nexxt=table[pointer]["next"]
while nexxt["next"]!=None:
nexxt=nexxt["next"]
names.append(nexxt["name"])
phones.append(nexxt["phone"])
pointer+=1
names1, phones1 = bad_sort(names, phones)
#names1, phones1 = Shell(names, phones)
for i in range(len(names1)):
print(names1[i]," - ",phones1[i],end='')
if i%4==0:
print("\n")
else:
print(", ",end='')
print("\n")
def test():
table=[]
for i in range(8):
table.append(None)
ht_insert(table, "Zyky", 1)
ht_insert(table, "Abba", 2)
ht_insert(table, "Babba", 3)
ht_insert(table, "Aaaaa", 4)
ht_insert(table, "Aakk", 5)
ht_insert(table, "Bfaw", 6)
ht_insert(table, "Uno", 7)
ht_insert(table, "Uk", 8)
ht_insert(table, "Uaa", 9)
ht_insert(table, "h", 10)
print(table)
print(ht_find(table,"Aakk"))
# ht_delete(table, "Aakk")
#ht_delete(table, "Aaaaa")
#print(table)
#ht_delete(table, "Uaa")
#ht_delete(table, "Zyky")
print(table)
ht_listall(table)
def run_shuffled(records_shuffled):
insertion_times=[]
finding_times=[]
deletion_times1=[]
print("Shuffled list: ")
for k in range(5):
lisst=[]
for i in range(5000):
lisst.append(None)
rd.shuffle(records_shuffled)
#А. Вставка всех записей
start=time.perf_counter()
for i in range(len(records_shuffled)):
ht_insert(lisst, records_shuffled[i][0], records_shuffled[i][1])
end=time.perf_counter()
insertion_times.append(end-start)
#Б. Поиск 100 случайных записей
names=[]
index=rd.randint(0,9899)
for i in range(100):
names.append(records_shuffled[index][0])
index+=1
for i in range(10):
names.append("A")
rd.shuffle(names)
start=time.perf_counter()
for i in range(len(names)):
ht_find(lisst,names[i])
end=time.perf_counter()
finding_times.append(end-start)
#В. Удаление 50 случайных записей
for i in range(10):
names.remove("A")
rd.shuffle(names)
deletion_times=[]
for i in range(50):
start=time.perf_counter()
ht_delete(lisst,names[i])
end=time.perf_counter()
ttt=end-start
deletion_times.append(ttt)
deletion_times1.append(deletion_times)
print("Run number ",k+1)
print("Insertion time: ",insertion_times[k])
print("Finding time: ",finding_times[k])
print("Deletion times: ","\n",deletion_times)
print("\n")
temp=0
for i in range(5):
temp+=insertion_times[i]
temp=temp/5
results = [
[u"Структура", u"Режим", u"Операция", u"Время (сек)"],
["HashTable", u"случайный", u"вставка", insertion_times[0]],
["HashTable", u"случайный", u"вставка", insertion_times[1]],
["HashTable", u"случайный", u"вставка", insertion_times[2]],
["HashTable", u"случайный", u"вставка", insertion_times[3]],
["HashTable", u"случайный", u"вставка", insertion_times[4]],
[u"Структура", u"Режим", u"Операция", u"Среднее время (сек)"],
["HashTable", u"случайный", u"вставка", temp,]
]
with codecs.open("results.csv", "a+", "utf-16") as f:
writer = csv.writer(f)
writer.writerows(results)
writer.writerows("\n")
temp=0
for i in range(5):
temp+=finding_times[i]
temp=temp/5
results = [
[u"Структура", u"Режим", u"Операция", u"Время (сек)"],
["HashTable", u"случайный", u"поиск", finding_times[0]],
["HashTable", u"случайный", u"поиск", finding_times[1]],
["HashTable", u"случайный", u"поиск", finding_times[2]],
["HashTable", u"случайный", u"поиск", finding_times[3]],
["HashTable", u"случайный", u"поиск", finding_times[4]],
[u"Структура", u"Режим", u"Операция", u"Среднее время (сек)"],
["HashTable", u"случайный", u"поиск", temp,]
]
with codecs.open("results.csv", "a+", "utf-16") as f:
writer = csv.writer(f)
writer.writerows(results)
writer.writerows("\n")
temp=0
del_times=[]
for i in range(5):
for j in range(50):
temp+=deletion_times1[i][j]
temp=temp/50
del_times.append(temp)
temp=0
temp=0
for i in range(5):
temp+=del_times[i]
temp=temp/5
results = [
[u"Структура", u"Режим", u"Операция", u"Время (сек)"],
["HashTable", u"случайный", u"удаление", del_times[0]],
["HashTable", u"случайный", u"удаление", del_times[1]],
["HashTable", u"случайный", u"удаление", del_times[2]],
["HashTable", u"случайный", u"удаление", del_times[3]],
["HashTable", u"случайный", u"удаление", del_times[4]],
[u"Структура", u"Режим", u"Операция", u"Среднее время (сек)"],
["HashTable", u"случайный", u"удаление", temp,]
]
with codecs.open("results.csv", "a+", "utf-16") as f:
writer = csv.writer(f)
writer.writerows(results)
writer.writerows("\n")
writer.writerows("\n")
def run_sorted(records_shuffled):
insertion_times=[]
finding_times=[]
deletion_times1=[]
print("Sorted list: ")
for k in range(5):
lisst=[]
for i in range(5000):
lisst.append(None)
#А. Вставка всех записей
start=time.perf_counter()
for i in range(len(records_shuffled)):
ht_insert(lisst, records_shuffled[i][0], records_shuffled[i][1])
end=time.perf_counter()
insertion_times.append(end-start)
#Б. Поиск 100 случайных записей
names=[]
index=rd.randint(0,9899)
for i in range(100):
names.append(records_shuffled[index][0])
index+=1
for i in range(10):
names.append("A")
rd.shuffle(names)
start=time.perf_counter()
for i in range(len(names)):
ht_find(lisst,names[i])
end=time.perf_counter()
finding_times.append(end-start)
#В. Удаление 50 случайных записей
for i in range(10):
names.remove("A")
rd.shuffle(names)
deletion_times=[]
for i in range(50):
start=time.perf_counter()
ht_delete(lisst,names[i])
end=time.perf_counter()
ttt=end-start
deletion_times.append(ttt)
deletion_times1.append(deletion_times)
print("Run number ",k+1)
print("Insertion time: ",insertion_times[k])
print("Finding time: ",finding_times[k])
print("Deletion average:", sum(deletion_times))
print("\n")
temp=0
for i in range(5):
temp+=insertion_times[i]
temp=temp/5
results = [
[u"Структура", u"Режим", u"Операция", u"Время (сек)"],
["HashTable", u"отсортированный", u"вставка", insertion_times[0]],
["HashTable", u"отсортированный", u"вставка", insertion_times[1]],
["HashTable", u"отсортированный", u"вставка", insertion_times[2]],
["HashTable", u"отсортированный", u"вставка", insertion_times[3]],
["HashTable", u"сотсортированный", u"вставка", insertion_times[4]],
[u"Структура", u"Режим", u"Операция", u"Среднее время (сек)"],
["HashTable", u"отсортированный", u"вставка", temp,]
]
with codecs.open("results.csv", "a+", "utf-16") as f:
writer = csv.writer(f)
writer.writerows(results)
writer.writerows("\n")
temp=0
for i in range(5):
temp+=finding_times[i]
temp=temp/5
results = [
[u"Структура", u"Режим", u"Операция", u"Время (сек)"],
["HashTable", u"отсортированный", u"поиск", finding_times[0]],
["HashTable", u"отсортированный", u"поиск", finding_times[1]],
["HashTable", u"отсортированный", u"поиск", finding_times[2]],
["HashTable", u"отсортированный", u"поиск", finding_times[3]],
["HashTable", u"отсортированный", u"поиск", finding_times[4]],
[u"Структура", u"Режим", u"Операция", u"Среднее время (сек)"],
["HashTable", u"отсортированный", u"поиск", temp,]
]
with codecs.open("results.csv", "a+", "utf-16") as f:
writer = csv.writer(f)
writer.writerows(results)
writer.writerows("\n")
temp=0
del_times=[]
for i in range(5):
for j in range(50):
temp+=deletion_times1[i][j]
temp=temp/50
del_times.append(temp)
temp=0
temp=0
for i in range(5):
temp+=del_times[i]
temp=temp/5
results = [
[u"Структура", u"Режим", u"Операция", u"Время (сек)"],
["HashTable", u"отсортированный", u"удаление", del_times[0]],
["HashTable", u"отсортированный", u"удаление", del_times[1]],
["HashTable", u"отсортированный", u"удаление", del_times[2]],
["HashTable", u"отсортированный", u"удаление", del_times[3]],
["HashTable", u"отсортированный", u"удаление", del_times[4]],
[u"Структура", u"Режим", u"Операция", u"Среднее время (сек)"],
["HashTable", u"отсортированный", u"удаление", temp,]
]
with codecs.open("results.csv", "a+", "utf-16") as f:
writer = csv.writer(f)
writer.writerows(results)
writer.writerows("\n")
writer.writerows("\n")
records_shuffled, records_sorted = records()
run_shuffled(records_shuffled)
run_sorted(records_sorted)

View File

@ -0,0 +1,241 @@
from MP_records import records
import random as rd
import time
import csv
import codecs
# ---------- Linked List Phone Book ----------
# Узел списка:
# {"name": name, "phone": phone, "next": next_node}
def ll_insert(head, name, phone):
"""
Добавляет новую запись или обновляет телефон по имени.
Возвращает голову списка.
"""
new_node = {
"name": name,
"phone": phone,
"next": None
}
if head is None:
return new_node
current = head
while True:
if current["name"] == name:
current["phone"] = phone
return head
if current["next"] is None:
break
current = current["next"]
current["next"] = new_node
return head
def ll_find(head, name):
"""
Ищет запись по имени.
Возвращает телефон или None.
"""
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"]
previous = head
current = head["next"]
while current is not None:
if current["name"] == name:
previous["next"] = current["next"]
return head
previous = current
current = current["next"]
return head
def ll_list_all(head):
"""
Возвращает список всех записей, отсортированный по имени.
"""
result = []
current = head
while current is not None:
result.append((current["name"], current["phone"]))
current = current["next"]
result.sort(key=lambda item: item[0])
return result
# ---------- Compatibility aliases ----------
# Можно оставить старые имена, если они уже используются в отчете/других файлах.
def insert(head, name, phone):
return ll_insert(head, name, phone)
def find(head, name):
return ll_find(head, name)
def delete(head, name):
return ll_delete(head, name)
def list_all(head):
return ll_list_all(head)
# ---------- Benchmark helpers ----------
def build_linked_list(records_list):
head = None
for name, phone in records_list:
head = ll_insert(head, name, phone)
return head
def measure_linked_list(records_list, mode_name, repeats=5):
"""
Выполняет 5 повторов:
1. вставка всех записей;
2. поиск 100 существующих и 10 отсутствующих имен;
3. удаление 50 существующих имен.
Возвращает строки для записи в CSV.
"""
rows = []
insertion_times = []
finding_times = []
deletion_times = []
for run_number in range(1, repeats + 1):
data = records_list[:]
if mode_name == "случайный":
rd.shuffle(data)
# А. Вставка всех записей
head = None
start = time.perf_counter()
for name, phone in data:
head = ll_insert(head, name, phone)
end = time.perf_counter()
insertion_time = end - start
insertion_times.append(insertion_time)
# Б. Поиск 100 существующих + 10 отсутствующих
existing_names = [name for name, phone in rd.sample(data, 100)]
missing_names = [f"None_{i}" for i in range(10)]
search_names = existing_names + missing_names
rd.shuffle(search_names)
start = time.perf_counter()
for name in search_names:
ll_find(head, name)
end = time.perf_counter()
finding_time = end - start
finding_times.append(finding_time)
# В. Удаление 50 существующих
delete_names = rd.sample(existing_names, 50)
start = time.perf_counter()
for name in delete_names:
head = ll_delete(head, name)
end = time.perf_counter()
deletion_time = end - start
deletion_times.append(deletion_time)
rows.append(["LinkedList", mode_name, "вставка", run_number, insertion_time])
rows.append(["LinkedList", mode_name, "поиск", run_number, finding_time])
rows.append(["LinkedList", mode_name, "удаление", run_number, deletion_time])
rows.append(["LinkedList", mode_name, "вставка", "среднее", sum(insertion_times) / repeats])
rows.append(["LinkedList", mode_name, "поиск", "среднее", sum(finding_times) / repeats])
rows.append(["LinkedList", mode_name, "удаление", "среднее", sum(deletion_times) / repeats])
return rows
def save_results(rows, filename="results.csv"):
with codecs.open(filename, "a+", "utf-16") as file:
writer = csv.writer(file)
writer.writerows(rows)
def run_shuffled(records_shuffled):
rows = measure_linked_list(records_shuffled, "случайный")
save_results(rows)
return rows
def run_sorted(records_sorted):
rows = measure_linked_list(records_sorted, "отсортированный")
save_results(rows)
return rows
# ---------- Manual tests ----------
def test():
head = None
head = ll_insert(head, "Ivan", "111")
head = ll_insert(head, "Anna", "222")
head = ll_insert(head, "Petr", "333")
print(ll_find(head, "Anna")) # 222
print(ll_find(head, "Unknown")) # None
head = ll_insert(head, "Anna", "999")
print(ll_find(head, "Anna")) # 999
head = ll_delete(head, "Ivan")
print(ll_list_all(head)) # [('Anna', '999'), ('Petr', '333')]
if __name__ == "__main__":
records_shuffled, records_sorted = records()
run_shuffled(records_shuffled)
run_sorted(records_sorted)

View File

@ -0,0 +1,34 @@
import random
VOWELS = "aeiou"
CONSONANTS = "bcdfghjklmnpqrstvwxyz"
def generate_name():
length = random.randint(4, 10)
name = ""
for i in range(length):
if i % 2 == 0:
name += random.choice(CONSONANTS)
else:
name += random.choice(VOWELS)
return name.capitalize()
def generate_unique_names(count):
names = set()
while len(names) < count:
names.add(generate_name())
return list(names)
if __name__ == "__main__":
names = generate_unique_names(5000)
with open("names.txt", "w", encoding="utf-8") as file:
for name in names:
file.write(name + "\n")
print("names.txt generated")

View File

@ -0,0 +1,73 @@
import random as rd
def Shell(arr):
N = len(arr)
n = N // 2
while n>0:
for i in range (0,N-n):
j=i
while j+n<N:
if arr[j]>arr[j+n]:
t=arr[j]
arr[j]=arr[j+n]
arr[j+n]=t
j=i
else:
j+=n
n=n//2
return arr
def records():
phones=[]
first=0
second=0
third=0
fourth=0
for i in range(10000):
phones.append(str(first)+str(second)+str(third)+str(fourth))
fourth+=1
if fourth==10:
third+=1
fourth=0
if third==10:
second+=1
third=0
if second==10:
first+=1
second=0
phones2=phones.copy()
f=open("names.txt","r")
count=0
names=[]
while count<5000:
name=f.readline()
names.append(name[:len(name)-1])
names.append(name[:len(name)-1])
count+=1
f.close()
names_sorted=names.copy()
for i in range(10000):
names_sorted[i]=names_sorted[i].encode()
Shell(names_sorted)
for i in range(10000):
names_sorted[i]=names_sorted[i].decode()
records_shuffled=[]
records_sorted=[]
count=0
while count<10000:
name_var=rd.randint(0,len(names)-1)
phone_var=rd.randint(0,len(phones2)-1)
records_shuffled.append((names[name_var],phones[count]))
records_sorted.append((names_sorted[count],phones2[phone_var]))
names.remove(names[name_var])
phones2.remove(phones2[phone_var])
count+=1
rd.shuffle(records_shuffled)
return records_shuffled, records_sorted
#print(records_shuffled)
#print(records_sorted)

Binary file not shown.

5000
SorokinAD/[1]lab_1/names.txt Normal file

File diff suppressed because it is too large Load Diff

Binary file not shown.

Binary file not shown.
Can't render this file because it has a wrong number of fields in line 37.

View File

@ -0,0 +1,143 @@
import csv
import os
from builders import TextFileMazeBuilder
from solver import MazeSolver
from strategies import (
BFSStrategy,
DFSStrategy,
AStarStrategy
)
# =========================================================
# Benchmark
# =========================================================
class BenchmarkRunner:
def __init__(self):
self.strategies = [
("BFS", BFSStrategy()),
("DFS", DFSStrategy()),
("A*", AStarStrategy()),
]
# =====================================================
# Run benchmark
# =====================================================
def run(
self,
maze_files: list[str],
runs_per_test: int = 5
):
results = []
builder = TextFileMazeBuilder()
for maze_file in maze_files:
print()
print(f"Testing: {maze_file}")
maze = builder.build_from_file(
maze_file
)
for strategy_name, strategy in self.strategies:
total_time = 0
total_visited = 0
total_path_length = 0
for _ in range(runs_per_test):
solver = MazeSolver(
maze,
strategy
)
path, stats = solver.solve()
total_time += stats.time_ms
total_visited += stats.visited_cells
total_path_length += stats.path_length
avg_time = (
total_time / runs_per_test
)
avg_visited = (
total_visited / runs_per_test
)
avg_path_length = (
total_path_length / runs_per_test
)
result = {
"maze": maze_file,
"strategy": strategy_name,
"time_ms": round(avg_time, 3),
"visited_cells": int(avg_visited),
"path_length": int(avg_path_length),
}
results.append(result)
print(
f"{strategy_name}: "
f"time={avg_time:.3f} ms, "
f"visited={avg_visited:.0f}, "
f"path={avg_path_length:.0f}"
)
self.save_to_csv(results)
# =====================================================
# Save CSV
# =====================================================
@staticmethod
def save_to_csv(results):
base_dir = os.path.dirname(__file__)
csv_path = os.path.join(
base_dir,
"benchmark_results.csv"
)
with open(
csv_path,
"w",
newline="",
encoding="utf-8"
) as file:
writer = csv.DictWriter(
file,
fieldnames=[
"maze",
"strategy",
"time_ms",
"visited_cells",
"path_length"
]
)
writer.writeheader()
for row in results:
writer.writerow(row)
print()
print(
f"Results saved to: {csv_path}"
)

View File

@ -0,0 +1,13 @@
maze,strategy,time_ms,visited_cells,path_length
mazes/small.txt,BFS,0.034,17,12
mazes/small.txt,DFS,0.026,13,12
mazes/small.txt,A*,0.048,17,12
mazes/open.txt,BFS,0.219,100,19
mazes/open.txt,DFS,0.135,55,55
mazes/open.txt,A*,0.334,100,19
mazes/medium.txt,BFS,0.093,36,0
mazes/medium.txt,DFS,0.059,36,0
mazes/medium.txt,A*,0.087,36,0
mazes/no_exit.txt,BFS,0.008,5,0
mazes/no_exit.txt,DFS,0.007,5,0
mazes/no_exit.txt,A*,0.011,5,0
1 maze strategy time_ms visited_cells path_length
2 mazes/small.txt BFS 0.034 17 12
3 mazes/small.txt DFS 0.026 13 12
4 mazes/small.txt A* 0.048 17 12
5 mazes/open.txt BFS 0.219 100 19
6 mazes/open.txt DFS 0.135 55 55
7 mazes/open.txt A* 0.334 100 19
8 mazes/medium.txt BFS 0.093 36 0
9 mazes/medium.txt DFS 0.059 36 0
10 mazes/medium.txt A* 0.087 36 0
11 mazes/no_exit.txt BFS 0.008 5 0
12 mazes/no_exit.txt DFS 0.007 5 0
13 mazes/no_exit.txt A* 0.011 5 0

View File

@ -0,0 +1,60 @@
from abc import ABC, abstractmethod
from cell import Cell
from maze import Maze
class MazeBuilder(ABC):
@abstractmethod
def build_from_file(self, filename: str) -> Maze:
pass
class TextFileMazeBuilder(MazeBuilder):
def build_from_file(self, filename: str) -> Maze:
with open(filename, "r", encoding="utf-8") as file:
lines = [line.rstrip("\n") for line in file]
cells = []
start = None
exit = None
for y, line in enumerate(lines):
row = []
for x, char in enumerate(line):
is_wall = char == "#"
is_start = char == "S"
is_exit = char == "E"
cell = Cell(
x=x,
y=y,
is_wall=is_wall,
is_start=is_start,
is_exit=is_exit
)
if is_start:
start = cell
if is_exit:
exit = cell
row.append(cell)
cells.append(row)
if start is None:
raise ValueError("Старт S не найден")
if exit is None:
raise ValueError("Выход E не найден")
return Maze(cells, start, exit)

View File

@ -0,0 +1,13 @@
from dataclasses import dataclass
@dataclass(frozen=True)
class Cell:
x: int
y: int
is_wall: bool = False
is_start: bool = False
is_exit: bool = False
def is_passable(self) -> bool:
return not self.is_wall

View File

@ -0,0 +1,91 @@
from abc import ABC, abstractmethod
from cell import Cell
from maze import Maze
class Player:
def __init__(self, start_cell: Cell):
self.current_cell = start_cell
# =========================================================
# Command
# =========================================================
class Command(ABC):
@abstractmethod
def execute(self):
pass
@abstractmethod
def undo(self):
pass
# =========================================================
# MoveCommand
# =========================================================
class MoveCommand(Command):
DIRECTIONS = {
"W": (0, -1),
"S": (0, 1),
"A": (-1, 0),
"D": (1, 0),
}
def __init__(
self,
player: Player,
maze: Maze,
direction: str
):
self.player = player
self.maze = maze
self.direction = direction.upper()
self.previous_cell = None
def execute(self):
if self.direction not in self.DIRECTIONS:
return False
dx, dy = self.DIRECTIONS[self.direction]
current = self.player.current_cell
new_x = current.x + dx
new_y = current.y + dy
target = self.maze.get_cell(
new_x,
new_y
)
if target is None:
return False
if not target.is_passable():
return False
self.previous_cell = current
self.player.current_cell = target
return True
def undo(self):
if self.previous_cell is not None:
self.player.current_cell = (
self.previous_cell
)

164
SorokinAD/[2]lab_2/main.py Normal file
View File

@ -0,0 +1,164 @@
from builders import TextFileMazeBuilder
from strategies import (
BFSStrategy,
DFSStrategy,
AStarStrategy
)
from solver import MazeSolver
from visualization import ConsoleView
from commands import (
Player,
MoveCommand
)
def test_strategy(name, strategy, maze):
print()
print("=" * 40)
view = ConsoleView()
solver = MazeSolver(
maze,
strategy
)
solver.add_observer(view)
path, stats = solver.solve()
print()
print(f"Strategy: {name}")
print(
f"Time: {stats.time_ms:.3f} ms"
)
print(
f"Visited cells: {stats.visited_cells}"
)
print(
f"Path length: {stats.path_length}"
)
print()
view.render(
maze,
path
)
# =========================================================
# Manual mode
# =========================================================
def manual_mode(maze):
print()
print("=" * 40)
print("MANUAL MODE")
print("W/A/S/D - move")
print("U - undo")
print("Q - quit")
view = ConsoleView()
player = Player(
maze.start
)
history = []
while True:
print()
view.render(
maze,
current=player.current_cell
)
if player.current_cell == maze.exit:
print()
print("YOU WIN!")
break
command_input = input(
"\nCommand: "
).upper()
if command_input == "Q":
break
if command_input == "U":
if history:
last_command = history.pop()
last_command.undo()
continue
command = MoveCommand(
player,
maze,
command_input
)
success = command.execute()
if success:
history.append(command)
else:
print("Invalid move")
def main():
builder = TextFileMazeBuilder()
maze = builder.build_from_file(
"mazes/small.txt"
)
# =====================================
# Strategies
# =====================================
test_strategy(
"BFS",
BFSStrategy(),
maze
)
test_strategy(
"DFS",
DFSStrategy(),
maze
)
test_strategy(
"A*",
AStarStrategy(),
maze
)
# =====================================
# Manual mode
# =====================================
manual_mode(maze)
if __name__ == "__main__":
main()

View File

@ -0,0 +1,33 @@
from cell import Cell
class Maze:
def __init__(self, cells: list[list[Cell]], start: Cell, exit: Cell):
self.cells = cells
self.height = len(cells)
self.width = len(cells[0]) if self.height > 0 else 0
self.start = start
self.exit = exit
def get_cell(self, x: int, y: int) -> Cell | None:
if 0 <= y < self.height and 0 <= x < self.width:
return self.cells[y][x]
return None
def get_neighbors(self, cell: Cell) -> list[Cell]:
directions = [
(0, -1),
(0, 1),
(-1, 0),
(1, 0),
]
neighbors = []
for dx, dy in directions:
neighbor = self.get_cell(cell.x + dx, cell.y + dy)
if neighbor is not None and neighbor.is_passable():
neighbors.append(neighbor)
return neighbors

View File

@ -0,0 +1,11 @@
####################
#S # # #
### ### ##### ### ##
# # # ##
# ### ### # ##### ##
# # # # # #
# # ##### ##### # #
# # # # # #
# ##### ##### # # #
# # # E#
####################

View File

@ -0,0 +1,5 @@
##########
#S# #E#
# ### ####
# # #
##########

View File

@ -0,0 +1,10 @@
S
E

View File

@ -0,0 +1,5 @@
##########
#S #E#
# ### ## #
# # #
##########

Binary file not shown.

Some files were not shown because too many files have changed in this diff Show More