Merge pull request '[1] lab1' (#277) from zhigalovrd/2026-rff_mp:zhigalovrd into develop
Reviewed-on: #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