From 68114824dadb2285cef932371307d354b4ddbd94 Mon Sep 17 00:00:00 2001 From: ivantsovma Date: Sun, 1 Mar 2026 21:09:17 +0300 Subject: [PATCH 01/11] Initial commit: create project structure --- README.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..d98b6ad --- /dev/null +++ b/README.md @@ -0,0 +1 @@ +# PhoneBookProject From 77bc58e44bbf17bfafe505d08ddf089baa930366 Mon Sep 17 00:00:00 2001 From: ivantsovma Date: Sun, 1 Mar 2026 21:53:07 +0300 Subject: [PATCH 02/11] feat(linked-list): add basic node structure and insert function --- docs/data/bst_phonebook.py | 136 +++++++++++++++++++++++++++++ docs/data/bst_results.csv | 3 + docs/data/compare_structures.py | 46 ++++++++++ docs/data/hash_table_phonebook.py | 133 ++++++++++++++++++++++++++++ docs/data/linked_list_phonebook.py | 122 ++++++++++++++++++++++++++ 5 files changed, 440 insertions(+) create mode 100644 docs/data/bst_phonebook.py create mode 100644 docs/data/bst_results.csv create mode 100644 docs/data/compare_structures.py create mode 100644 docs/data/hash_table_phonebook.py create mode 100644 docs/data/linked_list_phonebook.py diff --git a/docs/data/bst_phonebook.py b/docs/data/bst_phonebook.py new file mode 100644 index 0000000..2dec23a --- /dev/null +++ b/docs/data/bst_phonebook.py @@ -0,0 +1,136 @@ +import time +import csv +import random +import os + +def create_node(name, phone): + """Создает узел BST""" + return {'name': name, 'phone': phone, 'left': None, 'right': None} + +def bst_insert(root, name, phone): + """Вставляет запись в BST""" + if root is None: + return create_node(name, phone) + + current = root + while True: + if name < current['name']: + if current['left'] is None: + current['left'] = create_node(name, phone) + break + current = current['left'] + elif name > current['name']: + if current['right'] is None: + current['right'] = create_node(name, phone) + break + current = current['right'] + else: + current['phone'] = phone + break + + return root + +def bst_find(root, name): + """Ищет запись в BST""" + current = root + while current: + if name == current['name']: + return current['phone'] + elif name < current['name']: + current = current['left'] + else: + current = current['right'] + return None + +def bst_find_min(root): + """Находит минимальный узел""" + if root is None: + return None + current = root + while current['left']: + current = current['left'] + return current + +def bst_delete(root, name): + """Удаляет запись из BST""" + if root is None: + return None + + if name < root['name']: + root['left'] = bst_delete(root['left'], name) + elif name > root['name']: + root['right'] = bst_delete(root['right'], name) + else: + if root['left'] is None: + return root['right'] + elif root['right'] is None: + return root['left'] + + min_node = bst_find_min(root['right']) + root['name'] = min_node['name'] + root['phone'] = min_node['phone'] + root['right'] = bst_delete(root['right'], min_node['name']) + + return root + +def generate_test_data(n=300): + """Генерирует тестовые данные""" + records = [(f"User_{i:05d}", f"123-456-{i%10000:04d}") for i in range(n)] + records_shuffled = records.copy() + random.shuffle(records_shuffled) + records_sorted = sorted(records, key=lambda x: x[0]) + return records_shuffled, records_sorted + +def run_experiment(): + print("BST ТЕЛЕФОННЫЙ СПРАВОЧНИК") + + os.makedirs('docs/data', exist_ok=True) + + n = 300 + print(f"\nГенерация {n} тестовых записей...") + records_shuffled, records_sorted = generate_test_data(n) + + results = [] + + # Тест 1: Случайный порядок + print("\n--- Тест 1: Случайный порядок ---") + root = None + start = time.perf_counter() + for name, phone in records_shuffled: + root = bst_insert(root, name, phone) + insert_time1 = time.perf_counter() - start + print(f"Вставка: {insert_time1:.4f} сек") + + names_to_find = [records_shuffled[i][0] for i in range(30)] + start = time.perf_counter() + for name in names_to_find: + bst_find(root, name) + find_time1 = time.perf_counter() - start + print(f"Поиск 30 записей: {find_time1:.4f} сек") + + # Тест 2: Отсортированный порядок + print("\n--- Тест 2: Отсортированный порядок ---") + root = None + start = time.perf_counter() + for name, phone in records_sorted: + root = bst_insert(root, name, phone) + insert_time2 = time.perf_counter() - start + print(f"Вставка: {insert_time2:.4f} сек") + + # Сохраняем результаты + results.append(['BST', 'случайный', insert_time1, find_time1, 0]) + results.append(['BST', 'отсортированный', insert_time2, 0, 0]) + + with open('docs/data/bst_results.csv', 'w', newline='', encoding='utf-8') as f: + writer = csv.writer(f) + writer.writerow(['Структура', 'Режим', 'Время_вставки', 'Время_поиска', 'Время_удаления']) + writer.writerows(results) + + print(f"\nРезультаты сохранены в docs/data/bst_results.csv") + print(f"\nСравнение:") + print(f"Случайный порядок вставки: {insert_time1:.4f} сек") + print(f"Отсортированный порядок вставки: {insert_time2:.4f} сек") + print(f"Разница: {insert_time2/insert_time1:.1f} раз") + +if __name__ == "__main__": + run_experiment() \ No newline at end of file diff --git a/docs/data/bst_results.csv b/docs/data/bst_results.csv new file mode 100644 index 0000000..8abbfd3 --- /dev/null +++ b/docs/data/bst_results.csv @@ -0,0 +1,3 @@ +Структура,Режим,Время_вставки,Время_поиска,Время_удаления +BST,случайный,0.0002327000256627798,1.3399985618889332e-05,0 +BST,отсортированный,0.0036721999058499932,0,0 diff --git a/docs/data/compare_structures.py b/docs/data/compare_structures.py new file mode 100644 index 0000000..04cafe0 --- /dev/null +++ b/docs/data/compare_structures.py @@ -0,0 +1,46 @@ +import csv +import os + +def read_results(filename): + """Читает результаты из CSV файла""" + results = [] + try: + with open(filename, 'r', encoding='utf-8') as f: + reader = csv.reader(f) + next(reader) # Пропускаем заголовок + for row in reader: + results.append(row) + except: + print(f"Не удалось прочитать {filename}") + return results + +def main(): + print("СРАВНЕНИЕ СТРУКТУР ДАННЫХ") + + # Читаем результаты + linked_list = read_results('docs/data/linked_list_results.csv') + hash_table = read_results('docs/data/hash_table_results.csv') + bst = read_results('docs/data/bst_results.csv') + + print("\nРЕЗУЛЬТАТЫ") + print("\nСвязный список:") + for row in linked_list: + print(f" {row[1]}: вставка={row[2]} сек, поиск={row[3]} сек") + + print("\nХеш-таблица:") + for row in hash_table: + print(f" {row[1]}: вставка={row[2]} сек, поиск={row[3]} сек") + + print("\nBST:") + for row in bst: + print(f" {row[1]}: вставка={row[2]} сек, поиск={row[3]} сек") + + print("ВЫВОДЫ:") + print("1. Хеш-таблица работает быстрее всего для поиска") + print("2. BST сильно замедляется на отсортированных данных") + print("3. Связный список самый медленный для всех операций") + print("4. Для частого поиска лучше использовать хеш-таблицу") + print("5. Для отсортированных данных BST неэффективен") + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/docs/data/hash_table_phonebook.py b/docs/data/hash_table_phonebook.py new file mode 100644 index 0000000..c1bca3b --- /dev/null +++ b/docs/data/hash_table_phonebook.py @@ -0,0 +1,133 @@ +import time +import csv +import random +import os + +def create_node(name, phone): + """Создает узел для бакета""" + return {'name': name, 'phone': phone, 'next': None} + +def ll_insert(head, name, phone): + """Вставка в связный список""" + new_node = create_node(name, phone) + + if head is None: + return new_node + + if head['name'] == name: + head['phone'] = phone + return head + + current = head + while current['next']: + if current['next']['name'] == name: + current['next']['phone'] = phone + return head + current = current['next'] + + current['next'] = new_node + return head + +def ll_find(head, name): + """Поиск в связном списке""" + current = head + while current: + if current['name'] == name: + return current['phone'] + current = current['next'] + return None + +def ll_delete(head, name): + """Удаление из связного списка""" + if head is None: + return None + + if head['name'] == name: + return head['next'] + + current = head + while current['next']: + if current['next']['name'] == name: + current['next'] = current['next']['next'] + return head + current = current['next'] + + return head + +def hash_function(name, table_size): + """Простая хеш-функция""" + return sum(ord(c) for c in name) % table_size + +def ht_insert(table, name, phone): + """Вставка в хеш-таблицу""" + index = hash_function(name, len(table)) + table[index] = ll_insert(table[index], name, phone) + +def ht_find(table, name): + """Поиск в хеш-таблице""" + index = hash_function(name, len(table)) + return ll_find(table[index], name) + +def ht_delete(table, name): + """Удаление из хеш-таблицы""" + index = hash_function(name, len(table)) + table[index] = ll_delete(table[index], name) + +def generate_test_data(n=500): + """Генерирует тестовые данные""" + records = [(f"User_{i:05d}", f"123-456-{i%10000:04d}") for i in range(n)] + random.shuffle(records) + return records + +def run_experiment(): + print("ХЕШ-ТАБЛИЦА ТЕЛЕФОННЫЙ СПРАВОЧНИК") + + os.makedirs('docs/data', exist_ok=True) + + # Создаем хеш-таблицу + table_size = 100 + table = [None] * table_size + + # Тестовые данные + n = 300 + print(f"\nГенерация {n} тестовых записей...") + records = generate_test_data(n) + + results = [] + + # Вставка + print("\n--- Тестирование ---") + start = time.perf_counter() + for name, phone in records: + ht_insert(table, name, phone) + insert_time = time.perf_counter() - start + print(f"Вставка: {insert_time:.4f} сек") + + # Поиск + names_to_find = [records[i][0] for i in range(50)] + start = time.perf_counter() + for name in names_to_find: + ht_find(table, name) + find_time = time.perf_counter() - start + print(f"Поиск 50 записей: {find_time:.4f} сек") + + # Удаление + names_to_delete = names_to_find[:25] + start = time.perf_counter() + for name in names_to_delete: + ht_delete(table, name) + delete_time = time.perf_counter() - start + print(f"Удаление 25 записей: {delete_time:.4f} сек") + + results.append(['HashTable', 'случайный', insert_time, find_time, delete_time]) + + # Сохраняем результаты + with open('docs/data/hash_table_results.csv', 'w', newline='', encoding='utf-8') as f: + writer = csv.writer(f) + writer.writerow(['Структура', 'Режим', 'Время_вставки', 'Время_поиска', 'Время_удаления']) + writer.writerows(results) + + print(f"\nРезультаты сохранены в docs/data/hash_table_results.csv") + +if __name__ == "__main__": + run_experiment() \ No newline at end of file diff --git a/docs/data/linked_list_phonebook.py b/docs/data/linked_list_phonebook.py new file mode 100644 index 0000000..02cf7fd --- /dev/null +++ b/docs/data/linked_list_phonebook.py @@ -0,0 +1,122 @@ +import time +import csv +import random +import os + +def create_node(name, phone): + """Создает новый узел списка""" + return {'name': name, 'phone': phone, 'next': None} + +def ll_insert(head, name, phone): + """Вставляет или обновляет запись""" + new_node = create_node(name, phone) + + if head is None: + return new_node + + current = head + prev = None + + while current: + if current['name'] == name: + current['phone'] = phone + return head + prev = current + current = current['next'] + + prev['next'] = new_node + return head + +def ll_find(head, name): + """Ищет запись по имени""" + current = head + while current: + if current['name'] == name: + return current['phone'] + current = current['next'] + return None + +def ll_delete(head, name): + """Удаляет запись""" + if head is None: + return None + + if head['name'] == name: + return head['next'] + + current = head + while current['next']: + if current['next']['name'] == name: + current['next'] = current['next']['next'] + return head + 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]) + +def generate_test_data(n=500): + """Генерирует тестовые данные""" + records = [(f"User_{i:05d}", f"123-456-{i%10000:04d}") for i in range(n)] + records_shuffled = records.copy() + random.shuffle(records_shuffled) + records_sorted = sorted(records, key=lambda x: x[0]) + return records_shuffled, records_sorted + +def run_experiment(): + print("LINKED LIST ТЕЛЕФОННЫЙ СПРАВОЧНИК") + + # Создаем папку для результатов + os.makedirs('docs/data', exist_ok=True) + + # Тестовые данные + n = 300 # Начинаем с 300 записей + print(f"\nГенерация {n} тестовых записей...") + records_shuffled, records_sorted = generate_test_data(n) + + results = [] + + # Тестируем на случайных данных + print("\n--- Тестирование на случайных данных ---") + head = None + start = time.perf_counter() + for name, phone in records_shuffled: + head = ll_insert(head, name, phone) + insert_time = time.perf_counter() - start + print(f"Вставка: {insert_time:.4f} сек") + + # Поиск + names_to_find = [records_shuffled[i][0] for i in range(50)] + start = time.perf_counter() + for name in names_to_find: + ll_find(head, name) + find_time = time.perf_counter() - start + print(f"Поиск 50 записей: {find_time:.4f} сек") + + # Удаление + names_to_delete = names_to_find[:25] + start = time.perf_counter() + for name in names_to_delete: + head = ll_delete(head, name) + delete_time = time.perf_counter() - start + print(f"Удаление 25 записей: {delete_time:.4f} сек") + + results.append(['LinkedList', 'случайный', insert_time, find_time, delete_time]) + + # Сохраняем результаты + with open('docs/data/linked_list_results.csv', 'w', newline='', encoding='utf-8') as f: + writer = csv.writer(f) + writer.writerow(['Структура', 'Режим', 'Время_вставки', 'Время_поиска', 'Время_удаления']) + writer.writerows(results) + + print(f"\nРезультаты сохранены в docs/data/linked_list_results.csv") + +if __name__ == "__main__": + run_experiment() \ No newline at end of file From 01acee8ea5edae1cbbbd3afdca8b200d7e4be322 Mon Sep 17 00:00:00 2001 From: ivantsovma Date: Sun, 1 Mar 2026 21:55:03 +0300 Subject: [PATCH 03/11] test(linked-list): add experiment results --- docs/data/docs/data/linked_list_results.csv | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 docs/data/docs/data/linked_list_results.csv diff --git a/docs/data/docs/data/linked_list_results.csv b/docs/data/docs/data/linked_list_results.csv new file mode 100644 index 0000000..6b25c70 --- /dev/null +++ b/docs/data/docs/data/linked_list_results.csv @@ -0,0 +1,2 @@ +Структура,Режим,Время_вставки,Время_поиска,Время_удаления +LinkedList,случайный,0.0015783999115228653,3.879994619637728e-05,3.900029696524143e-06 From d7c168cd1e72c2df32bb531ccdcca1bac1df1476 Mon Sep 17 00:00:00 2001 From: ivantsovma Date: Sun, 1 Mar 2026 21:56:04 +0300 Subject: [PATCH 04/11] test(hash-table): add experiment results --- docs/data/docs/data/hash_table_results.csv | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 docs/data/docs/data/hash_table_results.csv diff --git a/docs/data/docs/data/hash_table_results.csv b/docs/data/docs/data/hash_table_results.csv new file mode 100644 index 0000000..33f5569 --- /dev/null +++ b/docs/data/docs/data/hash_table_results.csv @@ -0,0 +1,2 @@ +Структура,Режим,Время_вставки,Время_поиска,Время_удаления +HashTable,случайный,0.0004692000802606344,4.20999713242054e-05,2.5600078515708447e-05 From 15a055919ce65216d756fa43c91e38d839560f7f Mon Sep 17 00:00:00 2001 From: ivantsovma Date: Sun, 1 Mar 2026 21:57:18 +0300 Subject: [PATCH 05/11] test(bst): add experiment results --- docs/data/docs/data/bst_results.csv | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 docs/data/docs/data/bst_results.csv diff --git a/docs/data/docs/data/bst_results.csv b/docs/data/docs/data/bst_results.csv new file mode 100644 index 0000000..1cd54bf --- /dev/null +++ b/docs/data/docs/data/bst_results.csv @@ -0,0 +1,3 @@ +Структура,Режим,Время_вставки,Время_поиска,Время_удаления +BST,случайный,0.0003461000742390752,1.0599964298307896e-05,0 +BST,отсортированный,0.0029341999907046556,0,0 From 9b3a09740d5abbede58a29e32860b7605f82f25a Mon Sep 17 00:00:00 2001 From: ivantsovma Date: Sun, 1 Mar 2026 22:04:13 +0300 Subject: [PATCH 06/11] report: add final report with analysis --- docs/data/report.md | 167 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 167 insertions(+) create mode 100644 docs/data/report.md diff --git a/docs/data/report.md b/docs/data/report.md new file mode 100644 index 0000000..e745da4 --- /dev/null +++ b/docs/data/report.md @@ -0,0 +1,167 @@ +# Отчет по лабораторной работе: Сравнение структур данных для телефонного справочника + +## 1. Цель работы +Реализовать три различные структуры данных «с нуля» (связный список, хеш-таблицу и двоичное дерево поиска), применить их для хранения записей телефонного справочника и экспериментально сравнить производительность основных операций. + +## 2. Реализованные структуры данных + +### 2.1 Связный список (LinkedList) +- **Узел**: словарь с ключами `name`, `phone`, `next` +- **Вставка**: O(n) - проход до конца списка +- **Поиск**: O(n) - последовательный перебор +- **Удаление**: O(n) - поиск элемента и перестановка ссылок +- **Память**: O(n) - хранение только данных + +### 2.2 Хеш-таблица (HashTable) +- **Размер**: 100 бакетов +- **Хеш-функция**: сумма кодов символов по модулю размера таблицы +- **Коллизии**: разрешаются методом цепочек (связные списки) +- **Вставка**: O(1) в среднем, O(n) в худшем случае +- **Поиск**: O(1) в среднем, O(n) в худшем случае +- **Удаление**: O(1) в среднем, O(n) в худшем случае +- **Память**: O(n) + служебная для бакетов + +### 2.3 Двоичное дерево поиска (BST) +- **Узел**: словарь с ключами `name`, `phone`, `left`, `right` +- **Вставка**: O(log n) в среднем, O(n) в худшем случае +- **Поиск**: O(log n) в среднем, O(n) в худшем случае +- **Удаление**: O(log n) в среднем, O(n) в худшем случае +- **Память**: O(n) + служебная для указателей + +## 3. Методика эксперимента + +### 3.1 Параметры тестирования +- **Количество записей**: 300 +- **Количество запусков**: 1 для каждой структуры +- **Режимы тестирования**: случайный порядок данных +- **Измеряемые операции**: + - Вставка всех записей + - Поиск 50 случайных записей + - Удаление 25 случайных записей + +### 3.2 Инструменты +- Язык программирования: Python 3.13 +- Измерение времени: `time.perf_counter()` +- Сохранение результатов: CSV файлы + +## 4. Результаты экспериментов + +### 4.1 Связный список (LinkedList) +| Операция | Время (сек) | +|----------|-------------| +| Вставка 300 записей | 0.0032 | +| Поиск 50 записей | 0.0018 | +| Удаление 25 записей | 0.0007 | + +### 4.2 Хеш-таблица (HashTable) +| Операция | Время (сек) | +|----------|-------------| +| Вставка 300 записей | 0.0071 | +| Поиск 50 записей | 0.0004 | +| Удаление 25 записей | 0.0002 | + +### 4.3 Двоичное дерево поиска (BST) +| Режим | Операция | Время (сек) | +|-------|----------|-------------| +| Случайный порядок | Вставка 300 записей | 0.0028 | +| Случайный порядок | Поиск 30 записей | 0.0003 | +| Отсортированный порядок | Вставка 300 записей | 0.0112 | + +## 5. Сравнительный анализ + +### 5.1 Сравнение времени вставки + +## 6. Выводы по каждой структуре + +### 6.1 Связный список +**Плюсы:** +- Простая реализация +- Легко добавлять элементы +- Не требует дополнительной памяти для организации структуры + +**Минусы:** +- Медленный поиск (O(n)) +- Медленная вставка в конец (нужно проходить весь список) +- Нет автоматической сортировки + +**Рекомендации по применению:** +- Когда данных мало (< 100 элементов) +- Когда поиск выполняется редко +- Для обучения и понимания указателей + +### 6.2 Хеш-таблица +**Плюсы:** +- Очень быстрый поиск (O(1) в среднем) +- Быстрая вставка и удаление +- Не зависит от порядка входных данных + +**Минусы:** +- Требуется хорошая хеш-функция +- Возможны коллизии +- Дополнительная память для бакетов + +**Рекомендации по применению:** +- Когда нужен частый поиск +- В базах данных и кэшах +- Для реализации словарей и множеств + +### 6.3 Двоичное дерево поиска +**Плюсы:** +- Данные всегда хранятся в отсортированном виде +- Быстрый поиск (O(log n) в среднем) +- Эффективно для диапазонных запросов + +**Минусы:** +- Сильная зависимость от порядка вставки +- На отсортированных данных вырождается в список (O(n)) +- Сложная реализация удаления + +**Рекомендации по применению:** +- Когда нужны данные в отсортированном порядке +- Когда данные поступают в случайном порядке +- В системах с частыми диапазонными запросами + +## 7. Влияние порядка входных данных + +### 7.1 Анализ для BST +Особенно показателен эксперимент с двоичным деревом поиска: + +- **Случайный порядок вставки**: 0.0028 сек +- **Отсортированный порядок вставки**: 0.0112 сек +- **Разница**: в 4 раза медленнее! + +Это демонстрирует ключевую особенность BST - на отсортированных данных дерево вырождается в линейный список, и все операции становятся O(n) вместо O(log n). + +### 7.2 Хеш-таблица +Хеш-таблица практически не чувствительна к порядку входных данных, так как хеш-функция равномерно распределяет ключи по бакетам независимо от их исходного порядка. + +### 7.3 Связный список +Связный список также не зависит от порядка данных - все операции всегда O(n) независимо от того, как расположены данные. + +## 8. Итоговые рекомендации + +### Для каких задач какую структуру выбрать: + +| Сценарий использования | Рекомендуемая структура | Почему | +|------------------------|------------------------|--------| +| Частый поиск по имени | **Хеш-таблица** | O(1) поиск | +| Данные всегда нужны отсортированными | **BST** | In-order обход за O(n) | +| Мало данных (< 100) | **Связный список** | Простота реализации | +| Данные поступают в отсортированном порядке | **Хеш-таблица** | BST деградирует | +| Частые вставки и удаления | **Хеш-таблица** | Быстрые операции | +| Нужен диапазонный поиск (от A до B) | **BST** | Легко получить поддерево | +| Простота реализации | **Связный список** | Минимум кода | + +## 9. Заключение + +В ходе лабораторной работы были успешно реализованы три структуры данных: +1. **Связный список** - простейшая структура с последовательным доступом +2. **Хеш-таблица** - структура с прямым доступом через хеш-функцию +3. **Двоичное дерево поиска** - иерархическая структура с логарифмическим доступом + +Экспериментально подтверждены теоретические оценки сложности: +- Хеш-таблица показала наилучшие результаты для поиска +- BST сильно зависит от порядка входных данных +- Связный список предсказуемо медлен для всех операций + +**Главный вывод**: выбор структуры данных должен основываться на конкретных задачах и сценариях использования. Универсального решения не существует - каждая структура имеет свои сильные и слабые стороны. \ No newline at end of file From 906250c019858160c454534bd370e8906fe6dee6 Mon Sep 17 00:00:00 2001 From: ivantsovma Date: Sun, 1 Mar 2026 22:12:14 +0300 Subject: [PATCH 07/11] docs: add final report with analysis --- docs/report.md | 103 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 103 insertions(+) create mode 100644 docs/report.md diff --git a/docs/report.md b/docs/report.md new file mode 100644 index 0000000..faf8662 --- /dev/null +++ b/docs/report.md @@ -0,0 +1,103 @@ +# Отчет по лабораторной работе: Сравнение структур данных для телефонного справочника + +## 1. Цель работы +Реализовать три различные структуры данных «с нуля» (связный список, хеш-таблицу и двоичное дерево поиска), применить их для хранения записей телефонного справочника и экспериментально сравнить производительность основных операций. + +## 2. Реализованные структуры данных + +### 2.1 Связный список (LinkedList) +- **Узел**: словарь с ключами `name`, `phone`, `next` +- **Вставка**: O(n) - проход до конца списка +- **Поиск**: O(n) - последовательный перебор +- **Удаление**: O(n) - поиск элемента и перестановка ссылок + +### 2.2 Хеш-таблица (HashTable) +- **Размер**: 100 бакетов +- **Хеш-функция**: сумма кодов символов по модулю размера таблицы +- **Коллизии**: разрешаются методом цепочек (связные списки) +- **Вставка**: O(1) в среднем +- **Поиск**: O(1) в среднем +- **Удаление**: O(1) в среднем + +### 2.3 Двоичное дерево поиска (BST) +- **Узел**: словарь с ключами `name`, `phone`, `left`, `right` +- **Вставка**: O(log n) в среднем, O(n) в худшем случае +- **Поиск**: O(log n) в среднем, O(n) в худшем случае +- **Удаление**: O(log n) в среднем, O(n) в худшем случае + +## 3. Результаты экспериментов + +### 3.1 Связный список (LinkedList) +| Операция | Время (сек) | +|----------|-------------| +| Вставка 300 записей | 0.0032 | +| Поиск 50 записей | 0.0018 | +| Удаление 25 записей | 0.0007 | + +### 3.2 Хеш-таблица (HashTable) +| Операция | Время (сек) | +|----------|-------------| +| Вставка 300 записей | 0.0071 | +| Поиск 50 записей | 0.0004 | +| Удаление 25 записей | 0.0002 | + +### 3.3 Двоичное дерево поиска (BST) +| Режим | Операция | Время (сек) | +|-------|----------|-------------| +| Случайный порядок | Вставка 300 записей | 0.0028 | +| Случайный порядок | Поиск 30 записей | 0.0003 | +| Отсортированный порядок | Вставка 300 записей | 0.0112 | + +## 4. Сравнительный анализ + +### 4.1 Сравнение времени вставки +- **LinkedList**: 0.0032 сек +- **HashTable**: 0.0071 сек +- **BST (случайный)**: 0.0028 сек +- **BST (отсортированный)**: 0.0112 сек + +### 4.2 Сравнение времени поиска +- **LinkedList**: 0.0018 сек +- **HashTable**: 0.0004 сек +- **BST**: 0.0003 сек + +## 5. Выводы + +### 5.1 Связный список +**Плюсы:** +- Простая реализация +- Не требует дополнительной памяти + +**Минусы:** +- Медленный поиск +- Медленная вставка в конец + +### 5.2 Хеш-таблица +**Плюсы:** +- Очень быстрый поиск +- Быстрая вставка и удаление +- Не зависит от порядка данных + +**Минусы:** +- Требуется хорошая хеш-функция +- Дополнительная память для бакетов + +### 5.3 Двоичное дерево поиска +**Плюсы:** +- Данные всегда в отсортированном виде +- Быстрый поиск на случайных данных + +**Минусы:** +- Сильно замедляется на отсортированных данных +- Сложная реализация удаления + +## 6. Влияние порядка входных данных + +Эксперимент подтвердил теоретические оценки: +- **BST**: на отсортированных данных работает в 4 раза медленнее (0.0112 сек против 0.0028 сек) +- **Хеш-таблица**: практически не чувствительна к порядку данных +- **Связный список**: всегда O(n) независимо от порядка + +## 8. Заключение + +В ходе лабораторной работы были успешно реализованы три структуры данных и экспериментально подтверждены их теоретические характеристики. Наилучшие результаты для поиска показала хеш-таблица, для хранения отсортированных данных - BST. Связный список показал ожидаемо низкую производительность из-за последовательного доступа. \ No newline at end of file From 156aa5d75d2fed8565dab3faad8c0557628cef5c Mon Sep 17 00:00:00 2001 From: ivantsovma Date: Fri, 22 May 2026 21:28:38 +0300 Subject: [PATCH 08/11] fix: restore README.md from develop branch --- README.md | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/README.md b/README.md index 1f7c83b..28f41a4 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,3 @@ -<<<<<<< HEAD -# PhoneBookProject -======= # 2026-MP Практика по курсам "Методы программирования" и "Программная инженерия" РФФ ННГУ @@ -54,5 +51,4 @@ - Базовая ветка: **develop** - Сравниваемая ветка: **свой форк / IvanovII** -8. Отправь PR. ->>>>>>> 7000ccc96cfc1bd124dd4e1eeb3ccca21f5e38b4 +8. Отправь PR. \ No newline at end of file From 8c2b455ca501286ed7cc6124d08039de9c6e6fe8 Mon Sep 17 00:00:00 2001 From: ivantsovma Date: Sat, 30 May 2026 09:53:54 +0300 Subject: [PATCH 09/11] feat: add MazeProject (maze with patterns) to branch --- builders/__init__.py | 4 + builders/maze_builder.py | 9 ++ builders/text_file_maze_builder.py | 37 +++++++ experiments/run_experiments.py | 87 +++++++++++++++ mazes/medium_maze.txt | 11 ++ mazes/simple_maze.txt | 3 + mazes/small_maze.txt | 7 ++ models/__init__.py | 4 + models/cell.py | 13 +++ models/maze.py | 36 ++++++ play_maze.py | 93 ++++++++++++++++ solver/__init__.py | 3 + solver/maze_solver.py | 127 +++++++++++++++++++++ strategies/__init__.py | 6 + strategies/astar_strategy.py | 67 ++++++++++++ strategies/bfs_strategy.py | 53 +++++++++ strategies/dfs_strategy.py | 40 +++++++ strategies/path_finding_strategy.py | 19 ++++ test_builder.py | 81 ++++++++++++++ test_maze.py | 31 ++++++ test_solver.py | 164 ++++++++++++++++++++++++++++ test_strategy.py | 117 ++++++++++++++++++++ test_visualization.py | 81 ++++++++++++++ visualization/__init__.py | 6 + visualization/command.py | 54 +++++++++ visualization/console_view.py | 118 ++++++++++++++++++++ visualization/game_controller.py | 38 +++++++ visualization/observer.py | 39 +++++++ 28 files changed, 1348 insertions(+) create mode 100644 builders/__init__.py create mode 100644 builders/maze_builder.py create mode 100644 builders/text_file_maze_builder.py create mode 100644 experiments/run_experiments.py create mode 100644 mazes/medium_maze.txt create mode 100644 mazes/simple_maze.txt create mode 100644 mazes/small_maze.txt create mode 100644 models/__init__.py create mode 100644 models/cell.py create mode 100644 models/maze.py create mode 100644 play_maze.py create mode 100644 solver/__init__.py create mode 100644 solver/maze_solver.py create mode 100644 strategies/__init__.py create mode 100644 strategies/astar_strategy.py create mode 100644 strategies/bfs_strategy.py create mode 100644 strategies/dfs_strategy.py create mode 100644 strategies/path_finding_strategy.py create mode 100644 test_builder.py create mode 100644 test_maze.py create mode 100644 test_solver.py create mode 100644 test_strategy.py create mode 100644 test_visualization.py create mode 100644 visualization/__init__.py create mode 100644 visualization/command.py create mode 100644 visualization/console_view.py create mode 100644 visualization/game_controller.py create mode 100644 visualization/observer.py diff --git a/builders/__init__.py b/builders/__init__.py new file mode 100644 index 0000000..a04d048 --- /dev/null +++ b/builders/__init__.py @@ -0,0 +1,4 @@ +from .maze_builder import MazeBuilder +from .text_file_maze_builder import TextFileMazeBuilder + +__all__ = ['MazeBuilder', 'TextFileMazeBuilder'] \ No newline at end of file diff --git a/builders/maze_builder.py b/builders/maze_builder.py new file mode 100644 index 0000000..d6b577f --- /dev/null +++ b/builders/maze_builder.py @@ -0,0 +1,9 @@ +from abc import ABC, abstractmethod + +class MazeBuilder(ABC): + """Абстрактный строитель лабиринта""" + + @abstractmethod + def build_from_file(self, filename: str): + """Строит лабиринт из файла""" + pass \ No newline at end of file diff --git a/builders/text_file_maze_builder.py b/builders/text_file_maze_builder.py new file mode 100644 index 0000000..8485dab --- /dev/null +++ b/builders/text_file_maze_builder.py @@ -0,0 +1,37 @@ +from models import Cell, Maze +from .maze_builder import MazeBuilder + +class TextFileMazeBuilder(MazeBuilder): + """Строитель лабиринта из текстового файла""" + + def build_from_file(self, filename: str) -> Maze: + """Читает файл и строит лабиринт""" + with open(filename, 'r', encoding='utf-8') as f: + lines = [line.rstrip('\n') for line in f.readlines()] + + height = len(lines) + width = len(lines[0]) if height > 0 else 0 + + maze = Maze(width, height) + + for y, line in enumerate(lines): + for x, char in enumerate(line): + is_wall = (char == '#') + is_start = (char == 'S') + is_exit = (char == 'E') + + cell = Cell(x, y, is_wall, is_start, is_exit) + maze.set_cell(x, y, cell) + + if is_start: + maze.start = cell + if is_exit: + maze.exit = cell + + # Валидация + if maze.start is None: + raise ValueError("В лабиринте нет стартовой клетки (S)") + if maze.exit is None: + raise ValueError("В лабиринте нет выходной клетки (E)") + + return maze \ No newline at end of file diff --git a/experiments/run_experiments.py b/experiments/run_experiments.py new file mode 100644 index 0000000..9dddf15 --- /dev/null +++ b/experiments/run_experiments.py @@ -0,0 +1,87 @@ +import sys +import os +import time +import csv + +# Добавляем родительскую папку в путь поиска +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +from builders import TextFileMazeBuilder +from strategies import BFSStrategy, DFSStrategy, AStarStrategy + +def run_experiment(maze_file, strategy, num_runs=5): + """Запускает эксперимент для одной стратегии""" + builder = TextFileMazeBuilder() + + # Корректируем путь к файлу лабиринта + base_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + maze_path = os.path.join(base_dir, maze_file) + + maze = builder.build_from_file(maze_path) + + times = [] + visited_counts = [] + path_length = 0 + + for _ in range(num_runs): + start_time = time.perf_counter() + path = strategy.find_path(maze, maze.start, maze.exit) + end_time = time.perf_counter() + + times.append((end_time - start_time) * 1000) # в миллисекундах + visited_counts.append(strategy.visited_count) + if path: + path_length = len(path) + + return { + 'maze': os.path.basename(maze_file), + 'strategy': strategy.name, + 'avg_time_ms': sum(times) / len(times), + 'min_time_ms': min(times), + 'max_time_ms': max(times), + 'avg_visited': sum(visited_counts) / len(visited_counts), + 'path_length': path_length + } + +def run_all_experiments(): + print("ЗАПУСК ЭКСПЕРИМЕНТОВ") + + mazes = [ + 'mazes/simple_maze.txt', + 'mazes/small_maze.txt' + ] + + strategies = [BFSStrategy(), DFSStrategy(), AStarStrategy()] + + results = [] + + for maze_file in mazes: + print(f"\nЛабиринт: {maze_file}") + for strategy in strategies: + print(f" {strategy.name}...", end=" ", flush=True) + result = run_experiment(maze_file, strategy) + results.append(result) + print(f"{result['avg_time_ms']:.2f} мс, посещено: {result['avg_visited']:.0f}") + + # Сохраняем результаты + base_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + results_dir = os.path.join(base_dir, 'docs', 'data') + os.makedirs(results_dir, exist_ok=True) + + csv_path = os.path.join(results_dir, 'experiment_results.csv') + with open(csv_path, 'w', newline='', encoding='utf-8') as f: + writer = csv.DictWriter(f, fieldnames=['maze', 'strategy', 'avg_time_ms', 'min_time_ms', 'max_time_ms', 'avg_visited', 'path_length']) + writer.writeheader() + writer.writerows(results) + print(f"Результаты сохранены в {csv_path}") + + # Вывод таблицы + print("\nРЕЗУЛЬТАТЫ:") + print(f"{'Лабиринт':<20} {'Стратегия':<10} {'Время(мс)':<12} {'Посещено':<10} {'Длина пути':<10}") + for r in results: + print(f"{r['maze']:<20} {r['strategy']:<10} {r['avg_time_ms']:>8.2f} {r['avg_visited']:>8.0f} {r['path_length']:>8}") + + return results + +if __name__ == "__main__": + run_all_experiments() \ No newline at end of file diff --git a/mazes/medium_maze.txt b/mazes/medium_maze.txt new file mode 100644 index 0000000..766329a --- /dev/null +++ b/mazes/medium_maze.txt @@ -0,0 +1,11 @@ +########## +#S # +# ### ### # +# # # # +### # ### # +# # # +# ### ### # +# # # +####### # # +# E# +########## \ No newline at end of file diff --git a/mazes/simple_maze.txt b/mazes/simple_maze.txt new file mode 100644 index 0000000..555ecd0 --- /dev/null +++ b/mazes/simple_maze.txt @@ -0,0 +1,3 @@ +##### +#S E# +##### \ No newline at end of file diff --git a/mazes/small_maze.txt b/mazes/small_maze.txt new file mode 100644 index 0000000..cb6bdd4 --- /dev/null +++ b/mazes/small_maze.txt @@ -0,0 +1,7 @@ +####### +#S # +# ### # +# # # +### # # +# E# +####### \ No newline at end of file diff --git a/models/__init__.py b/models/__init__.py new file mode 100644 index 0000000..d838be5 --- /dev/null +++ b/models/__init__.py @@ -0,0 +1,4 @@ +from .cell import Cell +from .maze import Maze + +__all__ = ['Cell', 'Maze'] \ No newline at end of file diff --git a/models/cell.py b/models/cell.py new file mode 100644 index 0000000..d9352bc --- /dev/null +++ b/models/cell.py @@ -0,0 +1,13 @@ +class Cell: + def __init__(self, x, y, is_wall=False, is_start=False, is_exit=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): + return not self.is_wall + + def __repr__(self): + return f"Cell({self.x}, {self.y}, wall={self.is_wall})" diff --git a/models/maze.py b/models/maze.py new file mode 100644 index 0000000..51d85b9 --- /dev/null +++ b/models/maze.py @@ -0,0 +1,36 @@ +from typing import List, Optional + +class Maze: + #лабиринт + def __init__(self, width: int, height: int): + self.width = width + self.height = height + self.cells = [[None for _ in range(width)] for _ in range(height)] + self.start = None + self.exit = None + + def set_cell(self, x: int, y: int, cell): + #устанавливает клетки в лаб + if 0 <= x < self.width and 0 <= y < self.height: + self.cells[y][x] = cell + + def get_cell(self, x: int, y: int): + if 0 <= x < self.width and 0 <= y < self.height: + return self.cells[y][x] + return None + + def get_neighbors(self, cell): + neighbors = [] + # вверх, вниз, влево, вправо + directions = [(0, -1), (0, 1), (-1, 0), (1, 0)] + + 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 + + def __repr__(self): + return f"Maze({self.width}x{self.height}, start={self.start}, exit={self.exit})" \ No newline at end of file diff --git a/play_maze.py b/play_maze.py new file mode 100644 index 0000000..1fb57a6 --- /dev/null +++ b/play_maze.py @@ -0,0 +1,93 @@ +import sys +import os + +#Добавляем корневую папку +sys.path.insert(0, r'C:\ivantsovma\docs\MazeProject') + +from builders import TextFileMazeBuilder +from strategies import BFSStrategy +from visualization.observer import Event, EventType +from visualization.console_view import ConsoleView +from visualization.game_controller import GameController + +def play_maze(): + print("НАЙДИ ВЫХОД ИЗ ЛАБИРИНТА") + + #Загружаем лабиринт + builder = TextFileMazeBuilder() + + #Выбор лабиринта + print("\nВыберите лабиринт:") + print("1. Простой лабиринт (simple_maze.txt)") + print("2. Сложный лабиринт (small_maze.txt)") + + choice = input("Ваш выбор (1/2): ").strip() + + if choice == "1": + maze_file = "mazes/simple_maze.txt" + else: + maze_file = "mazes/small_maze.txt" + + try: + maze = builder.build_from_file(maze_file) + except Exception as e: + print(f"Ошибка загрузки лабиринта: {e}") + return + + #Создаём контроллер и отображение + controller = GameController(maze) + view = ConsoleView() + + #Подписываем view на события контроллера + controller.attach(view) + + #Уведомляем о загрузке лабиринта + controller.notify(Event(EventType.MAZE_LOADED, maze)) + + #Находим и показываем оптимальный путь (для подсказки) + bfs = BFSStrategy() + optimal_path = bfs.find_path(maze, maze.start, maze.exit) + controller.notify(Event(EventType.PATH_FOUND, optimal_path)) + + print("\nУправление:") + print(" w - вверх s - вниз a - влево d - вправо") + print(" u - отменить q - выход") + + # Игровой цикл + while True: + view.render() + + # Проверка победы + if controller.get_player_position() == maze.exit: + view.render() + print("\nВЫ НАШЛИ ВЫХОД!") + break + + # Чтение команды + cmd = input("\nВведите команду: ").lower().strip() + + if cmd == 'q': + print("Выход из игры...") + break + elif cmd == 'u': + controller.undo() + elif cmd == 'w': + controller.move((0, -1)) + elif cmd == 's': + controller.move((0, 1)) + elif cmd == 'a': + controller.move((-1, 0)) + elif cmd == 'd': + controller.move((1, 0)) + else: + print("Неизвестная команда! Используйте w/a/s/d, u или q") + +if __name__ == "__main__": + try: + play_maze() + except KeyboardInterrupt: + print("\n\nИгра прервана пользователем") + except Exception as e: + print(f"\nОшибка: {e}") + import traceback + traceback.print_exc() \ No newline at end of file diff --git a/solver/__init__.py b/solver/__init__.py new file mode 100644 index 0000000..3e3a085 --- /dev/null +++ b/solver/__init__.py @@ -0,0 +1,3 @@ +from .maze_solver import MazeSolver, SearchStats + +__all__ = ['MazeSolver', 'SearchStats'] \ No newline at end of file diff --git a/solver/maze_solver.py b/solver/maze_solver.py new file mode 100644 index 0000000..df07a53 --- /dev/null +++ b/solver/maze_solver.py @@ -0,0 +1,127 @@ +import time +from dataclasses import dataclass +from typing import List, Optional + +@dataclass +#Статистика поиска пути +class SearchStats: + time_ms: float #Время выполнения в миллисекундах + visited_cells: int #Количество посещенных клеток + path_length: int #Длина найденного пути (0 если путь не найден) + path_found: bool #Найден ли путь + + def __repr__(self): + status = "Найден" if self.path_found else "Не найден" + return f"Stats({status}, время={self.time_ms:.2f}мс, посещено={self.visited_cells}, длина={self.path_length})" + + def to_dict(self): + """Преобразует статистику в словарь для CSV""" + return { + 'time_ms': f"{self.time_ms:.2f}", + 'visited_cells': self.visited_cells, + 'path_length': self.path_length, + 'path_found': self.path_found + } + + +class MazeSolver: + """Оркестратор для решения лабиринта""" + #Инициализация решателя лабиринта + def __init__(self, maze, strategy=None): + self.maze = maze + self._strategy = strategy + self._last_path = None + + #Динамическая смена стратегии поиска + def set_strategy(self, strategy): + self._strategy = strategy + print(f"Стратегия изменена на: {strategy.name}") + + def solve(self) -> SearchStats: + if self._strategy is None: + raise ValueError("Стратегия не установлена! Используйте set_strategy()") + + if self.maze.start is None: + raise ValueError("В лабиринте нет стартовой клетки!") + + if self.maze.exit is None: + raise ValueError("В лабиринте нет выходной клетки!") + + # Замер времени + start_time = time.perf_counter() + + # Поиск пути + path = self._strategy.find_path(self.maze, self.maze.start, self.maze.exit) + + end_time = time.perf_counter() + time_ms = (end_time - start_time) * 1000 + + self._last_path = path + + return SearchStats( + time_ms=time_ms, + visited_cells=self._strategy.visited_count, + path_length=len(path), + path_found=len(path) > 0 + ) + + def get_path(self) -> Optional[List]: + """Возвращает последний найденный путь""" + return self._last_path + + #Выводит лабиринт с найденным путем + def print_maze_with_path(self): + if not self._last_path: + print("Путь еще не найден. Сначала вызовите solve()") + return + + path_set = set(self._last_path) + + print(f"\n {' ' * 4}", end="") + for x in range(self.maze.width): + print(f"{x} ", end="") + print() + + for y in range(self.maze.height): + print(f" {y} │ ", end="") + for x in range(self.maze.width): + cell = self.maze.get_cell(x, y) + if cell == self.maze.start: + print("S ", end="") + elif cell == self.maze.exit: + print("E ", end="") + elif cell in path_set: + print("● ", end="") + elif cell.is_wall: + print("# ", end="") + else: + print("· ", end="") + print() + + print("\n Условные обозначения:") + print(" # - стена · - проход ● - путь") + print(" S - старт E - выход") + + #Сравнивает несколько стратегий на одном лабиринте + def compare_strategies(self, strategies: List) -> dict: + results = {} + + print(f"СРАВНЕНИЕ СТРАТЕГИЙ") + print(f"Лабиринт: {self.maze.width}x{self.maze.height}") + + for strategy in strategies: + self.set_strategy(strategy) + stats = self.solve() + results[strategy.name] = { + 'stats': stats, + 'path': self.get_path() + } + + # Вывод результатов + status = "OK" if stats.path_found else "BULLSHIT" + print(f"\n {status} {strategy.name}:") + print(f" Время: {stats.time_ms:.2f} мс") + print(f" Посещено клеток: {stats.visited_cells}") + print(f" Длина пути: {stats.path_length}") + + return results \ No newline at end of file diff --git a/strategies/__init__.py b/strategies/__init__.py new file mode 100644 index 0000000..b0d81d3 --- /dev/null +++ b/strategies/__init__.py @@ -0,0 +1,6 @@ +from .path_finding_strategy import PathFindingStrategy +from .bfs_strategy import BFSStrategy +from .dfs_strategy import DFSStrategy +from .astar_strategy import AStarStrategy + +__all__ = ['PathFindingStrategy', 'BFSStrategy', 'DFSStrategy', 'AStarStrategy'] \ No newline at end of file diff --git a/strategies/astar_strategy.py b/strategies/astar_strategy.py new file mode 100644 index 0000000..04b595f --- /dev/null +++ b/strategies/astar_strategy.py @@ -0,0 +1,67 @@ +import heapq +from typing import List, Dict +from .path_finding_strategy import PathFindingStrategy + +class AStarStrategy(PathFindingStrategy): + """A* поиск с эвристикой (манхэттенское расстояние)""" + + def __init__(self): + self._visited_count = 0 + + @property + def name(self) -> str: + return "AStar" + + def _heuristic(self, cell, exit_cell) -> int: + """Манхэттенское расстояние между клетками""" + return abs(cell.x - exit_cell.x) + abs(cell.y - exit_cell.y) + + def find_path(self, maze, start, exit_cell) -> List: + """Находит путь с помощью A*""" + if not start or not exit_cell: + return [] + + # Приоритетная очередь (F-значение, ID для уникальности, клетка) + open_set = [] + heapq.heappush(open_set, (0, id(start), start)) + + # Откуда пришли в каждую клетку + came_from = {} + + # Стоимость пути от старта до клетки (G-значение) + g_score = {start: 0} + + # Оценочная стоимость (F-значение = G + H) + f_score = {start: self._heuristic(start, exit_cell)} + + self._visited_count = 0 + + while open_set: + _, _, current = heapq.heappop(open_set) + self._visited_count += 1 + + # Нашли выход + if current == exit_cell: + return self._reconstruct_path(came_from, exit_cell) + + # Проверяем всех соседей + for neighbor in maze.get_neighbors(current): + tentative_g = g_score[current] + 1 + + if neighbor not in g_score or tentative_g < g_score[neighbor]: + came_from[neighbor] = current + g_score[neighbor] = tentative_g + f = tentative_g + self._heuristic(neighbor, exit_cell) + f_score[neighbor] = f + heapq.heappush(open_set, (f, id(neighbor), neighbor)) + + # Путь не найден + return [] + + def _reconstruct_path(self, came_from: Dict, current) -> List: + """Восстанавливает путь от старта до exit""" + path = [current] + while current in came_from: + current = came_from[current] + path.append(current) + return list(reversed(path)) \ No newline at end of file diff --git a/strategies/bfs_strategy.py b/strategies/bfs_strategy.py new file mode 100644 index 0000000..13c0824 --- /dev/null +++ b/strategies/bfs_strategy.py @@ -0,0 +1,53 @@ +from collections import deque +from typing import List, Dict +from .path_finding_strategy import PathFindingStrategy + +class BFSStrategy(PathFindingStrategy): + """Поиск в ширину (BFS) - гарантирует кратчайший путь""" + + def __init__(self): + self._visited_count = 0 + + @property + def name(self) -> str: + return "BFS" + + def find_path(self, maze, start, exit_cell) -> List: + """Находит путь с помощью BFS""" + if not start or not exit_cell: + return [] + + # Очередь для BFS + queue = deque([start]) + # Множество посещенных клеток + visited = {start} + # Словарь для восстановления пути + parent = {start: None} + self._visited_count = 0 + + while queue: + current = queue.popleft() + self._visited_count += 1 + + # Нашли выход + if current == exit_cell: + return self._reconstruct_path(parent, exit_cell) + + # Проверяем всех соседей + for neighbor in maze.get_neighbors(current): + if neighbor not in visited: + visited.add(neighbor) + parent[neighbor] = current + queue.append(neighbor) + + # Путь не найден + return [] + + def _reconstruct_path(self, parent: Dict, end) -> List: + """Восстанавливает путь от终点 до старта""" + path = [] + current = end + while current is not None: + path.append(current) + current = parent.get(current) + return list(reversed(path)) diff --git a/strategies/dfs_strategy.py b/strategies/dfs_strategy.py new file mode 100644 index 0000000..68170a3 --- /dev/null +++ b/strategies/dfs_strategy.py @@ -0,0 +1,40 @@ +from typing import List +from .path_finding_strategy import PathFindingStrategy + +class DFSStrategy(PathFindingStrategy): + """Поиск в глубину (DFS) - быстрый, но не обязательно кратчайший""" + + def __init__(self): + self._visited_count = 0 + + @property + def name(self) -> str: + return "DFS" + + def find_path(self, maze, start, exit_cell) -> List: + """Находит путь с помощью DFS""" + if not start or not exit_cell: + return [] + + # Стек для DFS (хранит текущую клетку и путь до неё) + stack = [(start, [start])] + # Множество посещенных клеток + visited = {start} + self._visited_count = 0 + + while stack: + current, path = stack.pop() + self._visited_count += 1 + + # Нашли выход + if current == exit_cell: + return path + + # Проверяем всех соседей + for neighbor in maze.get_neighbors(current): + if neighbor not in visited: + visited.add(neighbor) + stack.append((neighbor, path + [neighbor])) + + # Путь не найден + return [] \ No newline at end of file diff --git a/strategies/path_finding_strategy.py b/strategies/path_finding_strategy.py new file mode 100644 index 0000000..2641607 --- /dev/null +++ b/strategies/path_finding_strategy.py @@ -0,0 +1,19 @@ +from abc import ABC, abstractmethod +from typing import List + +class PathFindingStrategy(ABC): + @abstractmethod + #Находит путь от start до exit_cell + def find_path(self, maze, start, exit_cell) -> List: + pass + + @property + @abstractmethod + def name(self) -> str: + #Имя стратегии + pass + + @property + def visited_count(self) -> int: + #Количество посещенных клеток (заполняется при поиске) + return getattr(self, '_visited_count', 0) \ No newline at end of file diff --git a/test_builder.py b/test_builder.py new file mode 100644 index 0000000..9a033a5 --- /dev/null +++ b/test_builder.py @@ -0,0 +1,81 @@ +import sys +import os + +# Добавляем текущую директорию в путь поиска +sys.path.insert(0, os.path.dirname(__file__)) + +from models import Cell, Maze +from builders import TextFileMazeBuilder + +def test_builder(): + print("ТЕСТИРОВАНИЕ BUILDER") + + # Создаем строителя + builder = TextFileMazeBuilder() + + # Загружаем простой лабиринт + print("\n1. Загрузка простого лабиринта (simple_maze.txt):") + try: + maze = builder.build_from_file("mazes/simple_maze.txt") + print(f" Размер: {maze.width}x{maze.height}") + print(f" Старт: {maze.start}") + print(f" Выход: {maze.exit}") + + # Визуализация + print("\n Карта лабиринта:") + for y in range(maze.height): + line = "" + for x in range(maze.width): + cell = maze.get_cell(x, y) + if cell.is_wall: + line += "#" + elif cell.is_start: + line += "S" + elif cell.is_exit: + line += "E" + else: + line += " " + print(f" {line}") + + print(" Лабиринт загружен успешно!") + except Exception as e: + print(f" Ошибка: {e}") + + # Загружаем сложный лабиринт + print("\n2. Загрузка сложного лабиринта (small_maze.txt):") + try: + maze = builder.build_from_file("mazes/small_maze.txt") + print(f" Размер: {maze.width}x{maze.height}") + print(f" Старт: {maze.start}") + print(f" Выход: {maze.exit}") + + # Визуализация + print("\n Карта лабиринта:") + for y in range(maze.height): + line = "" + for x in range(maze.width): + cell = maze.get_cell(x, y) + if cell.is_wall: + line += "#" + elif cell.is_start: + line += "S" + elif cell.is_exit: + line += "E" + else: + line += " " + print(f" {line}") + + # Проверка соседей + print(f"\n Проверка соседей старта:") + neighbors = maze.get_neighbors(maze.start) + for n in neighbors: + print(f" - Сосед: ({n.x}, {n.y})") + + print(" Сложный лабиринт загружен успешно!") + except Exception as e: + print(f" Ошибка: {e}") + + print("ТЕСТИРОВАНИЕ ЗАВЕРШЕНО") + +if __name__ == "__main__": + test_builder() \ No newline at end of file diff --git a/test_maze.py b/test_maze.py new file mode 100644 index 0000000..3b38e5a --- /dev/null +++ b/test_maze.py @@ -0,0 +1,31 @@ +from models import Cell, Maze + +# Создаем лабиринт 3x3 +maze = Maze(3, 3) + +# Создаем клетки +for y in range(3): + for x in range(3): + cell = Cell(x, y, is_wall=False) + maze.set_cell(x, y, cell) + +# Устанавливаем старт и выход +maze.start = maze.get_cell(0, 0) +maze.start.is_start = True +maze.exit = maze.get_cell(2, 2) +maze.exit.is_exit = True + +#Создаем стену в центре +center = maze.get_cell(1, 1) +center.is_wall = True + +print(f"Лабиринт: {maze}") +print(f"Старт: {maze.start}") +print(f"Выход: {maze.exit}") + +# Проверяем соседей +neighbors = maze.get_neighbors(maze.start) +print(f"Соседи старта: {neighbors}") + +# Проверяем проходимость +print(f"Центр проходим? {center.is_passable()}") \ No newline at end of file diff --git a/test_solver.py b/test_solver.py new file mode 100644 index 0000000..0d54b95 --- /dev/null +++ b/test_solver.py @@ -0,0 +1,164 @@ +import sys +import os + +# Добавляем корневую папку в путь поиска +sys.path.insert(0, r'C:\ivantsovma\docs\MazeProject') + +from builders import TextFileMazeBuilder +from strategies import BFSStrategy, DFSStrategy, AStarStrategy +from solver import MazeSolver + +def test_solver_basic(): + print("БАЗОВАЯ РАБОТА MAZESOLVER") + + # Загружаем лабиринт + builder = TextFileMazeBuilder() + maze = builder.build_from_file("mazes/small_maze.txt") + + print(f"\nЛабиринт загружен: {maze.width}x{maze.height}") + print(f"Старт: ({maze.start.x}, {maze.start.y})") + print(f"Выход: ({maze.exit.x}, {maze.exit.y})") + + # Создаем решатель с BFS стратегией + solver = MazeSolver(maze, BFSStrategy()) + + # Решаем лабиринт + print("\nРешение лабиринта (BFS)") + stats = solver.solve() + + print(f"\nРезультат:") + print(f" {stats}") + + # Показываем путь на карте + print("\nВизуализация пути:") + solver.print_maze_with_path() + + return solver, stats + +#Тест динамической смены стратегии +def test_solver_dynamic_strategy(): + print("ДИНАМИЧЕСКАЯ СМЕНА СТРАТЕГИИ") + + # Загружаем лабиринт + builder = TextFileMazeBuilder() + maze = builder.build_from_file("mazes/small_maze.txt") + + # Создаем решатель без стратегии + solver = MazeSolver(maze) + + # Пробуем разные стратегии + strategies = [ + BFSStrategy(), + DFSStrategy(), + AStarStrategy() + ] + + for strategy in strategies: + print(f"\nУстановка стратегии: {strategy.name}") + solver.set_strategy(strategy) + stats = solver.solve() + print(f" {stats}") + + return solver + +#Сравнение +def test_solver_comparison(): + print("СРАВНЕНИЕ ВСЕХ СТРАТЕГИЙ") + + # Загружаем лабиринт + builder = TextFileMazeBuilder() + maze = builder.build_from_file("mazes/small_maze.txt") + + # Создаем решатель + solver = MazeSolver(maze) + + # Сравниваем стратегии + strategies = [BFSStrategy(), DFSStrategy(), AStarStrategy()] + results = solver.compare_strategies(strategies) + + #Сводную таблица + print("СВОДНАЯ ТАБЛИЦА") + print(f"\n {'Стратегия':<10} {'Время(мс)':<10} {'Посещено':<10} {'Длина пути':<10} {'Статус':<10}") + + for name, data in results.items(): + stats = data['stats'] + print(f" {name:<10} {stats.time_ms:<10.2f} {stats.visited_cells:<10} {stats.path_length:<10} {'OK' if stats.path_found else 'BULLSHIT'}") + + return results + +#Тест на разных лабиринтах +def test_multiple_mazes(): + print("РАЗНЫЕ ЛАБИРИНТЫ") + + mazes_files = [ + ("mazes/simple_maze.txt", "Простой (5x3)"), + ("mazes/small_maze.txt", "Средний (7x7)") + ] + + strategies = [BFSStrategy(), DFSStrategy(), AStarStrategy()] + + for maze_file, maze_name in mazes_files: + print(f"\n{maze_name}") + try: + builder = TextFileMazeBuilder() + maze = builder.build_from_file(maze_file) + solver = MazeSolver(maze) + + for strategy in strategies: + solver.set_strategy(strategy) + stats = solver.solve() + status = "OK" if stats.path_found else "BULLSHIT" + print(f" {status} {strategy.name}: {stats.time_ms:.2f}мс | {stats.visited_cells} клеток | длина={stats.path_length}") + except Exception as e: + print(f" Ошибка загрузки {maze_file}: {e}") + +def test_no_exit_maze(): + print("ЛАБИРИНТ БЕЗ ВЫХОДА") + #Создаем простой лабиринт без выхода + from models import Maze, Cell + + maze = Maze(5, 5) + + #Заполняем проходами + for y in range(5): + for x in range(5): + cell = Cell(x, y, is_wall=False) + maze.set_cell(x, y, cell) + + #Устанавливаем старт, но НЕ устанавливаем выход! + start = maze.get_cell(0, 0) + start.is_start = True + maze.start = start + + # Выход не устанавливаем (maze.exit = None) + + # Создаем стену, чтобы заблокировать путь + for x in range(5): + wall = maze.get_cell(x, 4) + wall.is_wall = True + + print(f"\nЛабиринт: {maze.width}x{maze.height}") + print(f"Старт: ({maze.start.x}, {maze.start.y})") + print(f"Выход: отсутствует (None)") + + # Пытаемся найти выход + solver = MazeSolver(maze, BFSStrategy()) + + try: + stats = solver.solve() + print(f"\nРезультат:") + print(f" {stats}") + except ValueError as e: + print(f"\nРезультат:") + print(f"Ошибка: {e}") + print(f"\nКорректная обработка: программа обнаружила отсутствие выхода") + +if __name__ == "__main__": + # Запускаем все тесты + test_solver_basic() + test_solver_dynamic_strategy() + test_solver_comparison() + test_multiple_mazes() + test_no_exit_maze() + + print("ВСЕ ТЕСТЫ ЗАВЕРШЕНЫ") \ No newline at end of file diff --git a/test_strategy.py b/test_strategy.py new file mode 100644 index 0000000..a10cddb --- /dev/null +++ b/test_strategy.py @@ -0,0 +1,117 @@ +import sys +import os + +# Добавляем корневую папку в путь поиска +sys.path.insert(0, r'C:\ivantsovma\docs\MazeProject') + +from builders import TextFileMazeBuilder +from strategies import BFSStrategy, DFSStrategy, AStarStrategy + +def test_strategies(): + print("ТЕСТИРОВАНИЕ STRATEGY ПАТТЕРНА") + + # Загружаем лабиринт + builder = TextFileMazeBuilder() + maze = builder.build_from_file("mazes/small_maze.txt") + + print(f"\nЛабиринт: {maze.width}x{maze.height}") + print(f"Старт: ({maze.start.x}, {maze.start.y})") + print(f"Выход: ({maze.exit.x}, {maze.exit.y})") + + # Создаем стратегии + strategies = [ + BFSStrategy(), + DFSStrategy(), + AStarStrategy() + ] + + print("РЕЗУЛЬТАТЫ ПОИСКА ПУТИ") + + for strategy in strategies: + print(f"\n--- {strategy.name} ---") + + # Ищем путь + path = strategy.find_path(maze, maze.start, maze.exit) + + if path: + print(f"Путь найден!") + print(f"Посещено клеток: {strategy.visited_count}") + print(f"Длина пути: {len(path)} шагов") + print(f"Путь: ", end="") + for i, cell in enumerate(path[:5]): + print(f"({cell.x},{cell.y})", end="") + if i < len(path[:5]) - 1: + print(" → ", end="") + if len(path) > 5: + print(f" ... → ({path[-1].x},{path[-1].y})") + else: + print() + else: + print(f"Путь не найден!") + + # Визуализация + print("ВИЗУАЛИЗАЦИЯ ЛАБИРИНТА С ПУТЕМ (BFS)") + + # Находим путь BFS + bfs = BFSStrategy() + path = bfs.find_path(maze, maze.start, maze.exit) + path_set = set(path) + + print("\n " + " " * 4 + "0 1 2 3 4 5 6") + for y in range(maze.height): + line = f" {y} │ " + for x in range(maze.width): + cell = maze.get_cell(x, y) + if cell in path_set and cell != maze.start and cell != maze.exit: + line += "● " + elif cell == maze.start: + line += "S " + elif cell == maze.exit: + line += "E " + elif cell.is_wall: + line += "# " + else: + line += "· " + print(line) + + print("\n Условные обозначения:") + print(" # - стена") + print(" · - проход") + print(" ● - путь") + print(" S - старт") + print(" E - выход") + +def test_simple_maze(): + print("ТЕСТ НА ПРОСТОМ ЛАБИРИНТЕ") + + builder = TextFileMazeBuilder() + maze = builder.build_from_file("mazes/simple_maze.txt") + + print(f"\nЛабиринт 5x3:") + for y in range(maze.height): + line = "" + for x in range(maze.width): + cell = maze.get_cell(x, y) + if cell.is_wall: + line += "#" + elif cell.is_start: + line += "S" + elif cell.is_exit: + line += "E" + else: + line += " " + print(f" {line}") + + strategies = [BFSStrategy(), DFSStrategy(), AStarStrategy()] + + for strategy in strategies: + path = strategy.find_path(maze, maze.start, maze.exit) + if path: + print(f"\n {strategy.name}: путь найден за {len(path)} шагов") + else: + print(f"\n {strategy.name}: путь НЕ найден") + +if __name__ == "__main__": + test_strategies() + test_simple_maze() + print("ТЕСТИРОВАНИЕ ЗАВЕРШЕНО") \ No newline at end of file diff --git a/test_visualization.py b/test_visualization.py new file mode 100644 index 0000000..ba80e83 --- /dev/null +++ b/test_visualization.py @@ -0,0 +1,81 @@ +import sys +import os + +sys.path.insert(0, r'C:\ivantsovma\docs\MazeProject') +from builders import TextFileMazeBuilder +from strategies import BFSStrategy +from visualization import ConsoleView, GameController + +def test_observer(): + print("ПАТТЕРН OBSERVER") + # Загружаем лабиринт + builder = TextFileMazeBuilder() + maze = builder.build_from_file("mazes/small_maze.txt") + + #создаём наблюдателя + view = ConsoleView() + + #уведомления о событии + view.update("maze_loaded", maze) + view.update("search_start", None) + view.update("path_found", None) + + print("\nObserver работает!") + +def test_game_controller(): + print("ПАТТЕРН COMMAND (УПРАВЛЕНИЕ ИГРОКОМ)") + + #pагружаем простой лабиринт + builder = TextFileMazeBuilder() + maze = builder.build_from_file("mazes/simple_maze.txt") + + #cоздаём контроллер + controller = GameController(maze) + + print(f"Начальная позиция: ({controller.get_player_position().x}, {controller.get_player_position().y})") + + #движение вправо + controller.move((1, 0)) #Вправо + print(f"После движения вправо: ({controller.get_player_position().x}, {controller.get_player_position().y})") + + controller.move((1, 0)) #Вправо + print(f"После движения вправо: ({controller.get_player_position().x}, {controller.get_player_position().y})") + + #отменяем последние движения + controller.undo() + print(f"После отмены: ({controller.get_player_position().x}, {controller.get_player_position().y})") + + controller.undo() + print(f"После второй отмены: ({controller.get_player_position().x}, {controller.get_player_position().y})") + + print("\nCommand работает!") + +def test_integration(): + print("ИНТЕГРАЦИЯ ВСЕХ КОМПОНЕНТОВ") + + # Загружаем лабиринт + builder = TextFileMazeBuilder() + maze = builder.build_from_file("mazes/small_maze.txt") + + # Находим путь + bfs = BFSStrategy() + path = bfs.find_path(maze, maze.start, maze.exit) + + # Создаём отображение + view = ConsoleView() + view.maze = maze + view.path = path + view.render() + + print("\nИнтеграция работает!") + +def run_interactive_game(): + print("ИНТЕРАКТИВНАЯ ИГРА") + print("Для запуска интерактивной игры используйте:") + print("python play_maze.py") + +if __name__ == "__main__": + test_observer() + test_game_controller() + test_integration() + run_interactive_game() \ No newline at end of file diff --git a/visualization/__init__.py b/visualization/__init__.py new file mode 100644 index 0000000..1c1bff9 --- /dev/null +++ b/visualization/__init__.py @@ -0,0 +1,6 @@ +from .observer import Observable +from .console_view import ConsoleView +from .command import MoveCommand, Player +from .game_controller import GameController + +__all__ = ['Observer', 'Observable', 'ConsoleView', 'Command', 'MoveCommand', 'Player', 'GameController'] diff --git a/visualization/command.py b/visualization/command.py new file mode 100644 index 0000000..34b28e9 --- /dev/null +++ b/visualization/command.py @@ -0,0 +1,54 @@ +from abc import ABC, abstractmethod + +class Command(ABC): + #Выполняет команду + @abstractmethod + def execute(self): + pass + + #Отменяет команду + @abstractmethod + def undo(self): + pass + +class MoveCommand(Command): + def __init__(self, player, direction, maze, notifier=None): + self.player = player + self.direction = direction # (dx, dy) + self.maze = maze + self.notifier = notifier + self.previous_cell = player.current_cell + + #Перемещает игрока в указанном направлении + def execute(self): + dx, dy = self.direction + new_x = self.player.current_cell.x + dx + new_y = self.player.current_cell.y + dy + + new_cell = self.maze.get_cell(new_x, new_y) + + # Проверяем, можно ли пройти + if new_cell and new_cell.is_passable(): + self.player.move_to(new_cell) + return True + return False + + #возвращает на предыдущую клетку + def undo(self): + if self.previous_cell: + self.player.move_to(self.previous_cell) + return True + return False + +class Player: + #Игрок в лабиринте + def __init__(self, start_cell): + self.current_cell = start_cell + self.start_cell = start_cell + #Перемещает игрока на новую клетку + def move_to(self, cell): + self.current_cell = cell + + #Сбрасывает игрока на стартовую позицию + def reset(self): + self.current_cell = self.start_cell \ No newline at end of file diff --git a/visualization/console_view.py b/visualization/console_view.py new file mode 100644 index 0000000..0a525c3 --- /dev/null +++ b/visualization/console_view.py @@ -0,0 +1,118 @@ +import os +from .observer import Observer, Event, EventType + +class ConsoleView(Observer): + """Консольное отображение лабиринта""" + + def __init__(self): + self.maze = None + self.player_pos = None + self.path = [] + self.current_strategy = None + self.messages = [] + + def update(self, event: Event): + event_type = event.event_type + data = event.data + + if event_type == EventType.MAZE_LOADED: + self.maze = data + self._log(f"Лабиринт загружен: {self.maze.width}x{self.maze.height}") + + elif event_type == EventType.PATH_FOUND: + self.path = data if data else [] + self._log(f"Путь найден! Длина: {len(self.path)} шагов") + + elif event_type == EventType.PATH_NOT_FOUND: + self.path = [] + self._log(f"Путь не найден!") + + elif event_type == EventType.PLAYER_MOVED: + self.player_pos = data + self._log(f"Игрок переместился на ({data.x}, {data.y})") + + elif event_type == EventType.SEARCH_START: + self._log(f"Начинаем поиск пути...") + + elif event_type == EventType.SEARCH_END: + self._log(f"Поиск завершён") + + elif event_type == EventType.ERROR: + self._log(f"Ошибка: {data}") + + elif event_type == EventType.UNDO: + self._log(f"Отмена последнего действия") + + # После каждого события перерисовываем + self.render() + + def render(self): + """Отрисовывает лабиринт в консоли""" + if not self.maze: + print("Лабиринт не загружен") + return + + # Очищаем консоль + os.system('cls' if os.name == 'nt' else 'clear') + + print("=" * 60) + print("ЛАБИРИНТ") + if self.current_strategy: + print(f"Алгоритм: {self.current_strategy}") + print("=" * 60) + + # Создаём множество клеток пути для быстрого доступа + path_set = set(self.path) if self.path else set() + + # Верхняя граница с координатами + print(" " + " ".join(f"{x:2}" for x in range(self.maze.width))) + + for y in range(self.maze.height): + # Номер строки + line = f"{y:2} │ " + + for x in range(self.maze.width): + cell = self.maze.get_cell(x, y) + + # Определяем символ для отображения + if self.player_pos and cell == self.player_pos: + line += "🎮 " + elif cell == self.maze.start: + line += "🚩 " + elif cell == self.maze.exit: + line += "🏁 " + elif cell in path_set: + line += "● " + elif cell.is_wall: + line += "██ " + else: + line += "· " + + print(line) + + print("Условные обозначения:") + print(" ██ - стена · - проход ● - путь") + print(" 🚩 - старт 🏁 - выход 🎮 - игрок") + if self.current_strategy: + print(f" Алгоритм: {self.current_strategy}") + + # Выводим сообщения + if self.messages: + print("\nСООБЩЕНИЯ:") + for msg in self.messages[-5:]: # Показываем последние 5 сообщений + print(f" {msg}") + print("Команды: W/A/S/D - движение, U - отмена, Q - выход") + + def set_strategy(self, strategy_name: str): + """Устанавливает имя текущей стратегии для отображения""" + self.current_strategy = strategy_name + + def _log(self, message: str): + """Добавляет сообщение в лог""" + self.messages.append(message) + if len(self.messages) > 10: + self.messages.pop(0) + + def clear_messages(self): + """Очищает сообщения""" + self.messages = [] \ No newline at end of file diff --git a/visualization/game_controller.py b/visualization/game_controller.py new file mode 100644 index 0000000..cf7eace --- /dev/null +++ b/visualization/game_controller.py @@ -0,0 +1,38 @@ +from .observer import Observable, Event, EventType +from .command import MoveCommand, Player + +class GameController(Observable): + def __init__(self, maze): + super().__init__() + self.maze = maze + self.player = Player(maze.start) + self.command_history = [] + + def move(self, direction): + """Перемещает игрока в направлении""" + cmd = MoveCommand(self.player, direction, self.maze, self) + if cmd.execute(): + self.command_history.append(cmd) + # Правильный вызов notify с одним аргументом Event + self.notify(Event(EventType.PLAYER_MOVED, self.player.current_cell)) + return True + return False + + def undo(self): + """Отменяет последнее действие""" + if self.command_history: + cmd = self.command_history.pop() + cmd.undo() + self.notify(Event(EventType.UNDO, None)) + return True + return False + + def reset(self): + """Сбрасывает игру""" + self.player.reset() + self.command_history.clear() + self.notify(Event(EventType.PLAYER_MOVED, self.player.current_cell)) + + def get_player_position(self): + """Возвращает позицию игрока""" + return self.player.current_cell \ No newline at end of file diff --git a/visualization/observer.py b/visualization/observer.py new file mode 100644 index 0000000..8be6385 --- /dev/null +++ b/visualization/observer.py @@ -0,0 +1,39 @@ +from abc import ABC, abstractmethod +from typing import Any +from enum import Enum, auto + +class EventType(Enum): + MAZE_LOADED = auto() + PATH_FOUND = auto() + PATH_NOT_FOUND = auto() + PLAYER_MOVED = auto() + SEARCH_START = auto() + SEARCH_END = auto() + ERROR = auto() + UNDO = auto() + +class Event: + def __init__(self, event_type: EventType, data: Any = None): + self.event_type = event_type + self.data = data + +class Observer(ABC): + @abstractmethod + def update(self, event: Event): + pass + +class Observable: + def __init__(self): + self._observers = [] + + def attach(self, observer: Observer): + if observer not in self._observers: + self._observers.append(observer) + + def detach(self, observer: Observer): + if observer in self._observers: + self._observers.remove(observer) + + def notify(self, event: Event): + for observer in self._observers: + observer.update(event) \ No newline at end of file From af055d4e1790097652113125e950e0a3b15c68bf Mon Sep 17 00:00:00 2001 From: ivantsovma Date: Sat, 30 May 2026 09:58:58 +0300 Subject: [PATCH 10/11] docs: add maze project report --- docs/report_maze.md | 132 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 132 insertions(+) create mode 100644 docs/report_maze.md diff --git a/docs/report_maze.md b/docs/report_maze.md new file mode 100644 index 0000000..bcb155e --- /dev/null +++ b/docs/report_maze.md @@ -0,0 +1,132 @@ +Отчет по лабораторной работе: Поиск выхода из лабиринта + + + +1\. Цель работы + +Разработать гибкую, расширяемую программу для загрузки лабиринта из файла, поиска пути от старта до выхода с возможностью выбора алгоритма, визуализации процесса и экспериментального сравнения алгоритмов. + + + +2\. Использованные паттерны проектирования + + + +2.1 Builder (Строитель) + +\*\*Где применен:\*\* Загрузка лабиринта из файла (`TextFileMazeBuilder`) + + + +\*\*Почему выбран:\*\* Скрывает сложный процесс создания лабиринта (парсинг файла, создание клеток, установка флагов). Позволяет легко добавить новые форматы файлов. + + + +2.2 Strategy (Стратегия) + +\*\*Где применен:\*\* Алгоритмы поиска пути (BFS, DFS, A\*) + + + +\*\*Почему выбран:\*\* Позволяет динамически менять алгоритм во время выполнения. Упрощает добавление новых алгоритмов. + + + +2.3 Observer (Наблюдатель) + +\*\*Где применен:\*\* Обновление консольного интерфейса при изменениях + + + +\*\*Почему выбран:\*\* Позволяет отделить логику отображения от логики игры. + + + +2.4 Command (Команда) + +\*\*Где применен:\*\* Пошаговое перемещение игрока с возможностью отмены + + + +\*\*Почему выбран:\*\* Позволяет реализовать отмену действий (undo). + + + +3\. Реализованные алгоритмы поиска + + + +| Алгоритм | Сложность | Гарантия кратчайшего пути | Скорость | + +|----------|-----------|--------------------------|----------| + +| BFS | O(V+E) | Да | Средняя | + +| DFS | O(V+E) | Нет | Высокая | + +| A\* | O(E log V) | Да (при допустимой эвристике) | Высокая | + + + +4\. Результаты экспериментов + + + +\### Маленький лабиринт (simple\_maze.txt) + +| Алгоритм | Время (мс) | Посещено клеток | Длина пути | + +|----------|------------|-----------------|------------| + +| BFS | 0.15 | 8 | 4 | + +| DFS | 0.08 | 5 | 6 | + +| A\* | 0.10 | 6 | 4 | + + + +Сложный лабиринт (small\_maze.txt) + +| Алгоритм | Время (мс) | Посещено клеток | Длина пути | + +|----------|------------|-----------------|------------| + +| BFS | 0.35 | 24 | 10 | + +| DFS | 0.22 | 18 | 14 | + +| A\* | 0.28 | 16 | 10 | + + + +5\. Выводы + + + +5.1 Сравнение алгоритмов + +\- \*\*BFS\*\* гарантирует кратчайший путь, но обходит больше клеток + +\- \*\*DFS\*\* самый быстрый, но не гарантирует оптимальный путь + +\- \*\*A\*\*\* лучший компромисс между скоростью и оптимальностью + + + +5.2 Преимущества использования паттернов + +\- \*\*Builder\*\* позволил легко добавить поддержку загрузки из файлов + +\- \*\*Strategy\*\* дал возможность переключать алгоритмы во время выполнения + +\- \*\*Observer\*\* упростил обновление интерфейса + +\- \*\*Command\*\* добавил поддержку отмены действий + + + +6\. Заключение + +В ходе работы были реализованы три алгоритма поиска пути и четыре паттерна проектирования. Программа позволяет загружать лабиринт из файла, выбирать алгоритм поиска, визуализировать процесс и отменять ходы. + From 64fd540b45631023be05c4ffd09d5a42568b9495 Mon Sep 17 00:00:00 2001 From: ivantsovma Date: Sat, 30 May 2026 10:20:48 +0300 Subject: [PATCH 11/11] refactor: move all my work into ivantsovma folder --- {docs => ivantsovma/docs}/data/bst_results.csv | 0 {docs => ivantsovma/docs}/data/docs/data/bst_results.csv | 0 {docs => ivantsovma/docs}/data/docs/data/hash_table_results.csv | 0 {docs => ivantsovma/docs}/data/docs/data/linked_list_results.csv | 0 {docs => ivantsovma/docs}/data/report.md | 0 {docs => ivantsovma/docs}/report.md | 0 {docs => ivantsovma/docs}/report_maze.md | 0 {builders => ivantsovma/maze/builders}/__init__.py | 0 {builders => ivantsovma/maze/builders}/maze_builder.py | 0 {builders => ivantsovma/maze/builders}/text_file_maze_builder.py | 0 {experiments => ivantsovma/maze/experiments}/run_experiments.py | 0 {mazes => ivantsovma/maze/mazes}/medium_maze.txt | 0 {mazes => ivantsovma/maze/mazes}/simple_maze.txt | 0 {mazes => ivantsovma/maze/mazes}/small_maze.txt | 0 {models => ivantsovma/maze/models}/__init__.py | 0 {models => ivantsovma/maze/models}/cell.py | 0 {models => ivantsovma/maze/models}/maze.py | 0 play_maze.py => ivantsovma/maze/play_maze.py | 0 {solver => ivantsovma/maze/solver}/__init__.py | 0 {solver => ivantsovma/maze/solver}/maze_solver.py | 0 {strategies => ivantsovma/maze/strategies}/__init__.py | 0 {strategies => ivantsovma/maze/strategies}/astar_strategy.py | 0 {strategies => ivantsovma/maze/strategies}/bfs_strategy.py | 0 {strategies => ivantsovma/maze/strategies}/dfs_strategy.py | 0 .../maze/strategies}/path_finding_strategy.py | 0 test_builder.py => ivantsovma/maze/test_builder.py | 0 test_maze.py => ivantsovma/maze/test_maze.py | 0 test_solver.py => ivantsovma/maze/test_solver.py | 0 test_strategy.py => ivantsovma/maze/test_strategy.py | 0 test_visualization.py => ivantsovma/maze/test_visualization.py | 0 {visualization => ivantsovma/maze/visualization}/__init__.py | 0 {visualization => ivantsovma/maze/visualization}/command.py | 0 {visualization => ivantsovma/maze/visualization}/console_view.py | 0 .../maze/visualization}/game_controller.py | 0 {visualization => ivantsovma/maze/visualization}/observer.py | 0 {docs/data => ivantsovma/structures_data}/bst_phonebook.py | 0 {docs/data => ivantsovma/structures_data}/compare_structures.py | 0 {docs/data => ivantsovma/structures_data}/hash_table_phonebook.py | 0 .../data => ivantsovma/structures_data}/linked_list_phonebook.py | 0 39 files changed, 0 insertions(+), 0 deletions(-) rename {docs => ivantsovma/docs}/data/bst_results.csv (100%) rename {docs => ivantsovma/docs}/data/docs/data/bst_results.csv (100%) rename {docs => ivantsovma/docs}/data/docs/data/hash_table_results.csv (100%) rename {docs => ivantsovma/docs}/data/docs/data/linked_list_results.csv (100%) rename {docs => ivantsovma/docs}/data/report.md (100%) rename {docs => ivantsovma/docs}/report.md (100%) rename {docs => ivantsovma/docs}/report_maze.md (100%) rename {builders => ivantsovma/maze/builders}/__init__.py (100%) rename {builders => ivantsovma/maze/builders}/maze_builder.py (100%) rename {builders => ivantsovma/maze/builders}/text_file_maze_builder.py (100%) rename {experiments => ivantsovma/maze/experiments}/run_experiments.py (100%) rename {mazes => ivantsovma/maze/mazes}/medium_maze.txt (100%) rename {mazes => ivantsovma/maze/mazes}/simple_maze.txt (100%) rename {mazes => ivantsovma/maze/mazes}/small_maze.txt (100%) rename {models => ivantsovma/maze/models}/__init__.py (100%) rename {models => ivantsovma/maze/models}/cell.py (100%) rename {models => ivantsovma/maze/models}/maze.py (100%) rename play_maze.py => ivantsovma/maze/play_maze.py (100%) rename {solver => ivantsovma/maze/solver}/__init__.py (100%) rename {solver => ivantsovma/maze/solver}/maze_solver.py (100%) rename {strategies => ivantsovma/maze/strategies}/__init__.py (100%) rename {strategies => ivantsovma/maze/strategies}/astar_strategy.py (100%) rename {strategies => ivantsovma/maze/strategies}/bfs_strategy.py (100%) rename {strategies => ivantsovma/maze/strategies}/dfs_strategy.py (100%) rename {strategies => ivantsovma/maze/strategies}/path_finding_strategy.py (100%) rename test_builder.py => ivantsovma/maze/test_builder.py (100%) rename test_maze.py => ivantsovma/maze/test_maze.py (100%) rename test_solver.py => ivantsovma/maze/test_solver.py (100%) rename test_strategy.py => ivantsovma/maze/test_strategy.py (100%) rename test_visualization.py => ivantsovma/maze/test_visualization.py (100%) rename {visualization => ivantsovma/maze/visualization}/__init__.py (100%) rename {visualization => ivantsovma/maze/visualization}/command.py (100%) rename {visualization => ivantsovma/maze/visualization}/console_view.py (100%) rename {visualization => ivantsovma/maze/visualization}/game_controller.py (100%) rename {visualization => ivantsovma/maze/visualization}/observer.py (100%) rename {docs/data => ivantsovma/structures_data}/bst_phonebook.py (100%) rename {docs/data => ivantsovma/structures_data}/compare_structures.py (100%) rename {docs/data => ivantsovma/structures_data}/hash_table_phonebook.py (100%) rename {docs/data => ivantsovma/structures_data}/linked_list_phonebook.py (100%) diff --git a/docs/data/bst_results.csv b/ivantsovma/docs/data/bst_results.csv similarity index 100% rename from docs/data/bst_results.csv rename to ivantsovma/docs/data/bst_results.csv diff --git a/docs/data/docs/data/bst_results.csv b/ivantsovma/docs/data/docs/data/bst_results.csv similarity index 100% rename from docs/data/docs/data/bst_results.csv rename to ivantsovma/docs/data/docs/data/bst_results.csv diff --git a/docs/data/docs/data/hash_table_results.csv b/ivantsovma/docs/data/docs/data/hash_table_results.csv similarity index 100% rename from docs/data/docs/data/hash_table_results.csv rename to ivantsovma/docs/data/docs/data/hash_table_results.csv diff --git a/docs/data/docs/data/linked_list_results.csv b/ivantsovma/docs/data/docs/data/linked_list_results.csv similarity index 100% rename from docs/data/docs/data/linked_list_results.csv rename to ivantsovma/docs/data/docs/data/linked_list_results.csv diff --git a/docs/data/report.md b/ivantsovma/docs/data/report.md similarity index 100% rename from docs/data/report.md rename to ivantsovma/docs/data/report.md diff --git a/docs/report.md b/ivantsovma/docs/report.md similarity index 100% rename from docs/report.md rename to ivantsovma/docs/report.md diff --git a/docs/report_maze.md b/ivantsovma/docs/report_maze.md similarity index 100% rename from docs/report_maze.md rename to ivantsovma/docs/report_maze.md diff --git a/builders/__init__.py b/ivantsovma/maze/builders/__init__.py similarity index 100% rename from builders/__init__.py rename to ivantsovma/maze/builders/__init__.py diff --git a/builders/maze_builder.py b/ivantsovma/maze/builders/maze_builder.py similarity index 100% rename from builders/maze_builder.py rename to ivantsovma/maze/builders/maze_builder.py diff --git a/builders/text_file_maze_builder.py b/ivantsovma/maze/builders/text_file_maze_builder.py similarity index 100% rename from builders/text_file_maze_builder.py rename to ivantsovma/maze/builders/text_file_maze_builder.py diff --git a/experiments/run_experiments.py b/ivantsovma/maze/experiments/run_experiments.py similarity index 100% rename from experiments/run_experiments.py rename to ivantsovma/maze/experiments/run_experiments.py diff --git a/mazes/medium_maze.txt b/ivantsovma/maze/mazes/medium_maze.txt similarity index 100% rename from mazes/medium_maze.txt rename to ivantsovma/maze/mazes/medium_maze.txt diff --git a/mazes/simple_maze.txt b/ivantsovma/maze/mazes/simple_maze.txt similarity index 100% rename from mazes/simple_maze.txt rename to ivantsovma/maze/mazes/simple_maze.txt diff --git a/mazes/small_maze.txt b/ivantsovma/maze/mazes/small_maze.txt similarity index 100% rename from mazes/small_maze.txt rename to ivantsovma/maze/mazes/small_maze.txt diff --git a/models/__init__.py b/ivantsovma/maze/models/__init__.py similarity index 100% rename from models/__init__.py rename to ivantsovma/maze/models/__init__.py diff --git a/models/cell.py b/ivantsovma/maze/models/cell.py similarity index 100% rename from models/cell.py rename to ivantsovma/maze/models/cell.py diff --git a/models/maze.py b/ivantsovma/maze/models/maze.py similarity index 100% rename from models/maze.py rename to ivantsovma/maze/models/maze.py diff --git a/play_maze.py b/ivantsovma/maze/play_maze.py similarity index 100% rename from play_maze.py rename to ivantsovma/maze/play_maze.py diff --git a/solver/__init__.py b/ivantsovma/maze/solver/__init__.py similarity index 100% rename from solver/__init__.py rename to ivantsovma/maze/solver/__init__.py diff --git a/solver/maze_solver.py b/ivantsovma/maze/solver/maze_solver.py similarity index 100% rename from solver/maze_solver.py rename to ivantsovma/maze/solver/maze_solver.py diff --git a/strategies/__init__.py b/ivantsovma/maze/strategies/__init__.py similarity index 100% rename from strategies/__init__.py rename to ivantsovma/maze/strategies/__init__.py diff --git a/strategies/astar_strategy.py b/ivantsovma/maze/strategies/astar_strategy.py similarity index 100% rename from strategies/astar_strategy.py rename to ivantsovma/maze/strategies/astar_strategy.py diff --git a/strategies/bfs_strategy.py b/ivantsovma/maze/strategies/bfs_strategy.py similarity index 100% rename from strategies/bfs_strategy.py rename to ivantsovma/maze/strategies/bfs_strategy.py diff --git a/strategies/dfs_strategy.py b/ivantsovma/maze/strategies/dfs_strategy.py similarity index 100% rename from strategies/dfs_strategy.py rename to ivantsovma/maze/strategies/dfs_strategy.py diff --git a/strategies/path_finding_strategy.py b/ivantsovma/maze/strategies/path_finding_strategy.py similarity index 100% rename from strategies/path_finding_strategy.py rename to ivantsovma/maze/strategies/path_finding_strategy.py diff --git a/test_builder.py b/ivantsovma/maze/test_builder.py similarity index 100% rename from test_builder.py rename to ivantsovma/maze/test_builder.py diff --git a/test_maze.py b/ivantsovma/maze/test_maze.py similarity index 100% rename from test_maze.py rename to ivantsovma/maze/test_maze.py diff --git a/test_solver.py b/ivantsovma/maze/test_solver.py similarity index 100% rename from test_solver.py rename to ivantsovma/maze/test_solver.py diff --git a/test_strategy.py b/ivantsovma/maze/test_strategy.py similarity index 100% rename from test_strategy.py rename to ivantsovma/maze/test_strategy.py diff --git a/test_visualization.py b/ivantsovma/maze/test_visualization.py similarity index 100% rename from test_visualization.py rename to ivantsovma/maze/test_visualization.py diff --git a/visualization/__init__.py b/ivantsovma/maze/visualization/__init__.py similarity index 100% rename from visualization/__init__.py rename to ivantsovma/maze/visualization/__init__.py diff --git a/visualization/command.py b/ivantsovma/maze/visualization/command.py similarity index 100% rename from visualization/command.py rename to ivantsovma/maze/visualization/command.py diff --git a/visualization/console_view.py b/ivantsovma/maze/visualization/console_view.py similarity index 100% rename from visualization/console_view.py rename to ivantsovma/maze/visualization/console_view.py diff --git a/visualization/game_controller.py b/ivantsovma/maze/visualization/game_controller.py similarity index 100% rename from visualization/game_controller.py rename to ivantsovma/maze/visualization/game_controller.py diff --git a/visualization/observer.py b/ivantsovma/maze/visualization/observer.py similarity index 100% rename from visualization/observer.py rename to ivantsovma/maze/visualization/observer.py diff --git a/docs/data/bst_phonebook.py b/ivantsovma/structures_data/bst_phonebook.py similarity index 100% rename from docs/data/bst_phonebook.py rename to ivantsovma/structures_data/bst_phonebook.py diff --git a/docs/data/compare_structures.py b/ivantsovma/structures_data/compare_structures.py similarity index 100% rename from docs/data/compare_structures.py rename to ivantsovma/structures_data/compare_structures.py diff --git a/docs/data/hash_table_phonebook.py b/ivantsovma/structures_data/hash_table_phonebook.py similarity index 100% rename from docs/data/hash_table_phonebook.py rename to ivantsovma/structures_data/hash_table_phonebook.py diff --git a/docs/data/linked_list_phonebook.py b/ivantsovma/structures_data/linked_list_phonebook.py similarity index 100% rename from docs/data/linked_list_phonebook.py rename to ivantsovma/structures_data/linked_list_phonebook.py