Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b28de24dc0 | |||
| a66cac23d8 | |||
| b4b8bf522d | |||
| 3b7977e538 |
1
.gitignore
vendored
|
|
@ -7,6 +7,7 @@ __pycache__/
|
||||||
# C extensions
|
# C extensions
|
||||||
*.so
|
*.so
|
||||||
|
|
||||||
|
.DS_Store
|
||||||
# Distribution / packaging
|
# Distribution / packaging
|
||||||
.Python
|
.Python
|
||||||
build/
|
build/
|
||||||
|
|
|
||||||
0
BudakovIS/428.md
Normal file
0
DerbenevRY/428.md
Normal file
1
KuzminskiyAA/427.md
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
|
||||||
0
KuznetsovAS/427.md
Normal file
0
MashinDD/429.txt
Normal file
1
MininaVD/427.txt
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
427.txt
|
||||||
1
MininaVD/MininaVD
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
427.txt
|
||||||
0
MusinAA/428b.md
Normal file
1
MylnikovAS/427.md
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
|
||||||
BIN
ProninVV/427.md
Normal file
BIN
ProninVV/file.txt
Normal file
149
README.md
|
|
@ -16,7 +16,7 @@
|
||||||
|
|
||||||
### Крайний срок приема работ 25.05.2026 до 14:00
|
### Крайний срок приема работ 25.05.2026 до 14:00
|
||||||
|
|
||||||
## Задание 1 -- репозиторий
|
## Задание 0 -- репозиторий [отдельный срок на создание PR с папкой: 28.02.2026]
|
||||||
|
|
||||||
0. Создай пользователя (логин — фамилия+инициалы слитно транслитом, как в терминал-классе).
|
0. Создай пользователя (логин — фамилия+инициалы слитно транслитом, как в терминал-классе).
|
||||||
|
|
||||||
|
|
@ -43,12 +43,157 @@
|
||||||
|
|
||||||
6. Отправь ветку **в свой форк** на Gitea:
|
6. Отправь ветку **в свой форк** на Gitea:
|
||||||
```bash
|
```bash
|
||||||
git push origin IvanovII
|
git push origin
|
||||||
```
|
```
|
||||||
|
|
||||||
|
если просит, перед этим сделать git push --set-upstream origin
|
||||||
|
|
||||||
7. **Создай запрос на слияние (Pull Request):** На Gitea перейди в свой форк, выбери ветку `IvanovII`, нажмите **Запрос на слияние**. Убедитесь, что:
|
7. **Создай запрос на слияние (Pull Request):** На Gitea перейди в свой форк, выбери ветку `IvanovII`, нажмите **Запрос на слияние**. Убедитесь, что:
|
||||||
- Базовый репозиторий: **учебный** (преподавателя)
|
- Базовый репозиторий: **учебный** (преподавателя)
|
||||||
- Базовая ветка: **develop**
|
- Базовая ветка: **develop**
|
||||||
- Сравниваемая ветка: **свой форк / IvanovII**
|
- Сравниваемая ветка: **свой форк / IvanovII**
|
||||||
|
|
||||||
8. Отправь PR.
|
8. Отправь PR.
|
||||||
|
|
||||||
|
## Задание 1 -- структуры данных
|
||||||
|
***Напоминание: под каждое задание вы создаете отдельную ветку***
|
||||||
|
|
||||||
|
>Для оформления результатов заведи папку **docs** в своей папке и сохраняй туда отчет (в любом формате от .doc до .md, а то и .jpnb). Вспомогательные файлы клади в подпапку **data** внутри **docs**
|
||||||
|
|
||||||
|
**Цель работы**
|
||||||
|
|
||||||
|
Реализовать три различные структуры данных «с нуля», применить их для хранения записей телефонного справочника и экспериментально сравнить производительность основных операций. Вы должны собственными руками написать код, чтобы понять внутреннее устройство связного списка, хеш-таблицы и двоичного дерева поиска, а также осознать их сильные и слабые стороны на практике.
|
||||||
|
|
||||||
|
**!! Задание выполнять в структурной (процедурной) парадигме, не используя классы. Главное реализовать структуры данных «руками» и сравнить их производительность.**
|
||||||
|
|
||||||
|
### Базовые операции (обязательны для всех):
|
||||||
|
|
||||||
|
`insert(name, phone)` -- добавить или обновить запись.
|
||||||
|
|
||||||
|
`find(name)` -- phone или None.
|
||||||
|
|
||||||
|
`delete(name)` -- удалить запись, игнорировать отсутствие.
|
||||||
|
|
||||||
|
`list_all()` -- список всех записей, отсортированный по имени (для BST in‑order обход; для списка и хеш‑таблицы — собрать и отсортировать явно).
|
||||||
|
|
||||||
|
#### 1. Связный список (LinkedListPhoneBook)
|
||||||
|
|
||||||
|
Узел представляется словарём: `{'name': 'Имя', 'phone': '123', 'next': None}.`
|
||||||
|
|
||||||
|
**Функции:**
|
||||||
|
|
||||||
|
`def ll_insert(head, name, phone)` — проходит до конца (или сразу добавляет в конец) и возвращает новую голову (если вставка в начало) или изменяет список по ссылке. Удобнее возвращать новую голову, если вставка может быть в начало.
|
||||||
|
|
||||||
|
`def ll_find(head, name)` — ищет узел, возвращает телефон или None.
|
||||||
|
|
||||||
|
`def ll_delete(head, name)` — удаляет узел, возвращает новую голову.
|
||||||
|
|
||||||
|
`def ll_list_all(head)` — собирает все записи в список и сортирует (сортировка вынесена отдельно).
|
||||||
|
|
||||||
|
#### 2. Хеш-таблица
|
||||||
|
Хранится как список buckets фиксированной длины, каждый элемент — голова связного списка (или None).
|
||||||
|
|
||||||
|
**Функции:**
|
||||||
|
|
||||||
|
`def ht_insert(buckets, name, phone)` — вычисляет индекс, вызывает ll_insert для соответствующего бакета.
|
||||||
|
|
||||||
|
Аналогично `ht_find, ht_delete, ht_list_all` (последняя собирает все записи из всех бакетов и сортирует).
|
||||||
|
|
||||||
|
#### 3. Двоичное дерево поиска
|
||||||
|
Узел — словарь: `{'name': 'Имя', 'phone': '123', 'left': None, 'right': None}.`
|
||||||
|
|
||||||
|
**Функции:**
|
||||||
|
|
||||||
|
`def bst_insert(root, name, phone)` — рекурсивно или итеративно вставляет, возвращает новый корень (если корень меняется).
|
||||||
|
|
||||||
|
`def bst_find(root, name)` — поиск.
|
||||||
|
|
||||||
|
`def bst_delete(root, name)` — удаление, возвращает новый корень.
|
||||||
|
|
||||||
|
`def bst_list_all(root)` — центрированный обход (рекурсивно собирает записи в отсортированном порядке).
|
||||||
|
|
||||||
|
### Экспериментальная часть (подробно об измерении времени)
|
||||||
|
#### 1. Генерация тестовых данных
|
||||||
|
Создайте список records из N элементов (например, N = 10000). Каждый элемент — кортеж (name, phone).
|
||||||
|
|
||||||
|
Имена генерируйте как `f"User_{i:05d}"` (равномерное распределение) или случайные слова из небольшого набора (чтобы были повторения и коллизии). Для проверки влияния порядка подготовьте два варианта одного и того же набора:
|
||||||
|
|
||||||
|
`records_shuffled` — случайный порядок.
|
||||||
|
|
||||||
|
`records_sorted` — отсортированный по имени (по алфавиту).
|
||||||
|
|
||||||
|
#### 2. Инструменты замера времени
|
||||||
|
Используйте модуль **time**:
|
||||||
|
|
||||||
|
```python
|
||||||
|
import time
|
||||||
|
|
||||||
|
start = time.perf_counter()
|
||||||
|
# ... операции ...
|
||||||
|
end = time.perf_counter()
|
||||||
|
elapsed = end - start # время в секундах
|
||||||
|
```
|
||||||
|
|
||||||
|
Для многократных замеров удобен `timeit`, но в этой задаче достаточно просто обернуть код в цикл и усреднить.
|
||||||
|
|
||||||
|
#### 3. Проведение замеров
|
||||||
|
Для каждой структуры данных и для каждого режима входных данных (случайный / отсортированный) выполните:
|
||||||
|
|
||||||
|
- А. Вставка всех записей
|
||||||
|
|
||||||
|
Создайте пустую структуру.
|
||||||
|
|
||||||
|
Засеките время, выполните insert для каждой записи из входного списка.
|
||||||
|
|
||||||
|
Зафиксируйте общее время вставки.
|
||||||
|
|
||||||
|
- Б. Поиск 100 случайных записей
|
||||||
|
|
||||||
|
Возьмите 100 случайных имён из того же набора (гарантированно существующих) и 10 имён, которых нет (например, "None_{i}").
|
||||||
|
|
||||||
|
Засеките время на выполнение всех 110 вызовов find.
|
||||||
|
|
||||||
|
- В. Удаление 50 случайных записей
|
||||||
|
|
||||||
|
Выберите 50 случайных имён из набора.
|
||||||
|
|
||||||
|
Засеките время на выполнение delete для каждого.
|
||||||
|
|
||||||
|
|
||||||
|
**!! Важно: после вставки структура остаётся заполненной, поиск и удаление выполняются на ней же. Если нужно повторить замер для другого порядка данных — создавайте новую структуру и заполняйте заново.**
|
||||||
|
|
||||||
|
#### 4. Сохранение результатов
|
||||||
|
|
||||||
|
**!! Каждый эксперимент повторить минимум 5 раз и записывать и среднее время, и все замеры.**
|
||||||
|
|
||||||
|
Соберите все замеры в словарь или список, затем сохраните в CSV-файл:
|
||||||
|
|
||||||
|
```python
|
||||||
|
import csv
|
||||||
|
|
||||||
|
results = [
|
||||||
|
["Структура", "Режим", "Операция", "Время (сек)"],
|
||||||
|
["LinkedList", "случайный", "вставка", 0.123],
|
||||||
|
...
|
||||||
|
]
|
||||||
|
|
||||||
|
with open("results.csv", "w", newline="") as f:
|
||||||
|
writer = csv.writer(f)
|
||||||
|
writer.writerows(results)
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
#### 5. Анализ результатов
|
||||||
|
Постройте график (столбчатая диаграмма или линейный график) — можно в Excel, Google Sheets или с помощью matplotlib в Python.
|
||||||
|
|
||||||
|
Сравните:
|
||||||
|
|
||||||
|
- Как порядок входных данных влияет на скорость вставки в BST (деградация до O(n) на отсортированных данных).
|
||||||
|
|
||||||
|
- Почему хеш-таблица почти не чувствительна к порядку.
|
||||||
|
|
||||||
|
- Почему связный список всегда медленен при поиске.
|
||||||
|
|
||||||
|
- Как удаление работает в каждой структуре.
|
||||||
|
|
||||||
|
* Вывод должен содержать ответ на вопрос: какую структуру и для каких задач (частые вставки, частый поиск, необходимость получать данные в порядке) стоит выбирать в реальной жизни.*
|
||||||
0
SimonovaMS/428.md
Normal file
0
SimonovaMS/428.txt
Normal file
0
SolovevDS/428b.md
Normal file
1
VaravinVV/428b
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
428b
|
||||||
0
VolkovVA/428b.md
Normal file
0
YanyaevAA/428b.md
Normal file
1
YaroslavtsevAS/428.md
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
428
|
||||||
0
ZelentsovAV/428b.md
Normal file
0
anikinvd/428.md
Normal file
0
chizhikovaSM/428.md
Normal file
0
duznb/429.md.txt
Normal file
1
filippovavm/427
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
427
|
||||||
0
fomichevks/426.md.txt
Normal file
1
ivanchenkoam/427.txt
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
856
|
||||||
0
ivantsovma/428.txt
Normal file
0
kolesovve/427.md
Normal file
BIN
komissarovgo/427.md
Normal file
1
konnovaea/429
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
429
|
||||||
0
kornevma/426.md
Normal file
0
krasnovia/429.txt
Normal file
0
lomakinae/426
Normal file
0
meosyam/428.md.txt
Normal file
2
morozovns/1.py
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
print("Zadanie adin!!11!adin!11!")
|
||||||
|
print("patch")
|
||||||
1
morozovns/429
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
429
|
||||||
1
nehoroshevaa/428b.md
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
428b
|
||||||
BIN
nikolaevda/427.md
Normal file
0
novikovsd/428
Normal file
0
osininyai/427.md
Normal file
0
petryaninyas/426.md
Normal file
18
petryaninyas/task1/README.md
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
# Задание 1 — структуры данных
|
||||||
|
|
||||||
|
Процедурная реализация:
|
||||||
|
- linked_list.py
|
||||||
|
- hash_table.py
|
||||||
|
- bst.py
|
||||||
|
|
||||||
|
Эксперименты и отчёты:
|
||||||
|
- experiments.py
|
||||||
|
- plot_results.py
|
||||||
|
- results.csv
|
||||||
|
- docs/report.md
|
||||||
|
- docs/data/*.png
|
||||||
|
|
||||||
|
Запуск:
|
||||||
|
```bash
|
||||||
|
python main.py
|
||||||
|
```
|
||||||
118
petryaninyas/task1/bst.py
Normal file
|
|
@ -0,0 +1,118 @@
|
||||||
|
from typing import Any, Dict, List, Optional
|
||||||
|
|
||||||
|
|
||||||
|
Node = Dict[str, Any]
|
||||||
|
|
||||||
|
|
||||||
|
def _make_node(name: str, phone: str) -> Node:
|
||||||
|
return {"name": name, "phone": phone, "left": None, "right": None}
|
||||||
|
|
||||||
|
|
||||||
|
def bst_insert(root: Optional[Node], name: str, phone: str) -> Node:
|
||||||
|
new_node = _make_node(name, phone)
|
||||||
|
|
||||||
|
if root is None:
|
||||||
|
return new_node
|
||||||
|
|
||||||
|
current = root
|
||||||
|
parent = None
|
||||||
|
|
||||||
|
while current is not None:
|
||||||
|
parent = current
|
||||||
|
if name < current["name"]:
|
||||||
|
current = current["left"]
|
||||||
|
elif name > current["name"]:
|
||||||
|
current = current["right"]
|
||||||
|
else:
|
||||||
|
current["phone"] = phone
|
||||||
|
return root
|
||||||
|
|
||||||
|
if name < parent["name"]:
|
||||||
|
parent["left"] = new_node
|
||||||
|
else:
|
||||||
|
parent["right"] = new_node
|
||||||
|
|
||||||
|
return root
|
||||||
|
|
||||||
|
|
||||||
|
def bst_find(root: Optional[Node], name: str) -> Optional[str]:
|
||||||
|
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 _find_min_node(node: Node) -> Node:
|
||||||
|
current = node
|
||||||
|
while current["left"] is not None:
|
||||||
|
current = current["left"]
|
||||||
|
return current
|
||||||
|
|
||||||
|
|
||||||
|
def bst_delete(root: Optional[Node], name: str) -> Optional[Node]:
|
||||||
|
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:
|
||||||
|
child = current["left"] if current["left"] is not None else current["right"]
|
||||||
|
|
||||||
|
if parent is None:
|
||||||
|
return child
|
||||||
|
|
||||||
|
if parent["left"] is current:
|
||||||
|
parent["left"] = child
|
||||||
|
else:
|
||||||
|
parent["right"] = child
|
||||||
|
return root
|
||||||
|
|
||||||
|
succ_parent = current
|
||||||
|
successor = current["right"]
|
||||||
|
while successor["left"] is not None:
|
||||||
|
succ_parent = successor
|
||||||
|
successor = successor["left"]
|
||||||
|
|
||||||
|
current["name"] = successor["name"]
|
||||||
|
current["phone"] = successor["phone"]
|
||||||
|
|
||||||
|
successor_child = successor["right"]
|
||||||
|
if succ_parent["left"] is successor:
|
||||||
|
succ_parent["left"] = successor_child
|
||||||
|
else:
|
||||||
|
succ_parent["right"] = successor_child
|
||||||
|
|
||||||
|
return root
|
||||||
|
|
||||||
|
|
||||||
|
def bst_list_all(root: Optional[Node]) -> List[Dict[str, str]]:
|
||||||
|
result: List[Dict[str, str]] = []
|
||||||
|
stack: List[Node] = []
|
||||||
|
current = root
|
||||||
|
|
||||||
|
while current is not None or stack:
|
||||||
|
while current is not None:
|
||||||
|
stack.append(current)
|
||||||
|
current = current["left"]
|
||||||
|
|
||||||
|
current = stack.pop()
|
||||||
|
result.append({"name": current["name"], "phone": current["phone"]})
|
||||||
|
current = current["right"]
|
||||||
|
|
||||||
|
return result
|
||||||
BIN
petryaninyas/task1/docs/data/delete.png
Normal file
|
After Width: | Height: | Size: 64 KiB |
BIN
petryaninyas/task1/docs/data/find.png
Normal file
|
After Width: | Height: | Size: 64 KiB |
BIN
petryaninyas/task1/docs/data/insert.png
Normal file
|
After Width: | Height: | Size: 60 KiB |
109
petryaninyas/task1/docs/data/results.csv
Normal file
|
|
@ -0,0 +1,109 @@
|
||||||
|
Структура,Режим,Операция,Замер,Время (сек)
|
||||||
|
LinkedList,случайный,insert,1,4.2622492010
|
||||||
|
LinkedList,случайный,find,1,0.0314994130
|
||||||
|
LinkedList,случайный,delete,1,0.0149069000
|
||||||
|
LinkedList,случайный,insert,2,4.0154580330
|
||||||
|
LinkedList,случайный,find,2,0.0393284500
|
||||||
|
LinkedList,случайный,delete,2,0.0210732100
|
||||||
|
LinkedList,случайный,insert,3,4.0436019780
|
||||||
|
LinkedList,случайный,find,3,0.0344933660
|
||||||
|
LinkedList,случайный,delete,3,0.0152639850
|
||||||
|
LinkedList,случайный,insert,4,3.7182993220
|
||||||
|
LinkedList,случайный,find,4,0.0327698850
|
||||||
|
LinkedList,случайный,delete,4,0.0149959540
|
||||||
|
LinkedList,случайный,insert,5,3.7082228200
|
||||||
|
LinkedList,случайный,find,5,0.0303762490
|
||||||
|
LinkedList,случайный,delete,5,0.0141406560
|
||||||
|
LinkedList,случайный,insert,среднее,3.9495662708
|
||||||
|
LinkedList,случайный,find,среднее,0.0336934726
|
||||||
|
LinkedList,случайный,delete,среднее,0.0160761410
|
||||||
|
HashTable,случайный,insert,1,0.2059865770
|
||||||
|
HashTable,случайный,find,1,0.0014966100
|
||||||
|
HashTable,случайный,delete,1,0.0006891700
|
||||||
|
HashTable,случайный,insert,2,0.2024331460
|
||||||
|
HashTable,случайный,find,2,0.0015934880
|
||||||
|
HashTable,случайный,delete,2,0.0007212620
|
||||||
|
HashTable,случайный,insert,3,0.2126128040
|
||||||
|
HashTable,случайный,find,3,0.0016566220
|
||||||
|
HashTable,случайный,delete,3,0.0008358420
|
||||||
|
HashTable,случайный,insert,4,0.2157934910
|
||||||
|
HashTable,случайный,find,4,0.0015542810
|
||||||
|
HashTable,случайный,delete,4,0.0007269120
|
||||||
|
HashTable,случайный,insert,5,0.2079924580
|
||||||
|
HashTable,случайный,find,5,0.0013696990
|
||||||
|
HashTable,случайный,delete,5,0.0006616050
|
||||||
|
HashTable,случайный,insert,среднее,0.2089636952
|
||||||
|
HashTable,случайный,find,среднее,0.0015341400
|
||||||
|
HashTable,случайный,delete,среднее,0.0007269582
|
||||||
|
BST,случайный,insert,1,0.0166981280
|
||||||
|
BST,случайный,find,1,0.0001569360
|
||||||
|
BST,случайный,delete,1,0.0000917280
|
||||||
|
BST,случайный,insert,2,0.0184119040
|
||||||
|
BST,случайный,find,2,0.0001517110
|
||||||
|
BST,случайный,delete,2,0.0001163770
|
||||||
|
BST,случайный,insert,3,0.0174662270
|
||||||
|
BST,случайный,find,3,0.0001582930
|
||||||
|
BST,случайный,delete,3,0.0000892660
|
||||||
|
BST,случайный,insert,4,0.0191369100
|
||||||
|
BST,случайный,find,4,0.0002087170
|
||||||
|
BST,случайный,delete,4,0.0001067050
|
||||||
|
BST,случайный,insert,5,0.0184276900
|
||||||
|
BST,случайный,find,5,0.0002767720
|
||||||
|
BST,случайный,delete,5,0.0001067660
|
||||||
|
BST,случайный,insert,среднее,0.0180281718
|
||||||
|
BST,случайный,find,среднее,0.0001904858
|
||||||
|
BST,случайный,delete,среднее,0.0001021684
|
||||||
|
LinkedList,отсортированный,insert,1,2.9875078340
|
||||||
|
LinkedList,отсортированный,find,1,0.0237300610
|
||||||
|
LinkedList,отсортированный,delete,1,0.0111698260
|
||||||
|
LinkedList,отсортированный,insert,2,3.0573987940
|
||||||
|
LinkedList,отсортированный,find,2,0.0243270360
|
||||||
|
LinkedList,отсортированный,delete,2,0.0115366030
|
||||||
|
LinkedList,отсортированный,insert,3,2.9641987260
|
||||||
|
LinkedList,отсортированный,find,3,0.0236313330
|
||||||
|
LinkedList,отсортированный,delete,3,0.0112848510
|
||||||
|
LinkedList,отсортированный,insert,4,3.0345914950
|
||||||
|
LinkedList,отсортированный,find,4,0.0240271220
|
||||||
|
LinkedList,отсортированный,delete,4,0.0112117310
|
||||||
|
LinkedList,отсортированный,insert,5,2.9481954700
|
||||||
|
LinkedList,отсортированный,find,5,0.0239006100
|
||||||
|
LinkedList,отсортированный,delete,5,0.0110857710
|
||||||
|
LinkedList,отсортированный,insert,среднее,2.9983784638
|
||||||
|
LinkedList,отсортированный,find,среднее,0.0239232324
|
||||||
|
LinkedList,отсортированный,delete,среднее,0.0112577564
|
||||||
|
HashTable,отсортированный,insert,1,0.1997087560
|
||||||
|
HashTable,отсортированный,find,1,0.0017550400
|
||||||
|
HashTable,отсортированный,delete,1,0.0008407980
|
||||||
|
HashTable,отсортированный,insert,2,0.1968675190
|
||||||
|
HashTable,отсортированный,find,2,0.0019886760
|
||||||
|
HashTable,отсортированный,delete,2,0.0008920910
|
||||||
|
HashTable,отсортированный,insert,3,0.1907563580
|
||||||
|
HashTable,отсортированный,find,3,0.0018447440
|
||||||
|
HashTable,отсортированный,delete,3,0.0008684640
|
||||||
|
HashTable,отсортированный,insert,4,0.2625327630
|
||||||
|
HashTable,отсортированный,find,4,0.0016053140
|
||||||
|
HashTable,отсортированный,delete,4,0.0008098670
|
||||||
|
HashTable,отсортированный,insert,5,0.1936840590
|
||||||
|
HashTable,отсортированный,find,5,0.0019015160
|
||||||
|
HashTable,отсортированный,delete,5,0.0009053780
|
||||||
|
HashTable,отсортированный,insert,среднее,0.2087098910
|
||||||
|
HashTable,отсортированный,find,среднее,0.0018190580
|
||||||
|
HashTable,отсортированный,delete,среднее,0.0008633196
|
||||||
|
BST,отсортированный,insert,1,4.2195800190
|
||||||
|
BST,отсортированный,find,1,0.0389314570
|
||||||
|
BST,отсортированный,delete,1,0.0190308920
|
||||||
|
BST,отсортированный,insert,2,4.1356184250
|
||||||
|
BST,отсортированный,find,2,0.0383339310
|
||||||
|
BST,отсортированный,delete,2,0.0194247740
|
||||||
|
BST,отсортированный,insert,3,4.1204731890
|
||||||
|
BST,отсортированный,find,3,0.0388593320
|
||||||
|
BST,отсортированный,delete,3,0.0215428460
|
||||||
|
BST,отсортированный,insert,4,4.2120902370
|
||||||
|
BST,отсортированный,find,4,0.0378190250
|
||||||
|
BST,отсортированный,delete,4,0.0188528460
|
||||||
|
BST,отсортированный,insert,5,4.1304951260
|
||||||
|
BST,отсортированный,find,5,0.0359927840
|
||||||
|
BST,отсортированный,delete,5,0.0179617110
|
||||||
|
BST,отсортированный,insert,среднее,4.1636513992
|
||||||
|
BST,отсортированный,find,среднее,0.0379873058
|
||||||
|
BST,отсортированный,delete,среднее,0.0193626138
|
||||||
|
BIN
petryaninyas/task1/docs/Отчёт по работе.docx
Normal file
172
petryaninyas/task1/experiments.py
Normal file
|
|
@ -0,0 +1,172 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import csv
|
||||||
|
import random
|
||||||
|
import time
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Dict, List, Tuple
|
||||||
|
|
||||||
|
from linked_list import ll_insert, ll_find, ll_delete
|
||||||
|
from hash_table import ht_insert, ht_find, ht_delete
|
||||||
|
from bst import bst_insert, bst_find, bst_delete
|
||||||
|
from utils import generate_records, prepare_records_variants
|
||||||
|
|
||||||
|
|
||||||
|
Record = Tuple[str, str]
|
||||||
|
|
||||||
|
|
||||||
|
def make_missing_names(count: int = 10) -> List[str]:
|
||||||
|
return [f"None_{i}" for i in range(count)]
|
||||||
|
|
||||||
|
|
||||||
|
def pick_existing_names(records: List[Record], count: int, seed: int = 42) -> List[str]:
|
||||||
|
rng = random.Random(seed)
|
||||||
|
unique_names = list(dict.fromkeys(name for name, _ in records))
|
||||||
|
if len(unique_names) < count:
|
||||||
|
raise ValueError(f"Not enough unique names: need {count}, got {len(unique_names)}")
|
||||||
|
return rng.sample(unique_names, count)
|
||||||
|
|
||||||
|
|
||||||
|
def pick_delete_names(records: List[Record], count: int = 50, seed: int = 43) -> List[str]:
|
||||||
|
rng = random.Random(seed)
|
||||||
|
unique_names = list(dict.fromkeys(name for name, _ in records))
|
||||||
|
if len(unique_names) < count:
|
||||||
|
raise ValueError(f"Not enough unique names: need {count}, got {len(unique_names)}")
|
||||||
|
return rng.sample(unique_names, count)
|
||||||
|
|
||||||
|
|
||||||
|
def build_structure(structure_name: str, records: List[Record], buckets_count: int = 2048):
|
||||||
|
if structure_name == "linked_list":
|
||||||
|
structure = None
|
||||||
|
for name, phone in records:
|
||||||
|
structure = ll_insert(structure, name, phone)
|
||||||
|
return structure
|
||||||
|
|
||||||
|
if structure_name == "hash_table":
|
||||||
|
buckets = [None] * buckets_count
|
||||||
|
for name, phone in records:
|
||||||
|
buckets = ht_insert(buckets, name, phone)
|
||||||
|
return buckets
|
||||||
|
|
||||||
|
if structure_name == "bst":
|
||||||
|
root = None
|
||||||
|
for name, phone in records:
|
||||||
|
root = bst_insert(root, name, phone)
|
||||||
|
return root
|
||||||
|
|
||||||
|
raise ValueError(f"Unknown structure: {structure_name}")
|
||||||
|
|
||||||
|
|
||||||
|
def do_find(structure_name: str, structure: object, existing_names: List[str], missing_names: List[str]) -> None:
|
||||||
|
if structure_name == "linked_list":
|
||||||
|
for name in existing_names:
|
||||||
|
ll_find(structure, name)
|
||||||
|
for name in missing_names:
|
||||||
|
ll_find(structure, name)
|
||||||
|
return
|
||||||
|
|
||||||
|
if structure_name == "hash_table":
|
||||||
|
for name in existing_names:
|
||||||
|
ht_find(structure, name)
|
||||||
|
for name in missing_names:
|
||||||
|
ht_find(structure, name)
|
||||||
|
return
|
||||||
|
|
||||||
|
if structure_name == "bst":
|
||||||
|
for name in existing_names:
|
||||||
|
bst_find(structure, name)
|
||||||
|
for name in missing_names:
|
||||||
|
bst_find(structure, name)
|
||||||
|
return
|
||||||
|
|
||||||
|
raise ValueError(f"Unknown structure: {structure_name}")
|
||||||
|
|
||||||
|
|
||||||
|
def do_delete(structure_name: str, structure: object, delete_names: List[str]):
|
||||||
|
if structure_name == "linked_list":
|
||||||
|
for name in delete_names:
|
||||||
|
structure = ll_delete(structure, name)
|
||||||
|
return structure
|
||||||
|
|
||||||
|
if structure_name == "hash_table":
|
||||||
|
for name in delete_names:
|
||||||
|
structure = ht_delete(structure, name)
|
||||||
|
return structure
|
||||||
|
|
||||||
|
if structure_name == "bst":
|
||||||
|
for name in delete_names:
|
||||||
|
structure = bst_delete(structure, name)
|
||||||
|
return structure
|
||||||
|
|
||||||
|
raise ValueError(f"Unknown structure: {structure_name}")
|
||||||
|
|
||||||
|
|
||||||
|
def measure_once(structure_name: str, records: List[Record], buckets_count: int = 2048) -> Dict[str, float]:
|
||||||
|
existing_names = pick_existing_names(records, 100, seed=42)
|
||||||
|
missing_names = make_missing_names(10)
|
||||||
|
delete_names = pick_delete_names(records, 50, seed=43)
|
||||||
|
|
||||||
|
start = time.perf_counter()
|
||||||
|
structure = build_structure(structure_name, records, buckets_count=buckets_count)
|
||||||
|
insert_time = time.perf_counter() - start
|
||||||
|
|
||||||
|
start = time.perf_counter()
|
||||||
|
do_find(structure_name, structure, existing_names, missing_names)
|
||||||
|
find_time = time.perf_counter() - start
|
||||||
|
|
||||||
|
start = time.perf_counter()
|
||||||
|
structure = do_delete(structure_name, structure, delete_names)
|
||||||
|
delete_time = time.perf_counter() - start
|
||||||
|
|
||||||
|
return {"insert": insert_time, "find": find_time, "delete": delete_time}
|
||||||
|
|
||||||
|
|
||||||
|
def run_experiments(n: int = 10000, buckets_count: int = 2048, repeats: int = 5):
|
||||||
|
records = generate_records(n, repeat_names=False)
|
||||||
|
records_shuffled, records_sorted = prepare_records_variants(records)
|
||||||
|
|
||||||
|
datasets = [
|
||||||
|
("случайный", records_shuffled),
|
||||||
|
("отсортированный", records_sorted),
|
||||||
|
]
|
||||||
|
structures = [
|
||||||
|
("LinkedList", "linked_list"),
|
||||||
|
("HashTable", "hash_table"),
|
||||||
|
("BST", "bst"),
|
||||||
|
]
|
||||||
|
operations = ("insert", "find", "delete")
|
||||||
|
|
||||||
|
rows = [["Структура", "Режим", "Операция", "Замер", "Время (сек)"]]
|
||||||
|
|
||||||
|
for mode_name, dataset_records in datasets:
|
||||||
|
for human_name, structure_name in structures:
|
||||||
|
times_by_op = {op: [] for op in operations}
|
||||||
|
|
||||||
|
for attempt in range(1, repeats + 1):
|
||||||
|
result = measure_once(structure_name, dataset_records, buckets_count=buckets_count)
|
||||||
|
for op_name in operations:
|
||||||
|
elapsed = result[op_name]
|
||||||
|
times_by_op[op_name].append(elapsed)
|
||||||
|
rows.append([human_name, mode_name, op_name, attempt, f"{elapsed:.10f}"])
|
||||||
|
|
||||||
|
for op_name in operations:
|
||||||
|
avg_time = sum(times_by_op[op_name]) / len(times_by_op[op_name])
|
||||||
|
rows.append([human_name, mode_name, op_name, "среднее", f"{avg_time:.10f}"])
|
||||||
|
|
||||||
|
return rows
|
||||||
|
|
||||||
|
|
||||||
|
def save_results_csv(rows, filename: str = "results.csv"):
|
||||||
|
with open(filename, "w", newline="", encoding="utf-8") as f:
|
||||||
|
writer = csv.writer(f)
|
||||||
|
writer.writerows(rows)
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
rows = run_experiments(n=10000, buckets_count=2048, repeats=5)
|
||||||
|
save_results_csv(rows, "results.csv")
|
||||||
|
print("Saved results.csv")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
44
petryaninyas/task1/hash_table.py
Normal file
|
|
@ -0,0 +1,44 @@
|
||||||
|
|
||||||
|
|
||||||
|
from typing import Any, Dict, List, Optional
|
||||||
|
|
||||||
|
from linked_list import ll_insert, ll_find, ll_delete, ll_list_all
|
||||||
|
|
||||||
|
|
||||||
|
Bucket = Optional[Dict[str, Any]]
|
||||||
|
|
||||||
|
|
||||||
|
def _hash_name(name: str, buckets_count: int) -> int:
|
||||||
|
if buckets_count <= 0:
|
||||||
|
return 0
|
||||||
|
return sum(ord(ch) for ch in name) % buckets_count
|
||||||
|
|
||||||
|
|
||||||
|
def ht_insert(buckets: List[Bucket], name: str, phone: str) -> List[Bucket]:
|
||||||
|
if not buckets:
|
||||||
|
return buckets
|
||||||
|
index = _hash_name(name, len(buckets))
|
||||||
|
buckets[index] = ll_insert(buckets[index], name, phone)
|
||||||
|
return buckets
|
||||||
|
|
||||||
|
|
||||||
|
def ht_find(buckets: List[Bucket], name: str) -> Optional[str]:
|
||||||
|
if not buckets:
|
||||||
|
return None
|
||||||
|
index = _hash_name(name, len(buckets))
|
||||||
|
return ll_find(buckets[index], name)
|
||||||
|
|
||||||
|
|
||||||
|
def ht_delete(buckets: List[Bucket], name: str) -> List[Bucket]:
|
||||||
|
if not buckets:
|
||||||
|
return buckets
|
||||||
|
index = _hash_name(name, len(buckets))
|
||||||
|
buckets[index] = ll_delete(buckets[index], name)
|
||||||
|
return buckets
|
||||||
|
|
||||||
|
|
||||||
|
def ht_list_all(buckets: List[Bucket]) -> List[Dict[str, str]]:
|
||||||
|
records: List[Dict[str, str]] = []
|
||||||
|
for head in buckets:
|
||||||
|
records.extend(ll_list_all(head))
|
||||||
|
return sorted(records, key=lambda x: x["name"])
|
||||||
73
petryaninyas/task1/linked_list.py
Normal file
|
|
@ -0,0 +1,73 @@
|
||||||
|
|
||||||
|
|
||||||
|
from typing import Any, Dict, List, Optional
|
||||||
|
|
||||||
|
|
||||||
|
Node = Dict[str, Any]
|
||||||
|
|
||||||
|
|
||||||
|
def _make_node(name: str, phone: str) -> Node:
|
||||||
|
return {"name": name, "phone": phone, "next": None}
|
||||||
|
|
||||||
|
|
||||||
|
def sort_records(records: List[Dict[str, str]]) -> List[Dict[str, str]]:
|
||||||
|
|
||||||
|
return sorted(records, key=lambda x: x["name"])
|
||||||
|
|
||||||
|
|
||||||
|
def ll_insert(head: Optional[Node], name: str, phone: str) -> Node:
|
||||||
|
|
||||||
|
new_node = _make_node(name, phone)
|
||||||
|
|
||||||
|
if head is None:
|
||||||
|
return new_node
|
||||||
|
|
||||||
|
current = head
|
||||||
|
while current is not None:
|
||||||
|
if current["name"] == name:
|
||||||
|
current["phone"] = phone
|
||||||
|
return head
|
||||||
|
if current["next"] is None:
|
||||||
|
current["next"] = new_node
|
||||||
|
return head
|
||||||
|
current = current["next"]
|
||||||
|
|
||||||
|
return head
|
||||||
|
|
||||||
|
|
||||||
|
def ll_find(head: Optional[Node], name: str) -> Optional[str]:
|
||||||
|
current = head
|
||||||
|
while current is not None:
|
||||||
|
if current["name"] == name:
|
||||||
|
return current["phone"]
|
||||||
|
current = current["next"]
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def ll_delete(head: Optional[Node], name: str) -> Optional[Node]:
|
||||||
|
if head is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
if head["name"] == name:
|
||||||
|
return head["next"]
|
||||||
|
|
||||||
|
prev = head
|
||||||
|
current = head["next"]
|
||||||
|
|
||||||
|
while current is not None:
|
||||||
|
if current["name"] == name:
|
||||||
|
prev["next"] = current["next"]
|
||||||
|
return head
|
||||||
|
prev = current
|
||||||
|
current = current["next"]
|
||||||
|
|
||||||
|
return head
|
||||||
|
|
||||||
|
|
||||||
|
def ll_list_all(head: Optional[Node]) -> List[Dict[str, str]]:
|
||||||
|
records: List[Dict[str, str]] = []
|
||||||
|
current = head
|
||||||
|
while current is not None:
|
||||||
|
records.append({"name": current["name"], "phone": current["phone"]})
|
||||||
|
current = current["next"]
|
||||||
|
return sort_records(records)
|
||||||
21
petryaninyas/task1/main.py
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import csv
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from experiments import run_experiments, save_results_csv
|
||||||
|
from plot_results import build_graphs, load_average_results
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
rows = run_experiments(n=10000, buckets_count=2048, repeats=5)
|
||||||
|
save_results_csv(rows, "results.csv")
|
||||||
|
averaged = load_average_results("results.csv")
|
||||||
|
build_graphs(averaged, output_dir="docs/data")
|
||||||
|
print("Done.")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
66
petryaninyas/task1/plot_results.py
Normal file
|
|
@ -0,0 +1,66 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
import csv
|
||||||
|
from collections import defaultdict
|
||||||
|
from pathlib import Path
|
||||||
|
import matplotlib.pyplot as plt
|
||||||
|
from typing import List, Dict, Any
|
||||||
|
|
||||||
|
# Карта соответствия заголовков CSV для удобства поддержки
|
||||||
|
_CSV_KEYS = {
|
||||||
|
"STRUCTURE": "Структура",
|
||||||
|
"MODE": "Режим",
|
||||||
|
"OP": "Операция",
|
||||||
|
"MEASURE": "Замер",
|
||||||
|
"TIME": "Время (сек)"
|
||||||
|
}
|
||||||
|
|
||||||
|
def load_average_results(csv_file: str) -> List[Dict[str, Any]]:
|
||||||
|
"""Загружает только строки со средними значениями из CSV."""
|
||||||
|
with open(csv_file, "r", encoding="utf-8") as fh:
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
"structure": row[_CSV_KEYS["STRUCTURE"]],
|
||||||
|
"mode": row[_CSV_KEYS["MODE"]],
|
||||||
|
"operation": row[_CSV_KEYS["OP"]],
|
||||||
|
"time": float(row[_CSV_KEYS["TIME"]])
|
||||||
|
}
|
||||||
|
for row in csv.DictReader(fh)
|
||||||
|
if row[_CSV_KEYS["MEASURE"]] == "среднее"
|
||||||
|
]
|
||||||
|
|
||||||
|
def _render_chart(operation: str, data: List[Dict[str, Any]], save_path: Path) -> None:
|
||||||
|
"""Вспомогательная функция для отрисовки одного столбчатого графика."""
|
||||||
|
fig, ax = plt.subplots(figsize=(10, 5))
|
||||||
|
labels = [f"{rec['structure']}\n{rec['mode']}" for rec in data]
|
||||||
|
values = [rec["time"] for rec in data]
|
||||||
|
|
||||||
|
ax.bar(labels, values, color="cornflowerblue", edgecolor="navy")
|
||||||
|
ax.set_title(f"{operation.capitalize()} Performance Comparison")
|
||||||
|
ax.set_xlabel("Data Structure & Input Order")
|
||||||
|
ax.set_ylabel("Execution Time (seconds)")
|
||||||
|
ax.tick_params(axis="x", rotation=15)
|
||||||
|
fig.tight_layout()
|
||||||
|
fig.savefig(save_path, dpi=150)
|
||||||
|
plt.close(fig)
|
||||||
|
print(f"Saved chart: {save_path}")
|
||||||
|
|
||||||
|
def build_graphs(results: List[Dict[str, Any]], output_dir: str = "docs/data") -> None:
|
||||||
|
"""Группирует результаты по операциям и сохраняет графики."""
|
||||||
|
out_path = Path(output_dir)
|
||||||
|
out_path.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
grouped = defaultdict(list)
|
||||||
|
for record in results:
|
||||||
|
grouped[record["operation"]].append(record)
|
||||||
|
|
||||||
|
for op_name in ("insert", "find", "delete"):
|
||||||
|
if op_name in grouped:
|
||||||
|
target_file = out_path / f"{op_name}.png"
|
||||||
|
_render_chart(op_name, grouped[op_name], target_file)
|
||||||
|
|
||||||
|
def main():
|
||||||
|
avg_data = load_average_results("results.csv")
|
||||||
|
build_graphs(avg_data)
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
1
petryaninyas/task1/requirements.txt
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
matplotlib>=3.8
|
||||||
109
petryaninyas/task1/results.csv
Normal file
|
|
@ -0,0 +1,109 @@
|
||||||
|
Структура,Режим,Операция,Замер,Время (сек)
|
||||||
|
LinkedList,случайный,insert,1,6.8132659000
|
||||||
|
LinkedList,случайный,find,1,0.0553455000
|
||||||
|
LinkedList,случайный,delete,1,0.0304272000
|
||||||
|
LinkedList,случайный,insert,2,6.8263299000
|
||||||
|
LinkedList,случайный,find,2,0.0562645000
|
||||||
|
LinkedList,случайный,delete,2,0.0303858000
|
||||||
|
LinkedList,случайный,insert,3,6.8117682000
|
||||||
|
LinkedList,случайный,find,3,0.0567219000
|
||||||
|
LinkedList,случайный,delete,3,0.0302671000
|
||||||
|
LinkedList,случайный,insert,4,7.0393228000
|
||||||
|
LinkedList,случайный,find,4,0.0544359000
|
||||||
|
LinkedList,случайный,delete,4,0.0313359000
|
||||||
|
LinkedList,случайный,insert,5,7.0633509000
|
||||||
|
LinkedList,случайный,find,5,0.0552176000
|
||||||
|
LinkedList,случайный,delete,5,0.0318664000
|
||||||
|
LinkedList,случайный,insert,среднее,6.9108075400
|
||||||
|
LinkedList,случайный,find,среднее,0.0555970800
|
||||||
|
LinkedList,случайный,delete,среднее,0.0308564800
|
||||||
|
HashTable,случайный,insert,1,0.3898307000
|
||||||
|
HashTable,случайный,find,1,0.0022226000
|
||||||
|
HashTable,случайный,delete,1,0.0012723000
|
||||||
|
HashTable,случайный,insert,2,0.3844561000
|
||||||
|
HashTable,случайный,find,2,0.0024825000
|
||||||
|
HashTable,случайный,delete,2,0.0013425000
|
||||||
|
HashTable,случайный,insert,3,0.3740853000
|
||||||
|
HashTable,случайный,find,3,0.0026141000
|
||||||
|
HashTable,случайный,delete,3,0.0013710000
|
||||||
|
HashTable,случайный,insert,4,0.3870099000
|
||||||
|
HashTable,случайный,find,4,0.0022063000
|
||||||
|
HashTable,случайный,delete,4,0.0015940000
|
||||||
|
HashTable,случайный,insert,5,0.3869087000
|
||||||
|
HashTable,случайный,find,5,0.0022110000
|
||||||
|
HashTable,случайный,delete,5,0.0012524000
|
||||||
|
HashTable,случайный,insert,среднее,0.3844581400
|
||||||
|
HashTable,случайный,find,среднее,0.0023473000
|
||||||
|
HashTable,случайный,delete,среднее,0.0013664400
|
||||||
|
BST,случайный,insert,1,0.0375842000
|
||||||
|
BST,случайный,find,1,0.0003637000
|
||||||
|
BST,случайный,delete,1,0.0001893000
|
||||||
|
BST,случайный,insert,2,0.0364385000
|
||||||
|
BST,случайный,find,2,0.0003284000
|
||||||
|
BST,случайный,delete,2,0.0002408000
|
||||||
|
BST,случайный,insert,3,0.0359604000
|
||||||
|
BST,случайный,find,3,0.0003705000
|
||||||
|
BST,случайный,delete,3,0.0001883000
|
||||||
|
BST,случайный,insert,4,0.0371920000
|
||||||
|
BST,случайный,find,4,0.0003347000
|
||||||
|
BST,случайный,delete,4,0.0001711000
|
||||||
|
BST,случайный,insert,5,0.0385580000
|
||||||
|
BST,случайный,find,5,0.0003628000
|
||||||
|
BST,случайный,delete,5,0.0001828000
|
||||||
|
BST,случайный,insert,среднее,0.0371466200
|
||||||
|
BST,случайный,find,среднее,0.0003520200
|
||||||
|
BST,случайный,delete,среднее,0.0001944600
|
||||||
|
LinkedList,отсортированный,insert,1,6.7166487000
|
||||||
|
LinkedList,отсортированный,find,1,0.0614362000
|
||||||
|
LinkedList,отсортированный,delete,1,0.0324487000
|
||||||
|
LinkedList,отсортированный,insert,2,6.8582294000
|
||||||
|
LinkedList,отсортированный,find,2,0.0588546000
|
||||||
|
LinkedList,отсортированный,delete,2,0.0332572000
|
||||||
|
LinkedList,отсортированный,insert,3,6.6991410000
|
||||||
|
LinkedList,отсортированный,find,3,0.0521304000
|
||||||
|
LinkedList,отсортированный,delete,3,0.0296734000
|
||||||
|
LinkedList,отсортированный,insert,4,6.7166336000
|
||||||
|
LinkedList,отсортированный,find,4,0.0521848000
|
||||||
|
LinkedList,отсортированный,delete,4,0.0286064000
|
||||||
|
LinkedList,отсортированный,insert,5,6.6510829000
|
||||||
|
LinkedList,отсортированный,find,5,0.0547075000
|
||||||
|
LinkedList,отсортированный,delete,5,0.0285625000
|
||||||
|
LinkedList,отсортированный,insert,среднее,6.7283471200
|
||||||
|
LinkedList,отсортированный,find,среднее,0.0558627000
|
||||||
|
LinkedList,отсортированный,delete,среднее,0.0305096400
|
||||||
|
HashTable,отсортированный,insert,1,0.3645209000
|
||||||
|
HashTable,отсортированный,find,1,0.0032770000
|
||||||
|
HashTable,отсортированный,delete,1,0.0018631000
|
||||||
|
HashTable,отсортированный,insert,2,0.3579217000
|
||||||
|
HashTable,отсортированный,find,2,0.0032143000
|
||||||
|
HashTable,отсортированный,delete,2,0.0016427000
|
||||||
|
HashTable,отсортированный,insert,3,0.3684879000
|
||||||
|
HashTable,отсортированный,find,3,0.0026412000
|
||||||
|
HashTable,отсортированный,delete,3,0.0014870000
|
||||||
|
HashTable,отсортированный,insert,4,0.3873619000
|
||||||
|
HashTable,отсортированный,find,4,0.0028024000
|
||||||
|
HashTable,отсортированный,delete,4,0.0015786000
|
||||||
|
HashTable,отсортированный,insert,5,0.3642992000
|
||||||
|
HashTable,отсортированный,find,5,0.0031264000
|
||||||
|
HashTable,отсортированный,delete,5,0.0015886000
|
||||||
|
HashTable,отсортированный,insert,среднее,0.3685183200
|
||||||
|
HashTable,отсортированный,find,среднее,0.0030122600
|
||||||
|
HashTable,отсортированный,delete,среднее,0.0016320000
|
||||||
|
BST,отсортированный,insert,1,10.5552378000
|
||||||
|
BST,отсортированный,find,1,0.1016856000
|
||||||
|
BST,отсортированный,delete,1,0.0422728000
|
||||||
|
BST,отсортированный,insert,2,10.3035871000
|
||||||
|
BST,отсортированный,find,2,0.1008642000
|
||||||
|
BST,отсортированный,delete,2,0.0450330000
|
||||||
|
BST,отсортированный,insert,3,10.6304005000
|
||||||
|
BST,отсортированный,find,3,0.1073470000
|
||||||
|
BST,отсортированный,delete,3,0.0816121000
|
||||||
|
BST,отсортированный,insert,4,10.3183078000
|
||||||
|
BST,отсортированный,find,4,0.1005074000
|
||||||
|
BST,отсортированный,delete,4,0.0422195000
|
||||||
|
BST,отсортированный,insert,5,10.3131368000
|
||||||
|
BST,отсортированный,find,5,0.1001096000
|
||||||
|
BST,отсортированный,delete,5,0.0416660000
|
||||||
|
BST,отсортированный,insert,среднее,10.4241340000
|
||||||
|
BST,отсортированный,find,среднее,0.1021027600
|
||||||
|
BST,отсортированный,delete,среднее,0.0505606800
|
||||||
|
29
petryaninyas/task1/utils.py
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
import random
|
||||||
|
from typing import List, Tuple
|
||||||
|
|
||||||
|
Record = Tuple[str, str]
|
||||||
|
|
||||||
|
NAME_POOL = (
|
||||||
|
"User_Alex", "User_Bob", "User_Cat", "User_Dan", "User_Eva",
|
||||||
|
"User_Fox", "User_Geo", "User_Hen", "User_Ira", "User_Leo"
|
||||||
|
)
|
||||||
|
|
||||||
|
def generate_records(n: int, repeat_names: bool = False, seed: int = 42) -> List[Record]:
|
||||||
|
"""Генерирует n кортежей (имя, телефон)."""
|
||||||
|
rng = random.Random(seed)
|
||||||
|
if repeat_names:
|
||||||
|
return [
|
||||||
|
(rng.choice(NAME_POOL), str(rng.randint(10**9, 10**10 - 1)))
|
||||||
|
for _ in range(n)
|
||||||
|
]
|
||||||
|
return [
|
||||||
|
(f"User_{i:05d}", str(10**9 + i))
|
||||||
|
for i in range(n)
|
||||||
|
]
|
||||||
|
|
||||||
|
def prepare_records_variants(records: List[Record], seed: int = 42) -> Tuple[List[Record], List[Record]]:
|
||||||
|
"""Возвращает пару: (перемешанный список, отсортированный по имени список)."""
|
||||||
|
shuffled = records.copy()
|
||||||
|
random.Random(seed).shuffle(shuffled)
|
||||||
|
sorted_records = sorted(records, key=lambda rec: rec[0])
|
||||||
|
return shuffled, sorted_records
|
||||||
0
petryaninyas/task2/builders/__init__.py
Normal file
7
petryaninyas/task2/builders/maze_builder.py
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
from abc import ABC, abstractmethod
|
||||||
|
|
||||||
|
|
||||||
|
class MazeBuilder(ABC):
|
||||||
|
@abstractmethod
|
||||||
|
def buildFromFile(self, filename):
|
||||||
|
raise NotImplementedError
|
||||||
52
petryaninyas/task2/builders/text_file_maze_builder.py
Normal file
|
|
@ -0,0 +1,52 @@
|
||||||
|
from core.cell import Cell
|
||||||
|
from core.maze import Maze
|
||||||
|
from builders.maze_builder import MazeBuilder
|
||||||
|
|
||||||
|
|
||||||
|
class TextFileMazeBuilder(MazeBuilder):
|
||||||
|
def buildFromFile(self, filename):
|
||||||
|
with open(filename, "r", encoding="utf-8") as f:
|
||||||
|
lines = [line.rstrip("\n") for line in f]
|
||||||
|
|
||||||
|
if not lines:
|
||||||
|
raise ValueError("Maze file is empty")
|
||||||
|
|
||||||
|
width = max(len(line) for line in lines)
|
||||||
|
height = len(lines)
|
||||||
|
|
||||||
|
cells = []
|
||||||
|
startCell = None
|
||||||
|
exitCell = None
|
||||||
|
|
||||||
|
for y, line in enumerate(lines):
|
||||||
|
row = []
|
||||||
|
for x in range(width):
|
||||||
|
ch = line[x] if x < len(line) else "#"
|
||||||
|
|
||||||
|
if ch == "#":
|
||||||
|
cell = Cell(x, y, isWall=True)
|
||||||
|
elif ch == "S":
|
||||||
|
if startCell is not None:
|
||||||
|
raise ValueError("Multiple start cells found")
|
||||||
|
cell = Cell(x, y, isWall=False, isStart=True)
|
||||||
|
startCell = cell
|
||||||
|
elif ch == "E":
|
||||||
|
if exitCell is not None:
|
||||||
|
raise ValueError("Multiple exit cells found")
|
||||||
|
cell = Cell(x, y, isWall=False, isExit=True)
|
||||||
|
exitCell = cell
|
||||||
|
elif ch in (" ", "."):
|
||||||
|
cell = Cell(x, y, isWall=False)
|
||||||
|
elif ch.isdigit():
|
||||||
|
cell = Cell(x, y, isWall=False, weight=max(1, int(ch)))
|
||||||
|
else:
|
||||||
|
raise ValueError(f"Unsupported symbol '{ch}' at ({x}, {y})")
|
||||||
|
row.append(cell)
|
||||||
|
cells.append(row)
|
||||||
|
|
||||||
|
if startCell is None:
|
||||||
|
raise ValueError("Start cell 'S' not found")
|
||||||
|
if exitCell is None:
|
||||||
|
raise ValueError("Exit cell 'E' not found")
|
||||||
|
|
||||||
|
return Maze(cells, width, height, startCell, exitCell)
|
||||||
0
petryaninyas/task2/commands/__init__.py
Normal file
11
petryaninyas/task2/commands/command.py
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
from abc import ABC, abstractmethod
|
||||||
|
|
||||||
|
|
||||||
|
class Command(ABC):
|
||||||
|
@abstractmethod
|
||||||
|
def execute(self):
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def undo(self):
|
||||||
|
raise NotImplementedError
|
||||||
37
petryaninyas/task2/commands/move_command.py
Normal file
|
|
@ -0,0 +1,37 @@
|
||||||
|
from commands.command import Command
|
||||||
|
|
||||||
|
|
||||||
|
class MoveCommand(Command):
|
||||||
|
DIRECTION_TO_DELTA = {
|
||||||
|
"W": (0, -1),
|
||||||
|
"A": (-1, 0),
|
||||||
|
"S": (0, 1),
|
||||||
|
"D": (1, 0),
|
||||||
|
}
|
||||||
|
|
||||||
|
def __init__(self, player, maze, direction):
|
||||||
|
self.player = player
|
||||||
|
self.maze = maze
|
||||||
|
self.direction = direction.upper()
|
||||||
|
self.previousCell = None
|
||||||
|
|
||||||
|
def execute(self):
|
||||||
|
if self.direction not in self.DIRECTION_TO_DELTA:
|
||||||
|
return False
|
||||||
|
|
||||||
|
dx, dy = self.DIRECTION_TO_DELTA[self.direction]
|
||||||
|
current = self.player.currentCell
|
||||||
|
new_cell = self.maze.getCell(current.x + dx, current.y + dy)
|
||||||
|
|
||||||
|
if new_cell is None or not new_cell.isPassable():
|
||||||
|
return False
|
||||||
|
|
||||||
|
self.previousCell = current
|
||||||
|
self.player.setCell(new_cell)
|
||||||
|
return True
|
||||||
|
|
||||||
|
def undo(self):
|
||||||
|
if self.previousCell is None:
|
||||||
|
return False
|
||||||
|
self.player.setCell(self.previousCell)
|
||||||
|
return True
|
||||||
0
petryaninyas/task2/controller/__init__.py
Normal file
30
petryaninyas/task2/controller/game_controller.py
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
from commands.move_command import MoveCommand
|
||||||
|
|
||||||
|
|
||||||
|
class GameController:
|
||||||
|
def __init__(self, maze, player, view):
|
||||||
|
self.maze = maze
|
||||||
|
self.player = player
|
||||||
|
self.view = view
|
||||||
|
self.history = []
|
||||||
|
|
||||||
|
def move(self, direction):
|
||||||
|
command = MoveCommand(self.player, self.maze, direction)
|
||||||
|
if command.execute():
|
||||||
|
self.history.append(command)
|
||||||
|
self.view.update({"type": "move", "direction": direction})
|
||||||
|
self.view.render(self.maze, player_position=self.player.currentCell)
|
||||||
|
return True
|
||||||
|
print("Cannot move there")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def undo(self):
|
||||||
|
if not self.history:
|
||||||
|
print("Nothing to undo")
|
||||||
|
return False
|
||||||
|
command = self.history.pop()
|
||||||
|
if command.undo():
|
||||||
|
self.view.update({"type": "undo"})
|
||||||
|
self.view.render(self.maze, player_position=self.player.currentCell)
|
||||||
|
return True
|
||||||
|
return False
|
||||||
0
petryaninyas/task2/core/__init__.py
Normal file
26
petryaninyas/task2/core/cell.py
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Cell:
|
||||||
|
x: int
|
||||||
|
y: int
|
||||||
|
isWall: bool = False
|
||||||
|
isStart: bool = False
|
||||||
|
isExit: bool = False
|
||||||
|
weight: int = 1
|
||||||
|
|
||||||
|
def isPassable(self):
|
||||||
|
return not self.isWall
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
parts = [f"Cell({self.x}, {self.y}"]
|
||||||
|
if self.isWall:
|
||||||
|
parts.append("WALL")
|
||||||
|
if self.isStart:
|
||||||
|
parts.append("START")
|
||||||
|
if self.isExit:
|
||||||
|
parts.append("EXIT")
|
||||||
|
if self.weight != 1:
|
||||||
|
parts.append(f"w={self.weight}")
|
||||||
|
return ", ".join(parts) + ")"
|
||||||
49
petryaninyas/task2/core/maze.py
Normal file
|
|
@ -0,0 +1,49 @@
|
||||||
|
class Maze:
|
||||||
|
def __init__(self, cells, width, height, startCell=None, exitCell=None):
|
||||||
|
self.cells = cells
|
||||||
|
self.width = width
|
||||||
|
self.height = height
|
||||||
|
self.startCell = startCell
|
||||||
|
self.exitCell = exitCell
|
||||||
|
|
||||||
|
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 ((0, -1), (0, 1), (-1, 0), (1, 0)):
|
||||||
|
nx, ny = cell.x + dx, cell.y + dy
|
||||||
|
neighbor = self.getCell(nx, ny)
|
||||||
|
if neighbor is not None and neighbor.isPassable():
|
||||||
|
neighbors.append(neighbor)
|
||||||
|
return neighbors
|
||||||
|
|
||||||
|
def render_lines(self, player_position=None, path=None):
|
||||||
|
path_set = {(c.x, c.y) for c in path} if path else set()
|
||||||
|
player_pos = None if player_position is None else (player_position.x, player_position.y)
|
||||||
|
lines = []
|
||||||
|
for y in range(self.height):
|
||||||
|
row = []
|
||||||
|
for x in range(self.width):
|
||||||
|
cell = self.cells[y][x]
|
||||||
|
if player_pos == (x, y):
|
||||||
|
row.append("P")
|
||||||
|
elif cell.isStart:
|
||||||
|
row.append("S")
|
||||||
|
elif cell.isExit:
|
||||||
|
row.append("E")
|
||||||
|
elif cell.isWall:
|
||||||
|
row.append("#")
|
||||||
|
elif (x, y) in path_set:
|
||||||
|
row.append("*")
|
||||||
|
elif cell.weight > 1:
|
||||||
|
row.append(str(cell.weight))
|
||||||
|
else:
|
||||||
|
row.append(" ")
|
||||||
|
lines.append("".join(row))
|
||||||
|
return lines
|
||||||
|
|
||||||
|
def render(self, player_position=None, path=None):
|
||||||
|
return "\n".join(self.render_lines(player_position=player_position, path=path))
|
||||||
6
petryaninyas/task2/core/player.py
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
class Player:
|
||||||
|
def __init__(self, currentCell):
|
||||||
|
self.currentCell = currentCell
|
||||||
|
|
||||||
|
def setCell(self, cell):
|
||||||
|
self.currentCell = cell
|
||||||
11
petryaninyas/task2/core/search_stats.py
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class SearchStats:
|
||||||
|
timeMs: float
|
||||||
|
visitedCells: int
|
||||||
|
pathLength: int
|
||||||
|
path: list = field(default_factory=list)
|
||||||
|
found: bool = False
|
||||||
|
algorithm: str = ""
|
||||||
BIN
petryaninyas/task2/docs/отчёт по лаб 2.docx
Normal file
225
petryaninyas/task2/experiment.py
Normal file
|
|
@ -0,0 +1,225 @@
|
||||||
|
from pathlib import Path
|
||||||
|
from statistics import mean
|
||||||
|
import csv
|
||||||
|
import random
|
||||||
|
|
||||||
|
import matplotlib.pyplot as plt
|
||||||
|
|
||||||
|
from core.cell import Cell
|
||||||
|
from core.maze import Maze
|
||||||
|
from solver.maze_solver import MazeSolver
|
||||||
|
from strategies.astar_strategy import AStarStrategy
|
||||||
|
from strategies.bfs_strategy import BFSStrategy
|
||||||
|
from strategies.dfs_strategy import DFSStrategy
|
||||||
|
from strategies.dijkstra_strategy import DijkstraStrategy
|
||||||
|
|
||||||
|
|
||||||
|
BASE_DIR = Path(__file__).resolve().parent
|
||||||
|
OUT_DIR = BASE_DIR / "experiment_results"
|
||||||
|
|
||||||
|
|
||||||
|
def build_maze_from_symbols(lines):
|
||||||
|
height = len(lines)
|
||||||
|
width = max(len(line) for line in lines)
|
||||||
|
cells = []
|
||||||
|
start = None
|
||||||
|
exit_cell = None
|
||||||
|
for y, line in enumerate(lines):
|
||||||
|
row = []
|
||||||
|
for x in range(width):
|
||||||
|
ch = line[x] if x < len(line) else "#"
|
||||||
|
if ch == "#":
|
||||||
|
cell = Cell(x, y, isWall=True)
|
||||||
|
elif ch == "S":
|
||||||
|
cell = Cell(x, y, isWall=False, isStart=True)
|
||||||
|
start = cell
|
||||||
|
elif ch == "E":
|
||||||
|
cell = Cell(x, y, isWall=False, isExit=True)
|
||||||
|
exit_cell = cell
|
||||||
|
elif ch == " " or ch == ".":
|
||||||
|
cell = Cell(x, y, isWall=False)
|
||||||
|
elif ch.isdigit():
|
||||||
|
cell = Cell(x, y, isWall=False, weight=int(ch))
|
||||||
|
else:
|
||||||
|
raise ValueError(f"Unknown symbol '{ch}' at {x},{y}")
|
||||||
|
row.append(cell)
|
||||||
|
cells.append(row)
|
||||||
|
return Maze(cells, width, height, start, exit_cell)
|
||||||
|
|
||||||
|
|
||||||
|
def generate_empty_maze(width, height):
|
||||||
|
lines = [" " * width for _ in range(height)]
|
||||||
|
lines = [list(row) for row in lines]
|
||||||
|
lines[1][1] = "S"
|
||||||
|
lines[height - 2][width - 2] = "E"
|
||||||
|
return build_maze_from_symbols(["".join(row) for row in lines])
|
||||||
|
|
||||||
|
|
||||||
|
def generate_simple_maze(width, height):
|
||||||
|
grid = [["#" for _ in range(width)] for _ in range(height)]
|
||||||
|
for x in range(1, width - 1):
|
||||||
|
grid[1][x] = " "
|
||||||
|
for y in range(1, height - 1):
|
||||||
|
grid[y][width - 2] = " "
|
||||||
|
grid[1][1] = "S"
|
||||||
|
grid[height - 2][width - 2] = "E"
|
||||||
|
return build_maze_from_symbols(["".join(row) for row in grid])
|
||||||
|
|
||||||
|
|
||||||
|
def generate_branching_maze(width, height, seed=42, wall_density=0.30):
|
||||||
|
rng = random.Random(seed)
|
||||||
|
grid = [["#" for _ in range(width)] for _ in range(height)]
|
||||||
|
x, y = 1, 1
|
||||||
|
grid[y][x] = "S"
|
||||||
|
while (x, y) != (width - 2, height - 2):
|
||||||
|
candidates = []
|
||||||
|
for dx, dy in [(1, 0), (0, 1)]:
|
||||||
|
nx, ny = x + dx, y + dy
|
||||||
|
if 1 <= nx < width - 1 and 1 <= ny < height - 1:
|
||||||
|
candidates.append((nx, ny))
|
||||||
|
if not candidates:
|
||||||
|
break
|
||||||
|
x, y = rng.choice(candidates)
|
||||||
|
grid[y][x] = " "
|
||||||
|
grid[height - 2][width - 2] = "E"
|
||||||
|
|
||||||
|
# carve extra corridors and dead ends
|
||||||
|
for yy in range(1, height - 1):
|
||||||
|
for xx in range(1, width - 1):
|
||||||
|
if grid[yy][xx] == "#" and rng.random() > wall_density:
|
||||||
|
grid[yy][xx] = " "
|
||||||
|
grid[1][1] = "S"
|
||||||
|
grid[height - 2][width - 2] = "E"
|
||||||
|
return build_maze_from_symbols(["".join(row) for row in grid])
|
||||||
|
|
||||||
|
|
||||||
|
def generate_no_path_maze(width, height):
|
||||||
|
grid = [[" " for _ in range(width)] for _ in range(height)]
|
||||||
|
for x in range(width):
|
||||||
|
grid[height // 2][x] = "#"
|
||||||
|
grid[1][1] = "S"
|
||||||
|
grid[height - 2][width - 2] = "E"
|
||||||
|
return build_maze_from_symbols(["".join(row) for row in grid])
|
||||||
|
|
||||||
|
|
||||||
|
def generate_weighted_maze(width, height, seed=123):
|
||||||
|
rng = random.Random(seed)
|
||||||
|
grid = [[" " for _ in range(width)] for _ in range(height)]
|
||||||
|
for y in range(height):
|
||||||
|
for x in range(width):
|
||||||
|
r = rng.random()
|
||||||
|
if r < 0.12:
|
||||||
|
grid[y][x] = "#"
|
||||||
|
elif r < 0.25:
|
||||||
|
grid[y][x] = "3"
|
||||||
|
elif r < 0.40:
|
||||||
|
grid[y][x] = "2"
|
||||||
|
else:
|
||||||
|
grid[y][x] = "1"
|
||||||
|
# ensure path-ish
|
||||||
|
for x in range(width):
|
||||||
|
grid[1][x] = "1"
|
||||||
|
for y in range(1, height):
|
||||||
|
grid[y][width - 2] = "1"
|
||||||
|
grid[1][1] = "S"
|
||||||
|
grid[height - 2][width - 2] = "E"
|
||||||
|
return build_maze_from_symbols(["".join(row) for row in grid])
|
||||||
|
|
||||||
|
|
||||||
|
def bench_one_maze(maze_name, maze, strategies, repeats=5):
|
||||||
|
summary_rows = []
|
||||||
|
raw_rows = []
|
||||||
|
for strategy_name, strategy_factory in strategies:
|
||||||
|
times, visiteds, lengths = [], [], []
|
||||||
|
for run in range(1, repeats + 1):
|
||||||
|
solver = MazeSolver(maze)
|
||||||
|
solver.setStrategy(strategy_factory())
|
||||||
|
stats = solver.solve()
|
||||||
|
raw_rows.append([maze_name, strategy_name, run, f"{stats.timeMs:.6f}", stats.visitedCells, stats.pathLength])
|
||||||
|
times.append(stats.timeMs)
|
||||||
|
visiteds.append(stats.visitedCells)
|
||||||
|
lengths.append(stats.pathLength)
|
||||||
|
summary_rows.append([maze_name, strategy_name, f"{mean(times):.6f}", f"{mean(visiteds):.2f}", f"{mean(lengths):.2f}", repeats])
|
||||||
|
return summary_rows, raw_rows
|
||||||
|
|
||||||
|
|
||||||
|
def save_csv(path, rows):
|
||||||
|
with open(path, "w", newline="", encoding="utf-8") as f:
|
||||||
|
csv.writer(f).writerows(rows)
|
||||||
|
|
||||||
|
|
||||||
|
def plot_summary(summary_rows):
|
||||||
|
by_maze = {}
|
||||||
|
for row in summary_rows[1:]:
|
||||||
|
maze_name, strategy, avg_time, avg_visited, avg_len, runs = row
|
||||||
|
by_maze.setdefault(maze_name, []).append((strategy, float(avg_time), float(avg_visited), float(avg_len)))
|
||||||
|
|
||||||
|
for maze_name, items in by_maze.items():
|
||||||
|
items.sort(key=lambda t: t[0])
|
||||||
|
strategies = [i[0] for i in items]
|
||||||
|
x = list(range(len(strategies)))
|
||||||
|
|
||||||
|
plt.figure(figsize=(8, 4))
|
||||||
|
plt.bar(x, [i[1] for i in items])
|
||||||
|
plt.xticks(x, strategies)
|
||||||
|
plt.ylabel("ms")
|
||||||
|
plt.title(f"{maze_name} — avg time")
|
||||||
|
plt.tight_layout()
|
||||||
|
plt.savefig(OUT_DIR / f"{maze_name}_time.png", dpi=150)
|
||||||
|
plt.close()
|
||||||
|
|
||||||
|
plt.figure(figsize=(8, 4))
|
||||||
|
plt.bar(x, [i[2] for i in items])
|
||||||
|
plt.xticks(x, strategies)
|
||||||
|
plt.ylabel("cells")
|
||||||
|
plt.title(f"{maze_name} — visited cells")
|
||||||
|
plt.tight_layout()
|
||||||
|
plt.savefig(OUT_DIR / f"{maze_name}_visited.png", dpi=150)
|
||||||
|
plt.close()
|
||||||
|
|
||||||
|
plt.figure(figsize=(8, 4))
|
||||||
|
plt.bar(x, [i[3] for i in items])
|
||||||
|
plt.xticks(x, strategies)
|
||||||
|
plt.ylabel("cells")
|
||||||
|
plt.title(f"{maze_name} — path length")
|
||||||
|
plt.tight_layout()
|
||||||
|
plt.savefig(OUT_DIR / f"{maze_name}_length.png", dpi=150)
|
||||||
|
plt.close()
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
OUT_DIR.mkdir(exist_ok=True)
|
||||||
|
|
||||||
|
strategies = [
|
||||||
|
("BFS", BFSStrategy),
|
||||||
|
("DFS", DFSStrategy),
|
||||||
|
("A*", AStarStrategy),
|
||||||
|
("Dijkstra", DijkstraStrategy),
|
||||||
|
]
|
||||||
|
|
||||||
|
mazes = [
|
||||||
|
("small_10x10", generate_simple_maze(10, 10)),
|
||||||
|
("medium_50x50", generate_branching_maze(50, 50)),
|
||||||
|
("large_100x100", generate_branching_maze(100, 100, seed=99, wall_density=0.35)),
|
||||||
|
("empty_30x30", generate_empty_maze(30, 30)),
|
||||||
|
("no_path_30x30", generate_no_path_maze(30, 30)),
|
||||||
|
("weighted_30x30", generate_weighted_maze(30, 30)),
|
||||||
|
]
|
||||||
|
|
||||||
|
summary = [["maze", "strategy", "avg_time_ms", "avg_visited_cells", "avg_path_length", "runs"]]
|
||||||
|
raw = [["maze", "strategy", "run", "time_ms", "visited_cells", "path_length"]]
|
||||||
|
|
||||||
|
for maze_name, maze in mazes:
|
||||||
|
s_rows, r_rows = bench_one_maze(maze_name, maze, strategies, repeats=5)
|
||||||
|
summary.extend(s_rows)
|
||||||
|
raw.extend(r_rows)
|
||||||
|
|
||||||
|
save_csv(OUT_DIR / "summary.csv", summary)
|
||||||
|
save_csv(OUT_DIR / "raw.csv", raw)
|
||||||
|
plot_summary(summary)
|
||||||
|
|
||||||
|
print("Saved to", OUT_DIR.resolve())
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
BIN
petryaninyas/task2/experiment_results/empty_30x30_length.png
Normal file
|
After Width: | Height: | Size: 27 KiB |
BIN
petryaninyas/task2/experiment_results/empty_30x30_time.png
Normal file
|
After Width: | Height: | Size: 19 KiB |
BIN
petryaninyas/task2/experiment_results/empty_30x30_visited.png
Normal file
|
After Width: | Height: | Size: 23 KiB |
BIN
petryaninyas/task2/experiment_results/large_100x100_length.png
Normal file
|
After Width: | Height: | Size: 28 KiB |
BIN
petryaninyas/task2/experiment_results/large_100x100_time.png
Normal file
|
After Width: | Height: | Size: 22 KiB |
BIN
petryaninyas/task2/experiment_results/large_100x100_visited.png
Normal file
|
After Width: | Height: | Size: 25 KiB |
BIN
petryaninyas/task2/experiment_results/medium_50x50_length.png
Normal file
|
After Width: | Height: | Size: 24 KiB |
BIN
petryaninyas/task2/experiment_results/medium_50x50_time.png
Normal file
|
After Width: | Height: | Size: 19 KiB |
BIN
petryaninyas/task2/experiment_results/medium_50x50_visited.png
Normal file
|
After Width: | Height: | Size: 27 KiB |
BIN
petryaninyas/task2/experiment_results/no_path_30x30_length.png
Normal file
|
After Width: | Height: | Size: 23 KiB |
BIN
petryaninyas/task2/experiment_results/no_path_30x30_time.png
Normal file
|
After Width: | Height: | Size: 23 KiB |
BIN
petryaninyas/task2/experiment_results/no_path_30x30_visited.png
Normal file
|
After Width: | Height: | Size: 22 KiB |
121
petryaninyas/task2/experiment_results/raw.csv
Normal file
|
|
@ -0,0 +1,121 @@
|
||||||
|
maze,strategy,run,time_ms,visited_cells,path_length
|
||||||
|
small_10x10,BFS,1,0.086300,15,15
|
||||||
|
small_10x10,BFS,2,0.061100,15,15
|
||||||
|
small_10x10,BFS,3,0.059300,15,15
|
||||||
|
small_10x10,BFS,4,0.058400,15,15
|
||||||
|
small_10x10,BFS,5,0.058500,15,15
|
||||||
|
small_10x10,DFS,1,0.073400,15,15
|
||||||
|
small_10x10,DFS,2,0.063500,15,15
|
||||||
|
small_10x10,DFS,3,0.062500,15,15
|
||||||
|
small_10x10,DFS,4,0.062700,15,15
|
||||||
|
small_10x10,DFS,5,0.070900,15,15
|
||||||
|
small_10x10,A*,1,0.110100,15,15
|
||||||
|
small_10x10,A*,2,0.089200,15,15
|
||||||
|
small_10x10,A*,3,0.087800,15,15
|
||||||
|
small_10x10,A*,4,0.087600,15,15
|
||||||
|
small_10x10,A*,5,0.087000,15,15
|
||||||
|
small_10x10,Dijkstra,1,0.290000,15,15
|
||||||
|
small_10x10,Dijkstra,2,0.083300,15,15
|
||||||
|
small_10x10,Dijkstra,3,0.091500,15,15
|
||||||
|
small_10x10,Dijkstra,4,0.081000,15,15
|
||||||
|
small_10x10,Dijkstra,5,0.080400,15,15
|
||||||
|
medium_50x50,BFS,1,6.799200,1579,95
|
||||||
|
medium_50x50,BFS,2,6.960100,1579,95
|
||||||
|
medium_50x50,BFS,3,6.337000,1579,95
|
||||||
|
medium_50x50,BFS,4,7.431700,1579,95
|
||||||
|
medium_50x50,BFS,5,6.517900,1579,95
|
||||||
|
medium_50x50,DFS,1,6.463000,1277,647
|
||||||
|
medium_50x50,DFS,2,6.815500,1277,647
|
||||||
|
medium_50x50,DFS,3,5.816100,1277,647
|
||||||
|
medium_50x50,DFS,4,6.492400,1277,647
|
||||||
|
medium_50x50,DFS,5,6.532500,1277,647
|
||||||
|
medium_50x50,A*,1,6.940500,927,95
|
||||||
|
medium_50x50,A*,2,7.275400,927,95
|
||||||
|
medium_50x50,A*,3,7.062500,927,95
|
||||||
|
medium_50x50,A*,4,7.727600,927,95
|
||||||
|
medium_50x50,A*,5,7.321000,927,95
|
||||||
|
medium_50x50,Dijkstra,1,11.483200,1579,95
|
||||||
|
medium_50x50,Dijkstra,2,11.194200,1579,95
|
||||||
|
medium_50x50,Dijkstra,3,11.255200,1579,95
|
||||||
|
medium_50x50,Dijkstra,4,10.512500,1579,95
|
||||||
|
medium_50x50,Dijkstra,5,10.696400,1579,95
|
||||||
|
large_100x100,BFS,1,25.623500,5566,195
|
||||||
|
large_100x100,BFS,2,24.348800,5566,195
|
||||||
|
large_100x100,BFS,3,25.452600,5566,195
|
||||||
|
large_100x100,BFS,4,30.516900,5566,195
|
||||||
|
large_100x100,BFS,5,33.694700,5566,195
|
||||||
|
large_100x100,DFS,1,19.415200,3543,1531
|
||||||
|
large_100x100,DFS,2,19.919000,3543,1531
|
||||||
|
large_100x100,DFS,3,19.104600,3543,1531
|
||||||
|
large_100x100,DFS,4,20.000600,3543,1531
|
||||||
|
large_100x100,DFS,5,17.840200,3543,1531
|
||||||
|
large_100x100,A*,1,7.509300,853,195
|
||||||
|
large_100x100,A*,2,7.221200,853,195
|
||||||
|
large_100x100,A*,3,6.486700,853,195
|
||||||
|
large_100x100,A*,4,6.357600,853,195
|
||||||
|
large_100x100,A*,5,6.723800,853,195
|
||||||
|
large_100x100,Dijkstra,1,40.782300,5571,195
|
||||||
|
large_100x100,Dijkstra,2,41.155000,5571,195
|
||||||
|
large_100x100,Dijkstra,3,39.456200,5571,195
|
||||||
|
large_100x100,Dijkstra,4,41.388700,5571,195
|
||||||
|
large_100x100,Dijkstra,5,40.962500,5571,195
|
||||||
|
empty_30x30,BFS,1,4.143200,896,55
|
||||||
|
empty_30x30,BFS,2,3.987000,896,55
|
||||||
|
empty_30x30,BFS,3,3.777100,896,55
|
||||||
|
empty_30x30,BFS,4,3.682300,896,55
|
||||||
|
empty_30x30,BFS,5,3.737900,896,55
|
||||||
|
empty_30x30,DFS,1,4.024200,842,815
|
||||||
|
empty_30x30,DFS,2,4.333900,842,815
|
||||||
|
empty_30x30,DFS,3,5.411000,842,815
|
||||||
|
empty_30x30,DFS,4,4.677200,842,815
|
||||||
|
empty_30x30,DFS,5,5.177400,842,815
|
||||||
|
empty_30x30,A*,1,6.603700,784,55
|
||||||
|
empty_30x30,A*,2,6.200600,784,55
|
||||||
|
empty_30x30,A*,3,6.798400,784,55
|
||||||
|
empty_30x30,A*,4,7.178500,784,55
|
||||||
|
empty_30x30,A*,5,6.660800,784,55
|
||||||
|
empty_30x30,Dijkstra,1,6.396000,896,55
|
||||||
|
empty_30x30,Dijkstra,2,6.275200,896,55
|
||||||
|
empty_30x30,Dijkstra,3,6.845700,896,55
|
||||||
|
empty_30x30,Dijkstra,4,6.531200,896,55
|
||||||
|
empty_30x30,Dijkstra,5,6.783400,896,55
|
||||||
|
no_path_30x30,BFS,1,2.000100,450,0
|
||||||
|
no_path_30x30,BFS,2,1.797900,450,0
|
||||||
|
no_path_30x30,BFS,3,1.796200,450,0
|
||||||
|
no_path_30x30,BFS,4,1.774100,450,0
|
||||||
|
no_path_30x30,BFS,5,1.775200,450,0
|
||||||
|
no_path_30x30,DFS,1,2.090400,450,0
|
||||||
|
no_path_30x30,DFS,2,2.222600,450,0
|
||||||
|
no_path_30x30,DFS,3,2.454300,450,0
|
||||||
|
no_path_30x30,DFS,4,2.476200,450,0
|
||||||
|
no_path_30x30,DFS,5,2.073700,450,0
|
||||||
|
no_path_30x30,A*,1,3.651700,450,0
|
||||||
|
no_path_30x30,A*,2,3.495200,450,0
|
||||||
|
no_path_30x30,A*,3,3.754200,450,0
|
||||||
|
no_path_30x30,A*,4,3.286800,450,0
|
||||||
|
no_path_30x30,A*,5,3.335200,450,0
|
||||||
|
no_path_30x30,Dijkstra,1,3.050900,450,0
|
||||||
|
no_path_30x30,Dijkstra,2,3.109900,450,0
|
||||||
|
no_path_30x30,Dijkstra,3,3.292500,450,0
|
||||||
|
no_path_30x30,Dijkstra,4,3.418600,450,0
|
||||||
|
no_path_30x30,Dijkstra,5,3.212100,450,0
|
||||||
|
weighted_30x30,BFS,1,3.418900,788,55
|
||||||
|
weighted_30x30,BFS,2,3.368200,788,55
|
||||||
|
weighted_30x30,BFS,3,3.516400,788,55
|
||||||
|
weighted_30x30,BFS,4,3.224300,788,55
|
||||||
|
weighted_30x30,BFS,5,3.131100,788,55
|
||||||
|
weighted_30x30,DFS,1,3.291200,693,479
|
||||||
|
weighted_30x30,DFS,2,3.362300,693,479
|
||||||
|
weighted_30x30,DFS,3,3.523200,693,479
|
||||||
|
weighted_30x30,DFS,4,3.521400,693,479
|
||||||
|
weighted_30x30,DFS,5,3.332300,693,479
|
||||||
|
weighted_30x30,A*,1,1.181000,126,55
|
||||||
|
weighted_30x30,A*,2,1.080200,126,55
|
||||||
|
weighted_30x30,A*,3,1.368400,126,55
|
||||||
|
weighted_30x30,A*,4,1.109800,126,55
|
||||||
|
weighted_30x30,A*,5,1.079300,126,55
|
||||||
|
weighted_30x30,Dijkstra,1,6.112700,781,55
|
||||||
|
weighted_30x30,Dijkstra,2,5.464800,781,55
|
||||||
|
weighted_30x30,Dijkstra,3,5.794500,781,55
|
||||||
|
weighted_30x30,Dijkstra,4,6.171700,781,55
|
||||||
|
weighted_30x30,Dijkstra,5,6.640500,781,55
|
||||||
|
BIN
petryaninyas/task2/experiment_results/small_10x10_length.png
Normal file
|
After Width: | Height: | Size: 20 KiB |
BIN
petryaninyas/task2/experiment_results/small_10x10_time.png
Normal file
|
After Width: | Height: | Size: 23 KiB |
BIN
petryaninyas/task2/experiment_results/small_10x10_visited.png
Normal file
|
After Width: | Height: | Size: 20 KiB |
25
petryaninyas/task2/experiment_results/summary.csv
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
maze,strategy,avg_time_ms,avg_visited_cells,avg_path_length,runs
|
||||||
|
small_10x10,BFS,0.064720,15.00,15.00,5
|
||||||
|
small_10x10,DFS,0.066600,15.00,15.00,5
|
||||||
|
small_10x10,A*,0.092340,15.00,15.00,5
|
||||||
|
small_10x10,Dijkstra,0.125240,15.00,15.00,5
|
||||||
|
medium_50x50,BFS,6.809180,1579.00,95.00,5
|
||||||
|
medium_50x50,DFS,6.423900,1277.00,647.00,5
|
||||||
|
medium_50x50,A*,7.265400,927.00,95.00,5
|
||||||
|
medium_50x50,Dijkstra,11.028300,1579.00,95.00,5
|
||||||
|
large_100x100,BFS,27.927300,5566.00,195.00,5
|
||||||
|
large_100x100,DFS,19.255920,3543.00,1531.00,5
|
||||||
|
large_100x100,A*,6.859720,853.00,195.00,5
|
||||||
|
large_100x100,Dijkstra,40.748940,5571.00,195.00,5
|
||||||
|
empty_30x30,BFS,3.865500,896.00,55.00,5
|
||||||
|
empty_30x30,DFS,4.724740,842.00,815.00,5
|
||||||
|
empty_30x30,A*,6.688400,784.00,55.00,5
|
||||||
|
empty_30x30,Dijkstra,6.566300,896.00,55.00,5
|
||||||
|
no_path_30x30,BFS,1.828700,450.00,0.00,5
|
||||||
|
no_path_30x30,DFS,2.263440,450.00,0.00,5
|
||||||
|
no_path_30x30,A*,3.504620,450.00,0.00,5
|
||||||
|
no_path_30x30,Dijkstra,3.216800,450.00,0.00,5
|
||||||
|
weighted_30x30,BFS,3.331780,788.00,55.00,5
|
||||||
|
weighted_30x30,DFS,3.406080,693.00,479.00,5
|
||||||
|
weighted_30x30,A*,1.163740,126.00,55.00,5
|
||||||
|
weighted_30x30,Dijkstra,6.036840,781.00,55.00,5
|
||||||
|
BIN
petryaninyas/task2/experiment_results/weighted_30x30_length.png
Normal file
|
After Width: | Height: | Size: 24 KiB |
BIN
petryaninyas/task2/experiment_results/weighted_30x30_time.png
Normal file
|
After Width: | Height: | Size: 19 KiB |
BIN
petryaninyas/task2/experiment_results/weighted_30x30_visited.png
Normal file
|
After Width: | Height: | Size: 28 KiB |
59
petryaninyas/task2/main.py
Normal file
|
|
@ -0,0 +1,59 @@
|
||||||
|
from builders.text_file_maze_builder import TextFileMazeBuilder
|
||||||
|
from core.player import Player
|
||||||
|
from observer.console_view import ConsoleView
|
||||||
|
from solver.maze_solver import MazeSolver
|
||||||
|
from strategies.astar_strategy import AStarStrategy
|
||||||
|
from strategies.bfs_strategy import BFSStrategy
|
||||||
|
from strategies.dfs_strategy import DFSStrategy
|
||||||
|
from controller.game_controller import GameController
|
||||||
|
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
BASE_DIR = Path(__file__).resolve().parent
|
||||||
|
|
||||||
|
|
||||||
|
def run_demo():
|
||||||
|
builder = TextFileMazeBuilder()
|
||||||
|
maze = builder.buildFromFile(str(BASE_DIR / "mazes" / "maze_small.txt"))
|
||||||
|
|
||||||
|
view = ConsoleView()
|
||||||
|
view.update({"type": "maze_loaded", "message": "Maze loaded"})
|
||||||
|
view.render(maze)
|
||||||
|
|
||||||
|
solver = MazeSolver(maze)
|
||||||
|
solver.addObserver(view)
|
||||||
|
|
||||||
|
for strategy in (BFSStrategy(), DFSStrategy(), AStarStrategy()):
|
||||||
|
solver.setStrategy(strategy)
|
||||||
|
stats = solver.solve()
|
||||||
|
|
||||||
|
print()
|
||||||
|
print(f"=== {strategy.name} ===")
|
||||||
|
print(f"Time: {stats.timeMs:.3f} ms")
|
||||||
|
print(f"Visited cells: {stats.visitedCells}")
|
||||||
|
print(f"Path length: {stats.pathLength}")
|
||||||
|
print(f"Path found: {'yes' if stats.found else 'no'}")
|
||||||
|
|
||||||
|
view.render(maze, path=stats.path)
|
||||||
|
|
||||||
|
player = Player(maze.startCell)
|
||||||
|
controller = GameController(maze, player, view)
|
||||||
|
|
||||||
|
print("Manual mode: W/A/S/D move, Z undo, Q quit")
|
||||||
|
view.render(maze, player_position=player.currentCell)
|
||||||
|
|
||||||
|
while True:
|
||||||
|
cmd = input("Command: ").strip().upper()
|
||||||
|
if cmd == "Q":
|
||||||
|
break
|
||||||
|
if cmd == "Z":
|
||||||
|
controller.undo()
|
||||||
|
elif cmd in {"W", "A", "S", "D"}:
|
||||||
|
controller.move(cmd)
|
||||||
|
else:
|
||||||
|
print("Unknown command")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
run_demo()
|
||||||
9
petryaninyas/task2/mazes/maze_empty.txt
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
S
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
E
|
||||||
11
petryaninyas/task2/mazes/maze_large.txt
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
####################################################################################################
|
||||||
|
#S # # # # # # # # # # # # # # # E#
|
||||||
|
# # ### ### # ###### # ### # ## # #### # ####### # #### # # ### ## # ## # # ## # ## # ##### ### ##
|
||||||
|
# # # # # # # # # # # # # # # # # # # # # # # # # # #
|
||||||
|
# ##### # ######## # ### # ## # #### # ####### ## ### # # #### ####### ## ####### ####### # ### ##
|
||||||
|
# # # # # # # # # # # # # # # # # # # # #
|
||||||
|
### # # ###### # ########### ########### ### ####### # ####### ### # # ###### # ### ### # ### ####
|
||||||
|
# # # # # # # # # # # # # # # # # # # # # #
|
||||||
|
# ### ###### # ##### # ### # ####### # ### ### ## # ###### # ### # ### ###### # ### # ### ### ## #
|
||||||
|
# # # # # # # # #
|
||||||
|
####################################################################################################
|
||||||
11
petryaninyas/task2/mazes/maze_medium.txt
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
##################################################
|
||||||
|
#S # # # # # # E#
|
||||||
|
# # ### ### # ###### # ### # ## # #### # ####### ##
|
||||||
|
# # # # # # # # # # # # # #
|
||||||
|
# ##### # ######## # ### # ## # #### # ####### ## #
|
||||||
|
# # # # # # # # # #
|
||||||
|
### # # ###### # ########### ########### ### ######
|
||||||
|
# # # # # # # # # # #
|
||||||
|
# ### ###### # ##### # ### # ####### # ### ### ## #
|
||||||
|
# # # # #
|
||||||
|
##################################################
|
||||||
9
petryaninyas/task2/mazes/maze_no_path.txt
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
##########
|
||||||
|
#S #
|
||||||
|
# ###### #
|
||||||
|
# # #
|
||||||
|
##########
|
||||||
|
# #E#
|
||||||
|
# ###### #
|
||||||
|
# #
|
||||||
|
##########
|
||||||
7
petryaninyas/task2/mazes/maze_small.txt
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
##########
|
||||||
|
#S #E#
|
||||||
|
# ## # # ##
|
||||||
|
# # #
|
||||||
|
# #### # #
|
||||||
|
# # #
|
||||||
|
##########
|
||||||
10
petryaninyas/task2/mazes/maze_weighted.txt
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
1111111111111111111111111111
|
||||||
|
1S11111111111111111111111111
|
||||||
|
1111111111111111111111111111
|
||||||
|
1111111111111111111111111111
|
||||||
|
1111111111111222222222222111
|
||||||
|
1111111111111222222222222111
|
||||||
|
1111111111111333333333333111
|
||||||
|
1111111111111333333333333111
|
||||||
|
111111111111111111111111111E
|
||||||
|
1111111111111111111111111111
|
||||||