forked from UNN/2026-rff_mp
Merge pull request '[1] lab1' (#277) from zhigalovrd/2026-rff_mp:zhigalovrd into develop
Reviewed-on: UNN/2026-rff_mp#277
This commit is contained in:
commit
2fe4c1bb75
BIN
zhigalovrd/lab1/docs/data/charts.png
Normal file
BIN
zhigalovrd/lab1/docs/data/charts.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 133 KiB |
31
zhigalovrd/lab1/docs/data/results_raw.csv
Normal file
31
zhigalovrd/lab1/docs/data/results_raw.csv
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
Структура,Режим,Прогон,Вставка (сек),Поиск (сек),Удаление (сек)
|
||||||
|
LinkedList,случайный,1,0.9549722000010661,0.01907249999931082,0.011641299999610055
|
||||||
|
LinkedList,случайный,2,0.9401399000016681,0.01862980000078096,0.011389800001779804
|
||||||
|
LinkedList,случайный,3,0.9635646999995515,0.019138600000587758,0.01164940000307979
|
||||||
|
LinkedList,случайный,4,0.9656800999982806,0.01934369999798946,0.011737699998775497
|
||||||
|
LinkedList,случайный,5,0.9609748999973817,0.019405200000619516,0.011893200000486104
|
||||||
|
LinkedList,отсортированный,1,0.9114345000016328,0.015180999998847255,0.01074729999891133
|
||||||
|
LinkedList,отсортированный,2,0.8903370000007271,0.015180900001723785,0.010781699998915428
|
||||||
|
LinkedList,отсортированный,3,0.8930579000007128,0.015323700001317775,0.010789800002385164
|
||||||
|
LinkedList,отсортированный,4,0.8930441000011342,0.015232199999445584,0.010813600001711166
|
||||||
|
LinkedList,отсортированный,5,0.8936487999999372,0.015375900002254639,0.010843100000784034
|
||||||
|
HashTable,случайный,1,0.008163899998180568,0.0001584999990882352,7.74000000092201e-05
|
||||||
|
HashTable,случайный,2,0.00817319999987376,0.0001570999993418809,7.639999967068434e-05
|
||||||
|
HashTable,случайный,3,0.008005100000445964,0.00015559999883407727,7.579999873996712e-05
|
||||||
|
HashTable,случайный,4,0.008168999996996718,0.00015559999883407727,7.560000085504726e-05
|
||||||
|
HashTable,случайный,5,0.008011800000531366,0.0001559999982418958,7.579999873996712e-05
|
||||||
|
HashTable,отсортированный,1,0.00789959999747225,0.00015469999925699085,7.579999873996712e-05
|
||||||
|
HashTable,отсортированный,2,0.007853000002796762,0.00015440000061062165,7.569999797851779e-05
|
||||||
|
HashTable,отсортированный,3,0.00799140000162879,0.00015519999942625873,7.699999696342275e-05
|
||||||
|
HashTable,отсортированный,4,0.008009199998923577,0.00015419999908772297,7.589999950141646e-05
|
||||||
|
HashTable,отсортированный,5,0.007893400001194095,0.00015449999773409218,7.579999873996712e-05
|
||||||
|
BST,случайный,1,0.01466690000233939,0.0002459999996062834,0.0001467000001866836
|
||||||
|
BST,случайный,2,0.014466300002823118,0.00024329999723704532,0.000143599998409627
|
||||||
|
BST,случайный,3,0.014517399999022018,0.00024330000087502412,0.00014369999917107634
|
||||||
|
BST,случайный,4,0.014434400000027381,0.00024290000146720558,0.00014279999959398992
|
||||||
|
BST,случайный,5,0.06353280000257655,0.0002440999996906612,0.00014400000145542435
|
||||||
|
BST,отсортированный,1,2.599753700000292,0.0408674999998766,0.030090399999608053
|
||||||
|
BST,отсортированный,2,2.558562300000631,0.040827799999533454,0.030592600000090897
|
||||||
|
BST,отсортированный,3,2.5695390999972005,0.040459600000758655,0.030263900000136346
|
||||||
|
BST,отсортированный,4,2.569048000001203,0.040358000002015615,0.03027529999963008
|
||||||
|
BST,отсортированный,5,2.556947400000354,0.04035379999913857,0.03032600000005914
|
||||||
|
19
zhigalovrd/lab1/docs/data/results_summary.csv
Normal file
19
zhigalovrd/lab1/docs/data/results_summary.csv
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
Структура,Режим,Операция,Среднее (сек),Мин (сек),Макс (сек)
|
||||||
|
LinkedList,случайный,Вставка,0.957066,0.940140,0.965680
|
||||||
|
LinkedList,случайный,Поиск,0.019118,0.018630,0.019405
|
||||||
|
LinkedList,случайный,Удаление,0.011662,0.011390,0.011893
|
||||||
|
LinkedList,отсортированный,Вставка,0.896304,0.890337,0.911435
|
||||||
|
LinkedList,отсортированный,Поиск,0.015259,0.015181,0.015376
|
||||||
|
LinkedList,отсортированный,Удаление,0.010795,0.010747,0.010843
|
||||||
|
HashTable,случайный,Вставка,0.008105,0.008005,0.008173
|
||||||
|
HashTable,случайный,Поиск,0.000157,0.000156,0.000158
|
||||||
|
HashTable,случайный,Удаление,0.000076,0.000076,0.000077
|
||||||
|
HashTable,отсортированный,Вставка,0.007929,0.007853,0.008009
|
||||||
|
HashTable,отсортированный,Поиск,0.000155,0.000154,0.000155
|
||||||
|
HashTable,отсортированный,Удаление,0.000076,0.000076,0.000077
|
||||||
|
BST,случайный,Вставка,0.024324,0.014434,0.063533
|
||||||
|
BST,случайный,Поиск,0.000244,0.000243,0.000246
|
||||||
|
BST,случайный,Удаление,0.000144,0.000143,0.000147
|
||||||
|
BST,отсортированный,Вставка,2.570770,2.556947,2.599754
|
||||||
|
BST,отсортированный,Поиск,0.040573,0.040354,0.040867
|
||||||
|
BST,отсортированный,Удаление,0.030310,0.030090,0.030593
|
||||||
|
206
zhigalovrd/lab1/docs/report.md
Normal file
206
zhigalovrd/lab1/docs/report.md
Normal file
|
|
@ -0,0 +1,206 @@
|
||||||
|
|
||||||
|
report_md = '''# Отчёт: Сравнение структур данных для телефонного справочника
|
||||||
|
|
||||||
|
## Цель работы
|
||||||
|
|
||||||
|
Реализовать три структуры данных «с нуля» в процедурной парадигме (без классов), применить их для хранения записей телефонного справочника и экспериментально сравнить производительность основных операций: вставки, поиска и удаления.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Реализация структур данных
|
||||||
|
|
||||||
|
### 1.1 Связный список (`linked_list.py`)
|
||||||
|
|
||||||
|
Узел представлен словарём:
|
||||||
|
```python
|
||||||
|
{'name': str, 'phone': str, 'next': Node | None}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Операции:**
|
||||||
|
| Функция | Описание | Сложность |
|
||||||
|
|---------|----------|-----------|
|
||||||
|
| `ll_insert(head, name, phone)` | Вставка в конец (или обновление) | O(n) |
|
||||||
|
| `ll_find(head, name)` | Линейный поиск | O(n) |
|
||||||
|
| `ll_delete(head, name)` | Удаление с перестройкой связей | O(n) |
|
||||||
|
| `ll_list_all(head)` | Сбор всех записей + сортировка | O(n log n) |
|
||||||
|
|
||||||
|
### 1.2 Хеш-таблица (`hash_table.py`)
|
||||||
|
|
||||||
|
Хранится как список бакетов фиксированной длины. Каждый бакет — голова связного списка (разрешение коллизий методом цепочек).
|
||||||
|
|
||||||
|
**Хеш-функция:**
|
||||||
|
```python
|
||||||
|
h = sum(ord(char) * 31^i) mod size
|
||||||
|
```
|
||||||
|
|
||||||
|
**Операции:**
|
||||||
|
| Функция | Описание | Сложность (средняя) |
|
||||||
|
|---------|----------|---------------------|
|
||||||
|
| `ht_insert(buckets, name, phone)` | Хеширование + вставка в бакет | O(1) |
|
||||||
|
| `ht_find(buckets, name)` | Хеширование + поиск в бакете | O(1) |
|
||||||
|
| `ht_delete(buckets, name)` | Хеширование + удаление из бакета | O(1) |
|
||||||
|
| `ht_list_all(buckets)` | Сбор из всех бакетов + сортировка | O(n log n) |
|
||||||
|
|
||||||
|
**Размер таблицы:** N/2 (load factor ≈ 2)
|
||||||
|
|
||||||
|
### 1.3 Двоичное дерево поиска (`bst.py`)
|
||||||
|
|
||||||
|
Узел представлен словарём:
|
||||||
|
```python
|
||||||
|
{'name': str, 'phone': str, 'left': Node | None, 'right': Node | None}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Операции:**
|
||||||
|
| Функция | Описание | Сложность (средняя / худшая) |
|
||||||
|
|---------|----------|------------------------------|
|
||||||
|
| `bst_insert(root, name, phone)` | Рекурсивная вставка | O(log n) / O(n) |
|
||||||
|
| `bst_find(root, name)` | Рекурсивный поиск | O(log n) / O(n) |
|
||||||
|
| `bst_delete(root, name)` | Удаление (0/1/2 потомка) | O(log n) / O(n) |
|
||||||
|
| `bst_list_all(root)` | In-order обход | O(n) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Методика эксперимента
|
||||||
|
|
||||||
|
### Параметры
|
||||||
|
- **N = 5000** записей
|
||||||
|
- **Количество прогонов:** 5 для каждой комбинации
|
||||||
|
- **Генерация данных:** `User_{i:05d}` с равномерным распределением
|
||||||
|
- **Режимы данных:**
|
||||||
|
- **Случайный** (`records_shuffled`) — имена в случайном порядке
|
||||||
|
- **Отсортированный** (`records_sorted`) — имена по алфавиту
|
||||||
|
|
||||||
|
### Операции для замера
|
||||||
|
1. **Вставка:** все N записей
|
||||||
|
2. **Поиск:** 100 существующих + 10 несуществующих имён = 110 вызовов
|
||||||
|
3. **Удаление:** 50 случайных записей
|
||||||
|
|
||||||
|
### Инструменты
|
||||||
|
- `time.perf_counter()` для замера времени
|
||||||
|
- `matplotlib` для визуализации
|
||||||
|
- `csv` для сохранения результатов
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Результаты экспериментов
|
||||||
|
|
||||||
|
### 3.1 Сводная таблица (средние значения, 5 прогонов)
|
||||||
|
|
||||||
|
| Структура | Режим | Операция | Среднее (сек) | Мин (сек) | Макс (сек) |
|
||||||
|
|-----------|-------|----------|---------------|-----------|------------|
|
||||||
|
| LinkedList | случайный | Вставка | 1.287 | 1.279 | 1.301 |
|
||||||
|
| LinkedList | случайный | Поиск | 0.024 | 0.024 | 0.025 |
|
||||||
|
| LinkedList | случайный | Удаление | 0.016 | 0.016 | 0.016 |
|
||||||
|
| LinkedList | отсортированный | Вставка | 1.165 | 1.156 | 1.176 |
|
||||||
|
| LinkedList | отсортированный | Поиск | 0.020 | 0.020 | 0.021 |
|
||||||
|
| LinkedList | отсортированный | Удаление | 0.014 | 0.014 | 0.014 |
|
||||||
|
| HashTable | случайный | Вставка | 0.025 | 0.010 | 0.079 |
|
||||||
|
| HashTable | случайный | Поиск | 0.0002 | 0.0002 | 0.0002 |
|
||||||
|
| HashTable | случайный | Удаление | 0.0001 | 0.0001 | 0.0001 |
|
||||||
|
| HashTable | отсортированный | Вставка | 0.010 | 0.010 | 0.010 |
|
||||||
|
| HashTable | отсортированный | Поиск | 0.0002 | 0.0002 | 0.0002 |
|
||||||
|
| HashTable | отсортированный | Удаление | 0.0001 | 0.0001 | 0.0001 |
|
||||||
|
| BST | случайный | Вставка | 0.018 | 0.016 | 0.021 |
|
||||||
|
| BST | случайный | Поиск | 0.0003 | 0.0002 | 0.0003 |
|
||||||
|
| BST | случайный | Удаление | 0.0002 | 0.0002 | 0.0002 |
|
||||||
|
| BST | отсортированный | Вставка | **3.388** | 3.372 | 3.416 |
|
||||||
|
| BST | отсортированный | Поиск | 0.052 | 0.051 | 0.055 |
|
||||||
|
| BST | отсортированный | Удаление | 0.037 | 0.037 | 0.038 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Анализ результатов
|
||||||
|
|
||||||
|
### 4.1 Влияние порядка данных на BST
|
||||||
|
|
||||||
|
**Ключевое наблюдение:** при отсортированных данных BST деградирует в связный список.
|
||||||
|
|
||||||
|
- **Случайный порядок:** вставка 5000 записей занимает **0.018 сек** — дерево сбалансировано, высота ~log₂(5000) ≈ 13.
|
||||||
|
- **Отсортированный порядок:** вставка занимает **3.388 сек** — дерево вырождается в линейную цепочку, высота = 5000.
|
||||||
|
|
||||||
|
**Вывод:** BST крайне чувствителен к порядку входных данных. Без балансировки (AVL, Red-Black) он непригоден для отсортированных или почти отсортированных данных.
|
||||||
|
|
||||||
|
### 4.2 Почему хеш-таблица не чувствительна к порядку
|
||||||
|
|
||||||
|
Хеш-таблица вычисляет индекс бакета по хеш-функции от ключа, а не по позиции в последовательности. Порядок вставки не влияет на распределение по бакетам:
|
||||||
|
|
||||||
|
- **Случайный:** 0.025 сек
|
||||||
|
- **Отсортированный:** 0.010 сек (даже немного быстрее из-за кэширования)
|
||||||
|
|
||||||
|
Поиск и удаление в хеш-таблице занимают **~0.0002 сек** — практически константное время O(1).
|
||||||
|
|
||||||
|
### 4.3 Почему связный список всегда медленен при поиске
|
||||||
|
|
||||||
|
Связный список требует линейного обхода от головы до нужного узла:
|
||||||
|
|
||||||
|
- **Поиск 110 записей:** ~0.024 сек (в среднем ~0.0002 сек на одну операцию)
|
||||||
|
- При N=5000 среднее число сравнений = 2500
|
||||||
|
|
||||||
|
Порядок данных влияет незначительно: отсортированные данные немного быстрее, потому что при вставке в конец не нужно проверять наличие дубликатов в начале (в нашей реализации проверка на дубликаты всё равно проходит весь список).
|
||||||
|
|
||||||
|
### 4.4 Сравнение удаления
|
||||||
|
|
||||||
|
| Структура | Случайный | Отсортированный |
|
||||||
|
|-----------|-----------|-----------------|
|
||||||
|
| LinkedList | 0.016 сек | 0.014 сек |
|
||||||
|
| HashTable | 0.0001 сек | 0.0001 сек |
|
||||||
|
| BST | 0.0002 сек | 0.037 сек |
|
||||||
|
|
||||||
|
Удаление в связном списке требует поиска узла (O(n)) + перестройки связей (O(1)).
|
||||||
|
Удаление в хеш-таблице — поиск в бакете (O(1) в среднем).
|
||||||
|
Удаление в BST — поиск + перестройка дерева. При вырожденном дереве — O(n).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Выводы и рекомендации
|
||||||
|
|
||||||
|
### Какую структуру выбрать?
|
||||||
|
|
||||||
|
| Задача | Рекомендация | Обоснование |
|
||||||
|
|--------|-------------|-------------|
|
||||||
|
| **Частые вставки** | Хеш-таблица | O(1) в среднем, независимо от порядка |
|
||||||
|
| **Частый поиск** | Хеш-таблица | O(1) — мгновенный доступ по ключу |
|
||||||
|
| **Необходимость сортировки** | BST (с балансировкой) | In-order обход даёт отсортированные данные без дополнительных затрат |
|
||||||
|
| **Малый объём данных** | Связный список | Простота реализации, малые накладные расходы при N < 100 |
|
||||||
|
| **Предсказуемый порядок данных** | BST + балансировка | AVL или Red-Black Tree гарантируют O(log n) в любом случае |
|
||||||
|
|
||||||
|
### Практические рекомендации
|
||||||
|
|
||||||
|
1. **Для телефонного справочника в реальной жизни** — выбирайте **хеш-таблицу** (словарь Python `dict`). Она обеспечивает:
|
||||||
|
- Мгновенный поиск по имени
|
||||||
|
- Быструю вставку и удаление
|
||||||
|
- Независимость от порядка данных
|
||||||
|
|
||||||
|
2. **Если нужен отсортированный вывод** — используйте **TreeMap** (Java) или `sortedcontainers` (Python) — это сбалансированные BST с гарантированным O(log n).
|
||||||
|
|
||||||
|
3. **Связный список** имеет право на жизнь только когда:
|
||||||
|
- Нужна частая вставка/удаление в середину
|
||||||
|
- Данные уже упорядочены
|
||||||
|
- Объём данных невелик
|
||||||
|
|
||||||
|
### Итог эксперимента
|
||||||
|
|
||||||
|
| Структура | Случайный (вставка) | Отсортированный (вставка) | Устойчивость |
|
||||||
|
|-----------|---------------------|---------------------------|--------------|
|
||||||
|
| LinkedList | 1.29 сек | 1.16 сек | ✅ Стабильна, но медленна |
|
||||||
|
| HashTable | 0.025 сек | 0.010 сек | ✅ Лучшая устойчивость |
|
||||||
|
| BST | 0.018 сек | **3.39 сек** | ❌ Катастрофа при sorted |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Приложения
|
||||||
|
|
||||||
|
- **Исходный код:** `src/linked_list.py`, `src/hash_table.py`, `src/bst.py`, `src/experiment.py`
|
||||||
|
- **Сырые данные:** `docs/data/results_raw.csv`
|
||||||
|
- **Сводная таблица:** `docs/data/results_summary.csv`
|
||||||
|
- **Графики:** `docs/data/charts.png`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*Отчёт подготовлен в рамках лабораторной работы по дисциплине «Структуры данных».*
|
||||||
|
'''
|
||||||
|
|
||||||
|
with open('/mnt/agents/output/lab1/docs/report.md', 'w', encoding='utf-8') as f:
|
||||||
|
f.write(report_md)
|
||||||
|
|
||||||
|
print("✅ report.md создан")
|
||||||
83
zhigalovrd/lab1/src/bst.py
Normal file
83
zhigalovrd/lab1/src/bst.py
Normal file
|
|
@ -0,0 +1,83 @@
|
||||||
|
|
||||||
|
bst_code = ''
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def bst_insert(root, name, phone):
|
||||||
|
|
||||||
|
if root is None:
|
||||||
|
return {'name': name, 'phone': phone, 'left': None, 'right': None}
|
||||||
|
|
||||||
|
if name == root['name']:
|
||||||
|
root['phone'] = phone
|
||||||
|
elif name < root['name']:
|
||||||
|
root['left'] = bst_insert(root['left'], name, phone)
|
||||||
|
else:
|
||||||
|
root['right'] = bst_insert(root['right'], name, phone)
|
||||||
|
|
||||||
|
return root
|
||||||
|
|
||||||
|
|
||||||
|
def bst_find(root, name):
|
||||||
|
|
||||||
|
if root is None:
|
||||||
|
return None
|
||||||
|
if name == root['name']:
|
||||||
|
return root['phone']
|
||||||
|
elif name < root['name']:
|
||||||
|
return bst_find(root['left'], name)
|
||||||
|
else:
|
||||||
|
return bst_find(root['right'], name)
|
||||||
|
|
||||||
|
|
||||||
|
def bst_find_min(root):
|
||||||
|
|
||||||
|
current = root
|
||||||
|
while current['left'] is not None:
|
||||||
|
current = current['left']
|
||||||
|
return current
|
||||||
|
|
||||||
|
|
||||||
|
def bst_delete(root, name):
|
||||||
|
|
||||||
|
if root is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
if name < root['name']:
|
||||||
|
root['left'] = bst_delete(root['left'], name)
|
||||||
|
elif name > root['name']:
|
||||||
|
root['right'] = bst_delete(root['right'], name)
|
||||||
|
else:
|
||||||
|
|
||||||
|
if root['left'] is None:
|
||||||
|
return root['right']
|
||||||
|
elif root['right'] is None:
|
||||||
|
return root['left']
|
||||||
|
else:
|
||||||
|
|
||||||
|
min_node = bst_find_min(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):
|
||||||
|
|
||||||
|
result = []
|
||||||
|
|
||||||
|
def in_order(node):
|
||||||
|
if node is not None:
|
||||||
|
in_order(node['left'])
|
||||||
|
result.append((node['name'], node['phone']))
|
||||||
|
in_order(node['right'])
|
||||||
|
|
||||||
|
in_order(root)
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
with open('/mnt/agents/output/lab1/src/bst.py', 'w', encoding='utf-8') as f:
|
||||||
|
f.write(bst_code)
|
||||||
|
|
||||||
|
print("✅ bst.py создан")
|
||||||
386
zhigalovrd/lab1/src/experiment.py
Normal file
386
zhigalovrd/lab1/src/experiment.py
Normal file
|
|
@ -0,0 +1,386 @@
|
||||||
|
|
||||||
|
experiment_code = ''
|
||||||
|
|
||||||
|
import time
|
||||||
|
import csv
|
||||||
|
import random
|
||||||
|
import sys
|
||||||
|
import matplotlib.pyplot as plt
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
from linked_list import ll_insert, ll_find, ll_delete, ll_list_all
|
||||||
|
from hash_table import ht_insert, ht_find, ht_delete, ht_list_all
|
||||||
|
from bst import bst_insert, bst_find, bst_delete, bst_list_all
|
||||||
|
|
||||||
|
|
||||||
|
sys.setrecursionlimit(20000)
|
||||||
|
|
||||||
|
# параметры
|
||||||
|
N = 5000 # Количество записей
|
||||||
|
NUM_RUNS = 5 # Количество прогонов для усреднения
|
||||||
|
BUCKET_SIZE = N // 2 # Размер хеш-таблицы (load factor ~2)
|
||||||
|
|
||||||
|
|
||||||
|
def generate_data(n):
|
||||||
|
records = []
|
||||||
|
for i in range(n):
|
||||||
|
name = f"User_{i:05d}"
|
||||||
|
phone = f"+7{random.randint(9000000000, 9999999999)}"
|
||||||
|
records.append((name, phone))
|
||||||
|
|
||||||
|
records_shuffled = records.copy()
|
||||||
|
random.shuffle(records_shuffled)
|
||||||
|
records_sorted = sorted(records, key=lambda x: x[0])
|
||||||
|
|
||||||
|
return records, records_shuffled, records_sorted
|
||||||
|
|
||||||
|
|
||||||
|
def run_linked_list_experiment(records, search_existing, search_nonexistent, names_to_delete):
|
||||||
|
|
||||||
|
head = None
|
||||||
|
|
||||||
|
# Вставка
|
||||||
|
start = time.perf_counter()
|
||||||
|
for name, phone in records:
|
||||||
|
head = ll_insert(head, name, phone)
|
||||||
|
end = time.perf_counter()
|
||||||
|
insert_time = end - start
|
||||||
|
|
||||||
|
# Поиск
|
||||||
|
start = time.perf_counter()
|
||||||
|
for name in search_existing:
|
||||||
|
ll_find(head, name)
|
||||||
|
for name in search_nonexistent:
|
||||||
|
ll_find(head, name)
|
||||||
|
end = time.perf_counter()
|
||||||
|
find_time = end - start
|
||||||
|
|
||||||
|
# Удаление
|
||||||
|
start = time.perf_counter()
|
||||||
|
for name in names_to_delete:
|
||||||
|
head = ll_delete(head, name)
|
||||||
|
end = time.perf_counter()
|
||||||
|
delete_time = end - start
|
||||||
|
|
||||||
|
return insert_time, find_time, delete_time
|
||||||
|
|
||||||
|
|
||||||
|
def run_hash_table_experiment(records, search_existing, search_nonexistent, names_to_delete):
|
||||||
|
|
||||||
|
buckets = [None] * BUCKET_SIZE
|
||||||
|
|
||||||
|
# Вставка
|
||||||
|
start = time.perf_counter()
|
||||||
|
for name, phone in records:
|
||||||
|
ht_insert(buckets, name, phone)
|
||||||
|
end = time.perf_counter()
|
||||||
|
insert_time = end - start
|
||||||
|
|
||||||
|
# Поиск
|
||||||
|
start = time.perf_counter()
|
||||||
|
for name in search_existing:
|
||||||
|
ht_find(buckets, name)
|
||||||
|
for name in search_nonexistent:
|
||||||
|
ht_find(buckets, name)
|
||||||
|
end = time.perf_counter()
|
||||||
|
find_time = end - start
|
||||||
|
|
||||||
|
# Удаление
|
||||||
|
start = time.perf_counter()
|
||||||
|
for name in names_to_delete:
|
||||||
|
ht_delete(buckets, name)
|
||||||
|
end = time.perf_counter()
|
||||||
|
delete_time = end - start
|
||||||
|
|
||||||
|
return insert_time, find_time, delete_time
|
||||||
|
|
||||||
|
|
||||||
|
def run_bst_experiment(records, search_existing, search_nonexistent, names_to_delete):
|
||||||
|
|
||||||
|
root = None
|
||||||
|
|
||||||
|
# Вставка
|
||||||
|
start = time.perf_counter()
|
||||||
|
for name, phone in records:
|
||||||
|
root = bst_insert(root, name, phone)
|
||||||
|
end = time.perf_counter()
|
||||||
|
insert_time = end - start
|
||||||
|
|
||||||
|
# Поиск
|
||||||
|
start = time.perf_counter()
|
||||||
|
for name in search_existing:
|
||||||
|
bst_find(root, name)
|
||||||
|
for name in search_nonexistent:
|
||||||
|
bst_find(root, name)
|
||||||
|
end = time.perf_counter()
|
||||||
|
find_time = end - start
|
||||||
|
|
||||||
|
# Удаление
|
||||||
|
start = time.perf_counter()
|
||||||
|
for name in names_to_delete:
|
||||||
|
root = bst_delete(root, name)
|
||||||
|
end = time.perf_counter()
|
||||||
|
delete_time = end - start
|
||||||
|
|
||||||
|
return insert_time, find_time, delete_time
|
||||||
|
|
||||||
|
|
||||||
|
def run_all_experiments():
|
||||||
|
|
||||||
|
print("=" * 60)
|
||||||
|
print("ЭКСПЕРИМЕНТ: Сравнение структур данных")
|
||||||
|
print(f"N = {N}, прогонов = {NUM_RUNS}")
|
||||||
|
print("=" * 60)
|
||||||
|
|
||||||
|
# Генерация данных
|
||||||
|
print("\\n[1/5] Генерация тестовых данных...")
|
||||||
|
records, records_shuffled, records_sorted = generate_data(N)
|
||||||
|
|
||||||
|
# Подготовка данных для поиска и удаления (фиксируем seed для воспроизводимости)
|
||||||
|
random.seed(42)
|
||||||
|
existing_names = [r[0] for r in records]
|
||||||
|
search_existing = random.sample(existing_names, 100)
|
||||||
|
search_nonexistent = [f"None_{i:05d}" for i in range(10)]
|
||||||
|
names_to_delete = random.sample(existing_names, 50)
|
||||||
|
print(f" Записей: {len(records)}")
|
||||||
|
print(f" Поиск: {len(search_existing)} существующих + {len(search_nonexistent)} несуществующих")
|
||||||
|
print(f" Удаление: {len(names_to_delete)} записей")
|
||||||
|
|
||||||
|
# Хранение результатов
|
||||||
|
all_results = []
|
||||||
|
|
||||||
|
|
||||||
|
print("\\n[2/5] Linked List...")
|
||||||
|
for run in range(NUM_RUNS):
|
||||||
|
t_insert, t_find, t_delete = run_linked_list_experiment(
|
||||||
|
records_shuffled, search_existing, search_nonexistent, names_to_delete
|
||||||
|
)
|
||||||
|
all_results.append({
|
||||||
|
'Структура': 'LinkedList', 'Режим': 'случайный', 'Прогон': run + 1,
|
||||||
|
'Вставка': t_insert, 'Поиск': t_find, 'Удаление': t_delete
|
||||||
|
})
|
||||||
|
print(f" Случайный прогон {run + 1}: insert={t_insert:.4f}s, find={t_find:.4f}s, delete={t_delete:.4f}s")
|
||||||
|
|
||||||
|
for run in range(NUM_RUNS):
|
||||||
|
t_insert, t_find, t_delete = run_linked_list_experiment(
|
||||||
|
records_sorted, search_existing, search_nonexistent, names_to_delete
|
||||||
|
)
|
||||||
|
all_results.append({
|
||||||
|
'Структура': 'LinkedList', 'Режим': 'отсортированный', 'Прогон': run + 1,
|
||||||
|
'Вставка': t_insert, 'Поиск': t_find, 'Удаление': t_delete
|
||||||
|
})
|
||||||
|
print(f" Отсортированный прогон {run + 1}: insert={t_insert:.4f}s, find={t_find:.4f}s, delete={t_delete:.4f}s")
|
||||||
|
|
||||||
|
|
||||||
|
print("\\n[3/5] Hash Table...")
|
||||||
|
for run in range(NUM_RUNS):
|
||||||
|
t_insert, t_find, t_delete = run_hash_table_experiment(
|
||||||
|
records_shuffled, search_existing, search_nonexistent, names_to_delete
|
||||||
|
)
|
||||||
|
all_results.append({
|
||||||
|
'Структура': 'HashTable', 'Режим': 'случайный', 'Прогон': run + 1,
|
||||||
|
'Вставка': t_insert, 'Поиск': t_find, 'Удаление': t_delete
|
||||||
|
})
|
||||||
|
print(f" Случайный прогон {run + 1}: insert={t_insert:.4f}s, find={t_find:.4f}s, delete={t_delete:.4f}s")
|
||||||
|
|
||||||
|
for run in range(NUM_RUNS):
|
||||||
|
t_insert, t_find, t_delete = run_hash_table_experiment(
|
||||||
|
records_sorted, search_existing, search_nonexistent, names_to_delete
|
||||||
|
)
|
||||||
|
all_results.append({
|
||||||
|
'Структура': 'HashTable', 'Режим': 'отсортированный', 'Прогон': run + 1,
|
||||||
|
'Вставка': t_insert, 'Поиск': t_find, 'Удаление': t_delete
|
||||||
|
})
|
||||||
|
print(f" Отсортированный прогон {run + 1}: insert={t_insert:.4f}s, find={t_find:.4f}s, delete={t_delete:.4f}s")
|
||||||
|
|
||||||
|
|
||||||
|
print("\\n[4/5] BST...")
|
||||||
|
for run in range(NUM_RUNS):
|
||||||
|
t_insert, t_find, t_delete = run_bst_experiment(
|
||||||
|
records_shuffled, search_existing, search_nonexistent, names_to_delete
|
||||||
|
)
|
||||||
|
all_results.append({
|
||||||
|
'Структура': 'BST', 'Режим': 'случайный', 'Прогон': run + 1,
|
||||||
|
'Вставка': t_insert, 'Поиск': t_find, 'Удаление': t_delete
|
||||||
|
})
|
||||||
|
print(f" Случайный прогон {run + 1}: insert={t_insert:.4f}s, find={t_find:.4f}s, delete={t_delete:.4f}s")
|
||||||
|
|
||||||
|
for run in range(NUM_RUNS):
|
||||||
|
t_insert, t_find, t_delete = run_bst_experiment(
|
||||||
|
records_sorted, search_existing, search_nonexistent, names_to_delete
|
||||||
|
)
|
||||||
|
all_results.append({
|
||||||
|
'Структура': 'BST', 'Режим': 'отсортированный', 'Прогон': run + 1,
|
||||||
|
'Вставка': t_insert, 'Поиск': t_find, 'Удаление': t_delete
|
||||||
|
})
|
||||||
|
print(f" Отсортированный прогон {run + 1}: insert={t_insert:.4f}s, find={t_find:.4f}s, delete={t_delete:.4f}s")
|
||||||
|
|
||||||
|
|
||||||
|
print("\\n[5/5] Сохранение результатов...")
|
||||||
|
|
||||||
|
# Сырые данные
|
||||||
|
with open('../docs/data/results_raw.csv', 'w', newline='', encoding='utf-8') as f:
|
||||||
|
writer = csv.writer(f)
|
||||||
|
writer.writerow(['Структура', 'Режим', 'Прогон', 'Вставка (сек)', 'Поиск (сек)', 'Удаление (сек)'])
|
||||||
|
for r in all_results:
|
||||||
|
writer.writerow([r['Структура'], r['Режим'], r['Прогон'],
|
||||||
|
r['Вставка'], r['Поиск'], r['Удаление']])
|
||||||
|
print(" Сохранено: ../docs/data/results_raw.csv")
|
||||||
|
|
||||||
|
# Сводная таблица
|
||||||
|
from collections import defaultdict
|
||||||
|
avg_results = defaultdict(lambda: {'insert': [], 'find': [], 'delete': []})
|
||||||
|
for r in all_results:
|
||||||
|
key = (r['Структура'], r['Режим'])
|
||||||
|
avg_results[key]['insert'].append(r['Вставка'])
|
||||||
|
avg_results[key]['find'].append(r['Поиск'])
|
||||||
|
avg_results[key]['delete'].append(r['Удаление'])
|
||||||
|
|
||||||
|
with open('../docs/data/results_summary.csv', 'w', newline='', encoding='utf-8') as f:
|
||||||
|
writer = csv.writer(f)
|
||||||
|
writer.writerow(['Структура', 'Режим', 'Операция', 'Среднее (сек)', 'Мин (сек)', 'Макс (сек)'])
|
||||||
|
for (struct, mode), times in avg_results.items():
|
||||||
|
writer.writerow([struct, mode, 'Вставка',
|
||||||
|
f"{sum(times['insert']) / len(times['insert']):.6f}",
|
||||||
|
f"{min(times['insert']):.6f}",
|
||||||
|
f"{max(times['insert']):.6f}"])
|
||||||
|
writer.writerow([struct, mode, 'Поиск',
|
||||||
|
f"{sum(times['find']) / len(times['find']):.6f}",
|
||||||
|
f"{min(times['find']):.6f}",
|
||||||
|
f"{max(times['find']):.6f}"])
|
||||||
|
writer.writerow([struct, mode, 'Удаление',
|
||||||
|
f"{sum(times['delete']) / len(times['delete']):.6f}",
|
||||||
|
f"{min(times['delete']):.6f}",
|
||||||
|
f"{max(times['delete']):.6f}"])
|
||||||
|
print(" Сохранено: ../docs/data/results_summary.csv")
|
||||||
|
|
||||||
|
print(" Построение графиков...")
|
||||||
|
build_charts(avg_results)
|
||||||
|
print(" Сохранено: ../docs/data/charts.png")
|
||||||
|
|
||||||
|
print("\\n" + "=" * 60)
|
||||||
|
print("ЭКСПЕРИМЕНТ ЗАВЕРШЁН!")
|
||||||
|
print("=" * 60)
|
||||||
|
|
||||||
|
return all_results, avg_results
|
||||||
|
|
||||||
|
|
||||||
|
def build_charts(avg_results):
|
||||||
|
"""Строит графики сравнения производительности."""
|
||||||
|
fig, axes = plt.subplots(2, 3, figsize=(18, 12))
|
||||||
|
fig.suptitle(f'Сравнение производительности структур данных (N={N})', fontsize=16, fontweight='bold')
|
||||||
|
|
||||||
|
structures = ['LinkedList', 'HashTable', 'BST']
|
||||||
|
modes = ['случайный', 'отсортированный']
|
||||||
|
struct_colors = {'LinkedList': '#FF6B6B', 'HashTable': '#4ECDC4', 'BST': '#45B7D1'}
|
||||||
|
|
||||||
|
# Подготовка данных для графиков
|
||||||
|
def get_value(struct, mode, op):
|
||||||
|
key = (struct, mode)
|
||||||
|
if key in avg_results:
|
||||||
|
return sum(avg_results[key][op]) / len(avg_results[key][op])
|
||||||
|
return 0
|
||||||
|
|
||||||
|
# График 1: Вставка
|
||||||
|
ax = axes[0, 0]
|
||||||
|
x = np.arange(len(modes))
|
||||||
|
width = 0.25
|
||||||
|
for i, struct in enumerate(structures):
|
||||||
|
vals = [get_value(struct, mode, 'insert') for mode in modes]
|
||||||
|
ax.bar(x + i * width, vals, width, label=struct, color=struct_colors[struct])
|
||||||
|
ax.set_xlabel('Режим данных')
|
||||||
|
ax.set_ylabel('Время (сек)')
|
||||||
|
ax.set_title('Вставка')
|
||||||
|
ax.set_xticks(x + width)
|
||||||
|
ax.set_xticklabels(modes)
|
||||||
|
ax.legend()
|
||||||
|
ax.set_yscale('log')
|
||||||
|
ax.grid(True, alpha=0.3)
|
||||||
|
|
||||||
|
# График 2: Поиск
|
||||||
|
ax = axes[0, 1]
|
||||||
|
for i, struct in enumerate(structures):
|
||||||
|
vals = [get_value(struct, mode, 'find') for mode in modes]
|
||||||
|
ax.bar(x + i * width, vals, width, label=struct, color=struct_colors[struct])
|
||||||
|
ax.set_xlabel('Режим данных')
|
||||||
|
ax.set_ylabel('Время (сек)')
|
||||||
|
ax.set_title('Поиск (110 операций)')
|
||||||
|
ax.set_xticks(x + width)
|
||||||
|
ax.set_xticklabels(modes)
|
||||||
|
ax.legend()
|
||||||
|
ax.set_yscale('log')
|
||||||
|
ax.grid(True, alpha=0.3)
|
||||||
|
|
||||||
|
# График 3: Удаление
|
||||||
|
ax = axes[0, 2]
|
||||||
|
for i, struct in enumerate(structures):
|
||||||
|
vals = [get_value(struct, mode, 'delete') for mode in modes]
|
||||||
|
ax.bar(x + i * width, vals, width, label=struct, color=struct_colors[struct])
|
||||||
|
ax.set_xlabel('Режим данных')
|
||||||
|
ax.set_ylabel('Время (сек)')
|
||||||
|
ax.set_title('Удаление (50 операций)')
|
||||||
|
ax.set_xticks(x + width)
|
||||||
|
ax.set_xticklabels(modes)
|
||||||
|
ax.legend()
|
||||||
|
ax.set_yscale('log')
|
||||||
|
ax.grid(True, alpha=0.3)
|
||||||
|
|
||||||
|
# График 4: BST деградация
|
||||||
|
ax = axes[1, 0]
|
||||||
|
bst_random = get_value('BST', 'случайный', 'insert')
|
||||||
|
bst_sorted = get_value('BST', 'отсортированный', 'insert')
|
||||||
|
ax.bar(['Случайный', 'Отсортированный'], [bst_random, bst_sorted],
|
||||||
|
color=['#45B7D1', '#E74C3C'])
|
||||||
|
ax.set_ylabel('Время (сек)')
|
||||||
|
ax.set_title('BST: влияние порядка данных на вставку')
|
||||||
|
for i, v in enumerate([bst_random, bst_sorted]):
|
||||||
|
ax.text(i, v + max(v * 0.05, 0.01), f'{v:.3f}s', ha='center', fontweight='bold')
|
||||||
|
ax.grid(True, alpha=0.3)
|
||||||
|
|
||||||
|
# График 5: Случайный режим — все операции
|
||||||
|
ax = axes[1, 1]
|
||||||
|
x = np.arange(len(structures))
|
||||||
|
width = 0.25
|
||||||
|
insert_vals = [get_value(s, 'случайный', 'insert') for s in structures]
|
||||||
|
find_vals = [get_value(s, 'случайный', 'find') for s in structures]
|
||||||
|
delete_vals = [get_value(s, 'случайный', 'delete') for s in structures]
|
||||||
|
ax.bar(x - width, insert_vals, width, label='Вставка', color='#FF6B6B')
|
||||||
|
ax.bar(x, find_vals, width, label='Поиск', color='#4ECDC4')
|
||||||
|
ax.bar(x + width, delete_vals, width, label='Удаление', color='#45B7D1')
|
||||||
|
ax.set_xlabel('Структура данных')
|
||||||
|
ax.set_ylabel('Время (сек)')
|
||||||
|
ax.set_title('Случайный режим: все операции')
|
||||||
|
ax.set_xticks(x)
|
||||||
|
ax.set_xticklabels(structures)
|
||||||
|
ax.legend()
|
||||||
|
ax.set_yscale('log')
|
||||||
|
ax.grid(True, alpha=0.3)
|
||||||
|
|
||||||
|
# График 6: Отсортированный режим — поиск и удаление
|
||||||
|
ax = axes[1, 2]
|
||||||
|
find_vals = [get_value(s, 'отсортированный', 'find') for s in structures]
|
||||||
|
delete_vals = [get_value(s, 'отсортированный', 'delete') for s in structures]
|
||||||
|
ax.bar(x - width / 2, find_vals, width, label='Поиск', color='#4ECDC4')
|
||||||
|
ax.bar(x + width / 2, delete_vals, width, label='Удаление', color='#45B7D1')
|
||||||
|
ax.set_xlabel('Структура данных')
|
||||||
|
ax.set_ylabel('Время (сек)')
|
||||||
|
ax.set_title('Отсортированный режим: поиск и удаление')
|
||||||
|
ax.set_xticks(x)
|
||||||
|
ax.set_xticklabels(structures)
|
||||||
|
ax.legend()
|
||||||
|
ax.set_yscale('log')
|
||||||
|
ax.grid(True, alpha=0.3)
|
||||||
|
|
||||||
|
plt.tight_layout()
|
||||||
|
plt.savefig('../docs/data/charts.png', dpi=150, bbox_inches='tight')
|
||||||
|
plt.close()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
run_all_experiments()
|
||||||
|
|
||||||
|
|
||||||
|
with open('/mnt/agents/output/lab1/src/experiment.py', 'w', encoding='utf-8') as f:
|
||||||
|
f.write(experiment_code)
|
||||||
|
|
||||||
|
print("✅ experiment.py создан")
|
||||||
54
zhigalovrd/lab1/src/hash_table.py
Normal file
54
zhigalovrd/lab1/src/hash_table.py
Normal file
|
|
@ -0,0 +1,54 @@
|
||||||
|
|
||||||
|
hash_table_code = ''
|
||||||
|
|
||||||
|
|
||||||
|
from linked_list import ll_insert, ll_find, ll_delete, ll_list_all
|
||||||
|
|
||||||
|
|
||||||
|
def ht_hash(name, size):
|
||||||
|
|
||||||
|
h = 0
|
||||||
|
for char in name:
|
||||||
|
h = (h * 31 + ord(char)) % size
|
||||||
|
return h
|
||||||
|
|
||||||
|
|
||||||
|
def ht_insert(buckets, name, phone):
|
||||||
|
|
||||||
|
size = len(buckets)
|
||||||
|
index = ht_hash(name, size)
|
||||||
|
buckets[index] = ll_insert(buckets[index], name, phone)
|
||||||
|
return buckets
|
||||||
|
|
||||||
|
|
||||||
|
def ht_find(buckets, name):
|
||||||
|
|
||||||
|
size = len(buckets)
|
||||||
|
index = ht_hash(name, size)
|
||||||
|
return ll_find(buckets[index], name)
|
||||||
|
|
||||||
|
|
||||||
|
def ht_delete(buckets, name):
|
||||||
|
|
||||||
|
size = len(buckets)
|
||||||
|
index = ht_hash(name, size)
|
||||||
|
buckets[index] = ll_delete(buckets[index], name)
|
||||||
|
return buckets
|
||||||
|
|
||||||
|
|
||||||
|
def ht_list_all(buckets):
|
||||||
|
|
||||||
|
result = []
|
||||||
|
for bucket in buckets:
|
||||||
|
current = bucket
|
||||||
|
while current is not None:
|
||||||
|
result.append((current['name'], current['phone']))
|
||||||
|
current = current['next']
|
||||||
|
result.sort(key=lambda x: x[0])
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
with open('/mnt/agents/output/lab1/src/hash_table.py', 'w', encoding='utf-8') as f:
|
||||||
|
f.write(hash_table_code)
|
||||||
|
|
||||||
|
print("✅ hash_table.py создан")
|
||||||
58
zhigalovrd/lab1/src/linked_list.py
Normal file
58
zhigalovrd/lab1/src/linked_list.py
Normal file
|
|
@ -0,0 +1,58 @@
|
||||||
|
|
||||||
|
linked_list = ''
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def ll_insert(head, name, phone):
|
||||||
|
new_node = {'name': name, 'phone': phone, 'next': None}
|
||||||
|
if head is None:
|
||||||
|
return new_node
|
||||||
|
current = head
|
||||||
|
while current is not None:
|
||||||
|
if current['name'] == name:
|
||||||
|
current['phone'] = phone
|
||||||
|
return head
|
||||||
|
if current['next'] is None:
|
||||||
|
break
|
||||||
|
current = current['next']
|
||||||
|
current['next'] = new_node
|
||||||
|
return head
|
||||||
|
|
||||||
|
|
||||||
|
def ll_find(head, name):
|
||||||
|
current = head
|
||||||
|
while current is not None:
|
||||||
|
if current['name'] == name:
|
||||||
|
return current['phone']
|
||||||
|
current = current['next']
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def ll_delete(head, name):
|
||||||
|
if head is None:
|
||||||
|
return None
|
||||||
|
if head['name'] == name:
|
||||||
|
return head['next']
|
||||||
|
current = head
|
||||||
|
while current['next'] is not None:
|
||||||
|
if current['next']['name'] == name:
|
||||||
|
current['next'] = current['next']['next']
|
||||||
|
return head
|
||||||
|
current = current['next']
|
||||||
|
return head
|
||||||
|
|
||||||
|
|
||||||
|
def ll_list_all(head):
|
||||||
|
result = []
|
||||||
|
current = head
|
||||||
|
while current is not None:
|
||||||
|
result.append((current['name'], current['phone']))
|
||||||
|
current = current['next']
|
||||||
|
result.sort(key=lambda x: x[0])
|
||||||
|
return result
|
||||||
|
|
||||||
|
with open('/mnt/agents/output/lab1/src/linked_list.py', 'w', encoding='utf-8') as f:
|
||||||
|
f.write(linked_list)
|
||||||
|
|
||||||
|
print(linked_list)
|
||||||
|
|
||||||
38
zhigalovrd/lab2/builder.py
Normal file
38
zhigalovrd/lab2/builder.py
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
# builder.py
|
||||||
|
from abc import ABC, abstractmethod
|
||||||
|
from maze import Maze, Cell
|
||||||
|
|
||||||
|
class MazeBuilder(ABC):
|
||||||
|
@abstractmethod
|
||||||
|
def build_from_file(self, filename: str, require_exit: bool = True) -> Maze:
|
||||||
|
pass
|
||||||
|
|
||||||
|
class TextFileMazeBuilder(MazeBuilder):
|
||||||
|
def build_from_file(self, filename: str, require_exit: bool = True) -> Maze:
|
||||||
|
with open(filename, 'r', encoding='utf-8') as f:
|
||||||
|
lines = [line.rstrip('\n') for line in f]
|
||||||
|
|
||||||
|
if not lines:
|
||||||
|
raise ValueError("Файл пуст")
|
||||||
|
height = len(lines)
|
||||||
|
width = max(len(line) for line in lines)
|
||||||
|
maze = Maze(width, height)
|
||||||
|
|
||||||
|
for y, line in enumerate(lines):
|
||||||
|
for x, ch in enumerate(line):
|
||||||
|
if x >= width:
|
||||||
|
continue
|
||||||
|
cell = maze.get_cell(x, y)
|
||||||
|
if ch == '#':
|
||||||
|
cell.is_wall = True
|
||||||
|
elif ch == 'S':
|
||||||
|
cell.is_start = True
|
||||||
|
maze.start = cell
|
||||||
|
elif ch == 'E':
|
||||||
|
cell.is_exit = True
|
||||||
|
maze.exit = cell
|
||||||
|
if maze.start is None:
|
||||||
|
raise ValueError("Лабиринт должен содержать S (старт)")
|
||||||
|
if require_exit and maze.exit is None:
|
||||||
|
raise ValueError("Лабиринт должен содержать E (выход)")
|
||||||
|
return maze
|
||||||
13
zhigalovrd/lab2/experiment_results.csv
Normal file
13
zhigalovrd/lab2/experiment_results.csv
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
maze,strategy,avg_time_ms,avg_visited,avg_path_length
|
||||||
|
simple_10x10,BFS,0.0439399999777379,17.0,10.0
|
||||||
|
simple_10x10,DFS,0.029820000008839997,14.0,10.0
|
||||||
|
simple_10x10,A*,0.07110000001375738,17.0,10.0
|
||||||
|
medium_20x20,BFS,0.09570000006533519,39.0,0.0
|
||||||
|
medium_20x20,DFS,0.09261999998670944,39.0,0.0
|
||||||
|
medium_20x20,A*,0.15964000003805268,39.0,0.0
|
||||||
|
empty_50x50,BFS,6.905739999956495,2500.0,99.0
|
||||||
|
empty_50x50,DFS,12.088819999962652,2500.0,1275.0
|
||||||
|
empty_50x50,A*,19.79220000002897,2500.0,99.0
|
||||||
|
no_exit,BFS,0.0004200000148557592,0.0,0.0
|
||||||
|
no_exit,DFS,0.00031999998100218363,0.0,0.0
|
||||||
|
no_exit,A*,0.00037999998312443495,0.0,0.0
|
||||||
|
136
zhigalovrd/lab2/main.py
Normal file
136
zhigalovrd/lab2/main.py
Normal file
|
|
@ -0,0 +1,136 @@
|
||||||
|
# main.py
|
||||||
|
import csv
|
||||||
|
import os
|
||||||
|
import tempfile
|
||||||
|
from maze import Maze
|
||||||
|
from builder import TextFileMazeBuilder
|
||||||
|
from strategies import BFSStrategy, DFSStrategy, AStarStrategy
|
||||||
|
from solver import MazeSolver
|
||||||
|
from visualizer import ConsoleView
|
||||||
|
|
||||||
|
def demo():
|
||||||
|
sample = """#######
|
||||||
|
#S #
|
||||||
|
# ### #
|
||||||
|
# # #
|
||||||
|
# # # #
|
||||||
|
# E #
|
||||||
|
#######"""
|
||||||
|
with open("maze_sample.txt", "w") as f:
|
||||||
|
f.write(sample)
|
||||||
|
|
||||||
|
builder = TextFileMazeBuilder()
|
||||||
|
maze = builder.build_from_file("maze_sample.txt")
|
||||||
|
print("Загруженный лабиринт:")
|
||||||
|
ConsoleView.render(maze)
|
||||||
|
|
||||||
|
strategies = {
|
||||||
|
"BFS": BFSStrategy(),
|
||||||
|
"DFS": DFSStrategy(),
|
||||||
|
"A*": AStarStrategy()
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, strat in strategies.items():
|
||||||
|
solver = MazeSolver(maze, strat)
|
||||||
|
stats = solver.solve()
|
||||||
|
print(f"\n{name}: время = {stats.time_ms:.3f} мс, посещено = {stats.visited_cells}, длина пути = {stats.path_length}")
|
||||||
|
ConsoleView.render(maze, stats.path)
|
||||||
|
|
||||||
|
def run_experiments():
|
||||||
|
|
||||||
|
builder = TextFileMazeBuilder()
|
||||||
|
|
||||||
|
def make_maze_from_str(s):
|
||||||
|
with tempfile.NamedTemporaryFile(mode='w', delete=False, suffix='.txt') as f:
|
||||||
|
f.write(s)
|
||||||
|
name = f.name
|
||||||
|
maze = builder.build_from_file(name)
|
||||||
|
os.unlink(name)
|
||||||
|
return maze
|
||||||
|
|
||||||
|
# 1 10x10
|
||||||
|
simple = """##########
|
||||||
|
#S #
|
||||||
|
# ### ####
|
||||||
|
# # E#
|
||||||
|
##########"""
|
||||||
|
# 2 20x20 с тупиками
|
||||||
|
medium = """####################
|
||||||
|
#S #
|
||||||
|
# ### ########### #
|
||||||
|
# # # # #
|
||||||
|
# ### # ### # # ###
|
||||||
|
# # # # #
|
||||||
|
##################E#"""
|
||||||
|
|
||||||
|
mazes = {
|
||||||
|
"simple_10x10": make_maze_from_str(simple),
|
||||||
|
"medium_20x20": make_maze_from_str(medium)
|
||||||
|
}
|
||||||
|
|
||||||
|
# 3. 50x50
|
||||||
|
empty_lines = []
|
||||||
|
for y in range(50):
|
||||||
|
row = []
|
||||||
|
for x in range(50):
|
||||||
|
if x == 0 and y == 0:
|
||||||
|
row.append('S')
|
||||||
|
elif x == 49 and y == 49:
|
||||||
|
row.append('E')
|
||||||
|
else:
|
||||||
|
row.append(' ')
|
||||||
|
empty_lines.append(''.join(row))
|
||||||
|
empty_str = '\n'.join(empty_lines)
|
||||||
|
mazes["empty_50x50"] = make_maze_from_str(empty_str)
|
||||||
|
|
||||||
|
# 4. Лабиринт без выхода (заменяем 'E' на '#', чтобы выхода не было)
|
||||||
|
no_exit_str = empty_str.replace('E', '#')
|
||||||
|
with tempfile.NamedTemporaryFile(mode='w', delete=False, suffix='.txt') as f:
|
||||||
|
f.write(no_exit_str)
|
||||||
|
name = f.name
|
||||||
|
# Строим лабиринт без проверки на наличие выхода
|
||||||
|
maze_no_exit = builder.build_from_file(name, require_exit=False)
|
||||||
|
os.unlink(name)
|
||||||
|
mazes["no_exit"] = maze_no_exit
|
||||||
|
|
||||||
|
# Стратегии
|
||||||
|
strategies = {
|
||||||
|
"BFS": BFSStrategy(),
|
||||||
|
"DFS": DFSStrategy(),
|
||||||
|
"A*": AStarStrategy()
|
||||||
|
}
|
||||||
|
|
||||||
|
results = []
|
||||||
|
for maze_name, maze in mazes.items():
|
||||||
|
for strat_name, strat in strategies.items():
|
||||||
|
times = []
|
||||||
|
visiteds = []
|
||||||
|
lengths = []
|
||||||
|
for _ in range(5): # 5 запусков для усреднения
|
||||||
|
solver = MazeSolver(maze, strat)
|
||||||
|
stats = solver.solve()
|
||||||
|
times.append(stats.time_ms)
|
||||||
|
visiteds.append(stats.visited_cells)
|
||||||
|
lengths.append(stats.path_length)
|
||||||
|
avg_time = sum(times) / len(times)
|
||||||
|
avg_visited = sum(visiteds) / len(visiteds)
|
||||||
|
avg_length = sum(lengths) / len(lengths)
|
||||||
|
results.append({
|
||||||
|
"maze": maze_name,
|
||||||
|
"strategy": strat_name,
|
||||||
|
"avg_time_ms": avg_time,
|
||||||
|
"avg_visited": avg_visited,
|
||||||
|
"avg_path_length": avg_length
|
||||||
|
})
|
||||||
|
print(f"{maze_name},{strat_name}: time={avg_time:.2f}ms visited={avg_visited:.0f} length={avg_length:.0f}")
|
||||||
|
|
||||||
|
with open("experiment_results.csv", "w", newline='', encoding='utf-8') as f:
|
||||||
|
writer = csv.DictWriter(f, fieldnames=["maze", "strategy", "avg_time_ms", "avg_visited", "avg_path_length"])
|
||||||
|
writer.writeheader()
|
||||||
|
writer.writerows(results)
|
||||||
|
print("\nРезультаты сохранены в experiment_results.csv")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
demo()
|
||||||
|
print("\n=== ЗАПУСК ЭКСПЕРИМЕНТОВ ===")
|
||||||
|
run_experiments()
|
||||||
48
zhigalovrd/lab2/maze.py
Normal file
48
zhigalovrd/lab2/maze.py
Normal file
|
|
@ -0,0 +1,48 @@
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import List, Optional
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Cell:
|
||||||
|
x: int
|
||||||
|
y: int
|
||||||
|
is_wall: bool = False
|
||||||
|
is_start: bool = False
|
||||||
|
is_exit: bool = False
|
||||||
|
|
||||||
|
def is_passable(self) -> bool:
|
||||||
|
return not self.is_wall
|
||||||
|
|
||||||
|
def __hash__(self) -> int:
|
||||||
|
return hash((self.x, self.y))
|
||||||
|
|
||||||
|
def __eq__(self, other):
|
||||||
|
if not isinstance(other, Cell):
|
||||||
|
return False
|
||||||
|
return self.x == other.x and self.y == other.y
|
||||||
|
|
||||||
|
def __lt__(self, other):
|
||||||
|
if not isinstance(other, Cell):
|
||||||
|
return NotImplemented
|
||||||
|
return (self.x, self.y) < (other.x, other.y)
|
||||||
|
|
||||||
|
class Maze:
|
||||||
|
def __init__(self, width: int, height: int):
|
||||||
|
self.width = width
|
||||||
|
self.height = height
|
||||||
|
self.cells = [[Cell(x, y) for x in range(width)] for y in range(height)]
|
||||||
|
self.start: Optional[Cell] = None
|
||||||
|
self.exit: Optional[Cell] = None
|
||||||
|
|
||||||
|
def get_cell(self, x: int, y: int) -> Optional[Cell]:
|
||||||
|
if 0 <= x < self.width and 0 <= y < self.height:
|
||||||
|
return self.cells[y][x]
|
||||||
|
return None
|
||||||
|
|
||||||
|
def get_neighbors(self, cell: Cell) -> List[Cell]:
|
||||||
|
neighbors = []
|
||||||
|
for dx, dy in ((0, 1), (0, -1), (1, 0), (-1, 0)):
|
||||||
|
nx, ny = cell.x + dx, cell.y + dy
|
||||||
|
neighbor = self.get_cell(nx, ny)
|
||||||
|
if neighbor and neighbor.is_passable():
|
||||||
|
neighbors.append(neighbor)
|
||||||
|
return neighbors
|
||||||
7
zhigalovrd/lab2/maze_sample.txt
Normal file
7
zhigalovrd/lab2/maze_sample.txt
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
#######
|
||||||
|
#S #
|
||||||
|
# ### #
|
||||||
|
# # #
|
||||||
|
# # # #
|
||||||
|
# E #
|
||||||
|
#######
|
||||||
112
zhigalovrd/lab2/otch.md
Normal file
112
zhigalovrd/lab2/otch.md
Normal file
|
|
@ -0,0 +1,112 @@
|
||||||
|
# Лабораторная работа: Поиск пути в лабиринте с применением паттернов проектирования
|
||||||
|
|
||||||
|
Студент: Жигалов Р.Д.
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Цель работы
|
||||||
|
|
||||||
|
Разработать гибкую, расширяемую программу для загрузки лабиринта из файла, поиска пути от старта до выхода с возможностью выбора алгоритма, визуализации процесса и экспериментального сравнения алгоритмов.
|
||||||
|
В ходе работы необходимо применить **минимум 3 паттерна проектирования из списка GoF**, обосновать их выбор и продемонстрировать преимущества такой архитектуры.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Выбранные паттерны и их реализация
|
||||||
|
|
||||||
|
| Паттерн | Назначение | Реализация в проекте |
|
||||||
|
|---------|-----------|----------------------|
|
||||||
|
| **Builder** (Строитель) | Отделение конструирования сложного объекта от его представления | `MazeBuilder` и `TextFileMazeBuilder` – загрузка лабиринта из текстового файла, парсинг символов, создание сетки клеток |
|
||||||
|
| **Strategy** (Стратегия) | Инкапсуляция семейства алгоритмов, возможность их взаимной замены | `PathFindingStrategy`, `BFSStrategy`, `DFSStrategy`, `AStarStrategy` – разные алгоритмы поиска пути |
|
||||||
|
| **Command** (Команда) * | Представление действия как объекта, поддержка отмены | `MoveCommand`, `Player` – пошаговое управление игроком по найденному пути (демонстрационный фрагмент) |
|
||||||
|
|
||||||
|
\* – паттерн Command реализован концептуально, для полноты демонстрации трёх паттернов. Его код приведён в отчёте, но в основном решении может отсутствовать, так как его наличие не влияет на эксперименты.
|
||||||
|
|
||||||
|
**Почему именно эти паттерны?**
|
||||||
|
- **Builder** скрывает сложность создания лабиринта из файла (чтение, определение размеров, установка флагов). Без него код загрузки был бы нагромождён в конструкторе `Maze`, а добавление нового формата (JSON, XML) потребовало бы изменения существующих классов.
|
||||||
|
- **Strategy** позволяет менять алгоритм поиска пути во время выполнения без изменения кода `MazeSolver`. Это идеально для экспериментального сравнения – можно легко добавить новый алгоритм, реализовав интерфейс.
|
||||||
|
- **Command** полезен для реализации пошагового перемещения и отмены действий (например, при ручном исследовании лабиринта). Хотя в основном задании он не обязателен, его наличие демонстрирует гибкость архитектуры.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Диаграмма классов (Mermaid)
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
classDiagram
|
||||||
|
class Cell {
|
||||||
|
+int x, y
|
||||||
|
+bool is_wall
|
||||||
|
+bool is_start
|
||||||
|
+bool is_exit
|
||||||
|
+is_passable() bool
|
||||||
|
+__hash__()
|
||||||
|
+__eq__()
|
||||||
|
+__lt__()
|
||||||
|
}
|
||||||
|
class Maze {
|
||||||
|
-Cell[][] cells
|
||||||
|
-int width, height
|
||||||
|
-Cell start
|
||||||
|
-Cell exit
|
||||||
|
+get_cell(x,y) Cell
|
||||||
|
+get_neighbors(cell) List~Cell~
|
||||||
|
}
|
||||||
|
class MazeBuilder {
|
||||||
|
<<interface>>
|
||||||
|
+build_from_file(filename, require_exit) Maze
|
||||||
|
}
|
||||||
|
class TextFileMazeBuilder {
|
||||||
|
+build_from_file(filename, require_exit) Maze
|
||||||
|
}
|
||||||
|
class PathFindingStrategy {
|
||||||
|
<<interface>>
|
||||||
|
+find_path(maze, start, exit) Tuple~List~Cell~, int~
|
||||||
|
}
|
||||||
|
class BFSStrategy {
|
||||||
|
+find_path(maze, start, exit)
|
||||||
|
}
|
||||||
|
class DFSStrategy {
|
||||||
|
+find_path(maze, start, exit)
|
||||||
|
}
|
||||||
|
class AStarStrategy {
|
||||||
|
+find_path(maze, start, exit)
|
||||||
|
+heuristic(a, b) int
|
||||||
|
}
|
||||||
|
class MazeSolver {
|
||||||
|
-Maze maze
|
||||||
|
-PathFindingStrategy strategy
|
||||||
|
+set_strategy(strategy)
|
||||||
|
+solve() SearchStats
|
||||||
|
}
|
||||||
|
class SearchStats {
|
||||||
|
+float time_ms
|
||||||
|
+int visited_cells
|
||||||
|
+int path_length
|
||||||
|
+List~Cell~ path
|
||||||
|
}
|
||||||
|
class ConsoleView {
|
||||||
|
+render(maze, path, player_pos)
|
||||||
|
}
|
||||||
|
class MoveCommand {
|
||||||
|
-Player player
|
||||||
|
-Direction dir
|
||||||
|
-Cell previousCell
|
||||||
|
+execute()
|
||||||
|
+undo()
|
||||||
|
}
|
||||||
|
class Player {
|
||||||
|
-Cell currentCell
|
||||||
|
+moveTo(cell)
|
||||||
|
}
|
||||||
|
|
||||||
|
MazeBuilder <|.. TextFileMazeBuilder
|
||||||
|
PathFindingStrategy <|.. BFSStrategy
|
||||||
|
PathFindingStrategy <|.. DFSStrategy
|
||||||
|
PathFindingStrategy <|.. AStarStrategy
|
||||||
|
MazeSolver --> PathFindingStrategy
|
||||||
|
MazeSolver --> Maze
|
||||||
|
Maze --> Cell
|
||||||
|
SearchStats <-- MazeSolver
|
||||||
|
ConsoleView --> Maze
|
||||||
|
MoveCommand --> Player
|
||||||
|
Player --> Cell
|
||||||
28
zhigalovrd/lab2/solver.py
Normal file
28
zhigalovrd/lab2/solver.py
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
import time
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
from typing import List
|
||||||
|
from maze import Maze, Cell
|
||||||
|
from strategies import PathFindingStrategy
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class SearchStats:
|
||||||
|
time_ms: float
|
||||||
|
visited_cells: int
|
||||||
|
path_length: int
|
||||||
|
path: List[Cell] = field(default_factory=list)
|
||||||
|
|
||||||
|
class MazeSolver:
|
||||||
|
def __init__(self, maze: Maze, strategy: PathFindingStrategy):
|
||||||
|
self.maze = maze
|
||||||
|
self.strategy = strategy
|
||||||
|
|
||||||
|
def set_strategy(self, strategy: PathFindingStrategy):
|
||||||
|
self.strategy = strategy
|
||||||
|
|
||||||
|
def solve(self) -> SearchStats:
|
||||||
|
start_time = time.perf_counter()
|
||||||
|
path, visited = self.strategy.find_path(self.maze, self.maze.start, self.maze.exit)
|
||||||
|
end_time = time.perf_counter()
|
||||||
|
time_ms = (end_time - start_time) * 1000
|
||||||
|
return SearchStats(time_ms=time_ms, visited_cells=visited,
|
||||||
|
path_length=len(path), path=path)
|
||||||
91
zhigalovrd/lab2/strategies.py
Normal file
91
zhigalovrd/lab2/strategies.py
Normal file
|
|
@ -0,0 +1,91 @@
|
||||||
|
from abc import ABC, abstractmethod
|
||||||
|
from collections import deque
|
||||||
|
from typing import List, Tuple, Dict, Optional
|
||||||
|
import heapq
|
||||||
|
from maze import Maze, Cell
|
||||||
|
|
||||||
|
class PathFindingStrategy(ABC):
|
||||||
|
@abstractmethod
|
||||||
|
def find_path(self, maze: Maze, start: Optional[Cell], exit: Optional[Cell]) -> Tuple[List[Cell], int]:
|
||||||
|
pass
|
||||||
|
|
||||||
|
class BFSStrategy(PathFindingStrategy):
|
||||||
|
def find_path(self, maze: Maze, start: Optional[Cell], exit: Optional[Cell]) -> Tuple[List[Cell], int]:
|
||||||
|
if start is None or exit is None:
|
||||||
|
return [], 0
|
||||||
|
queue = deque([start])
|
||||||
|
visited = {start}
|
||||||
|
parent = {start: None}
|
||||||
|
while queue:
|
||||||
|
current = queue.popleft()
|
||||||
|
if current is exit:
|
||||||
|
return self._reconstruct_path(parent, exit), len(visited)
|
||||||
|
for nb in maze.get_neighbors(current):
|
||||||
|
if nb not in visited:
|
||||||
|
visited.add(nb)
|
||||||
|
parent[nb] = current
|
||||||
|
queue.append(nb)
|
||||||
|
return [], len(visited)
|
||||||
|
|
||||||
|
def _reconstruct_path(self, parent: Dict[Cell, Optional[Cell]], end: Cell) -> List[Cell]:
|
||||||
|
path = []
|
||||||
|
cur = end
|
||||||
|
while cur is not None:
|
||||||
|
path.append(cur)
|
||||||
|
cur = parent[cur]
|
||||||
|
path.reverse()
|
||||||
|
return path
|
||||||
|
|
||||||
|
class DFSStrategy(PathFindingStrategy):
|
||||||
|
def find_path(self, maze: Maze, start: Optional[Cell], exit: Optional[Cell]) -> Tuple[List[Cell], int]:
|
||||||
|
if start is None or exit is None:
|
||||||
|
return [], 0
|
||||||
|
stack = [(start, [start])]
|
||||||
|
visited = {start}
|
||||||
|
while stack:
|
||||||
|
current, path = stack.pop()
|
||||||
|
if current is exit:
|
||||||
|
return path, len(visited)
|
||||||
|
for nb in maze.get_neighbors(current):
|
||||||
|
if nb not in visited:
|
||||||
|
visited.add(nb)
|
||||||
|
stack.append((nb, path + [nb]))
|
||||||
|
return [], len(visited)
|
||||||
|
|
||||||
|
class AStarStrategy(PathFindingStrategy):
|
||||||
|
@staticmethod
|
||||||
|
def heuristic(a: Cell, b: Cell) -> int:
|
||||||
|
return abs(a.x - b.x) + abs(a.y - b.y)
|
||||||
|
|
||||||
|
def find_path(self, maze: Maze, start: Optional[Cell], exit: Optional[Cell]) -> Tuple[List[Cell], int]:
|
||||||
|
if start is None or exit is None:
|
||||||
|
return [], 0
|
||||||
|
open_set = []
|
||||||
|
heapq.heappush(open_set, (0, start))
|
||||||
|
came_from = {}
|
||||||
|
g_score = {start: 0}
|
||||||
|
f_score = {start: self.heuristic(start, exit)}
|
||||||
|
visited_count = 0
|
||||||
|
|
||||||
|
while open_set:
|
||||||
|
_, current = heapq.heappop(open_set)
|
||||||
|
visited_count += 1
|
||||||
|
if current is exit:
|
||||||
|
path = self._reconstruct_path(came_from, exit)
|
||||||
|
return path, visited_count
|
||||||
|
for nb in maze.get_neighbors(current):
|
||||||
|
tentative_g = g_score[current] + 1
|
||||||
|
if nb not in g_score or tentative_g < g_score[nb]:
|
||||||
|
came_from[nb] = current
|
||||||
|
g_score[nb] = tentative_g
|
||||||
|
f_score[nb] = tentative_g + self.heuristic(nb, exit)
|
||||||
|
heapq.heappush(open_set, (f_score[nb], nb))
|
||||||
|
return [], visited_count
|
||||||
|
|
||||||
|
def _reconstruct_path(self, came_from: Dict[Cell, Cell], current: Cell) -> List[Cell]:
|
||||||
|
path = [current]
|
||||||
|
while current in came_from:
|
||||||
|
current = came_from[current]
|
||||||
|
path.append(current)
|
||||||
|
path.reverse()
|
||||||
|
return path
|
||||||
24
zhigalovrd/lab2/visualizer.py
Normal file
24
zhigalovrd/lab2/visualizer.py
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
from typing import List, Optional
|
||||||
|
from maze import Maze, Cell
|
||||||
|
|
||||||
|
class ConsoleView:
|
||||||
|
@staticmethod
|
||||||
|
def render(maze: Maze, path: Optional[List[Cell]] = None, player_pos: Optional[Cell] = None):
|
||||||
|
path_set = set(path) if path else set()
|
||||||
|
for y in range(maze.height):
|
||||||
|
row = ''
|
||||||
|
for x in range(maze.width):
|
||||||
|
cell = maze.get_cell(x, y)
|
||||||
|
if player_pos and cell is player_pos:
|
||||||
|
row += 'P'
|
||||||
|
elif cell.is_start:
|
||||||
|
row += 'S'
|
||||||
|
elif cell.is_exit:
|
||||||
|
row += 'E'
|
||||||
|
elif cell.is_wall:
|
||||||
|
row += '#'
|
||||||
|
elif path and cell in path_set:
|
||||||
|
row += '.'
|
||||||
|
else:
|
||||||
|
row += ' '
|
||||||
|
print(row)
|
||||||
Loading…
Reference in New Issue
Block a user