Compare commits

..

13 Commits

19 changed files with 605 additions and 190 deletions

View File

@ -1 +0,0 @@
428b

2
MusinAA/.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
.vscode/
*/tests/

148
MusinAA/docs/Report 1.ipynb Normal file
View File

@ -0,0 +1,148 @@
{
"cells": [
{
"cell_type": "markdown",
"id": "2acfa743",
"metadata": {},
"source": [
"# 0. Подготовим окружение"
]
},
{
"cell_type": "code",
"execution_count": 26,
"id": "4689b73e",
"metadata": {},
"outputs": [],
"source": [
"import sys\n",
"import os\n",
"sys.path.insert(0, os.path.abspath( '../task1'))\n",
"sys.path.insert(0, os.path.abspath( '../'))"
]
},
{
"cell_type": "markdown",
"id": "37cc11a5",
"metadata": {},
"source": [
"# 1. Генерация тестовых данных\n",
"\n",
"Создадим список records из N=10000 элементов. Каждый элемент — кортеж (name, phone). \n",
"Имена возъмём случайные из небольшого набора (чтобы были повторения и коллизии). \n",
"Для проверки влияния порядка подготовим два варианта: \n",
"\n",
"_records_shuffled_ — случайный порядок. \n",
"_records_sorted_ — отсортированный по имени (по алфавиту)."
]
},
{
"cell_type": "code",
"execution_count": 27,
"id": "a3b5c31b",
"metadata": {},
"outputs": [],
"source": [
"from util.randomNames import generate_test_data\n",
"from util.timeTester import test\n",
"\n",
"records_shuffled = generate_test_data(N=10000)\n",
"records_sorted = generate_test_data(N=10000, _sorted=True)"
]
},
{
"cell_type": "markdown",
"id": "c2f4989c",
"metadata": {},
"source": [
"# 2. Проведение замеров"
]
},
{
"cell_type": "code",
"execution_count": 28,
"id": "df12d41d",
"metadata": {},
"outputs": [],
"source": [
"# Подготовим функции СД, которые будем тестировать\n",
"from structures.LinkedList import *\n",
"from structures.HashTable import *\n",
"from structures.BinaryTree import *\n",
"\n",
"func_list = {\"Связанный список\" : (ll_insert, ll_find, ll_delete),\n",
" \"Хэш-таблица\" : (ht_insert, ht_find, ht_delete),\n",
" \"Бинарное дерево\" : (bst_insert, bst_find, bst_delete)}"
]
},
{
"cell_type": "code",
"execution_count": 29,
"id": "cc8d0436",
"metadata": {},
"outputs": [],
"source": [
"# Проведём замеры\n",
"report = [[\"Структура\", \"Режим\", \"Вставка\", \"Поиск\", \"Удаление\"]]\n",
"records = {\"Cлучайный\" : records_shuffled, \"Отсортированный\" : records_sorted}\n",
"\n",
"TEST_ITERATIONS_NUM = 5\n",
"\n",
"for _ in range(TEST_ITERATIONS_NUM):\n",
" for mode, data in records.items():\n",
" for struct_name, fns in func_list.items():\n",
" result = test(data, *fns)\n",
" row = [struct_name, mode,\n",
" result[\"insert_time\"],\n",
" result[\"find_time\"],\n",
" result[\"delete_time\"]]\n",
" report.append(row)"
]
},
{
"cell_type": "code",
"execution_count": 30,
"id": "2eedf056",
"metadata": {},
"outputs": [],
"source": [
"# Сохраним данные в csv\n",
"import csv\n",
"with open(\"data/results.csv\", \"w\", newline=\"\") as f:\n",
" writer = csv.writer(f)\n",
" writer.writerows(report)"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "8523ae48",
"metadata": {},
"outputs": [],
"source": [
"TODO проверить работает ли оно вообще"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.14.3"
}
},
"nbformat": 4,
"nbformat_minor": 5
}

View File

@ -0,0 +1,31 @@
Структура,Режим,Вставка,Поиск,Удаление
Связанный список,Cлучайный,4.661078881000321,0.012941928999680385,0.020117076999667916
Хэш-таблица,Cлучайный,0.18879180299973086,0.0014481220005109208,0.0012560499999381136
Бинарное дерево,Cлучайный,0.01616830700004357,0.00016560199946979992,0.00016564300040045055
Связанный список,Отсортированный,5.038275819999399,0.07141196100019442,0.1353478549999636
Хэш-таблица,Отсортированный,0.1631836659998953,0.003129734999674838,0.003202291999514273
Бинарное дерево,Отсортированный,0.16634428900033527,0.0017354540004816954,0.001874036000117485
Связанный список,Cлучайный,4.664316974999565,0.014653709000413073,0.02280332600003021
Хэш-таблица,Cлучайный,0.186777711999639,0.0010283499996148748,0.0009878339997158037
Бинарное дерево,Cлучайный,0.016256722999969497,0.00015175599946815055,0.0001649409996389295
Связанный список,Отсортированный,5.907060995999927,0.08458327099924645,0.12409427100010362
Хэш-таблица,Отсортированный,0.18781050100005814,0.003518858999996155,0.0038270310005827923
Бинарное дерево,Отсортированный,0.17500397699950554,0.0019018089997189236,0.001833940000324219
Связанный список,Cлучайный,5.755159846999959,0.016742109000006167,0.026386416000605095
Хэш-таблица,Cлучайный,0.22951744300007704,0.0009921219998432207,0.0011742059996322496
Бинарное дерево,Cлучайный,0.01580532200023299,0.00017768500038073398,0.00019722199976968113
Связанный список,Отсортированный,6.30150953500015,0.11176149099992472,0.1474957850005012
Хэш-таблица,Отсортированный,0.18557919999966543,0.003155624000100943,0.002713390000280924
Бинарное дерево,Отсортированный,0.19082545899982506,0.001985636999961571,0.002389950000178942
Связанный список,Cлучайный,6.395210606000546,0.014804241999627266,0.02304338900012226
Хэш-таблица,Cлучайный,0.1902105980007036,0.000852088000101503,0.0009626670007492066
Бинарное дерево,Cлучайный,0.01663886400001502,0.00016400000004068715,0.000184548000106588
Связанный список,Отсортированный,4.850914527999521,0.0771323629996914,0.1152741280002374
Хэш-таблица,Отсортированный,0.17607759100064868,0.002924628000073426,0.0033850670006358996
Бинарное дерево,Отсортированный,0.19345043999965128,0.0018585970001367969,0.0019752460002564476
Связанный список,Cлучайный,4.803303787999539,0.015972447000422108,0.0223228390004806
Хэш-таблица,Cлучайный,0.19020581800032232,0.0011616620004133438,0.0009839170006671338
Бинарное дерево,Cлучайный,0.016469425000650517,0.000160212000082538,0.00017693399968266021
Связанный список,Отсортированный,4.741838529000233,0.075463203000254,0.10462550600004761
Хэш-таблица,Отсортированный,0.16722737300005974,0.004002030999799899,0.005207103999964602
Бинарное дерево,Отсортированный,0.16575109800032806,0.0020921670002280734,0.002277146999404067
1 Структура Режим Вставка Поиск Удаление
2 Связанный список Cлучайный 4.661078881000321 0.012941928999680385 0.020117076999667916
3 Хэш-таблица Cлучайный 0.18879180299973086 0.0014481220005109208 0.0012560499999381136
4 Бинарное дерево Cлучайный 0.01616830700004357 0.00016560199946979992 0.00016564300040045055
5 Связанный список Отсортированный 5.038275819999399 0.07141196100019442 0.1353478549999636
6 Хэш-таблица Отсортированный 0.1631836659998953 0.003129734999674838 0.003202291999514273
7 Бинарное дерево Отсортированный 0.16634428900033527 0.0017354540004816954 0.001874036000117485
8 Связанный список Cлучайный 4.664316974999565 0.014653709000413073 0.02280332600003021
9 Хэш-таблица Cлучайный 0.186777711999639 0.0010283499996148748 0.0009878339997158037
10 Бинарное дерево Cлучайный 0.016256722999969497 0.00015175599946815055 0.0001649409996389295
11 Связанный список Отсортированный 5.907060995999927 0.08458327099924645 0.12409427100010362
12 Хэш-таблица Отсортированный 0.18781050100005814 0.003518858999996155 0.0038270310005827923
13 Бинарное дерево Отсортированный 0.17500397699950554 0.0019018089997189236 0.001833940000324219
14 Связанный список Cлучайный 5.755159846999959 0.016742109000006167 0.026386416000605095
15 Хэш-таблица Cлучайный 0.22951744300007704 0.0009921219998432207 0.0011742059996322496
16 Бинарное дерево Cлучайный 0.01580532200023299 0.00017768500038073398 0.00019722199976968113
17 Связанный список Отсортированный 6.30150953500015 0.11176149099992472 0.1474957850005012
18 Хэш-таблица Отсортированный 0.18557919999966543 0.003155624000100943 0.002713390000280924
19 Бинарное дерево Отсортированный 0.19082545899982506 0.001985636999961571 0.002389950000178942
20 Связанный список Cлучайный 6.395210606000546 0.014804241999627266 0.02304338900012226
21 Хэш-таблица Cлучайный 0.1902105980007036 0.000852088000101503 0.0009626670007492066
22 Бинарное дерево Cлучайный 0.01663886400001502 0.00016400000004068715 0.000184548000106588
23 Связанный список Отсортированный 4.850914527999521 0.0771323629996914 0.1152741280002374
24 Хэш-таблица Отсортированный 0.17607759100064868 0.002924628000073426 0.0033850670006358996
25 Бинарное дерево Отсортированный 0.19345043999965128 0.0018585970001367969 0.0019752460002564476
26 Связанный список Cлучайный 4.803303787999539 0.015972447000422108 0.0223228390004806
27 Хэш-таблица Cлучайный 0.19020581800032232 0.0011616620004133438 0.0009839170006671338
28 Бинарное дерево Cлучайный 0.016469425000650517 0.000160212000082538 0.00017693399968266021
29 Связанный список Отсортированный 4.741838529000233 0.075463203000254 0.10462550600004761
30 Хэш-таблица Отсортированный 0.16722737300005974 0.004002030999799899 0.005207103999964602
31 Бинарное дерево Отсортированный 0.16575109800032806 0.0020921670002280734 0.002277146999404067

View File

@ -0,0 +1,88 @@
"""
Двоичное дерево поиска
Узел словарь:
{'name': 'Имя', 'phone': '123', 'left': None, 'right': None}.
"""
def bst_insert(root: dict|None, name: str, phone: str) -> dict:
"""Итеративно вставляет, возвращает новый корень (если корень меняется)."""
if root == None:
return {'name': name, 'phone': phone, 'left': None, 'right': None}
# '674' < '722' == True, lol
current = root
while True:
if current['name'] == name:
current['phone'] = phone
return root
elif name < current['name']:
if current['left'] == None:
current['left'] = bst_insert(None, name, phone)
return root
else:
current = current['left']
else:
if current['right'] == None:
current['right'] = bst_insert(None, name, phone)
return root
else:
current = current['right']
# Увы, это самый лаконичный вариант, который я придумал.
def bst_find(root: dict|None, name: str) -> str|None:
"""Поиск в ширину."""
node = find_node_to_delete(root, name)
if node != None:
return node['phone']
def find_node_to_delete(root: dict|None, name: str) -> dict|None:
"""Поиск в ширину."""
while root != None:
if root['name'] == name:
return root
elif name < root['name']:
root = root['left']
else:
root = root['right']
return None
def find_minimal_child(root: dict) -> dict|None:
while root['left']:
root = root['left']
return root
def bst_delete(root: dict, name: str) -> None:
"""Удаляет узел и возвращает новый корень."""
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:
# Случай 1: нет детей или один ребенок
if root['left'] is None:
return root['right']
elif root['right'] is None:
return root['left']
# Случай 2: два ребенка
min_node = find_minimal_child(root['right'])
root['name'] = min_node['name']
root['phone'] = min_node['phone']
root['right'] = bst_delete(root['right'], min_node['name'])
return root
def bst_list_all(root: dict) -> list:
"""Центрированный обход.
Рекурсивно собирает записи в отсортированном порядке."""
if root is None:
return []
node_values = {"name": root['name'], "phone": root['phone']}
return bst_list_all(root['left']) + [node_values] + bst_list_all(root['right'])

View File

@ -0,0 +1,59 @@
"""
Хеш-таблица
Хранится как список buckets фиксированной длины,
каждый элемент голова связного списка (или None).
"""
from task1.structures.LinkedList import *
def hash_fun(name: str, size: int) -> int:
"""Принимает имя и возвращает индекс бакета для него."""
if size <= 0:
raise ValueError("size должен быть больше 0")
hashSum = 0
n = size+1
base = 1103 # ord('я')
for letter in name:
hashSum += ord(letter) * pow(base, n)
n -= 1
return int(hashSum) % size
def ht_insert(buckets: list|None, name: str, phone: str, blen:int = 50) -> list:
"""Возвращает новый массив бакетов
Вычисляет индекс, вызывает ll_insert для соответствующего бакета.
Функция не меняет размер массива бакетов автоматически!"""
if buckets == [] or buckets == None:
buckets = [None] * blen
# raise ValueError("Длинна buckets должна быть больше 0")
size = len(buckets)
index = hash_fun(name, size)
buckets[index] = ll_insert(buckets[index], name, phone)
return buckets
def ht_delete(buckets: list, name: str) -> list:
"""Возвращает новый массив бакетов без элемента с именем name"""
if buckets == []:
raise ValueError("Длинна buckets должна быть больше 0")
size = len(buckets)
index = hash_fun(name, size)
buckets[index] = ll_delete(buckets[index], name)
return buckets
def ht_find(buckets: list|None, name: str) -> str|None:
if buckets == [] or buckets == None:
raise ValueError("Длинна buckets должна быть больше 0")
size = len(buckets)
index = hash_fun(name, size)
return ll_find(buckets[index], name)
def ht_list_all(buckets):
"""Собирает все записи из всех бакетов и сортирует"""
allRecords = []
for bucket in buckets:
allRecords.extend(ll_list_all(bucket))
return sorted(allRecords, key=lambda x: x[0])

View File

@ -0,0 +1,60 @@
"""
Связный список (LinkedListPhoneBook)
Узел представляется словарём:
{'name': 'Имя', 'phone': '123', 'next': None}.
"""
def ll_insert(head : dict|None, name: str, phone: str) -> dict:
"""
Проходит до конца (или сразу добавляет в конец) и возвращает новую
голову (если вставка в начало) или изменяет список по ссылке.
Удобнее возвращать новую голову, если вставка может быть в начало.
"""
newNode = {'name': name, 'phone': phone, 'next': None}
if head == None:
return newNode
currentNode = head
while currentNode['next'] != None:
currentNode = currentNode['next']
currentNode['next'] = newNode
return head
def ll_find(head : dict|None, name: str) -> str|None:
"""Ищет узел, возвращает телефон или None."""
currentNode = head
while currentNode != None:
if currentNode['name'] == name:
return currentNode['phone']
currentNode = currentNode['next']
return None
def ll_delete(head : dict|None, name: str) -> dict|None:
"""Удаляет узел, возвращает новую голову."""
if head == None:
return None
if head['name'] == name:
return head['next']
currentNode = head
while currentNode['next'] != None:
if currentNode['next']['name'] == name:
currentNode['next'] = currentNode['next']['next']
return head
currentNode = currentNode['next']
return head
def ll_list_all(head: dict|None) -> list:
"""Cобирает все записи в список и сортирует.
сортировка вынесена отдельно)."""
records = []
currentNode = head
while currentNode != None:
records.append((currentNode['name'], currentNode['phone']))
currentNode = currentNode['next']
records.sort(key=lambda item: item[0])
return records

View File

@ -0,0 +1,55 @@
import random
names_pool = (
"Иван", "Мария", "Петр", "Анна", "Сергей", "Елена", "Алексей", "Ольга",
"Дмитрий", "Татьяна", "Михаил", "Наталья", "Андрей", "Ирина", "Николай",
"Светлана", "Владимир", "Екатерина", "Александр", "Юлия", "Павел", "Ксения",
"Виктор", "Анастасия", "Артем", "Виктория", "Максим", "Полина", "Даниил",
"София", "Евгений", "Алиса", "Станислав", "Дарья", "Георгий", "Вероника",
"Кирилл", "Маргарита", "Тимофей", "Арина", "Руфина", "Илларион", "Стелла",
"Роман", "Валерия", "Игорь", "Алина", "Олег", "Диана", "Юрий", "Милана",
"Василий", "Ева", "Никита", "Алиса", "Константин", "Кира", "Денис", "Ангелина",
"Вячеслав", "Мирослава", "Григорий", "Эмилия", "Леонид", "Василиса", "Руслан",
"Стефания", "Арсений", "Есения", "Антон", "Яна", "Матвей", "Любовь", "Семен",
"Надежда", "Федор", "Софья", "Лев", "Варвара", "Егор", "Амелия", "Борис",
"Агата", "Захар", "Камилла", "Давид", "Олеся", "Ярослав", "Людмила", "Данила",
"Регина", "Марк", "Каролина", "Артур", "Нелли", "Глеб", "Инна", "Платон",
"Нина", "Святослав", "Римма", "Родион", "Лидия", "Эдуард", "Жанна", "Вадим",
"Рената", "Савелий", "Алла", "Назар", "Снежана", "Демид", "Лариса", "Филипп",
"Злата", "Тимур", "Майя", "Клим", "Эльвира", "Дамир", "Таисия", "Илья",
"Роза", "Виталий", "Азалия", "Степан", "Лиана", "Богдан", "Инесса", "Эрик",
"Ариана", "Алан", "Юлиана", "Лука", "Антонина", "Мирон", "Клавдия", "Гордей",
"Руслана", "Макар", "Елизавета", "Северин", "Александра", "Моисей", "Агафья",
"Наум", "Серафима", "Влад", "Фаина", "Кузьма", "Пелагея", "Ермак", "Ульяна",
"Тарас", "Марианна", "Остап", "Бронислава", "Архип", "Владислава", "Фома",
"Станислава", "Еремей", "Зинаида", "Прохор", "Раиса", "Мстислав", "Галина",
"Ростислав", "Валентина", "Серафим", "Евдокия", "Лаврентий", "Кристина",
"Никон", "Анфиса", "Феликс", "Лия", "Иннокентий", "Роксана", "Всеволод",
"Эвелина", "Модест", "Юнона", "Трофим", "Изабелла", "Аполлон", "Глория",
"Касьян", "Аврора", "Любомир", "Адель", "Бронислав", "Доминика", "Афанасий",
"Фрида", "Евстафий", "Ассоль", "Венедикт", "Цветана", "Епифан", "Мелисса",
"Добрыня"
)
_non_existent_names = [
"Ноль", "Целковый", "Полушка", "Четвертушка", "Осьмушка",
"Пудовичок", "Медячок", "Серебрячок", "Золотничок", "Девятичок"
]
assert set(names_pool).isdisjoint(set(_non_existent_names)), \
"В списке несуществующих имён существуют существующие имена сущностей"
names_pool_to_find = random.choices(names_pool, k=100) + _non_existent_names
def generate_phone(phone_len=11) -> str:
# 88005553535
return str(random.randint(10**phone_len, 10**(phone_len+1)-1))
def generate_test_data(N=10000, _sorted=False):
records = []
for i in range(N):
name = random.choice(names_pool)
phone = generate_phone()
records.append((name, phone))
if _sorted:
return sorted(records)
return records

View File

@ -0,0 +1,37 @@
import time
import random
from typing import Callable, Any
from task1.util.randomNames import names_pool_to_find, names_pool
def test(records: list,
insert_func: Callable[[Any, str, str], Any],
find_func: Callable[[Any, str], Any],
delete_func: Callable[[Any, str], Any]) -> dict:
data = None
# Вставка всех записей
start = time.perf_counter()
for item in records:
data = insert_func(data, item[0], item[1])
end = time.perf_counter()
insert_time = end - start
# Поиск 110 случайных записей
start = time.perf_counter()
for name in names_pool_to_find:
find_func(data, name)
end = time.perf_counter()
find_time = end - start
# Удаление 50 случайных записей
start = time.perf_counter()
for name in random.choices(names_pool, k = 50):
data = delete_func(data, name)
end = time.perf_counter()
delete_time = end - start
return {
"insert_time" : insert_time ,
"find_time" : find_time ,
"delete_time": delete_time
}

View File

@ -0,0 +1,72 @@
from abc import ABC, abstractmethod
from itertools import product
import sys
from task2.mazeObjects.maze import Maze
from task2.mazeObjects.cell import Cell
class MazeBuilder(ABC):
"""Интерфейс MazeBuilder с методом buildFromFile(filename)"""
@abstractmethod
def buildFromFile(self, filename: str):
"""Создание лабиринта из файла."""
class TextFileMazeBuilder(MazeBuilder):
"""Читает файл, парсит символы,
создаёт объекты Cell,
задаёт координаты и флаги,
после чего возвращает готовый Maze."""
start = {'x': 0, 'y': 0}
end = {'x': 0, 'y': 0}
def cellStrategy(self, letter: str) -> Cell:
if letter == '#':
return Cell(isWall=True)
elif letter == ' ':
return Cell()
elif letter == 'S':
return Cell(isStart=True)
elif letter == 'E':
return Cell(isExit=True)
else:
sys.stderr.write(f"Неизвестный символ '{letter}' при загрузке из файла\n")
return Cell()
def updateStartEnd(self, letter: str, x:int, y:int) -> None:
if letter == 'S':
self.start = {'x': x, 'y': y}
elif letter == 'E':
self.end = {'x': x, 'y': y}
def generate_row_from_txt(self, filename: str) -> list[str]:
with open(filename) as file:
text = file.read()
text = text.strip()
if not text:
raise ValueError(f"Файл \"{filename}\" пуст")
text = text.split('\n')
return text
def buildFromFile(self, filename: str):
rows = self.generate_row_from_txt(filename)
height = len(rows)
width = len(rows[0])
array = [[Cell() for j in range(width)] for i in range(height)]
# Здесь x и y где-то перепутаны, но мне лень это чинить
try:
for x, y in product(range(width), range(height)):
cell = self.cellStrategy(rows[y][x])
self.updateStartEnd(rows[y][x], x, y)
cell.x = x
cell.y = y
array[y][x] = cell
except IndexError:
raise ValueError(f"Строка {x+1} имеет длину {len(rows[x])}, ожидалось {width}")
return Maze(array, self.start, self.end)

View File

View File

@ -0,0 +1,13 @@
class Cell:
"""Хранит координаты (x, y)
флаги isWall, isStart, isExit
метод isPassable() (возвращает True для прохода, если не стена)."""
def __init__(self, x: int = 0, y: int = 0, isWall:bool = False, isStart:bool = False, isExit:bool = False):
self.x = x
self.y = y
self.isWall = isWall
self.isStart = isStart
self.isExit = isExit
def isPassable(self):
return not self.isWall

View File

@ -0,0 +1,40 @@
from task2.mazeObjects.cell import Cell
class Maze:
"""Хранит двумерный массив клеток,
ширину, высоту, ссылки на стартовую и выходную клетку.
Методы:
getCell(x, y), getNeighbors(cell) возвращает список соседних проходимых клеток
(вверх, вниз, влево, вправо, если в пределах границ и не стена)."""
def __init__(self, mazeArray: list[list[Cell]], start: dict, end: dict) -> None:
self.mazeArray = mazeArray
self.width = len(mazeArray)
self.height = len(mazeArray[0])
self.startCell = self.getCell(start['x'], start['y'])
self.endCell = self.getCell(end['x'], end['y'])
def getCell(self, x: int, y: int):
return self.mazeArray[y][x]
def checkCell(self, x: int, y: int):
if not(0 <= x and x < self.width):
return False
if not(0 <= y and y < self.height):
return False
return self.getCell(x, y).isPassable()
def getNeighbors(self, cell: Cell):
point = (cell.x, cell.y)
offsets = ((0, 1),
(0, -1),
(-1, 0),
(1, 0))
passableCells = []
for ofst in offsets:
x = point[0]+ofst[0]
y = point[1]+ofst[1]
if self.checkCell(x, y):
passableCells.append(self.getCell(x, y))
return passableCells

183
README.md
View File

@ -197,186 +197,3 @@ with open("results.csv", "w", newline="") as f:
- Как удаление работает в каждой структуре.
* Вывод должен содержать ответ на вопрос: какую структуру и для каких задач (частые вставки, частый поиск, необходимость получать данные в порядке) стоит выбирать в реальной жизни.*
## Задание: Поиск выхода из лабиринта (объектно-ориентированная реализация с паттернами)
### Цель работы
Разработать гибкую, расширяемую программу для загрузки лабиринта из файла, поиска пути от старта до выхода с возможностью выбора алгоритма, визуализации процесса и экспериментального сравнения алгоритмов. В ходе работы необходимо применить минимум 3 паттерна проектирования из списка GoF, обосновать их выбор и продемонстрировать преимущества такой архитектуры.
### Общая схема приложения (пример)
```mermaid
classDiagram
class Maze {
-Cell[] cells
-int width, height
-Cell start
-Cell exit
+getCell(x,y): Cell
+getNeighbors(cell): List~Cell~
}
class Cell {
-int x, y
-bool isWall
-bool isStart
-bool isExit
+isPassable(): bool
}
class MazeBuilder {
<<interface>>
+buildFromFile(filename): Maze
}
class TextFileMazeBuilder {
+buildFromFile(filename): Maze
}
class PathFindingStrategy {
<<interface>>
+findPath(maze, start, exit): List~Cell~
}
class BFSStrategy
class DFSStrategy
class AStarStrategy
class DijkstraStrategy
class SearchStats {
+timeMs: float
+visitedCells: int
+pathLength: int
}
class MazeSolver {
-Maze maze
-PathFindingStrategy strategy
+setStrategy(strategy)
+solve(): SearchStats
}
class Command {
<<interface>>
+execute()
+undo()
}
class MoveCommand {
-Player player
-Direction dir
-Cell previousCell
+execute()
+undo()
}
class Player {
-Cell currentCell
+moveTo(cell)
}
class Observer {
<<interface>>
+update(event)
}
class ConsoleView {
+update(event)
+render(maze, player, path)
}
MazeBuilder <|.. TextFileMazeBuilder
MazeBuilder --> Maze : creates
PathFindingStrategy <|.. BFSStrategy
PathFindingStrategy <|.. DFSStrategy
PathFindingStrategy <|.. AStarStrategy
PathFindingStrategy <|.. DijkstraStrategy
MazeSolver --> PathFindingStrategy : uses
MazeSolver --> Maze : uses
Command <|.. MoveCommand
MoveCommand --> Player
Player --> Cell
Observer <|.. ConsoleView
MazeSolver --> Observer : notifies
```
### Выполнение
#### Этап 1. Модель лабиринта (без паттернов, просто классы)
**Задача:** Создать классы `Cell` и `Maze`, которые представляют карту лабиринта.
- `Cell` хранит координаты (x, y), флаги `isWall`, `isStart`, `isExit`, метод `isPassable()` (возвращает `True` для прохода, если не стена).
- `Maze` хранит двумерный массив клеток, ширину, высоту, ссылки на стартовую и выходную клетку. Методы: `getCell(x, y)`, `getNeighbors(cell)` возвращает список соседних проходимых клеток (вверх, вниз, влево, вправо, если в пределах границ и не стена).
**Результат:** Лабиринт можно создать вручную в коде, но загрузку пока не делаем.
#### Этап 2. Загрузка лабиринта из файла применение паттерна **Builder**
**Задача:** Реализовать загрузку лабиринта из текстового файла, где `#` стена, ` ` (пробел) проход, `S` старт, `E` выход.
- Создать интерфейс `MazeBuilder` с методом `buildFromFile(filename)`.
- Реализовать класс `TextFileMazeBuilder`, который читает файл, парсит символы, создаёт объекты `Cell`, задаёт координаты и флаги, после чего возвращает готовый `Maze`.
Процесс построения лабиринта сложный (парсинг, валидация, установка старта/выхода). Builder скрывает детали создания от клиента. В будущем можно легко добавить другой формат (например, JSON или бинарный) через новую реализацию `MazeBuilder`.
#### Этап 3. Стратегии поиска пути паттерн **Strategy**
**Задача:** Реализовать семейство алгоритмов поиска пути от старта до выхода.
- Создать интерфейс `PathFindingStrategy` с методом `findPath(maze, start, exit)`, возвращающим список клеток пути (от старта до выхода включительно) или пустой список, если пути нет.
- Реализовать минимум 3 стратегии:
- **BFS** (поиск в ширину) гарантирует кратчайший путь по количеству шагов.
- **DFS** (поиск в глубину) быстрый, но не обязательно кратчайший.
- **A*** (с эвристикой, например, манхэттенское расстояние) компромисс между скоростью и оптимальностью.
- (Опционально) **Дейкстра** полезна для взвешенных лабиринтов, но в базовом варианте все шаги имеют вес 1, тогда она совпадает с BFS.
Каждая стратегия возвращает путь. Для BFS/DFS используйте очередь/стек, для A* приоритетную очередь (heapq). Важно: алгоритмы не должны модифицировать сам лабиринт, только читать состояние клеток.
Strategy позволяет легко переключать алгоритмы во время выполнения, не меняя код остальной программы. Новый алгоритм можно добавить, реализовав интерфейс.
#### Этап 4. Класс-оркестратор **MazeSolver** (использует Strategy)
**Задача:** Создать класс, который принимает лабиринт и стратегию, выполняет поиск и собирает статистику.
- `MazeSolver` содержит поля `maze` и `strategy`.
- Метод `setStrategy(strategy)` для динамической смены алгоритма.
- Метод `solve()` вызывает `strategy.findPath(...)` и возвращает объект `SearchStats` (время выполнения в миллисекундах, количество посещённых клеток, длина найденного пути).
- Для замера времени используйте `time.perf_counter()` до и после вызова стратегии.
#### Этап 5. Визуализация и пошаговое управление паттерны **Observer** и **Command** (по желанию)
**5.1. Наблюдатель (Observer)** обновление консольного интерфейса.
- Создать интерфейс `Observer` с методом `update(event)`, где `event` может быть строкой или объектом с типом события (`"path_found"`, `"move"`, `"maze_loaded"`).
- Реализовать класс `ConsoleView`, который отображает лабиринт, текущее положение игрока (если реализован пошаговый режим) и найденный путь. Метод `render(maze, player_position, path)` рисует карту в консоли.
- `MazeSolver` (или отдельный контроллер) может иметь список наблюдателей и уведомлять их при изменении состояния.
**5.2. Команда (Command)** для пошагового перемещения игрока по найденному пути (или ручного управления).
- Создать интерфейс `Command` с методами `execute()` и `undo()`.
- Реализовать `MoveCommand`, который принимает игрока (`Player`), направление и изменяет его позицию, сохраняя предыдущую для отмены.
- Создать класс `Player`, хранящий текущую клетку.
- Консольное меню позволяет вводить команды (W/A/S/D), выполнять `MoveCommand`, при необходимости отменять последний ход (Ctrl+Z). Это опционально, но очень наглядно демонстрирует паттерн.
*Observer можно реализовать только для вывода сообщений о начале/конце поиска, а Command для демонстрации undo при ручном исследовании лабиринта.*
#### Этап 6. Экспериментальная часть (аналогично заданию со структурами данных)
**Задача:** Сравнить эффективность реализованных стратегий на лабиринтах разной сложности.
1. **Подготовка тестовых лабиринтов:**
- Маленький (10×10) с простым путём.
- Средний (50×50) с тупиками.
- Большой (100×100) с запутанной структурой.
- «Пустой» лабиринт (без стен) для демонстрации максимальной производительности.
- «Без выхода» чтобы проверить обработку отсутствия пути.
2. **Замеры:**
- Для каждого лабиринта и каждой стратегии запустить `solve()` 510 раз, усреднить время, количество посещённых клеток, длину пути.
- Записать результаты в CSV: `лабиринт,стратегия,время_мс,посещено_клеток,длина_пути`.
3. **Анализ:**
- Построить графики для каждого лабиринта.
- Проанализировать и написать выводы по итогам (эффективность того или иного алгоритма в разных случаях).
4. **Дополнительное задание:** Реализовать взвешенные клетки (например, болото вес 3, песок вес 2, асфальт вес 1) и сравнить Дейкстру с A* на взвешенном графе.
#### Этап 7. Отчёт
**Структура отчёта:**
1. Описание задачи и выбранных паттернов (с диаграммой классов из Mermaid).
2. Листинги ключевых классов (можно выборочно) или ссылка на репозиторий.
3. Результаты экспериментов (таблицы, графики).
4. Анализ эффективности алгоритмов и применимости паттернов.
5. Выводы: как ООП и паттерны помогли сделать код гибким и расширяемым. Что было бы сложно изменить без них.
### Советы
- Для A* самая простая эвристика: `abs(x1 - x2) + abs(y1 - y2)`.
- При поиске пути надо хранить предшественников (`parent` для каждой посещённой клетки), чтобы восстановить путь.
- Для BFS/DFS используй `deque` (очередь) и `list` (стек).
- Визуализацию в консоли можно сделать с помощью `os.system('cls' if os.name == 'nt' else 'clear')` для перерисовки.

View File

@ -1,6 +0,0 @@
{\rtf1\ansi\ansicpg1251\cocoartf2869
\cocoatextscaling0\cocoaplatform0{\fonttbl}
{\colortbl;\red255\green255\blue255;}
{\*\expandedcolortbl;;}
\paperw11900\paperh16840\margl1440\margr1440\vieww11520\viewh8400\viewkind0
}