Merge pull request '[1] lab1' (#277) from zhigalovrd/2026-rff_mp:zhigalovrd into develop

Reviewed-on: #277
This commit is contained in:
IvanBoy 2026-05-30 11:45:12 +00:00
commit 2fe4c1bb75
17 changed files with 1334 additions and 0 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 133 KiB

View 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
1 Структура Режим Прогон Вставка (сек) Поиск (сек) Удаление (сек)
2 LinkedList случайный 1 0.9549722000010661 0.01907249999931082 0.011641299999610055
3 LinkedList случайный 2 0.9401399000016681 0.01862980000078096 0.011389800001779804
4 LinkedList случайный 3 0.9635646999995515 0.019138600000587758 0.01164940000307979
5 LinkedList случайный 4 0.9656800999982806 0.01934369999798946 0.011737699998775497
6 LinkedList случайный 5 0.9609748999973817 0.019405200000619516 0.011893200000486104
7 LinkedList отсортированный 1 0.9114345000016328 0.015180999998847255 0.01074729999891133
8 LinkedList отсортированный 2 0.8903370000007271 0.015180900001723785 0.010781699998915428
9 LinkedList отсортированный 3 0.8930579000007128 0.015323700001317775 0.010789800002385164
10 LinkedList отсортированный 4 0.8930441000011342 0.015232199999445584 0.010813600001711166
11 LinkedList отсортированный 5 0.8936487999999372 0.015375900002254639 0.010843100000784034
12 HashTable случайный 1 0.008163899998180568 0.0001584999990882352 7.74000000092201e-05
13 HashTable случайный 2 0.00817319999987376 0.0001570999993418809 7.639999967068434e-05
14 HashTable случайный 3 0.008005100000445964 0.00015559999883407727 7.579999873996712e-05
15 HashTable случайный 4 0.008168999996996718 0.00015559999883407727 7.560000085504726e-05
16 HashTable случайный 5 0.008011800000531366 0.0001559999982418958 7.579999873996712e-05
17 HashTable отсортированный 1 0.00789959999747225 0.00015469999925699085 7.579999873996712e-05
18 HashTable отсортированный 2 0.007853000002796762 0.00015440000061062165 7.569999797851779e-05
19 HashTable отсортированный 3 0.00799140000162879 0.00015519999942625873 7.699999696342275e-05
20 HashTable отсортированный 4 0.008009199998923577 0.00015419999908772297 7.589999950141646e-05
21 HashTable отсортированный 5 0.007893400001194095 0.00015449999773409218 7.579999873996712e-05
22 BST случайный 1 0.01466690000233939 0.0002459999996062834 0.0001467000001866836
23 BST случайный 2 0.014466300002823118 0.00024329999723704532 0.000143599998409627
24 BST случайный 3 0.014517399999022018 0.00024330000087502412 0.00014369999917107634
25 BST случайный 4 0.014434400000027381 0.00024290000146720558 0.00014279999959398992
26 BST случайный 5 0.06353280000257655 0.0002440999996906612 0.00014400000145542435
27 BST отсортированный 1 2.599753700000292 0.0408674999998766 0.030090399999608053
28 BST отсортированный 2 2.558562300000631 0.040827799999533454 0.030592600000090897
29 BST отсортированный 3 2.5695390999972005 0.040459600000758655 0.030263900000136346
30 BST отсортированный 4 2.569048000001203 0.040358000002015615 0.03027529999963008
31 BST отсортированный 5 2.556947400000354 0.04035379999913857 0.03032600000005914

View 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
1 Структура Режим Операция Среднее (сек) Мин (сек) Макс (сек)
2 LinkedList случайный Вставка 0.957066 0.940140 0.965680
3 LinkedList случайный Поиск 0.019118 0.018630 0.019405
4 LinkedList случайный Удаление 0.011662 0.011390 0.011893
5 LinkedList отсортированный Вставка 0.896304 0.890337 0.911435
6 LinkedList отсортированный Поиск 0.015259 0.015181 0.015376
7 LinkedList отсортированный Удаление 0.010795 0.010747 0.010843
8 HashTable случайный Вставка 0.008105 0.008005 0.008173
9 HashTable случайный Поиск 0.000157 0.000156 0.000158
10 HashTable случайный Удаление 0.000076 0.000076 0.000077
11 HashTable отсортированный Вставка 0.007929 0.007853 0.008009
12 HashTable отсортированный Поиск 0.000155 0.000154 0.000155
13 HashTable отсортированный Удаление 0.000076 0.000076 0.000077
14 BST случайный Вставка 0.024324 0.014434 0.063533
15 BST случайный Поиск 0.000244 0.000243 0.000246
16 BST случайный Удаление 0.000144 0.000143 0.000147
17 BST отсортированный Вставка 2.570770 2.556947 2.599754
18 BST отсортированный Поиск 0.040573 0.040354 0.040867
19 BST отсортированный Удаление 0.030310 0.030090 0.030593

View 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 создан")

View 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 создан")

View 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 создан")

View 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 создан")

View 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)

View 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

View 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
1 maze strategy avg_time_ms avg_visited avg_path_length
2 simple_10x10 BFS 0.0439399999777379 17.0 10.0
3 simple_10x10 DFS 0.029820000008839997 14.0 10.0
4 simple_10x10 A* 0.07110000001375738 17.0 10.0
5 medium_20x20 BFS 0.09570000006533519 39.0 0.0
6 medium_20x20 DFS 0.09261999998670944 39.0 0.0
7 medium_20x20 A* 0.15964000003805268 39.0 0.0
8 empty_50x50 BFS 6.905739999956495 2500.0 99.0
9 empty_50x50 DFS 12.088819999962652 2500.0 1275.0
10 empty_50x50 A* 19.79220000002897 2500.0 99.0
11 no_exit BFS 0.0004200000148557592 0.0 0.0
12 no_exit DFS 0.00031999998100218363 0.0 0.0
13 no_exit A* 0.00037999998312443495 0.0 0.0

136
zhigalovrd/lab2/main.py Normal file
View 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
View 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

View File

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

112
zhigalovrd/lab2/otch.md Normal file
View 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
View 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)

View 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

View 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)