diff --git a/MarkinAM/1/delete_chart.svg b/MarkinAM/1/delete_chart.svg new file mode 100644 index 0000000..f7deca6 --- /dev/null +++ b/MarkinAM/1/delete_chart.svg @@ -0,0 +1,1291 @@ + + + + + + + + 2026-05-23T09:42:52.742816 + image/svg+xml + + + Matplotlib v3.10.0, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/MarkinAM/1/docs/report.md b/MarkinAM/1/docs/report.md new file mode 100644 index 0000000..76c40e8 --- /dev/null +++ b/MarkinAM/1/docs/report.md @@ -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). Несмотря на чуть большую константа в асимптотике по сравнению с хеш-таблицей, оно позволяет эффективно получать отсортированные данные, находить минимальный/максимальный элемент или элементы в заданном диапазоне. +#### Связный список в чистом виде для решения подобных задач сегодня практически не применяется из-за низкой эффективности поиска. diff --git a/MarkinAM/1/find_chart.svg b/MarkinAM/1/find_chart.svg new file mode 100644 index 0000000..77531ad --- /dev/null +++ b/MarkinAM/1/find_chart.svg @@ -0,0 +1,1265 @@ + + + + + + + + 2026-05-23T09:42:52.505767 + image/svg+xml + + + Matplotlib v3.10.0, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/MarkinAM/1/insert_chart.svg b/MarkinAM/1/insert_chart.svg new file mode 100644 index 0000000..fb78f9f --- /dev/null +++ b/MarkinAM/1/insert_chart.svg @@ -0,0 +1,1195 @@ + + + + + + + + 2026-05-23T09:42:52.279712 + image/svg+xml + + + Matplotlib v3.10.0, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/MarkinAM/1/main.py b/MarkinAM/1/main.py new file mode 100644 index 0000000..145b108 --- /dev/null +++ b/MarkinAM/1/main.py @@ -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("Результаты сохранены") \ No newline at end of file diff --git a/MarkinAM/1/plot.py b/MarkinAM/1/plot.py new file mode 100644 index 0000000..c50f23e --- /dev/null +++ b/MarkinAM/1/plot.py @@ -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() \ No newline at end of file diff --git a/MarkinAM/1/results.csv b/MarkinAM/1/results.csv new file mode 100644 index 0000000..52aa388 --- /dev/null +++ b/MarkinAM/1/results.csv @@ -0,0 +1,109 @@ +;;;; () +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 diff --git a/MarkinAM/1/structures.py b/MarkinAM/1/structures.py new file mode 100644 index 0000000..ad32785 --- /dev/null +++ b/MarkinAM/1/structures.py @@ -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 \ No newline at end of file diff --git a/MarkinAM/2/classes.py b/MarkinAM/2/classes.py new file mode 100644 index 0000000..774b381 --- /dev/null +++ b/MarkinAM/2/classes.py @@ -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})" diff --git a/MarkinAM/2/experiment.py b/MarkinAM/2/experiment.py new file mode 100644 index 0000000..af05f03 --- /dev/null +++ b/MarkinAM/2/experiment.py @@ -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("=== ЭКСПЕРИМЕНТ ЗАВЕРШЕН ===") \ No newline at end of file diff --git a/MarkinAM/2/maze_builder.py b/MarkinAM/2/maze_builder.py new file mode 100644 index 0000000..8d00a16 --- /dev/null +++ b/MarkinAM/2/maze_builder.py @@ -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) \ No newline at end of file diff --git a/MarkinAM/2/mazes/large.txt b/MarkinAM/2/mazes/large.txt new file mode 100644 index 0000000..8d1437e --- /dev/null +++ b/MarkinAM/2/mazes/large.txt @@ -0,0 +1,100 @@ +#################################################################################################### +S # # # # # # # # # # # # ## +# # # ### ### # ### ####### # ######### # ### # # # # # ####### # # # # ##### # # # ##### ##### #### +# # # # # # # # # # # # # # # # # # # # # # # # # # # # ## +# ##### # # ##### # ### # ##### ##### # ### # # # ##### ### ####### ### # ### ######### ##### ### ## +# # # # # # # # # # # # # # # # # # # # # # # # ## +# ##### ##### # ##### ##### # ### ### # ##### # ##### ### ### ### ### ##### ### # ### # # ##### #### +# # # # # # # # # # # # # # # # # # # # # # # # ## +# # # ##### ######### ### # # # # # ### # ####### # ### ####### ####### ##### ##### # # # # # ### ## +# # # # # # # # # # # # # # # # # # # # # # # # # # ## +### ########### ### ##### # # # ### ##### # ### ##### ### ### # # ### # # # ### # ### # # ### # # ## +# # # # # # # # # # # # # # # # # # # # # # # # # # # # ## +# # ### ### # ### ##### # # ### # ### # ##### ##### # ##### ####### ### # ### # ### ######### # # ## +# # # # # # # # # # # # # # # # # # # # # # # # # # # ## +##### ### # ### # # ##### ### ### # ##### # ##### ####### ####### # # ##### # ####### ##### # # #### +# # # # # # # # # # # # # # # # # # # # # ## +# ##### ##### ### # # ### # ####### ####### # # ### ### # # ### ########### ### ### ##### ####### ## +# # # # # # # # # # # # # # # # # # # # # ## +# # ########### # ### # ##### ##### # ####### # # ### ####### ########### # # ### ### ##### ### # ## +# # # # # # # # # # # # # # # # # # # # # # # # # ## +# # # # ### # ##### ##### ####### # ### # # ####### ### # # ### ##### # ### ### ####### # # # ### ## +# # # # # # # # # # # # # # # # # # # # # # # # ## +# ### ### # ### # # # ##### ####### # ### # # ### # # # ######### ### # ##### # ### ######### # #### +# # # # # # # # # # # # # # # # # # # # # # # # # # # # ## +### # # ##### ####### # ### ### # # ##### ####### # # ##### # # # # # ##### ### ##### ### # # ### ## +# # # # # # # # # # # # # # # # # # # # # # # # # ## +# ##### ### ########### # ### # ##### # ### ### # # ### ####### # ##### ######### ##### ### ##### ## +# # # # # # # # # # # # # # # # # # # # # # # ## +# ### # # # # ### # # # ### # # ##### ### # ######### ### # ####### ##### ##### ##### # # ### # #### +# # # # # # # # # # # # # # # # # # # # # # # ## +# ############# # ### ##### # ### # # # ########### # # ### # ####### # ### # ######### ##### ### ## +# # # # # # # # # # # # # # # # # # # # # # # # # ## +# # # # ##### ##### ### ### ### ##### # ####### # ####### ##### # # # # # ### # # ### # ### ##### ## +# # # # # # # # # # # # # # # # # # # # # # # # # ## +# # # ### # ##### ########### ### ### ### # # ######### # # # ### ########### # ### # ### ### ### ## +# # # # # # # # # # # # # # # # # # # # # # # # ## +# # ### # ##### ##### ######### ######### # ### ##### # ### # ### # ############# # # # ### ### #### +# # # # # # # # # # # # # # # # # # # # # # # # # # # # ## +### # ##### # ##### ##### # # ### # # # # # # ### # # # ### ### ### # ### # # # ### ######### # # ## +# # # # # # # # # # # # # # # # # # # # # # # # ## +# ########### ######### # # ########### # ### # ######### ########### ##### # # # ######### # ### ## +# # # # # # # E # # # # # # # # # # # # # # ## +# ##### # # ### ##### ##### ### ##### # ### ### # ######### ### ####### # # # ### ######### ### # ## +# # # # # # # # # # # # # # # # # # # # # # # ## +### # ################# # ### # ### # ### # # ##### # # # ### ##### ##### # ##### # ### # # ##### ## +# # # # # # # # # # # # # # # # # # # # # # # # ## +# # # # ##### # ##### ##### # ### # ##### # ### ####################### # ### # ##### # ##### ###### +# # # # # # # # # # # # # # # # # # # # # # # # # # # # ## +# ### # # ### # # # ### # # # # ### # ####### ### ##### # # # # # ##### # # # # # ### # # ####### ## +# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # ## +# ##### # # # # # # ### ### # ### # # # ####### ### ##### ######### ### # # # # ### # ####### # #### +# # # # # # # # # # # # # # # # # # # # # ## +# # ########### ####### ####### # # ##### ### # ####### # # ### ########### ######### # ####### # ## +# # # # # # # # # # # # # # # # # # # # # # # ## +# ##### # ### ### # ##### ### ##### # # ### # ### # # ####### ####### # # ##### # ####### # ##### ## +# # # # # # # # # # # # # # # # # # # # # # # ## +# # ### # # ### ############### # ####### # ############# # ### ### ##### ### ### # ######### # # ## +# # # # # # # # # # # # # # # # # # # # # # # ## +# # # ########### ### ### # ########### # # # # # ##### ### ############### # # ### # ### # # # # ## +# # # # # # # # # # # # # # # # # # # # # # # # # # ## +# ####### ### ### # ### # ### # # # ### # # ### ### ##### ### # ### # # ##### ####### # ####### # ## +# # # # # # # # # # # # # # # # # # # # # # # # # ## +# # ### ####### ##### ######### ##### ### # # ### ### # ### ### # ##### # ########### ##### # ### ## +# # # # # # # # # # # # # # # # # # # # # # ## +# # # ####### ### ##### ### ### # ##### ##### ### ### # # ### ############# # ##### # # # ### ### ## +# # # # # # # # # # # # # # # # # # # # # # # # # # # ## +# # ##### # # # ### ##### ### # # # # # ####### # # ### ########### # # # ##### # ### # ### ### #### +# # # # # # # # # # # # # # # # # # # # # # # # # # # ## +# ####### # # ####### ##### # ##### ####### # ##### ##### # # # # ##### ### # # ### ### # ### ### ## +# # # # # # # # # # # # # # # # # # # # # # # # ## +# # ### # # ##### # ### ######### ### # # # ##### ### ############### ### # # ####### ##### ### # ## +# # # # # # # # # # # # # # # # # # # # # # # # # ## +# # # ##### ### ##### ####### # ### ### ####### ##### # # ### ##### # # ##### ### # # # # ### # # ## +# # # # # # # # # # # # # # # # # # # # # # # # # # ## +##### ####### ##### ### ### ############# ##### # # ####### # # # ### # # # # # ### ##### # # # # ## +# # # # # # # # # # # # # # # # # # # # ## +# ####### ### # # ##### # ####### ### # ### ##### ### ########### ##### ### # # # ##### ######### ## +# # # # # # # # # # # # # # # # # # # # # # # ## +### # ##### ### ######### # ### # # ##### ### # ####### ####### ##### ### ####### # # ##### ##### ## +# # # # # # # # # # # # # # # # # # # # # # # # # # ## +# # ######### ##### # # ##### ### # # ##### # ### # # # # # # ### # ####### # ##### # # # ### # #### +# # # # # # # # # # # # # # # # # # # # # # # # # # ## +# ### # # # # # ##### # # # ##### # ### ####### # # ##### # # # ### ##### # ####### # # ######### ## +# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # ## +# # # # # ####### ### # # ##### # ### ### # # # # ##### ### ### # ### # ##### ### # ##### ### ### ## +# # # # # # # # # # # # # # # # # # # # # # # # ## +####### ##### ##### ##### # # ##### ### ##### ######### # # # ### ##### # ##### # # ####### ### #### +# # # # # # # # # # # # # # # # # # # # # # # # # # ## +# # # ### # ### ##### # ##### # # ### # ####### ##### # # # # ### # # # ####### ##### # # # # ### ## +# # # # # # # # # # # # # # # # # # # # # # # ## +# ### # ##### ##### ##### ########### ####### ### # ########### ### # ####### ######### ##### # # ## +# # # # # # # # # # # # # # # # # # # # # ## +# ### ### # ######### # ##### # ####### # # ####### ### # ####### ### ##### ### # # # # # ####### ## +# # # # # # # # # # # # # # # # # # # # # # # ## +# # ### # # # # ### ### # # ############# ########### ####### # ### ##### ##### # ####### ##### # ## +# # # # # # # # # # # # # # # # # # # # # # # # # ## +# ### ####### ##### # ########### # ### ### # ##### # ##### ####### # # ### # ######### # # # # # ## +# # # # # # # # # # # ## +################################################################################################### +#################################################################################################### diff --git a/MarkinAM/2/mazes/medium.txt b/MarkinAM/2/mazes/medium.txt new file mode 100644 index 0000000..e6182f2 --- /dev/null +++ b/MarkinAM/2/mazes/medium.txt @@ -0,0 +1,18 @@ +################################################## +#S # +# ################ ##### ############# # +# # # # # +# ########### ##### ##### ########### # +# # # # # +# # ######### ################ ######### # +# # # # # +# ############ ################# ### # # +# # # # # # +##### ##### ### ########### #### # # # + # # E # # # # # # +#### ##### ### ########### # #### # # # # +# # # # # # # # # +# ### ##### ############# ### ## # # # # +# # # # # # # # +########### ############ ##### ### ######### # +################################################## \ No newline at end of file diff --git a/MarkinAM/2/mazes/no_exit.txt b/MarkinAM/2/mazes/no_exit.txt new file mode 100644 index 0000000..abe48ba --- /dev/null +++ b/MarkinAM/2/mazes/no_exit.txt @@ -0,0 +1,3 @@ +########## +#S # +########## \ No newline at end of file diff --git a/MarkinAM/2/mazes/open.txt b/MarkinAM/2/mazes/open.txt new file mode 100644 index 0000000..69002e4 --- /dev/null +++ b/MarkinAM/2/mazes/open.txt @@ -0,0 +1,6 @@ +S.................................................................................................... +......................................................................................................... +......................................................................................................... +......................................................................................................... +......................................................................................................... +.........................................................................................E............... \ No newline at end of file diff --git a/MarkinAM/2/mazes/small.txt b/MarkinAM/2/mazes/small.txt new file mode 100644 index 0000000..69339b7 --- /dev/null +++ b/MarkinAM/2/mazes/small.txt @@ -0,0 +1,10 @@ +########## +# E # +# ###### # +# # # # +# # ## # # +# # ## # # +# # # # +# ###### # +# S # +########## \ No newline at end of file diff --git a/MarkinAM/2/observer.py b/MarkinAM/2/observer.py new file mode 100644 index 0000000..298fcbb --- /dev/null +++ b/MarkinAM/2/observer.py @@ -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) + diff --git a/MarkinAM/2/plots/plot_large.png b/MarkinAM/2/plots/plot_large.png new file mode 100644 index 0000000..92076c8 Binary files /dev/null and b/MarkinAM/2/plots/plot_large.png differ diff --git a/MarkinAM/2/plots/plot_medium.png b/MarkinAM/2/plots/plot_medium.png new file mode 100644 index 0000000..4972343 Binary files /dev/null and b/MarkinAM/2/plots/plot_medium.png differ diff --git a/MarkinAM/2/plots/plot_open.png b/MarkinAM/2/plots/plot_open.png new file mode 100644 index 0000000..f31e8e5 Binary files /dev/null and b/MarkinAM/2/plots/plot_open.png differ diff --git a/MarkinAM/2/plots/plot_small.png b/MarkinAM/2/plots/plot_small.png new file mode 100644 index 0000000..924c9a2 Binary files /dev/null and b/MarkinAM/2/plots/plot_small.png differ diff --git a/MarkinAM/2/report.docx b/MarkinAM/2/report.docx new file mode 100644 index 0000000..60b5193 Binary files /dev/null and b/MarkinAM/2/report.docx differ diff --git a/MarkinAM/2/results.csv b/MarkinAM/2/results.csv new file mode 100644 index 0000000..a43efc7 --- /dev/null +++ b/MarkinAM/2/results.csv @@ -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 diff --git a/MarkinAM/2/solver.py b/MarkinAM/2/solver.py new file mode 100644 index 0000000..44b04ba --- /dev/null +++ b/MarkinAM/2/solver.py @@ -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 \ No newline at end of file diff --git a/MarkinAM/2/strategies.py b/MarkinAM/2/strategies.py new file mode 100644 index 0000000..c06eea1 --- /dev/null +++ b/MarkinAM/2/strategies.py @@ -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 [] \ No newline at end of file diff --git a/MarkinAM/428b.md b/MarkinAM/428b.md new file mode 100644 index 0000000..e69de29