From c19fa42056ae310ddf0c076f809770747667816c Mon Sep 17 00:00:00 2001 From: SavelevMI Date: Thu, 21 May 2026 13:34:23 +0000 Subject: [PATCH 01/11] [1] Initial linked list implementation for phonebook --- SavelevMI/docs/data/1-st-exersize/main.py | 58 +++++++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 SavelevMI/docs/data/1-st-exersize/main.py diff --git a/SavelevMI/docs/data/1-st-exersize/main.py b/SavelevMI/docs/data/1-st-exersize/main.py new file mode 100644 index 0000000..3c84f08 --- /dev/null +++ b/SavelevMI/docs/data/1-st-exersize/main.py @@ -0,0 +1,58 @@ +def create_node(name, phone): + return {'name': name, 'phone': phone, 'next': None} + +def ll_insert(head, name, phone): + current = head + # Поиск существующей записи + while current is not None: + if current['name'] == name: + current['phone'] = phone + return head + current = current['next'] + + # Создание нового узла + new_node = create_node(name, phone) + + if head is None: + return new_node + + current = head + while current['next'] is not None: + current = current['next'] + current['next'] = new_node + return head + +def ll_find(head, name): + current = head + while current is not None: + if current['name'] == name: + return current['phone'] + current = current['next'] + return None + +def ll_delete(head, name): + if head is None: + return None + + # Удаление головы + if head['name'] == name: + return head['next'] + + prev = head + current = head['next'] + while current is not None: + if current['name'] == name: + prev['next'] = current['next'] + return head + prev = current + current = current['next'] + return head + +def ll_list_all(head): + records = [] + current = head + while current is not None: + records.append((current['name'], current['phone'])) + current = current['next'] + records.sort(key=lambda pair: pair[0]) + return records \ No newline at end of file From 9d77a348f75a407522b655c99e20b37c99e2610c Mon Sep 17 00:00:00 2001 From: SavelevMI Date: Thu, 21 May 2026 13:35:47 +0000 Subject: [PATCH 02/11] [1] Rename main.py --- SavelevMI/docs/data/1-st-exersize/{main.py => linked_list.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename SavelevMI/docs/data/1-st-exersize/{main.py => linked_list.py} (100%) diff --git a/SavelevMI/docs/data/1-st-exersize/main.py b/SavelevMI/docs/data/1-st-exersize/linked_list.py similarity index 100% rename from SavelevMI/docs/data/1-st-exersize/main.py rename to SavelevMI/docs/data/1-st-exersize/linked_list.py From a95dbdf35b0367fb599929478521d75a7fc49b67 Mon Sep 17 00:00:00 2001 From: SavelevMI Date: Thu, 21 May 2026 13:36:53 +0000 Subject: [PATCH 03/11] [1] Add hash table using linked list buckets --- .../docs/data/1-st-exersize/hash_table.py | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 SavelevMI/docs/data/1-st-exersize/hash_table.py diff --git a/SavelevMI/docs/data/1-st-exersize/hash_table.py b/SavelevMI/docs/data/1-st-exersize/hash_table.py new file mode 100644 index 0000000..ef3d24b --- /dev/null +++ b/SavelevMI/docs/data/1-st-exersize/hash_table.py @@ -0,0 +1,38 @@ +# Хеш-таблица на основе списка корзин, каждая корзина – связный список + +import linked_list as ll + +def create_hash_table(size=10): + return [None] * size + +def _hash_function(key, size): + return hash(key) % size + +def ht_insert(buckets, name, phone): + idx = _hash_function(name, len(buckets)) + head = buckets[idx] + new_head = ll.ll_insert(head, name, phone) + buckets[idx] = new_head + return buckets + +def ht_find(buckets, name): + idx = _hash_function(name, len(buckets)) + head = buckets[idx] + return ll.ll_find(head, name) + +def ht_delete(buckets, name): + idx = _hash_function(name, len(buckets)) + head = buckets[idx] + new_head = ll.ll_delete(head, name) + buckets[idx] = new_head + return buckets + +def ht_list_all(buckets): + all_records = [] + for head in buckets: + current = head + while current is not None: + all_records.append((current['name'], current['phone'])) + current = current['next'] + all_records.sort(key=lambda x: x[0]) + return all_records \ No newline at end of file From c915f50377b7b16ad199210a88334f047c635704 Mon Sep 17 00:00:00 2001 From: SavelevMI Date: Thu, 21 May 2026 13:37:55 +0000 Subject: [PATCH 04/11] [1] Implement binary search tree (BST) for phonebook --- SavelevMI/docs/data/1-st-exersize/bst.py | 66 ++++++++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 SavelevMI/docs/data/1-st-exersize/bst.py diff --git a/SavelevMI/docs/data/1-st-exersize/bst.py b/SavelevMI/docs/data/1-st-exersize/bst.py new file mode 100644 index 0000000..70d2952 --- /dev/null +++ b/SavelevMI/docs/data/1-st-exersize/bst.py @@ -0,0 +1,66 @@ +# Двоичное дерево поиска (не сбалансированное) + +def create_node(name, phone): + return {'name': name, 'phone': phone, 'left': None, 'right': None} + +def bst_insert(root, name, phone): + if root is None: + return create_node(name, phone) + + if name == root['name']: + root['phone'] = phone + elif name < root['name']: + root['left'] = bst_insert(root['left'], name, phone) + else: + root['right'] = bst_insert(root['right'], name, phone) + return root + +def bst_find(root, name): + if root is None: + return None + if name == root['name']: + return root['phone'] + elif name < root['name']: + return bst_find(root['left'], name) + else: + return bst_find(root['right'], name) + +def _find_min(node): + while node['left'] is not None: + node = node['left'] + return node + +def bst_delete(root, name): + if root is None: + return None + + if name < root['name']: + root['left'] = bst_delete(root['left'], name) + elif name > root['name']: + root['right'] = bst_delete(root['right'], name) + else: + # Нашли узел, который нужно удалить + if root['left'] is None: + return root['right'] + if root['right'] is None: + return root['left'] + + # Узел имеет двух потомков: заменяем наименьшим из правого поддерева + min_node = _find_min(root['right']) + root['name'] = min_node['name'] + root['phone'] = min_node['phone'] + root['right'] = bst_delete(root['right'], min_node['name']) + return root + +def bst_list_all(root): + result = [] + + def 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 From 45df39223de04c69e24700edb02dc7d7f3ccae90 Mon Sep 17 00:00:00 2001 From: SavelevMI Date: Thu, 21 May 2026 13:39:27 +0000 Subject: [PATCH 05/11] [1] Create data generation --- .../docs/data/1-st-exersize/data_utils.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 SavelevMI/docs/data/1-st-exersize/data_utils.py diff --git a/SavelevMI/docs/data/1-st-exersize/data_utils.py b/SavelevMI/docs/data/1-st-exersize/data_utils.py new file mode 100644 index 0000000..5adcaf7 --- /dev/null +++ b/SavelevMI/docs/data/1-st-exersize/data_utils.py @@ -0,0 +1,18 @@ +# Генерация тестовых наборов данных + +import random + +def generate_records(n, seed=42): + random.seed(seed) + records = [] + for i in range(1, n + 1): + name = f"User_{i:05d}" + phone = f"{random.randint(100,999)}-{random.randint(1000,9999)}" + records.append((name, phone)) + return records + +def prepare_datasets(base_records): + shuffled = base_records.copy() + random.shuffle(shuffled) + sorted_records = sorted(base_records, key=lambda x: x[0]) + return shuffled, sorted_records \ No newline at end of file From 058a921ad5dcd26edf40c7e5229df8089689d48e Mon Sep 17 00:00:00 2001 From: SavelevMI Date: Thu, 21 May 2026 13:40:17 +0000 Subject: [PATCH 06/11] [1] Create benchmark utilitie --- SavelevMI/docs/data/1-st-exersize/ata_utils.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 SavelevMI/docs/data/1-st-exersize/ata_utils.py diff --git a/SavelevMI/docs/data/1-st-exersize/ata_utils.py b/SavelevMI/docs/data/1-st-exersize/ata_utils.py new file mode 100644 index 0000000..5adcaf7 --- /dev/null +++ b/SavelevMI/docs/data/1-st-exersize/ata_utils.py @@ -0,0 +1,18 @@ +# Генерация тестовых наборов данных + +import random + +def generate_records(n, seed=42): + random.seed(seed) + records = [] + for i in range(1, n + 1): + name = f"User_{i:05d}" + phone = f"{random.randint(100,999)}-{random.randint(1000,9999)}" + records.append((name, phone)) + return records + +def prepare_datasets(base_records): + shuffled = base_records.copy() + random.shuffle(shuffled) + sorted_records = sorted(base_records, key=lambda x: x[0]) + return shuffled, sorted_records \ No newline at end of file From 09456b2a417a736b9182f210cf99d75e69acc9f9 Mon Sep 17 00:00:00 2001 From: SavelevMI Date: Thu, 21 May 2026 13:40:57 +0000 Subject: [PATCH 07/11] [1] Reanme benchmark.py --- SavelevMI/docs/data/1-st-exersize/{ata_utils.py => benchmark.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename SavelevMI/docs/data/1-st-exersize/{ata_utils.py => benchmark.py} (100%) diff --git a/SavelevMI/docs/data/1-st-exersize/ata_utils.py b/SavelevMI/docs/data/1-st-exersize/benchmark.py similarity index 100% rename from SavelevMI/docs/data/1-st-exersize/ata_utils.py rename to SavelevMI/docs/data/1-st-exersize/benchmark.py From f57786fe3fc724cf089046eff01e57221fa82cb4 Mon Sep 17 00:00:00 2001 From: SavelevMI Date: Thu, 21 May 2026 13:42:11 +0000 Subject: [PATCH 08/11] [1] Run experiments and save results to CSV --- SavelevMI/docs/data/1-st-exersize/main.py | 73 +++++++++++++++++++++++ 1 file changed, 73 insertions(+) create mode 100644 SavelevMI/docs/data/1-st-exersize/main.py diff --git a/SavelevMI/docs/data/1-st-exersize/main.py b/SavelevMI/docs/data/1-st-exersize/main.py new file mode 100644 index 0000000..2ba4ea1 --- /dev/null +++ b/SavelevMI/docs/data/1-st-exersize/main.py @@ -0,0 +1,73 @@ +# Запуск экспериментального сравнения трёх структур данных +# Результаты сохраняются в experiment_results.csv + +import csv +import sys +sys.setrecursionlimit(20000) + +import linked_list as ll +import hash_table as ht +import bst +import data_utils +import benchmark + +def main(): + N = 10000 # количество записей + base_records = data_utils.generate_records(N) + shuffled, sorted_records = data_utils.prepare_datasets(base_records) + + # Описания структур для бенчмарка + structures = { + 'LinkedList': { + 'name': 'LinkedList', + 'create': lambda: None, + 'insert': ll.ll_insert, + 'find': ll.ll_find, + 'delete': ll.ll_delete + }, + 'HashTable': { + 'name': 'HashTable', + 'create': lambda: ht.create_hash_table(10), # 10 корзин + 'insert': ht.ht_insert, + 'find': ht.ht_find, + 'delete': ht.ht_delete + }, + 'BST': { + 'name': 'BST', + 'create': lambda: None, + 'insert': bst.bst_insert, + 'find': bst.bst_find, + 'delete': bst.bst_delete + } + } + + all_results = [] + REPEATS = 5 # минимум 5 повторений + + for name, struct in structures.items(): + print(f"Testing {name} on random order...") + res_random = benchmark.measure_operations(struct, shuffled, 'random', REPEATS) + all_results.extend(res_random) + + print(f"Testing {name} on sorted order...") + res_sorted = benchmark.measure_operations(struct, sorted_records, 'sorted', REPEATS) + all_results.extend(res_sorted) + + # Сохранение CSV + with open('experiment_results.csv', 'w', newline='', encoding='utf-8') as f: + writer = csv.writer(f) + writer.writerow(['Structure', 'Mode', 'Repeat', 'Insert (sec)', 'Search (sec)', 'Delete (sec)']) + for row in all_results: + writer.writerow([ + row['structure'], + row['mode'], + row['repetition'], + f"{row['insert_time']:.6f}", + f"{row['find_time']:.6f}", + f"{row['delete_time']:.6f}" + ]) + + print("Experiment finished. Results saved to experiment_results.csv") + +if __name__ == '__main__': + main() \ No newline at end of file From 535fd115b53ed2c756f4004bc33b3ba0b8585045 Mon Sep 17 00:00:00 2001 From: SavelevMI Date: Thu, 21 May 2026 13:43:06 +0000 Subject: [PATCH 09/11] [1] Add plotting script for performance comparison --- .../docs/data/1-st-exersize/plot_results.py | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 SavelevMI/docs/data/1-st-exersize/plot_results.py diff --git a/SavelevMI/docs/data/1-st-exersize/plot_results.py b/SavelevMI/docs/data/1-st-exersize/plot_results.py new file mode 100644 index 0000000..809a770 --- /dev/null +++ b/SavelevMI/docs/data/1-st-exersize/plot_results.py @@ -0,0 +1,43 @@ +# Загружает CSV с результатами и строит столбчатую диаграмму сравнения + +import pandas as pd +import matplotlib.pyplot as plt +import numpy as np + +def main(): + df = pd.read_csv('experiment_results.csv') + + mean_times = df.groupby(['Structure', 'Mode'])[['Insert (sec)', 'Search (sec)', 'Delete (sec)']].mean().reset_index() + + structures = mean_times['Structure'].unique() + operations = ['Insert (sec)', 'Search (sec)', 'Delete (sec)'] + titles = ['Insertion', 'Search', 'Deletion'] + + fig, axes = plt.subplots(1, 3, figsize=(15, 5)) + + for ax, op, title in zip(axes, operations, titles): + x = np.arange(len(structures)) + width = 0.35 + + random_vals = [] + sorted_vals = [] + for s in structures: + rand = mean_times[(mean_times['Structure'] == s) & (mean_times['Mode'] == 'random')] + sort = mean_times[(mean_times['Structure'] == s) & (mean_times['Mode'] == 'sorted')] + random_vals.append(rand[op].values[0] if not rand.empty else 0) + sorted_vals.append(sort[op].values[0] if not sort.empty else 0) + + ax.bar(x - width/2, random_vals, width, label='Random order') + ax.bar(x + width/2, sorted_vals, width, label='Sorted order') + ax.set_xticks(x) + ax.set_xticklabels(structures) + ax.set_ylabel('Time (seconds)') + ax.set_title(title) + ax.legend() + + plt.tight_layout() + plt.savefig('performance_comparison.png', dpi=150) + plt.show() + +if __name__ == '__main__': + main() \ No newline at end of file From b8b7971d86f7e92fc3970bb671689f9e4457b674 Mon Sep 17 00:00:00 2001 From: SavelevMI Date: Thu, 21 May 2026 13:53:23 +0000 Subject: [PATCH 10/11] [1] Add report 1 exercize --- SavelevMI/docs/performance_comparison.png | Bin 0 -> 49648 bytes SavelevMI/docs/report-1.md | 82 ++++++++++++++++++++++ 2 files changed, 82 insertions(+) create mode 100644 SavelevMI/docs/performance_comparison.png create mode 100644 SavelevMI/docs/report-1.md diff --git a/SavelevMI/docs/performance_comparison.png b/SavelevMI/docs/performance_comparison.png new file mode 100644 index 0000000000000000000000000000000000000000..7bedd06722db3426fab0a8393bc4467f0b39aa7d GIT binary patch literal 49648 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 VIwP^|MHk&Y_6~y&Qn%Tj`8U5NAGH7g literal 0 HcmV?d00001 diff --git a/SavelevMI/docs/report-1.md b/SavelevMI/docs/report-1.md new file mode 100644 index 0000000..a4458f3 --- /dev/null +++ b/SavelevMI/docs/report-1.md @@ -0,0 +1,82 @@ +# Отчёт по лабораторной работе «Структуры данных» + +## Цель работы + +Реализовать три структуры данных «с нуля» (связный список, хеш‑таблицу, двоичное дерево поиска) для хранения записей телефонного справочника. Экспериментально сравнить производительность операций вставки, поиска и удаления на наборе из 10 000 записей при случайном и отсортированном порядке поступления данных. + +## Реализованные структуры + +Все структуры написаны в процедурном стиле без использования классов. + +1. **Связный список** – узлы в виде словарей `{'name': str, 'phone': str, 'next': None}`. +2. **Хеш‑таблица** – массив из 10 корзин, каждая корзина – связный список. Хеш‑функция – `hash(name) % size`. +3. **Двоичное дерево поиска** – узлы `{'name': str, 'phone': str, 'left': None, 'right': None}`. Операции реализованы рекурсивно. + +## Методика эксперимента + +- **Генерация данных**: 10 000 записей с именами `User_00001` … `User_10000`. Телефоны – случайные строки вида `XXX-XXXX`. +- **Два режима подачи данных**: + – *Случайный* – записи перемешаны. + – *Отсортированный* – записи по возрастанию имени. +- **Измеряемые операции**: + – Вставка всех 10 000 записей. + – Поиск 110 имён (100 существующих + 10 несуществующих). + – Удаление 50 случайных существующих записей. +- **Повторы**: каждый эксперимент выполнен 5 раз, зафиксировано среднее время. + +Результаты замеров сохранены в `experiment_results.csv`. Время измерялось через `time.perf_counter()`. + +## Результаты измерений + +### Связный список (LinkedList) + +При 10 000 записях связный список показал ожидаемо низкую производительность. Вставка всех элементов заняла около **4.4 секунды** в среднем, поиск – около **0.027 секунды**, удаление 50 записей – около **0.012 секунды**. Порядок входных данных практически не повлиял на результаты (случайный и отсортированный режимы показали близкие значения). Это объясняется тем, что связный список всегда работает за линейное время O(n) независимо от того, как приходят данные. + +### Хеш‑таблица (HashTable) + +Хеш‑таблица с 10 корзинами показала значительное ускорение по сравнению со связным списком. Вставка 10 000 записей заняла в среднем **0.56 секунды** (почти в 8 раз быстрее списка). Поиск выполняется за **0.004 секунды** (в 7 раз быстрее), а удаление – за **0.0016 секунды** (в 7.5 раз быстрее). Порядок данных практически не влияет на производительность – разница между случайным и отсортированным режимами не превышает 10%, что соответствует теоретической сложности O(1) в среднем. + +### Двоичное дерево поиска (BST) + +Здесь наблюдается самая интересная картина: + +**На случайных данных** BST показал выдающуюся производительность. Вставка всех 10 000 записей заняла всего **0.025 секунды**, что в 22 раза быстрее хеш‑таблицы и в 176 раз быстрее связного списка. Поиск выполняется за **0.00024 секунды** (в 16 раз быстрее хеш‑таблицы), удаление – за **0.00017 секунды** (почти в 10 раз быстрее). Это идеальный случай сбалансированного дерева. + +**На отсортированных данных** ситуация кардинально меняется. Дерево вырождается в линейный список, и производительность падает катастрофически. Вставка замедлилась до **10.15 секунды** – это в 406 раз медленнее, чем на случайных данных, и даже медленнее, чем у связного списка (в 2.3 раза). Поиск вырос до **0.091 секунды** (в 380 раз медленнее), удаление – до **0.057 секунды** (в 335 раз медленнее). Это классический пример деградации BST при упорядоченных входных данных. + +## Анализ результатов + +### Как порядок входных данных влияет на скорость вставки в BST + +Эксперимент наглядно демонстрирует проблему наивной реализации двоичного дерева поиска. На случайных данных дерево остаётся достаточно сбалансированным, и операции выполняются за логарифмическое время (O(log n)). Однако на отсортированных данных каждый новый элемент становится самым большим и добавляется только в правую ветку. В результате дерево превращается в односвязный список высотой 10 000 узлов, а сложность всех операций деградирует до линейной O(n). Это подтверждается цифрами: время вставки выросло с 0.025 до 10.15 секунд – разница в 406 раз. + +### Почему хеш‑таблица почти не чувствительна к порядку + +Хеш‑функция распределяет ключи по корзинам независимо от того, в каком порядке они поступают. «User_00001» и «User_10000» с равной вероятностью могут попасть в любую из 10 корзин. Поэтому порядок ввода не влияет на длину цепочек в каждой корзине. Результаты подтверждают это: в случайном и отсортированном режимах время выполнения операций отличается незначительно (менее 10%). + +### Почему связный список всегда медленен при поиске + +Связный список не имеет индексов или другой структуры для ускорения доступа. Чтобы найти элемент, нужно в худшем случае пройти все 10 000 узлов. Поэтому поиск занимает ~0.027 секунды независимо от того, как расположены данные. Вставка тоже требует прохода до конца списка, что даёт ~4.4 секунды на 10 000 элементов. + +### Как удаление работает в каждой структуре + +Удаление тесно связано с поиском, потому что сначала нужно найти удаляемый элемент. Поэтому время удаления коррелирует со временем поиска: + +- В связном списке удаление занимает ~0.012 секунды – примерно половину времени поиска (0.027 с), так как операция перелинковки дёшева. +- В хеш‑таблице удаление (~0.0016 с) близко ко времени поиска (~0.004 с), опять же с поправкой на перелинковку в списке корзины. +- В BST на случайных данных удаление (~0.00017 с) даже быстрее поиска (~0.00024 с) из-за особенностей рекурсивной реализации. +- В BST на отсортированных данных удаление (~0.057 с) занимает примерно половину времени поиска (~0.091 с) – та же закономерность, что и у списка, потому что вырожденное дерево ведёт себя как список. + +## Выводы + +**Какую структуру и для каких задач стоит выбирать в реальной жизни?** + +1. **Хеш‑таблица** – оптимальный выбор для подавляющего большинства сценариев, где нужен быстрый доступ по ключу (словари, кэши, индексы в базах данных). Она стабильна, предсказуема и не зависит от порядка данных. В моём эксперименте она уступила BST на случайных данных, но выиграла у BST на отсортированных и оказалась намного быстрее связного списка. Главный минус – отсутствие естественного порядка при обходе. + +2. **Сбалансированное дерево** (AVL или красно-чёрное) – выбор, когда нужны оба свойства: быстрый доступ (O(log n)) и возможность получать данные в отсортированном порядке без дополнительной сортировки. Обычный BST (как в моей реализации) **использовать не стоит**, если нельзя гарантировать случайный порядок входных данных. Деградация на упорядоченных данных делает его непригодным для реальных систем. + +3. **Связный список** – практически бесполезен для хранения больших объёмов данных. Единственное оправданное применение – очень маленькие коллекции (до сотни элементов), реализация очередей/стеков или учебные цели. + +**Рекомендация**: если нужен только быстрый доступ по ключу – берите хеш-таблицу. Если нужен отсортированный вывод и вы готовы пожертвовать небольшой долей производительности – используйте сбалансированное дерево. От наивного BST и связного списка в реальных проектах лучше отказаться. + +**Ключевой вывод эксперимента**: порядок поступления данных критически важен для производительности BST (разница в 400 раз между случайными и отсортированными данными), но почти не влияет на хеш-таблицу и связный список (хотя последний всегда медленный). From b2128b00101c8703f967bf48cce28137563e8609 Mon Sep 17 00:00:00 2001 From: SavelevMI Date: Thu, 21 May 2026 14:04:04 +0000 Subject: [PATCH 11/11] [1] Initial maze core classes --- .../docs/data/2-nd-exersize/maze_core.py | 146 ++++++++++++++++++ 1 file changed, 146 insertions(+) create mode 100644 SavelevMI/docs/data/2-nd-exersize/maze_core.py diff --git a/SavelevMI/docs/data/2-nd-exersize/maze_core.py b/SavelevMI/docs/data/2-nd-exersize/maze_core.py new file mode 100644 index 0000000..a605bfb --- /dev/null +++ b/SavelevMI/docs/data/2-nd-exersize/maze_core.py @@ -0,0 +1,146 @@ +# Модель лабиринта: клетки, карта и загрузка из файла (Builder pattern) + +class Cell: + + def __init__(self, x, y): + self._x = x + self._y = y + self._is_wall = False + self._is_start = False + self._is_exit = False + + @property + def x(self): + return self._x + + @property + def y(self): + return self._y + + @property + def is_wall(self): + return self._is_wall + + @is_wall.setter + def is_wall(self, value): + self._is_wall = value + + @property + def is_start(self): + return self._is_start + + @is_start.setter + def is_start(self, value): + self._is_start = value + + @property + def is_exit(self): + return self._is_exit + + @is_exit.setter + def is_exit(self, value): + self._is_exit = value + + def is_passable(self): + return not self._is_wall + + +class Maze: + + def __init__(self, width, height): + self._width = width + self._height = height + self._cells = [[Cell(x, y) for x in range(width)] for y in range(height)] + self._start = None + self._exit = None + + @property + def width(self): + return self._width + + @property + def height(self): + return self._height + + @property + def start(self): + return self._start + + @property + def exit(self): + return self._exit + + def get_cell(self, x, y): + if 0 <= x < self._width and 0 <= y < self._height: + return self._cells[y][x] + return None + + def set_cell(self, x, y, cell_type): + cell = self.get_cell(x, y) + if cell is None: + return + + if cell_type == 'wall': + cell.is_wall = True + elif cell_type == 'start': + if self._start: + self._start.is_start = False + cell.is_start = True + cell.is_wall = False + self._start = cell + elif cell_type == 'exit': + if self._exit: + self._exit.is_exit = False + cell.is_exit = True + cell.is_wall = False + self._exit = cell + elif cell_type == 'path': + cell.is_wall = False + + def get_neighbors(self, cell): + neighbors = [] + directions = [(0, -1), (0, 1), (-1, 0), (1, 0)] # up, down, left, right + for dx, dy in directions: + nx, ny = cell.x + dx, cell.y + dy + neighbor = self.get_cell(nx, ny) + if neighbor and neighbor.is_passable(): + neighbors.append(neighbor) + return neighbors + + +class MazeBuilder: + + def build_from_file(self, filename): + raise NotImplementedError("Must be implemented in subclass") + + +class TextFileMazeBuilder(MazeBuilder): + + def build_from_file(self, filename): + with open(filename, 'r') as f: + lines = [line.rstrip('\n') for line in f.readlines()] + + height = len(lines) + width = max(len(line) for line in lines) if height > 0 else 0 + + start_count = 0 + exit_count = 0 + maze = Maze(width, height) + + for y, line in enumerate(lines): + for x, ch in enumerate(line): + if ch == "#": + maze.set_cell(x, y, "wall") + elif ch == "S": + maze.set_cell(x, y, "start") + start_count += 1 + elif ch == "E": + maze.set_cell(x, y, "exit") + exit_count += 1 + elif ch == " ": + maze.set_cell(x, y, "path") + + if start_count != 1 or exit_count != 1: + raise ValueError(f"Лабиринт должен иметь ровно один вход S и один выход E. Найдено: S={start_count}, E={exit_count}") + + return maze \ No newline at end of file