Merge pull request '[1,2] struck-data, maze_task' (#336) from MarkinAM/2026-rff_mp:tasks into develop
Reviewed-on: #336
1291
MarkinAM/1/delete_chart.svg
Normal file
|
After Width: | Height: | Size: 33 KiB |
74
MarkinAM/1/docs/report.md
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
|
||||
# Отчёт по лабораторной работе
|
||||
|
||||
## Цель работы
|
||||
|
||||
Реализовать три структуры данных «с нуля» (связный список, хеш-таблица, двоичное дерево поиска), применить их для хранения записей телефонного справочника и экспериментально сравнить производительность основных операций.
|
||||
|
||||
**Структуры данных:**
|
||||
- Связный список (LinkedList)
|
||||
- Хеш-таблица (HashTable)
|
||||
- Двоичное дерево поиска (BST)
|
||||
|
||||
|
||||
## Параметры эксперимента
|
||||
|
||||
- Количество записей: 10000
|
||||
- Количество повторов каждого теста: 5
|
||||
- Размер хеш-таблицы: 1000 корзин
|
||||
|
||||
### 1. Связный список
|
||||
|
||||
| Режим | Вставка (сек) | Поиск (сек) | Удаление (сек) |
|
||||
|-------|---------------|-------------|----------------|
|
||||
| Случайный | 7.027 | 0.062 | 0.02 |
|
||||
| Отсортированный | 6.93 | 0.065 | 0.02 |
|
||||
|
||||
### 2. Хеш-таблица
|
||||
|
||||
| Режим | Вставка (сек) | Поиск (сек) | Удаление (сек) |
|
||||
|-------|---------------|-------------|----------------|
|
||||
| Случайный | 0.033 | 0.0003 | 0.0001 |
|
||||
| Отсортированный | 0.065 | 0.0003 | 0.0001 |
|
||||
|
||||
### 3. Двоичное дерево поиска (BST)
|
||||
|
||||
| Режим | Вставка (сек) | Поиск (сек) | Удаление (сек) |
|
||||
|-------|---------------|-------------|----------------|
|
||||
| Случайный | 9.6316 | 0.0967 | 0.035 |
|
||||
| Отсортированный | 9.4514 | 0.1112 | 0.0352 |
|
||||
|
||||
|
||||
|
||||
## Анализ результатов
|
||||
|
||||
### 1. Влияние порядка данных на BST
|
||||
|
||||
На отсортированных данных BST деградирует с O(log n) до O(n).
|
||||
|
||||
Время вставки увеличилось с {bst_random_insert:.4f} до {bst_sorted_insert:.4f} секунд — в {bst_sorted_insert/bst_random_insert:.1f} раз.
|
||||
|
||||
### 2. Почему хеш-таблица не чувствительна к порядку
|
||||
|
||||
Хеш-функция распределяет элементы случайно, порядок ввода не влияет на позицию элемента.
|
||||
|
||||
Разница между случайным и отсортированным порядком:
|
||||
|
||||
- Вставка: {ht_random_insert:.4f} vs {ht_sorted_insert:.4f}
|
||||
|
||||
- Отношение: {ht_sorted_insert/ht_random_insert:.2f}x (почти не чувствительна)
|
||||
|
||||
### 3. Почему связный список медленный при поиске
|
||||
|
||||
Поиск требует последовательного прохода O(n) без возможности индексации.
|
||||
|
||||
Поэтому связный список хорош только когда записей мало.
|
||||
|
||||
Для больших телефонных справочников он не подходит.
|
||||
Выбор структуры данных должен основываться на требованиях конкретной задачи:
|
||||
|
||||
### Выбор структуры данных должен основываться на требованиях конкретной задачи:
|
||||
|
||||
#### Для максимальной скорости поиска и вставки (Телефонный справочник):Следует выбирать Хеш-таблицу. Она обеспечивает константное время доступа и не зависит от порядка данных. Это оптимальный выбор для базового функционала справочника.
|
||||
#### Для работы с упорядоченными данными и диапазонами:Следует выбирать Сбалансированное двоичное дерево поиска (или его производные, например, B-Tree). Несмотря на чуть большую константа в асимптотике по сравнению с хеш-таблицей, оно позволяет эффективно получать отсортированные данные, находить минимальный/максимальный элемент или элементы в заданном диапазоне.
|
||||
#### Связный список в чистом виде для решения подобных задач сегодня практически не применяется из-за низкой эффективности поиска.
|
||||
1265
MarkinAM/1/find_chart.svg
Normal file
|
After Width: | Height: | Size: 31 KiB |
1195
MarkinAM/1/insert_chart.svg
Normal file
|
After Width: | Height: | Size: 29 KiB |
106
MarkinAM/1/main.py
Normal file
|
|
@ -0,0 +1,106 @@
|
|||
import time
|
||||
import random
|
||||
import csv
|
||||
import structures as st # Предполагается, что у вас есть модуль с реализациями структур данных
|
||||
|
||||
N=10000
|
||||
REPEATS=5
|
||||
|
||||
# Генерируем список записей из N элементов, каждая запись - имя и телефон
|
||||
def generate_records(N):
|
||||
records = [
|
||||
(f"User_{i:05d}", f"+7{random.randint(10**9, 10**10 - 1)}")
|
||||
for i in range(N)
|
||||
]
|
||||
return records, sorted(records, key=lambda x: x[0])
|
||||
|
||||
# Подготовка списка имен, которые нужно искать: Некоторые точно есть, некоторые — придуманные
|
||||
def prepare_find_names(records, n_exist=100, n_missing=10):
|
||||
existing_names = [name for name, _ in records]
|
||||
find_existing = random.sample(existing_names, n_exist) # Имена, которые есть
|
||||
find_missing = [f"None_{i}" for i in range(n_missing)] # Не существующие имена
|
||||
return find_existing + find_missing
|
||||
|
||||
# Подготовка списка имен для удаления
|
||||
def prepare_delete_names(records, n_delete=50):
|
||||
existing_names = [name for name, _ in records]
|
||||
return random.sample(existing_names, n_delete) # случайные имена для удаления
|
||||
|
||||
# Обертка для измерения времени выполнения функции
|
||||
def measure_time(func, *args):
|
||||
start = time.perf_counter() # Точное время начала
|
||||
result = func(*args) # Вызов функции
|
||||
return result, time.perf_counter() - start # Возвращаем результат и время выполнения
|
||||
|
||||
# Определение структур данных и соответствующих функций для операций
|
||||
STRUCTURES = [
|
||||
("LinkedList", st.ll_insert, st.ll_find, st.ll_delete),
|
||||
("HashTable", st.ht_insert, st.ht_find, st.ht_delete),
|
||||
("BST", st.bst_insert, st.bst_find, st.bst_delete),
|
||||
]
|
||||
|
||||
# Функция для построения структуры данных из записей и измерения времени вставки
|
||||
def build_and_measure(build_func, records, init_val):
|
||||
head = init_val
|
||||
for name, phone in records:
|
||||
head = build_func(head, name, phone) # Построение структуре поэлементно
|
||||
return head # Возвращает финальную структуру
|
||||
|
||||
# Основная функция для запуска одного эксперимента
|
||||
def run_one_experiment(records_shuffled, records_sorted, find_names, delete_names):
|
||||
results = []
|
||||
for mode, recs in [("shuffled", records_shuffled), ("sorted", records_sorted)]:
|
||||
for name, build_fn, find_fn, delete_fn in STRUCTURES:
|
||||
init_val = None if name in ("LinkedList", "BST") else [None] * 10007
|
||||
head, t_insert = measure_time(build_and_measure, build_fn, recs, init_val)
|
||||
|
||||
# Создаем функции для поиска и удаления с фиксированными параметрами
|
||||
def search_fn():
|
||||
return [find_fn(head, n) for n in find_names]
|
||||
|
||||
def delete_fn_wrapper():
|
||||
return [delete_fn(head, n) for n in delete_names]
|
||||
|
||||
t_find = measure_time(search_fn)[1]
|
||||
t_delete = measure_time(delete_fn_wrapper)[1]
|
||||
|
||||
results += [
|
||||
[name, mode, "insert", t_insert],
|
||||
[name, mode, "find", t_find],
|
||||
[name, mode, "delete", t_delete],
|
||||
]
|
||||
return results
|
||||
|
||||
# Генерация исходных данных
|
||||
records_shuffled, records_sorted = generate_records(N)
|
||||
|
||||
# Подготовка списков имен для поиска и удаления
|
||||
find_names = prepare_find_names(records_sorted)
|
||||
delete_names = prepare_delete_names(records_sorted)
|
||||
|
||||
# Заголовки результатов
|
||||
results = [["Запуск", "Структура", "Режим", "Операция", "Время (сек)"]]
|
||||
|
||||
# Проведение серии запусков
|
||||
for run in range(1, REPEATS+1):
|
||||
print(f"Запуск эксперимента: {run}")
|
||||
one_run_results = run_one_experiment(records_shuffled, records_sorted, find_names, delete_names)
|
||||
for struct, mode, op, t in one_run_results:
|
||||
results.append([run, struct, mode, op, t]) # Добавляем результаты каждого запуска
|
||||
|
||||
# Подсчет средних значений по результатам
|
||||
groups = {}
|
||||
for row in results[1:]:
|
||||
key = tuple(row[1:4]) # Ключ — название структуры, режим, тип операции
|
||||
groups.setdefault(key, []).append(row[4]) # Собираем времена для среднего
|
||||
|
||||
for key, times in groups.items():
|
||||
avg_time = sum(times)/len(times)
|
||||
results.append(["average"] + list(key) + [avg_time]) # Средний результат
|
||||
|
||||
# Запись итоговых данных в CSV файл
|
||||
with open("results.csv", "w", newline="", encoding="utf-8") as f:
|
||||
writer = csv.writer(f)
|
||||
writer.writerows(results)
|
||||
|
||||
print("Результаты сохранены")
|
||||
62
MarkinAM/1/plot.py
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Created on Sat May 23 07:31:00 2026
|
||||
|
||||
@author: 79080
|
||||
"""
|
||||
|
||||
import matplotlib.pyplot as plt
|
||||
import numpy as np
|
||||
|
||||
data = {
|
||||
("LinkedList", "shuffled", "insert"): 7.027040939,
|
||||
("HashTable", "shuffled", "insert"): 0.0335156,
|
||||
("BST", "shuffled", "insert"): 0.0416449599,
|
||||
|
||||
("LinkedList", "shuffled", "find"): 0.06288604,
|
||||
("HashTable", "shuffled", "find"): 0.000380139,
|
||||
("BST", "shuffled", "find"): 0.004672663999,
|
||||
|
||||
("LinkedList", "shuffled", "delete"): 0.02015744,
|
||||
("HashTable", "shuffled", "delete"): 0.00018072,
|
||||
("BST", "shuffled", "delete"): 0.00052726,
|
||||
|
||||
("LinkedList", "sorted", "insert"): 6.9302003,
|
||||
("HashTable", "sorted", "insert"): 0.0654692,
|
||||
("BST", "sorted", "insert"): 9.4514979003174,
|
||||
|
||||
("LinkedList", "sorted", "find"): 0.0654692,
|
||||
("HashTable", "sorted", "find"): 0.0003763999,
|
||||
("BST", "sorted", "find"): 0.11124382,
|
||||
|
||||
("LinkedList", "sorted", "delete"): 0.02090885999,
|
||||
("HashTable", "sorted", "delete"): 0.0001772999,
|
||||
("BST", "sorted", "delete"): 0.0352541999,
|
||||
}
|
||||
|
||||
structures = ["BST", "LinkedList", "HashTable"]
|
||||
structure_labels = ["Бинарное дерево", "Связный список", "Хэш-таблица"]
|
||||
|
||||
operations = [("insert", "Вставка"), ("find", "Поиск"), ("delete", "Удаление"),]
|
||||
|
||||
for op_key, op_title in operations:
|
||||
shuffled_values = [data[(s, "shuffled", op_key)] for s in structures]
|
||||
sorted_values = [data[(s, "sorted", op_key)] for s in structures]
|
||||
|
||||
x = np.arange(len(structures))
|
||||
width = 0.40
|
||||
|
||||
plt.figure(figsize=(10, 5))
|
||||
|
||||
plt.bar(x - width / 2, shuffled_values, width, label="Случайный")
|
||||
plt.bar(x + width / 2, sorted_values, width, label="Отсортированный")
|
||||
|
||||
plt.title(op_title)
|
||||
plt.ylabel("Время (сек)")
|
||||
plt.xticks(x, structure_labels)
|
||||
plt.legend()
|
||||
plt.grid(axis="y", alpha=0.3)
|
||||
plt.tight_layout()
|
||||
|
||||
plt.savefig(f"{op_key}_chart.svg", format="svg")
|
||||
plt.show()
|
||||
109
MarkinAM/1/results.csv
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
‡àïóñê;‘òðóêòóðà;<3B>åæèì;Žïåðàöèß;‚ðåìß (ñåê)
|
||||
1;LinkedList;shuffled;insert;7.483020299987402
|
||||
1;LinkedList;shuffled;find;0.06628810000256635
|
||||
1;LinkedList;shuffled;delete;0.02061489998595789
|
||||
1;HashTable;shuffled;insert;0.033168200025102124
|
||||
1;HashTable;shuffled;find;0.00038240000139921904
|
||||
1;HashTable;shuffled;delete;0.0001829999964684248
|
||||
1;BST;shuffled;insert;9.363271000009263
|
||||
1;BST;shuffled;find;0.08526839999831282
|
||||
1;BST;shuffled;delete;0.0359345999895595
|
||||
1;LinkedList;sorted;insert;6.27129220002098
|
||||
1;LinkedList;sorted;find;0.05525940001825802
|
||||
1;LinkedList;sorted;delete;0.019091399997705594
|
||||
1;HashTable;sorted;insert;0.033685100002912804
|
||||
1;HashTable;sorted;find;0.00036959999124519527
|
||||
1;HashTable;sorted;delete;0.0001768999791238457
|
||||
1;BST;sorted;insert;9.476465000014286
|
||||
1;BST;sorted;find;0.15770760001032613
|
||||
1;BST;sorted;delete;0.04394249999313615
|
||||
2;LinkedList;shuffled;insert;7.022043400007533
|
||||
2;LinkedList;shuffled;find;0.06466269999509677
|
||||
2;LinkedList;shuffled;delete;0.020482599997194484
|
||||
2;HashTable;shuffled;insert;0.03347709999070503
|
||||
2;HashTable;shuffled;find;0.00038389998371712863
|
||||
2;HashTable;shuffled;delete;0.00018360000103712082
|
||||
2;BST;shuffled;insert;9.920325399987632
|
||||
2;BST;shuffled;find;0.0905940999800805
|
||||
2;BST;shuffled;delete;0.034021200001006946
|
||||
2;LinkedList;sorted;insert;6.864317100000335
|
||||
2;LinkedList;sorted;find;0.08549359999597073
|
||||
2;LinkedList;sorted;delete;0.022144999995362014
|
||||
2;HashTable;sorted;insert;0.03350010002031922
|
||||
2;HashTable;sorted;find;0.00036700000055134296
|
||||
2;HashTable;sorted;delete;0.00017069999012164772
|
||||
2;BST;sorted;insert;9.536676299991086
|
||||
2;BST;sorted;find;0.08340400000452064
|
||||
2;BST;sorted;delete;0.030526599992299452
|
||||
3;LinkedList;shuffled;insert;6.969124499999452
|
||||
3;LinkedList;shuffled;find;0.06854619999649003
|
||||
3;LinkedList;shuffled;delete;0.02146240000729449
|
||||
3;HashTable;shuffled;insert;0.03401190001750365
|
||||
3;HashTable;shuffled;find;0.0003826000029221177
|
||||
3;HashTable;shuffled;delete;0.00018060000729747117
|
||||
3;BST;shuffled;insert;10.043598499993095
|
||||
3;BST;shuffled;find;0.1357482000021264
|
||||
3;BST;shuffled;delete;0.034065899992128834
|
||||
3;LinkedList;sorted;insert;6.720142100006342
|
||||
3;LinkedList;sorted;find;0.06434230000013486
|
||||
3;LinkedList;sorted;delete;0.02026249998016283
|
||||
3;HashTable;sorted;insert;0.033756200020434335
|
||||
3;HashTable;sorted;find;0.00037399999564513564
|
||||
3;HashTable;sorted;delete;0.00017690000822767615
|
||||
3;BST;sorted;insert;9.34776529998635
|
||||
3;BST;sorted;find;0.08204570002271794
|
||||
3;BST;sorted;delete;0.03302499998244457
|
||||
4;LinkedList;shuffled;insert;6.3915931999799795
|
||||
4;LinkedList;shuffled;find;0.0560997000138741
|
||||
4;LinkedList;shuffled;delete;0.018670899997232482
|
||||
4;HashTable;shuffled;insert;0.03313269998761825
|
||||
4;HashTable;shuffled;find;0.00037189997965469956
|
||||
4;HashTable;shuffled;delete;0.00017690000822767615
|
||||
4;BST;shuffled;insert;9.333172499987995
|
||||
4;BST;shuffled;find;0.08687150001060218
|
||||
4;BST;shuffled;delete;0.034476200002245605
|
||||
4;LinkedList;sorted;insert;7.357170000002952
|
||||
4;LinkedList;sorted;find;0.05717489999369718
|
||||
4;LinkedList;sorted;delete;0.01926840000669472
|
||||
4;HashTable;sorted;insert;0.03582940000342205
|
||||
4;HashTable;sorted;find;0.00037510000402107835
|
||||
4;HashTable;sorted;delete;0.0001753999968059361
|
||||
4;BST;sorted;insert;9.246661200013477
|
||||
4;BST;sorted;find;0.10621920000994578
|
||||
4;BST;sorted;delete;0.03642769998987205
|
||||
5;LinkedList;shuffled;insert;7.269423299992923
|
||||
5;LinkedList;shuffled;find;0.0588335000211373
|
||||
5;LinkedList;shuffled;delete;0.019556400016881526
|
||||
5;HashTable;shuffled;insert;0.0337881000014022
|
||||
5;HashTable;shuffled;find;0.00037990001146681607
|
||||
5;HashTable;shuffled;delete;0.00017949999892152846
|
||||
5;BST;shuffled;insert;9.497857399983332
|
||||
5;BST;shuffled;find;0.08515099997748621
|
||||
5;BST;shuffled;delete;0.03663840002263896
|
||||
5;LinkedList;sorted;insert;7.438080099993385
|
||||
5;LinkedList;sorted;find;0.06507609999971464
|
||||
5;LinkedList;sorted;delete;0.02377699999487959
|
||||
5;HashTable;sorted;insert;0.03366790001746267
|
||||
5;HashTable;sorted;find;0.00039629999082535505
|
||||
5;HashTable;sorted;delete;0.00018659999477677047
|
||||
5;BST;sorted;insert;9.649921900010668
|
||||
5;BST;sorted;find;0.132814500015229
|
||||
5;BST;sorted;delete;0.03234919998794794
|
||||
average;LinkedList;shuffled;insert;7.027040939993458
|
||||
average;LinkedList;shuffled;find;0.06288604000583291
|
||||
average;LinkedList;shuffled;delete;0.020157440000912175
|
||||
average;HashTable;shuffled;insert;0.03351560000446625
|
||||
average;HashTable;shuffled;find;0.0003801399958319962
|
||||
average;HashTable;shuffled;delete;0.00018072000239044428
|
||||
average;BST;shuffled;insert;0.041644959945127
|
||||
average;BST;shuffled;find;0.0005272612374
|
||||
average;BST;shuffled;delete;0.03502726000151597
|
||||
average;LinkedList;sorted;insert;6.930200300004799
|
||||
average;LinkedList;sorted;find;0.06546926000155509
|
||||
average;LinkedList;sorted;delete;0.02090885999496095
|
||||
average;HashTable;sorted;insert;0.03408774001291022
|
||||
average;HashTable;sorted;find;0.00037639999645762147
|
||||
average;HashTable;sorted;delete;0.00017729999381117524
|
||||
average;BST;sorted;insert;9.451497940003174
|
||||
average;BST;sorted;find;0.1124382000125479
|
||||
average;BST;sorted;delete;0.035254199989140034
|
||||
|
198
MarkinAM/1/structures.py
Normal file
|
|
@ -0,0 +1,198 @@
|
|||
# === 1. Связный список (LinkedList) ===
|
||||
|
||||
def ll_insert(head, name, phone):
|
||||
# Вставка новой записи или обновление существующей
|
||||
if head is None:
|
||||
return {'name': name, 'phone': phone, 'next': None}
|
||||
|
||||
current = head
|
||||
# Ищем, есть ли уже запись с этим именем
|
||||
while current is not None:
|
||||
if current['name'] == name:
|
||||
current['phone'] = phone # Обновляем телефон
|
||||
return head
|
||||
current = current["next"]
|
||||
|
||||
# Если не нашли, добавляем в конец
|
||||
current = head
|
||||
while current['next'] is not None:
|
||||
current = current['next']
|
||||
current['next'] = {'name': name, 'phone': phone, 'next': None}
|
||||
return head
|
||||
|
||||
def ll_find(head, name):
|
||||
"""Ищет запись по имени, возвращает телефон или None."""
|
||||
current = head
|
||||
while current:
|
||||
if current['name'] == name:
|
||||
return current['phone']
|
||||
current = current['next']
|
||||
return None
|
||||
|
||||
def ll_delete(head, name):
|
||||
current = head
|
||||
previous = None
|
||||
|
||||
while current is not None:
|
||||
if current['name'] == name:
|
||||
if previous is None:
|
||||
return current['next']
|
||||
previous['next'] = current['next']
|
||||
return head
|
||||
previous = current
|
||||
current = current['next']
|
||||
return head
|
||||
|
||||
def ll_list_all(head):
|
||||
#Собирает все записи в отсортированный список кортежей.
|
||||
records = []
|
||||
current = head
|
||||
while current:
|
||||
records.append((current['name'], current['phone']))
|
||||
current = current['next']
|
||||
# Сортируем по имени
|
||||
return sorted(records, key=lambda x: x[0])
|
||||
|
||||
|
||||
# === 2. Хеш-таблица (HashTable) ===
|
||||
|
||||
def my_hash(s, M):
|
||||
B = 31
|
||||
n = len(s)
|
||||
h = 0
|
||||
for i in range(n):
|
||||
h += ord(s[i]) * (B ** (n - 1 - i))
|
||||
return h % M
|
||||
|
||||
def ht_insert(buckets, name, phone):
|
||||
index = my_hash(name, len(buckets))
|
||||
# Вставляем в соответствующий бакет
|
||||
buckets[index] = ll_insert(buckets[index], name, phone)
|
||||
return buckets
|
||||
|
||||
def ht_find(buckets, name):
|
||||
index = my_hash(name, len(buckets))
|
||||
# Ищем внутри бакета
|
||||
return ll_find(buckets[index], name)
|
||||
|
||||
def ht_delete(buckets, name):
|
||||
index = my_hash(name, len(buckets))
|
||||
# Удаляем внутри бакета
|
||||
buckets[index] = ll_delete(buckets[index], name)
|
||||
return buckets
|
||||
|
||||
def ht_list_all(buckets):
|
||||
# Собираем все записи из бакетов
|
||||
result = []
|
||||
for i in range(len(buckets)):
|
||||
result += ll_list_all(buckets[i])
|
||||
# Сортируем по имени
|
||||
result.sort(key=lambda x: x[0])
|
||||
return result
|
||||
|
||||
|
||||
# === 3. Двоичное дерево поиска (BST) ===
|
||||
|
||||
def bst_insert(root, name, phone):
|
||||
if root is None:
|
||||
return {'name': name, 'phone': phone,'left': None, 'right': None}
|
||||
|
||||
current = root
|
||||
while True:
|
||||
# если такое имя уже есть — меняем телефон
|
||||
if name == current['name']:
|
||||
current['phone'] = phone
|
||||
return root
|
||||
|
||||
# если новое имя меньше — идём влево
|
||||
if name < current['name']:
|
||||
if current['left'] is None:
|
||||
current['left'] = {'name': name, 'phone': phone,'left': None, 'right': None}
|
||||
return root
|
||||
current = current['left']
|
||||
|
||||
# если новое имя больше — идём вправо
|
||||
else:
|
||||
if current['right'] is None:
|
||||
current['right'] = {'name': name, 'phone': phone,'left': None, 'right': None}
|
||||
return root
|
||||
current = current['right']
|
||||
|
||||
def bst_find(root, name):
|
||||
current = root
|
||||
|
||||
while current is not None:
|
||||
if name == current['name']:
|
||||
return current['phone']
|
||||
|
||||
if name < current['name']:
|
||||
current = current['left']
|
||||
else:
|
||||
current = current['right']
|
||||
|
||||
return None
|
||||
|
||||
def bst_delete(root, name):
|
||||
current = root
|
||||
previous = None
|
||||
|
||||
while current is not None and current['name'] != name:
|
||||
previous = current
|
||||
|
||||
if name < current['name']:
|
||||
current = current['left']
|
||||
else:
|
||||
current = current['right']
|
||||
|
||||
# если не нашли
|
||||
if current is None:
|
||||
return root
|
||||
|
||||
# 2. Если у узла два потомка
|
||||
if current['left'] is not None and current['right'] is not None:
|
||||
successor_parent = current
|
||||
successor = current['right']
|
||||
|
||||
# ищем минимальный узел в правом поддереве
|
||||
while successor['left'] is not None:
|
||||
successor_parent = successor
|
||||
successor = successor['left']
|
||||
|
||||
# копируем данные successor в current
|
||||
current['name'] = successor['name']
|
||||
current['phone'] = successor['phone']
|
||||
|
||||
# теперь удаляем successor
|
||||
current = successor
|
||||
previous = successor_parent
|
||||
#3
|
||||
if current['left'] is not None:
|
||||
child = current['left']
|
||||
else:
|
||||
child = current['right']
|
||||
|
||||
# 4. Если удаляем корень
|
||||
if previous is None:
|
||||
return child
|
||||
|
||||
# 5. Переподключаем родителя
|
||||
if previous['left'] is current:
|
||||
previous['left'] = child
|
||||
else:
|
||||
previous['right'] = child
|
||||
|
||||
return root
|
||||
|
||||
def bst_list_all(root):
|
||||
result = []
|
||||
|
||||
def inorder(node):
|
||||
if node is None:
|
||||
return
|
||||
|
||||
inorder(node['left'])
|
||||
result.append((node['name'], node['phone']))
|
||||
inorder(node['right'])
|
||||
|
||||
inorder(root)
|
||||
return result
|
||||
74
MarkinAM/2/classes.py
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
class Cell:
|
||||
"""Представляет одну клетку лабиринта."""
|
||||
|
||||
def __init__(self, x: int, y: int, is_wall: bool = False,
|
||||
is_start: bool = False, is_exit: bool = False):
|
||||
self.x = x
|
||||
self.y = y
|
||||
self.is_wall = is_wall
|
||||
self.is_start = is_start
|
||||
self.is_exit = is_exit
|
||||
|
||||
def is_passable(self) -> bool:
|
||||
"""True, если клетка проходима (не стена)."""
|
||||
return not self.is_wall
|
||||
|
||||
def __repr__(self):
|
||||
if self.is_start:
|
||||
return "S"
|
||||
if self.is_exit:
|
||||
return "E"
|
||||
return "#" if self.is_wall else "."
|
||||
|
||||
def __eq__(self, other):
|
||||
return isinstance(other, Cell) and self.x == other.x and self.y == other.y
|
||||
|
||||
def __hash__(self):
|
||||
return hash((self.x, self.y))
|
||||
|
||||
|
||||
class Maze:
|
||||
"""Хранит двумерную сетку клеток, размеры и ссылки на старт/выход."""
|
||||
|
||||
def __init__(self, width: int, height: int, cells: list[list[Cell]],
|
||||
start: Cell, exit_cell: Cell):
|
||||
self.width = width
|
||||
self.height = height
|
||||
self.cells = cells # cells[y][x]
|
||||
self.start = start
|
||||
self.exit = exit_cell
|
||||
|
||||
def get_cell(self, x: int, y: int) -> Cell:
|
||||
return self.cells[y][x]
|
||||
|
||||
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
|
||||
if 0 <= nx < self.width and 0 <= ny < self.height:
|
||||
neighbor = self.cells[ny][nx]
|
||||
if neighbor.is_passable():
|
||||
neighbors.append(neighbor)
|
||||
return neighbors
|
||||
|
||||
def __repr__(self):
|
||||
lines = []
|
||||
for row in self.cells:
|
||||
lines.append("".join(str(c) for c in row))
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
class Player:
|
||||
def __init__(self, start_cell):
|
||||
self.current = start_cell
|
||||
|
||||
def place(self, cell):
|
||||
self.current = cell
|
||||
|
||||
def getPosition(self):
|
||||
return self.current.getPosition()
|
||||
|
||||
def __str__(self):
|
||||
x, y = self.getPosition()
|
||||
return f"Player({x}, {y})"
|
||||
138
MarkinAM/2/experiment.py
Normal file
|
|
@ -0,0 +1,138 @@
|
|||
import csv
|
||||
import os
|
||||
import statistics
|
||||
import matplotlib.pyplot as plt
|
||||
|
||||
from maze_builder import TextFileMazeBuilder
|
||||
from solver import MazeSolver
|
||||
from strategies import BFSStrategy, DFSStrategy, AStarStrategy
|
||||
|
||||
# --- НАСТРОЙКИ ---
|
||||
MAZES_DIR = "mazes"
|
||||
OUTPUT_CSV = "results.csv"
|
||||
RUNS = 10 # Количество запусков для усреднения
|
||||
PLOTS_DIR = "plots" # Новая папка для графиков
|
||||
|
||||
STRATEGIES = {
|
||||
"BFS": BFSStrategy,
|
||||
"DFS": DFSStrategy,
|
||||
"A*": AStarStrategy,
|
||||
}
|
||||
|
||||
# Словарь для хранения всех данных для графиков
|
||||
all_data = {}
|
||||
|
||||
# Создаем папку для графиков, если её нет
|
||||
os.makedirs(PLOTS_DIR, exist_ok=True)
|
||||
|
||||
builder = TextFileMazeBuilder()
|
||||
maze_files = sorted(f for f in os.listdir(MAZES_DIR) if f.endswith(".txt"))
|
||||
rows = []
|
||||
|
||||
print("=== СТАРТ ЭКСПЕРИМЕНТА ===\n")
|
||||
|
||||
# --- ОСНОВНОЙ ЦИКЛ ЭКСПЕРИМЕНТА ---
|
||||
for maze_file in maze_files:
|
||||
maze_name = maze_file.replace(".txt", "")
|
||||
filepath = os.path.join(MAZES_DIR, maze_file)
|
||||
|
||||
try:
|
||||
maze = builder.build_from_file(filepath)
|
||||
except ValueError as e:
|
||||
print(f" [!] Пропуск {maze_file}: {e}")
|
||||
continue # Переходим к следующему файлу, если этот не загрузился
|
||||
|
||||
# Эта строка теперь выполняется для каждого успешного лабиринта
|
||||
print(f"\n{'='*50}")
|
||||
print(f"Лабиринт: {maze_name} ({maze.width}×{maze.height})")
|
||||
|
||||
all_data[maze_name] = {}
|
||||
|
||||
for strat_name, StratClass in STRATEGIES.items():
|
||||
times, visited_counts, path_lengths = [], [], []
|
||||
run_stats = []
|
||||
|
||||
for run_num in range(1, RUNS + 1):
|
||||
solver = MazeSolver(maze, StratClass())
|
||||
stats = solver.solve()
|
||||
|
||||
times.append(stats.time_ms)
|
||||
visited_counts.append(stats.visited_cells)
|
||||
path_lengths.append(stats.path_length)
|
||||
|
||||
# Сохраняем данные каждой попытки
|
||||
run_stats.append({
|
||||
'попытка': run_num,
|
||||
'время_мс': stats.time_ms,
|
||||
'посещено_клеток': stats.visited_cells,
|
||||
'длина_пути': stats.path_length
|
||||
})
|
||||
|
||||
print(f" {strat_name} | Попытка {run_num}/{RUNS} | Время: {stats.time_ms:.2f} мс")
|
||||
|
||||
# Вычисляем средние значения
|
||||
avg_time = statistics.mean(times)
|
||||
avg_visited = statistics.mean(visited_counts)
|
||||
|
||||
valid_path_lengths = [p for p in path_lengths if p is not None]
|
||||
avg_path = statistics.mean(valid_path_lengths) if valid_path_lengths else None
|
||||
|
||||
print(f" {strat_name:10s} | СРЕДНЕЕ: время {avg_time:.2f} мс | "
|
||||
f"посещено {avg_visited:.1f} | путь {avg_path if avg_path is not None else '—'}")
|
||||
|
||||
rows.append({
|
||||
"лабиринт": maze_name,
|
||||
"стратегия": strat_name,
|
||||
"время_мс": round(avg_time, 6),
|
||||
"посещено_клеток": round(avg_visited, 1),
|
||||
"длина_пути": round(avg_path, 1) if avg_path is not None else None,
|
||||
})
|
||||
|
||||
all_data[maze_name][strat_name] = run_stats
|
||||
|
||||
# --- СОХРАНЕНИЕ CSV ---
|
||||
with open(OUTPUT_CSV, "w", newline="", encoding="utf-8") as csvfile:
|
||||
fieldnames = ["лабиринт", "стратегия", "время_мс", "посещено_клеток", "длина_пути"]
|
||||
writer = csv.DictWriter(csvfile, fieldnames=fieldnames)
|
||||
writer.writeheader()
|
||||
writer.writerows(rows)
|
||||
print(f"\n✓ Результаты сохранены в {OUTPUT_CSV}")
|
||||
|
||||
# --- ПОСТРОЕНИЕ ГРАФИКОВ ---
|
||||
print("\n=== ПОСТРОЕНИЕ ГРАФИКОВ ===")
|
||||
for maze_name, strategies_data in all_data.items():
|
||||
print(f"\nСтроим графики для лабиринта: {maze_name}")
|
||||
|
||||
# Создаем ОДИН график для времени выполнения
|
||||
fig, ax = plt.subplots(figsize=(10, 6))
|
||||
fig.suptitle(f"Сравнение времени выполнения алгоритмов\nЛабиринт '{maze_name}'", fontsize=14)
|
||||
|
||||
ax.set_title("Время выполнения (мс)")
|
||||
ax.set_xlabel("Номер попытки")
|
||||
ax.set_ylabel("Время (мс)")
|
||||
|
||||
for strat_name in STRATEGIES.keys():
|
||||
# Извлекаем только данные о времени выполнения
|
||||
data_points = [
|
||||
run['время_мс'] for run in strategies_data.get(strat_name, [])
|
||||
]
|
||||
|
||||
if data_points:
|
||||
x_values = range(1, len(data_points) + 1)
|
||||
ax.plot(x_values, data_points,
|
||||
marker='o',
|
||||
label=strat_name,
|
||||
linewidth=2)
|
||||
|
||||
ax.legend(title="Алгоритм")
|
||||
ax.grid(True, which='both', linestyle='--', linewidth=0.5)
|
||||
|
||||
plt.tight_layout(rect=[0, 0.03, 1, 0.95])
|
||||
|
||||
# Сохраняем график в папку 'plots'
|
||||
plot_filename = os.path.join(PLOTS_DIR, f"plot_{maze_name}.png")
|
||||
plt.savefig(plot_filename)
|
||||
plt.close()
|
||||
|
||||
print(f"\n✓ Графики времени выполнения сохранены в папку '{PLOTS_DIR}'")
|
||||
print("=== ЭКСПЕРИМЕНТ ЗАВЕРШЕН ===")
|
||||
52
MarkinAM/2/maze_builder.py
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
from abc import ABC, abstractmethod
|
||||
from classes import Cell, Maze
|
||||
|
||||
|
||||
class MazeBuilder(ABC):
|
||||
#Интерфейс строителя лабиринта (паттерн Builder).
|
||||
|
||||
@abstractmethod
|
||||
def build_from_file(self, filename: str) -> Maze:
|
||||
#Читает файл и возвращает готовый объект Maze.
|
||||
...
|
||||
|
||||
|
||||
class TextFileMazeBuilder(MazeBuilder):
|
||||
|
||||
def build_from_file(self, filename: str) -> Maze:
|
||||
with open(filename, "r", encoding="utf-8") as f:
|
||||
lines = f.read().splitlines()
|
||||
|
||||
if not lines:
|
||||
raise ValueError("Файл лабиринта пуст.")
|
||||
|
||||
height = len(lines)
|
||||
width = max(len(line) for line in lines)
|
||||
|
||||
# Дополняем строки до одинаковой длины (стенами)
|
||||
lines = [line.ljust(width, "#") for line in lines]
|
||||
|
||||
cells: list[list[Cell]] = []
|
||||
start: Cell | None = None
|
||||
exit_cell: Cell | None = None
|
||||
|
||||
for y, line in enumerate(lines):
|
||||
row = []
|
||||
for x, ch in enumerate(line):
|
||||
is_wall = ch == "#"
|
||||
is_start = ch == "S"
|
||||
is_exit = ch == "E"
|
||||
cell = Cell(x, y, is_wall=is_wall, is_start=is_start, is_exit=is_exit)
|
||||
if is_start:
|
||||
start = cell
|
||||
if is_exit:
|
||||
exit_cell = cell
|
||||
row.append(cell)
|
||||
cells.append(row)
|
||||
|
||||
if start is None:
|
||||
raise ValueError("Лабиринт не содержит стартовой клетки (S).")
|
||||
if exit_cell is None:
|
||||
raise ValueError("Лабиринт не содержит выхода (E).")
|
||||
|
||||
return Maze(width, height, cells, start, exit_cell)
|
||||
100
MarkinAM/2/mazes/large.txt
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
####################################################################################################
|
||||
S # # # # # # # # # # # # ##
|
||||
# # # ### ### # ### ####### # ######### # ### # # # # # ####### # # # # ##### # # # ##### ##### ####
|
||||
# # # # # # # # # # # # # # # # # # # # # # # # # # # # ##
|
||||
# ##### # # ##### # ### # ##### ##### # ### # # # ##### ### ####### ### # ### ######### ##### ### ##
|
||||
# # # # # # # # # # # # # # # # # # # # # # # # ##
|
||||
# ##### ##### # ##### ##### # ### ### # ##### # ##### ### ### ### ### ##### ### # ### # # ##### ####
|
||||
# # # # # # # # # # # # # # # # # # # # # # # # ##
|
||||
# # # ##### ######### ### # # # # # ### # ####### # ### ####### ####### ##### ##### # # # # # ### ##
|
||||
# # # # # # # # # # # # # # # # # # # # # # # # # # ##
|
||||
### ########### ### ##### # # # ### ##### # ### ##### ### ### # # ### # # # ### # ### # # ### # # ##
|
||||
# # # # # # # # # # # # # # # # # # # # # # # # # # # # ##
|
||||
# # ### ### # ### ##### # # ### # ### # ##### ##### # ##### ####### ### # ### # ### ######### # # ##
|
||||
# # # # # # # # # # # # # # # # # # # # # # # # # # # ##
|
||||
##### ### # ### # # ##### ### ### # ##### # ##### ####### ####### # # ##### # ####### ##### # # ####
|
||||
# # # # # # # # # # # # # # # # # # # # # ##
|
||||
# ##### ##### ### # # ### # ####### ####### # # ### ### # # ### ########### ### ### ##### ####### ##
|
||||
# # # # # # # # # # # # # # # # # # # # # ##
|
||||
# # ########### # ### # ##### ##### # ####### # # ### ####### ########### # # ### ### ##### ### # ##
|
||||
# # # # # # # # # # # # # # # # # # # # # # # # # ##
|
||||
# # # # ### # ##### ##### ####### # ### # # ####### ### # # ### ##### # ### ### ####### # # # ### ##
|
||||
# # # # # # # # # # # # # # # # # # # # # # # # ##
|
||||
# ### ### # ### # # # ##### ####### # ### # # ### # # # ######### ### # ##### # ### ######### # ####
|
||||
# # # # # # # # # # # # # # # # # # # # # # # # # # # # ##
|
||||
### # # ##### ####### # ### ### # # ##### ####### # # ##### # # # # # ##### ### ##### ### # # ### ##
|
||||
# # # # # # # # # # # # # # # # # # # # # # # # # ##
|
||||
# ##### ### ########### # ### # ##### # ### ### # # ### ####### # ##### ######### ##### ### ##### ##
|
||||
# # # # # # # # # # # # # # # # # # # # # # # ##
|
||||
# ### # # # # ### # # # ### # # ##### ### # ######### ### # ####### ##### ##### ##### # # ### # ####
|
||||
# # # # # # # # # # # # # # # # # # # # # # # ##
|
||||
# ############# # ### ##### # ### # # # ########### # # ### # ####### # ### # ######### ##### ### ##
|
||||
# # # # # # # # # # # # # # # # # # # # # # # # # ##
|
||||
# # # # ##### ##### ### ### ### ##### # ####### # ####### ##### # # # # # ### # # ### # ### ##### ##
|
||||
# # # # # # # # # # # # # # # # # # # # # # # # # ##
|
||||
# # # ### # ##### ########### ### ### ### # # ######### # # # ### ########### # ### # ### ### ### ##
|
||||
# # # # # # # # # # # # # # # # # # # # # # # # ##
|
||||
# # ### # ##### ##### ######### ######### # ### ##### # ### # ### # ############# # # # ### ### ####
|
||||
# # # # # # # # # # # # # # # # # # # # # # # # # # # # ##
|
||||
### # ##### # ##### ##### # # ### # # # # # # ### # # # ### ### ### # ### # # # ### ######### # # ##
|
||||
# # # # # # # # # # # # # # # # # # # # # # # # ##
|
||||
# ########### ######### # # ########### # ### # ######### ########### ##### # # # ######### # ### ##
|
||||
# # # # # # # E # # # # # # # # # # # # # # ##
|
||||
# ##### # # ### ##### ##### ### ##### # ### ### # ######### ### ####### # # # ### ######### ### # ##
|
||||
# # # # # # # # # # # # # # # # # # # # # # # ##
|
||||
### # ################# # ### # ### # ### # # ##### # # # ### ##### ##### # ##### # ### # # ##### ##
|
||||
# # # # # # # # # # # # # # # # # # # # # # # # ##
|
||||
# # # # ##### # ##### ##### # ### # ##### # ### ####################### # ### # ##### # ##### ######
|
||||
# # # # # # # # # # # # # # # # # # # # # # # # # # # # ##
|
||||
# ### # # ### # # # ### # # # # ### # ####### ### ##### # # # # # ##### # # # # # ### # # ####### ##
|
||||
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # ##
|
||||
# ##### # # # # # # ### ### # ### # # # ####### ### ##### ######### ### # # # # ### # ####### # ####
|
||||
# # # # # # # # # # # # # # # # # # # # # ##
|
||||
# # ########### ####### ####### # # ##### ### # ####### # # ### ########### ######### # ####### # ##
|
||||
# # # # # # # # # # # # # # # # # # # # # # # ##
|
||||
# ##### # ### ### # ##### ### ##### # # ### # ### # # ####### ####### # # ##### # ####### # ##### ##
|
||||
# # # # # # # # # # # # # # # # # # # # # # # ##
|
||||
# # ### # # ### ############### # ####### # ############# # ### ### ##### ### ### # ######### # # ##
|
||||
# # # # # # # # # # # # # # # # # # # # # # # ##
|
||||
# # # ########### ### ### # ########### # # # # # ##### ### ############### # # ### # ### # # # # ##
|
||||
# # # # # # # # # # # # # # # # # # # # # # # # # # ##
|
||||
# ####### ### ### # ### # ### # # # ### # # ### ### ##### ### # ### # # ##### ####### # ####### # ##
|
||||
# # # # # # # # # # # # # # # # # # # # # # # # # ##
|
||||
# # ### ####### ##### ######### ##### ### # # ### ### # ### ### # ##### # ########### ##### # ### ##
|
||||
# # # # # # # # # # # # # # # # # # # # # # ##
|
||||
# # # ####### ### ##### ### ### # ##### ##### ### ### # # ### ############# # ##### # # # ### ### ##
|
||||
# # # # # # # # # # # # # # # # # # # # # # # # # # # ##
|
||||
# # ##### # # # ### ##### ### # # # # # ####### # # ### ########### # # # ##### # ### # ### ### ####
|
||||
# # # # # # # # # # # # # # # # # # # # # # # # # # # ##
|
||||
# ####### # # ####### ##### # ##### ####### # ##### ##### # # # # ##### ### # # ### ### # ### ### ##
|
||||
# # # # # # # # # # # # # # # # # # # # # # # # ##
|
||||
# # ### # # ##### # ### ######### ### # # # ##### ### ############### ### # # ####### ##### ### # ##
|
||||
# # # # # # # # # # # # # # # # # # # # # # # # # ##
|
||||
# # # ##### ### ##### ####### # ### ### ####### ##### # # ### ##### # # ##### ### # # # # ### # # ##
|
||||
# # # # # # # # # # # # # # # # # # # # # # # # # # ##
|
||||
##### ####### ##### ### ### ############# ##### # # ####### # # # ### # # # # # ### ##### # # # # ##
|
||||
# # # # # # # # # # # # # # # # # # # # ##
|
||||
# ####### ### # # ##### # ####### ### # ### ##### ### ########### ##### ### # # # ##### ######### ##
|
||||
# # # # # # # # # # # # # # # # # # # # # # # ##
|
||||
### # ##### ### ######### # ### # # ##### ### # ####### ####### ##### ### ####### # # ##### ##### ##
|
||||
# # # # # # # # # # # # # # # # # # # # # # # # # # ##
|
||||
# # ######### ##### # # ##### ### # # ##### # ### # # # # # # ### # ####### # ##### # # # ### # ####
|
||||
# # # # # # # # # # # # # # # # # # # # # # # # # # ##
|
||||
# ### # # # # # ##### # # # ##### # ### ####### # # ##### # # # ### ##### # ####### # # ######### ##
|
||||
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # ##
|
||||
# # # # # ####### ### # # ##### # ### ### # # # # ##### ### ### # ### # ##### ### # ##### ### ### ##
|
||||
# # # # # # # # # # # # # # # # # # # # # # # # ##
|
||||
####### ##### ##### ##### # # ##### ### ##### ######### # # # ### ##### # ##### # # ####### ### ####
|
||||
# # # # # # # # # # # # # # # # # # # # # # # # # # ##
|
||||
# # # ### # ### ##### # ##### # # ### # ####### ##### # # # # ### # # # ####### ##### # # # # ### ##
|
||||
# # # # # # # # # # # # # # # # # # # # # # # ##
|
||||
# ### # ##### ##### ##### ########### ####### ### # ########### ### # ####### ######### ##### # # ##
|
||||
# # # # # # # # # # # # # # # # # # # # # ##
|
||||
# ### ### # ######### # ##### # ####### # # ####### ### # ####### ### ##### ### # # # # # ####### ##
|
||||
# # # # # # # # # # # # # # # # # # # # # # # ##
|
||||
# # ### # # # # ### ### # # ############# ########### ####### # ### ##### ##### # ####### ##### # ##
|
||||
# # # # # # # # # # # # # # # # # # # # # # # # # ##
|
||||
# ### ####### ##### # ########### # ### ### # ##### # ##### ####### # # ### # ######### # # # # # ##
|
||||
# # # # # # # # # # # ##
|
||||
###################################################################################################
|
||||
####################################################################################################
|
||||
18
MarkinAM/2/mazes/medium.txt
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
##################################################
|
||||
#S #
|
||||
# ################ ##### ############# #
|
||||
# # # # #
|
||||
# ########### ##### ##### ########### #
|
||||
# # # # #
|
||||
# # ######### ################ ######### #
|
||||
# # # # #
|
||||
# ############ ################# ### # #
|
||||
# # # # # #
|
||||
##### ##### ### ########### #### # # #
|
||||
# # E # # # # # #
|
||||
#### ##### ### ########### # #### # # # #
|
||||
# # # # # # # # #
|
||||
# ### ##### ############# ### ## # # # #
|
||||
# # # # # # # #
|
||||
########### ############ ##### ### ######### #
|
||||
##################################################
|
||||
3
MarkinAM/2/mazes/no_exit.txt
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
##########
|
||||
#S #
|
||||
##########
|
||||
6
MarkinAM/2/mazes/open.txt
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
S....................................................................................................
|
||||
.........................................................................................................
|
||||
.........................................................................................................
|
||||
.........................................................................................................
|
||||
.........................................................................................................
|
||||
.........................................................................................E...............
|
||||
10
MarkinAM/2/mazes/small.txt
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
##########
|
||||
# E #
|
||||
# ###### #
|
||||
# # # #
|
||||
# # ## # #
|
||||
# # ## # #
|
||||
# # # #
|
||||
# ###### #
|
||||
# S #
|
||||
##########
|
||||
71
MarkinAM/2/observer.py
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
from abc import ABC, abstractmethod
|
||||
from classes import Maze, Cell
|
||||
|
||||
|
||||
class Observer(ABC):
|
||||
"""Интерфейс наблюдателя."""
|
||||
|
||||
@abstractmethod
|
||||
def update(self, event: dict) -> None:
|
||||
"""
|
||||
event — словарь с ключом "type":
|
||||
"maze_loaded" — лабиринт загружен
|
||||
"path_found" — путь найден
|
||||
"no_path" — путь не найден
|
||||
"""
|
||||
...
|
||||
|
||||
|
||||
class ConsoleView(Observer):
|
||||
"""
|
||||
Наблюдатель: выводит лабиринт и путь в консоль.
|
||||
|
||||
Символы:
|
||||
# — стена
|
||||
. — проход
|
||||
S — старт
|
||||
E — выход
|
||||
* — найденный путь
|
||||
@ — текущее положение игрока
|
||||
"""
|
||||
|
||||
def update(self, event: dict) -> None:
|
||||
event_type = event.get("type")
|
||||
|
||||
if event_type == "maze_loaded":
|
||||
print("\n[ConsoleView] Лабиринт загружен:")
|
||||
self.render(event["maze"])
|
||||
|
||||
elif event_type == "path_found":
|
||||
print("\n[ConsoleView] Путь найден!")
|
||||
self.render(event["maze"], path=event.get("path"), player=event.get("player"))
|
||||
|
||||
elif event_type == "no_path":
|
||||
print("\n[ConsoleView] Путь не найден.")
|
||||
|
||||
elif event_type == "move":
|
||||
print(f"\n[ConsoleView] Игрок переместился в ({event['x']}, {event['y']})")
|
||||
self.render(event["maze"], path=event.get("path"), player=event.get("player"))
|
||||
|
||||
def render(self, maze: Maze, path: list[Cell] | None = None,
|
||||
player: Cell | None = None) -> None:
|
||||
path_set = set(path) if path else set()
|
||||
|
||||
for y in range(maze.height):
|
||||
row_str = ""
|
||||
for x in range(maze.width):
|
||||
cell = maze.get_cell(x, y)
|
||||
if player and cell == player:
|
||||
row_str += "@"
|
||||
elif cell.is_start:
|
||||
row_str += "S"
|
||||
elif cell.is_exit:
|
||||
row_str += "E"
|
||||
elif cell in path_set:
|
||||
row_str += "*"
|
||||
elif cell.is_wall:
|
||||
row_str += "#"
|
||||
else:
|
||||
row_str += "."
|
||||
print(row_str)
|
||||
|
||||
BIN
MarkinAM/2/plots/plot_large.png
Normal file
|
After Width: | Height: | Size: 72 KiB |
BIN
MarkinAM/2/plots/plot_medium.png
Normal file
|
After Width: | Height: | Size: 67 KiB |
BIN
MarkinAM/2/plots/plot_open.png
Normal file
|
After Width: | Height: | Size: 61 KiB |
BIN
MarkinAM/2/plots/plot_small.png
Normal file
|
After Width: | Height: | Size: 77 KiB |
BIN
MarkinAM/2/report.docx
Normal file
13
MarkinAM/2/results.csv
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
лабиринт,стратегия,время_мс,посещено_клеток,длина_пути
|
||||
large,BFS,7.59029,2952,662
|
||||
large,DFS,10.92704,4082,1566
|
||||
large,A*,5.40801,1073,662
|
||||
medium,BFS,0.2096,80,22
|
||||
medium,DFS,0.79632,300,28
|
||||
medium,A*,0.13978,28,22
|
||||
open,BFS,1.57724,550,95
|
||||
open,DFS,1.04963,303,303
|
||||
open,A*,0.64328,95,95
|
||||
small,BFS,0.06765,25,13
|
||||
small,DFS,0.03831,13,13
|
||||
small,A*,0.06389,13,13
|
||||
|
71
MarkinAM/2/solver.py
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
import time
|
||||
from dataclasses import dataclass
|
||||
|
||||
from classes import Maze, Cell
|
||||
from strategies import PathFindingStrategy
|
||||
from observer import Observer
|
||||
|
||||
|
||||
@dataclass
|
||||
class SearchStats:
|
||||
"""Результаты одного запуска поиска."""
|
||||
time_ms: float # время выполнения в миллисекундах
|
||||
visited_cells: int # количество посещённых клеток
|
||||
path_length: int # длина найденного пути (0 если не найден)
|
||||
path: list[Cell] # сам путь
|
||||
|
||||
|
||||
class MazeSolver:
|
||||
|
||||
def __init__(self, maze: Maze, strategy: PathFindingStrategy | None = None):
|
||||
self.maze = maze
|
||||
self.strategy = strategy
|
||||
self._observers: list[Observer] = []
|
||||
|
||||
# ── Strategy ──────────────────────────────────────────────────────────────
|
||||
|
||||
def set_strategy(self, strategy: PathFindingStrategy) -> None:
|
||||
"""Динамически меняет алгоритм поиска."""
|
||||
self.strategy = strategy
|
||||
|
||||
# ── Observer ──────────────────────────────────────────────────────────────
|
||||
|
||||
def add_observer(self, observer: Observer) -> None:
|
||||
self._observers.append(observer)
|
||||
|
||||
def remove_observer(self, observer: Observer) -> None:
|
||||
self._observers.remove(observer)
|
||||
|
||||
def _notify(self, event: dict) -> None:
|
||||
for obs in self._observers:
|
||||
obs.update(event)
|
||||
|
||||
# ── Solve ─────────────────────────────────────────────────────────────────
|
||||
|
||||
def solve(self) -> SearchStats:
|
||||
"""Запускает поиск пути и возвращает статистику."""
|
||||
if self.strategy is None:
|
||||
raise RuntimeError("Стратегия не задана. Используйте set_strategy().")
|
||||
|
||||
self._notify({"type": "maze_loaded", "maze": self.maze})
|
||||
|
||||
t_start = time.perf_counter()
|
||||
path = self.strategy.find_path(self.maze, self.maze.start, self.maze.exit)
|
||||
t_end = time.perf_counter()
|
||||
|
||||
time_ms = (t_end - t_start) * 1000
|
||||
visited = getattr(self.strategy, "visited_count", 0)
|
||||
|
||||
stats = SearchStats(
|
||||
time_ms=time_ms,
|
||||
visited_cells=visited,
|
||||
path_length=len(path),
|
||||
path=path,
|
||||
)
|
||||
|
||||
if path:
|
||||
self._notify({"type": "path_found", "maze": self.maze, "path": path})
|
||||
else:
|
||||
self._notify({"type": "no_path"})
|
||||
|
||||
return stats
|
||||
119
MarkinAM/2/strategies.py
Normal file
|
|
@ -0,0 +1,119 @@
|
|||
from abc import ABC, abstractmethod
|
||||
from collections import deque
|
||||
import heapq
|
||||
|
||||
from classes import Cell, Maze
|
||||
|
||||
|
||||
class PathFindingStrategy(ABC):
|
||||
"""Интерфейс стратегии поиска пути."""
|
||||
|
||||
@abstractmethod
|
||||
def find_path(self, maze: Maze, start: Cell, exit_cell: Cell) -> list[Cell]:
|
||||
...
|
||||
|
||||
# Вспомогательный метод восстановления пути по словарю предшественников
|
||||
@staticmethod
|
||||
def _reconstruct_path(came_from: dict, start: Cell, goal: Cell) -> list[Cell]:
|
||||
path = []
|
||||
current = goal
|
||||
while current != start:
|
||||
path.append(current)
|
||||
current = came_from[current]
|
||||
path.append(start)
|
||||
path.reverse()
|
||||
return path
|
||||
|
||||
|
||||
# ── BFS ──────────────────────────────────────────────────────────────────────
|
||||
|
||||
class BFSStrategy(PathFindingStrategy):
|
||||
|
||||
|
||||
def find_path(self, maze: Maze, start: Cell, exit_cell: Cell) -> list[Cell]:
|
||||
queue = deque([start])
|
||||
came_from: dict[Cell, Cell | None] = {start: None}
|
||||
self.visited_count = 0
|
||||
|
||||
while queue:
|
||||
current = queue.popleft()
|
||||
self.visited_count += 1
|
||||
|
||||
if current == exit_cell:
|
||||
return self._reconstruct_path(came_from, start, exit_cell)
|
||||
|
||||
for neighbor in maze.get_neighbors(current):
|
||||
if neighbor not in came_from:
|
||||
came_from[neighbor] = current
|
||||
queue.append(neighbor)
|
||||
|
||||
return [] # путь не найден
|
||||
|
||||
|
||||
# ── DFS ──────────────────────────────────────────────────────────────────────
|
||||
|
||||
class DFSStrategy(PathFindingStrategy):
|
||||
|
||||
|
||||
def find_path(self, maze: Maze, start: Cell, exit_cell: Cell) -> list[Cell]:
|
||||
stack = [start]
|
||||
came_from: dict[Cell, Cell | None] = {start: None}
|
||||
self.visited_count = 0
|
||||
|
||||
while stack:
|
||||
current = stack.pop()
|
||||
self.visited_count += 1
|
||||
|
||||
if current == exit_cell:
|
||||
return self._reconstruct_path(came_from, start, exit_cell)
|
||||
|
||||
for neighbor in maze.get_neighbors(current):
|
||||
if neighbor not in came_from:
|
||||
came_from[neighbor] = current
|
||||
stack.append(neighbor)
|
||||
|
||||
return []
|
||||
|
||||
|
||||
# ── A* ───────────────────────────────────────────────────────────────────────
|
||||
|
||||
class AStarStrategy(PathFindingStrategy):
|
||||
"""A* с манхэттенской эвристикой"""
|
||||
|
||||
def __init__(self):
|
||||
self.visited_count = 0
|
||||
|
||||
def _heuristic(self, a: Cell, b: Cell) -> int:
|
||||
return abs(a.x - b.x) + abs(a.y - b.y)
|
||||
|
||||
def find_path(self, maze: Maze, start: Cell, exit_cell: Cell) -> list[Cell]:
|
||||
g_score = {start: 0}
|
||||
parent: dict[Cell] = {start: None}
|
||||
open_heap = [(self._heuristic(start, exit_cell), 0, start)]
|
||||
closed_set: set[Cell] = set() # уже обработанные клетки
|
||||
self.visited_count = 0
|
||||
counter = 0 # счётчик для устранения неоднозначности
|
||||
|
||||
while open_heap:
|
||||
_, _, current = heapq.heappop(open_heap)
|
||||
|
||||
if current in closed_set:
|
||||
continue
|
||||
closed_set.add(current)
|
||||
self.visited_count += 1
|
||||
|
||||
if current == exit_cell:
|
||||
return self._reconstruct_path(parent, start, exit_cell)
|
||||
|
||||
for neighbor in maze.get_neighbors(current):
|
||||
if neighbor in closed_set:
|
||||
continue
|
||||
tentative_g = g_score[current]
|
||||
if tentative_g < g_score.get(neighbor, float('inf')):
|
||||
g_score[neighbor] = tentative_g
|
||||
parent[neighbor] = current
|
||||
f = tentative_g + self._heuristic(neighbor, exit_cell)
|
||||
counter += 1
|
||||
heapq.heappush(open_heap, (f, counter, neighbor))
|
||||
|
||||
return []
|
||||