From e19ddcdcb1d874ede237c5d4d48bf4148c24850f Mon Sep 17 00:00:00 2001 From: komissarovgo Date: Sat, 25 Apr 2026 00:57:56 +0300 Subject: [PATCH 01/20] =?UTF-8?q?=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8?= =?UTF-8?q?=D0=BB=20=D1=82=D0=B5=D0=BA=D1=81=D1=82=D0=BE=D0=B2=D1=8B=D0=B9?= =?UTF-8?q?=20=D1=84=D0=B0=D0=B9=D0=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- komissarovgo/docs/data/CodePhoneBook.py.txt | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 komissarovgo/docs/data/CodePhoneBook.py.txt diff --git a/komissarovgo/docs/data/CodePhoneBook.py.txt b/komissarovgo/docs/data/CodePhoneBook.py.txt new file mode 100644 index 0000000..e69de29 -- 2.43.0 From e04af3d0443841038013f92ede69402f49d54a67 Mon Sep 17 00:00:00 2001 From: komissarovgo Date: Sat, 25 Apr 2026 22:33:17 +0300 Subject: [PATCH 02/20] =?UTF-8?q?=D1=83=D0=B4=D0=B0=D0=BB=D1=91=D0=BD=20?= =?UTF-8?q?=D1=84=D0=B0=D0=B9=D0=BB=20code?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- komissarovgo/docs/data/CodePhoneBook.py.txt | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 komissarovgo/docs/data/CodePhoneBook.py.txt diff --git a/komissarovgo/docs/data/CodePhoneBook.py.txt b/komissarovgo/docs/data/CodePhoneBook.py.txt deleted file mode 100644 index e69de29..0000000 -- 2.43.0 From 39816756a3b0bf518b6850fa8ba752c671612198 Mon Sep 17 00:00:00 2001 From: komissarovgo Date: Sat, 25 Apr 2026 22:48:01 +0300 Subject: [PATCH 03/20] =?UTF-8?q?=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=20=D0=BD=D0=BE=D0=B2=D1=8B=D0=B9=20=D1=84=D0=B0?= =?UTF-8?q?=D0=B9=D0=BB=20=D0=B4=D0=BB=D1=8F=20=D0=BA=D0=BE=D0=B4=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- komissarovgo/docs/data/CodePhoneBook.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 komissarovgo/docs/data/CodePhoneBook.py diff --git a/komissarovgo/docs/data/CodePhoneBook.py b/komissarovgo/docs/data/CodePhoneBook.py new file mode 100644 index 0000000..e69de29 -- 2.43.0 From 9f4b31ab1f3cfa5f4912e9c32fb038981d8785f9 Mon Sep 17 00:00:00 2001 From: komissarovgo Date: Sat, 25 Apr 2026 23:49:07 +0300 Subject: [PATCH 04/20] =?UTF-8?q?=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D1=8B=20=D0=B1=D0=B8=D0=B1=D0=BB=D0=B8=D0=BE=D1=82?= =?UTF-8?q?=D0=B5=D0=BA=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- komissarovgo/docs/data/CodePhoneBook.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/komissarovgo/docs/data/CodePhoneBook.py b/komissarovgo/docs/data/CodePhoneBook.py index e69de29..8bb29ad 100644 --- a/komissarovgo/docs/data/CodePhoneBook.py +++ b/komissarovgo/docs/data/CodePhoneBook.py @@ -0,0 +1,4 @@ +import time +import random +import csv +import sys \ No newline at end of file -- 2.43.0 From b07adc7cb4fd66be1ed555b48627e8536a6c7769 Mon Sep 17 00:00:00 2001 From: komissarovgo Date: Sun, 26 Apr 2026 00:23:49 +0300 Subject: [PATCH 05/20] =?UTF-8?q?=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D0=B0=20=D1=80=D0=B5=D0=B0=D0=BB=D0=B8=D0=B7=D0=B0?= =?UTF-8?q?=D1=86=D0=B8=D1=8F=20=D1=81=D0=B2=D1=8F=D0=B7=D0=BD=D0=BE=D0=B3?= =?UTF-8?q?=D0=BE=20=D1=81=D0=BF=D0=B8=D1=81=D0=BA=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- komissarovgo/docs/data/CodePhoneBook.py | 64 ++++++++++++++++++++++++- 1 file changed, 63 insertions(+), 1 deletion(-) diff --git a/komissarovgo/docs/data/CodePhoneBook.py b/komissarovgo/docs/data/CodePhoneBook.py index 8bb29ad..77d0fac 100644 --- a/komissarovgo/docs/data/CodePhoneBook.py +++ b/komissarovgo/docs/data/CodePhoneBook.py @@ -1,4 +1,66 @@ import time import random import csv -import sys \ No newline at end of file +import sys + +# 1. LinkedList + +def ll_insert(head, name, phone): + + new_node = {'name': name, 'phone': phone, 'next': None} + + if head is None: + return new_node + + if head['name'] == name: + head['phone'] = phone + return head + + current = head + while current['next'] is not None: + 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 is not None: + if current['name'] == name: + return current['phone'] + current = current['next'] + return None + + +def ll_delete(head, name): + + if head is None: + return None + + if head['name'] == name: + return head['next'] + + current = head + while current['next'] is not None: + if current['next']['name'] == name: + current['next'] = current['next']['next'] + return head + current = current['next'] + + return head + + +def ll_list_all(head): + + records = [] + current = head + while current is not None: + records.append((current['name'], current['phone'])) + current = current['next'] + records.sort(key=lambda x: x[0]) + return records \ No newline at end of file -- 2.43.0 From 76b52b99e40af76591e2a6beb8b04a1192c391e7 Mon Sep 17 00:00:00 2001 From: komissarovgo Date: Mon, 27 Apr 2026 18:51:01 +0300 Subject: [PATCH 06/20] =?UTF-8?q?=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D0=B0=20=D1=80=D0=B5=D0=B0=D0=BB=D0=B8=D0=B7=D0=B0?= =?UTF-8?q?=D1=86=D0=B8=D1=8F=20=D1=85=D0=B5=D1=88-=D1=82=D0=B0=D0=B1?= =?UTF-8?q?=D0=BB=D0=B8=D1=86=D1=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- komissarovgo/docs/data/CodePhoneBook.py | 38 +++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/komissarovgo/docs/data/CodePhoneBook.py b/komissarovgo/docs/data/CodePhoneBook.py index 77d0fac..66dfdf0 100644 --- a/komissarovgo/docs/data/CodePhoneBook.py +++ b/komissarovgo/docs/data/CodePhoneBook.py @@ -63,4 +63,42 @@ def ll_list_all(head): records.append((current['name'], current['phone'])) current = current['next'] records.sort(key=lambda x: x[0]) + return records + +# 2. Hash Function + +def hash_function(name, table_size): + return sum(ord(c) for c in name) % table_size + + +def ht_create(size=1000): + return [None] * size + + +def ht_insert(buckets, name, phone): + size = len(buckets) + index = hash_function(name, size) + buckets[index] = ll_insert(buckets[index], name, phone) + + +def ht_find(buckets, name): + size = len(buckets) + index = hash_function(name, size) + return ll_find(buckets[index], name) + + +def ht_delete(buckets, name): + size = len(buckets) + index = hash_function(name, size) + buckets[index] = ll_delete(buckets[index], name) + + +def ht_list_all(buckets): + records = [] + for bucket in buckets: + current = bucket + while current is not None: + records.append((current['name'], current['phone'])) + current = current['next'] + records.sort(key=lambda x: x[0]) return records \ No newline at end of file -- 2.43.0 From e1ad783b4959ad8ee29b157c3f8b87e562702b3c Mon Sep 17 00:00:00 2001 From: komissarovgo Date: Mon, 27 Apr 2026 19:51:58 +0300 Subject: [PATCH 07/20] =?UTF-8?q?=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D0=B0=20=D1=80=D0=B5=D0=B0=D0=BB=D0=B8=D0=B7=D0=B0?= =?UTF-8?q?=D1=86=D0=B8=D1=8F=20=D0=B4=D0=B5=D1=80=D0=B5=D0=B2=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- komissarovgo/docs/data/CodePhoneBook.py | 79 +++++++++++++++++++++++++ 1 file changed, 79 insertions(+) diff --git a/komissarovgo/docs/data/CodePhoneBook.py b/komissarovgo/docs/data/CodePhoneBook.py index 66dfdf0..167b324 100644 --- a/komissarovgo/docs/data/CodePhoneBook.py +++ b/komissarovgo/docs/data/CodePhoneBook.py @@ -65,6 +65,8 @@ def ll_list_all(head): records.sort(key=lambda x: x[0]) return records + + # 2. Hash Function def hash_function(name, table_size): @@ -101,4 +103,81 @@ def ht_list_all(buckets): records.append((current['name'], current['phone'])) current = current['next'] records.sort(key=lambda x: x[0]) + return records + + + +#3. Tree function + + +def bst_insert(root, name, phone): + + if root is None: + return {'name': name, 'phone': phone, 'left': None, 'right': None} + + if name < root['name']: + root['left'] = bst_insert(root['left'], name, phone) + elif name > root['name']: + root['right'] = bst_insert(root['right'], name, phone) + else: + root['phone'] = phone + + return root + + +def bst_find(root, name): + + current = root + while current is not None: + if name == current['name']: + return current['phone'] + elif name < current['name']: + current = current['left'] + else: + current = current['right'] + return None + + +def bst_find_min(node): + + current = node + while current['left'] is not None: + current = current['left'] + return current + + +def bst_delete(root, name): + + if root is None: + return None + + if name < root['name']: + root['left'] = bst_delete(root['left'], name) + elif name > root['name']: + root['right'] = bst_delete(root['right'], name) + else: + if root['left'] is None: + return root['right'] + elif root['right'] is None: + return root['left'] + + min_node = bst_find_min(root['right']) + root['name'] = min_node['name'] + root['phone'] = min_node['phone'] + root['right'] = bst_delete(root['right'], min_node['name']) + + return root + + +def bst_list_all(root): + + records = [] + + def inorder_traversal(node): + if node is not None: + inorder_traversal(node['left']) + records.append((node['name'], node['phone'])) + inorder_traversal(node['right']) + + inorder_traversal(root) return records \ No newline at end of file -- 2.43.0 From a095930b979b624f49bfe42495941d2262f7a587 Mon Sep 17 00:00:00 2001 From: komissarovgo Date: Mon, 27 Apr 2026 20:12:20 +0300 Subject: [PATCH 08/20] =?UTF-8?q?=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D1=8B=20=D0=B3=D0=B5=D0=BD=D0=B5=D1=80=D0=B0=D1=86?= =?UTF-8?q?=D0=B8=D1=8F=20=D1=82=D0=B5=D1=81=D1=82=D0=BE=D0=B2=D1=8B=D1=85?= =?UTF-8?q?=20=D0=B4=D0=B0=D0=BD=D0=BD=D1=8B=D1=85=20=D0=B8=20=D0=B7=D0=B0?= =?UTF-8?q?=D0=BC=D0=B5=D1=80=20=D0=B2=D1=80=D0=B5=D0=BC=D0=B5=D0=BD=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- komissarovgo/docs/data/CodePhoneBook.py | 117 +++++++++++++++++++++++- 1 file changed, 116 insertions(+), 1 deletion(-) diff --git a/komissarovgo/docs/data/CodePhoneBook.py b/komissarovgo/docs/data/CodePhoneBook.py index 167b324..396dec9 100644 --- a/komissarovgo/docs/data/CodePhoneBook.py +++ b/komissarovgo/docs/data/CodePhoneBook.py @@ -180,4 +180,119 @@ def bst_list_all(root): inorder_traversal(node['right']) inorder_traversal(root) - return records \ No newline at end of file + return records + + + +#EXPERIMENTAL PART + +# 1. Test data generation + +def generate_records(count=10000): + + records = [] + for i in range(count): + name = f"User_{i:05d}" + phone = f"+7-{random.randint(100,999)}-{random.randint(100,999)}-{random.randint(1000,9999)}" + records.append((name, phone)) + + shuffled = records.copy() + random.shuffle(shuffled) + sorted_records = sorted(records, key=lambda x: x[0]) + + return shuffled, sorted_records + + + +# 2. Timing + +def measure_insertion(structure_name, records): + + times = [] + filled_structure = None + + for run in range(5): + if structure_name == "linked_list": + structure = None + elif structure_name == "hash_table": + structure = ht_create(1000) + elif structure_name == "bst": + structure = None + + start = time.perf_counter() + + for name, phone in records: + if structure_name == "linked_list": + structure = ll_insert(structure, name, phone) + elif structure_name == "hash_table": + ht_insert(structure, name, phone) + elif structure_name == "bst": + structure = bst_insert(structure, name, phone) + + end = time.perf_counter() + times.append(end - start) + + if run == 4: + filled_structure = structure + + return times, filled_structure + + +def measure_search(structure_name, structure, search_names): + + times = [] + + for run in range(5): + start = time.perf_counter() + + for name in search_names: + if structure_name == "linked_list": + ll_find(structure, name) + elif structure_name == "hash_table": + ht_find(structure, name) + elif structure_name == "bst": + bst_find(structure, name) + + end = time.perf_counter() + times.append(end - start) + + return times + + +def measure_deletion(structure_name, original_structure, delete_names): + + times = [] + + for run in range(5): + if structure_name == "linked_list": + all_records = ll_list_all(original_structure) + test_structure = None + for name, phone in all_records: + test_structure = ll_insert(test_structure, name, phone) + + elif structure_name == "hash_table": + all_records = ht_list_all(original_structure) + test_structure = ht_create(1000) + for name, phone in all_records: + ht_insert(test_structure, name, phone) + + elif structure_name == "bst": + all_records = bst_list_all(original_structure) + test_structure = None + for name, phone in all_records: + test_structure = bst_insert(test_structure, name, phone) + + start = time.perf_counter() + + for name in delete_names: + if structure_name == "linked_list": + test_structure = ll_delete(test_structure, name) + elif structure_name == "hash_table": + ht_delete(test_structure, name) + elif structure_name == "bst": + test_structure = bst_delete(test_structure, name) + + end = time.perf_counter() + times.append(end - start) + + return times \ No newline at end of file -- 2.43.0 From e792b1ac2f58755da4a86aa630ebde006495c62c Mon Sep 17 00:00:00 2001 From: komissarovgo Date: Sun, 3 May 2026 11:02:27 +0300 Subject: [PATCH 09/20] =?UTF-8?q?=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D1=8B:=20=D1=82=D0=B0=D0=B1=D0=BB=D0=B8=D1=86?= =?UTF-8?q?=D0=B0,=20=D0=B3=D1=80=D0=B0=D1=84=D0=B8=D0=BA=D0=B8=20=D0=B8?= =?UTF-8?q?=20=D1=84=D0=B8=D0=BD=D0=B0=D0=BB=D1=8C=D0=BD=D0=B0=D1=8F=20?= =?UTF-8?q?=D1=87=D0=B0=D1=81=D1=82=D1=8C=20=D0=BA=D0=BE=D0=B4=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- komissarovgo/docs/data/CodePhoneBook.py | 202 ++++++++++++++++++++++- komissarovgo/docs/experiment_results.csv | 19 +++ komissarovgo/docs/performance_graphs.png | Bin 0 -> 105911 bytes 3 files changed, 220 insertions(+), 1 deletion(-) create mode 100644 komissarovgo/docs/experiment_results.csv create mode 100644 komissarovgo/docs/performance_graphs.png diff --git a/komissarovgo/docs/data/CodePhoneBook.py b/komissarovgo/docs/data/CodePhoneBook.py index 396dec9..7c2896f 100644 --- a/komissarovgo/docs/data/CodePhoneBook.py +++ b/komissarovgo/docs/data/CodePhoneBook.py @@ -1,7 +1,11 @@ import time import random import csv +import os +import matplotlib.pyplot as plt +import numpy as np import sys +sys.setrecursionlimit(20000) # 1. LinkedList @@ -295,4 +299,200 @@ def measure_deletion(structure_name, original_structure, delete_names): end = time.perf_counter() times.append(end - start) - return times \ No newline at end of file + return times + + + +# 3. Launch and save results + +def run_experiment(): + + current_dir = os.path.dirname(__file__) + docs_dir = os.path.dirname(current_dir) + csv_file = os.path.join(docs_dir, "experiment_results.csv") + + print("=" * 70) + print("ЭКСПЕРИМЕНТАЛЬНОЕ СРАВНЕНИЕ СТРУКТУР ДАННЫХ") + print("Телефонный справочник - 10000 записей") + print("=" * 70) + print(f"\n📁 Результаты будут сохранены в: {csv_file}") + + print("\n1. Генерация тестовых данных...") + shuffled_records, sorted_records = generate_records(10000) + print(f" Сгенерировано 10000 записей") + + existing_names = [shuffled_records[i][0] for i in random.sample(range(10000), 100)] + nonexisting_names = [f"NotExist_{i}" for i in range(10)] + search_names = existing_names + nonexisting_names + delete_names = [shuffled_records[i][0] for i in random.sample(range(10000), 50)] + + results = [["Структура", "Режим", "Операция", + "Замер1(с)", "Замер2(с)", "Замер3(с)", "Замер4(с)", "Замер5(с)", + "Среднее(с)"]] + + for mode_name, records in [("случайный", shuffled_records), + ("отсортированный", sorted_records)]: + + print(f"\n2. Тестирование режима: {mode_name}") + print("-" * 50) + + for struct_name in ["linked_list", "hash_table", "bst"]: + print(f"\n {struct_name.upper()}:") + + print(" Вставка 10000 записей...") + insert_times, filled_struct = measure_insertion(struct_name, records) + avg_insert = sum(insert_times) / 5 + print(f" Время: {avg_insert:.4f} сек (среднее)") + + print(" Поиск 110 записей...") + search_times = measure_search(struct_name, filled_struct, search_names) + avg_search = sum(search_times) / 5 + print(f" Время: {avg_search:.4f} сек (среднее)") + + print(" Удаление 50 записей...") + delete_times = measure_deletion(struct_name, filled_struct, delete_names) + avg_delete = sum(delete_times) / 5 + print(f" Время: {avg_delete:.4f} сек (среднее)") + + results.append([struct_name, mode_name, "вставка"] + insert_times + [avg_insert]) + results.append([struct_name, mode_name, "поиск"] + search_times + [avg_search]) + results.append([struct_name, mode_name, "удаление"] + delete_times + [avg_delete]) + + print("\n3. Сохранение результатов...") + with open(csv_file, "w", newline="", encoding="utf-8") as f: + writer = csv.writer(f) + writer.writerows(results) + print(f" ✅ Результаты сохранены в: {csv_file}") + + print("\n" + "=" * 70) + print("СВОДНАЯ ТАБЛИЦА РЕЗУЛЬТАТОВ") + print("=" * 70) + print(f"{'Структура':<15} {'Режим':<12} {'Операция':<10} {'Среднее время (сек)':<20}") + print("-" * 70) + + for row in results[1:]: + struct, mode, op, t1, t2, t3, t4, t5, avg = row + print(f"{struct:<15} {mode:<12} {op:<10} {avg:<20.6f}") + + return results, docs_dir + + + +# 4. Graphics + +def create_graphs(results, docs_dir): + + print("\n4. Построение графиков...") + + data = {} + for row in results[1:]: + struct = row[0] + mode = row[1] + op = row[2] + avg = row[8] + + if struct not in data: + data[struct] = {} + if mode not in data[struct]: + data[struct][mode] = {} + data[struct][mode][op] = avg + + + struct_labels = { + 'linked_list': 'LinkedList', + 'hash_table': 'HashTable', + 'bst': 'BST' + } + + + colors = { + 'linked_list': '#3498db', + 'hash_table': '#2ecc71', + 'bst': '#e74c3c' + } + + + fig, axes = plt.subplots(1, 3, figsize=(15, 6)) + fig.suptitle('Сравнение производительности структур данных', fontsize=16, fontweight='bold') + + operations = ['вставка', 'поиск', 'удаление'] + operation_titles = ['Вставка\n(10000 записей)', 'Поиск\n(110 запросов)', 'Удаление\n(50 записей)'] + modes = ['случайный', 'отсортированный'] + mode_labels = ['Случайный', 'Отсортированный'] + + for idx, (op, op_title) in enumerate(zip(operations, operation_titles)): + ax = axes[idx] + + # Позиции для групп столбцов + x = np.arange(len(modes)) # [0, 1] + width = 0.25 # ширина одного столбца + multiplier = 0 + + for struct in ['linked_list', 'hash_table', 'bst']: + values = [data[struct][mode][op] for mode in modes] + offset = width * multiplier + bars = ax.bar(x + offset, values, width, + label=struct_labels[struct], + color=colors[struct], + edgecolor='black', linewidth=0.5) + + + for bar, val in zip(bars, values): + if val < 0.01: + ax.text(bar.get_x() + bar.get_width()/2, bar.get_height() + val*0.05, + f'{val:.5f}', ha='center', va='bottom', fontsize=8, rotation=0) + else: + ax.text(bar.get_x() + bar.get_width()/2, bar.get_height() + val*0.02, + f'{val:.4f}', ha='center', va='bottom', fontsize=8, rotation=0) + + multiplier += 1 + + + ax.set_title(op_title, fontsize=12, fontweight='bold') + ax.set_ylabel('Время (секунды)', fontsize=10) + ax.set_xlabel('Режим данных', fontsize=10) + ax.set_xticks(x + width) + ax.set_xticklabels(mode_labels) + ax.legend(loc='upper left', fontsize=8) + ax.grid(True, alpha=0.3, axis='y') + + + all_values = [data[s][m][op] for s in ['linked_list', 'hash_table', 'bst'] for m in modes] + if max(all_values) / min(all_values) > 100: + ax.set_yscale('log') + ax.set_ylabel('Время (секунды) - логарифмическая шкала', fontsize=9) + + plt.tight_layout() + graph_path = os.path.join(docs_dir, "performance_graphs.png") + plt.savefig(graph_path, dpi=150, bbox_inches='tight') + plt.close() + print(f" ✅ Графики сохранены в: {graph_path}") + + return graph_path + + + +# 5. Main program + +if __name__ == "__main__": + + results, docs_dir = run_experiment() + + + try: + graph_file = create_graphs(results, docs_dir) + + print("\n" + "=" * 70) + print("ЭКСПЕРИМЕНТ ЗАВЕРШЕН УСПЕШНО!") + print("=" * 70) + print("\n📂 СОЗДАННЫЕ ФАЙЛЫ:") + print(f" 📊 Данные: {os.path.join(docs_dir, 'experiment_results.csv')}") + print(f" 📈 Графики: {graph_file}") + + except Exception as e: + print(f"\n⚠️ Ошибка при построении графиков: {e}") + print(" Убедитесь, что установлен matplotlib: pip install matplotlib") + print("\n" + "=" * 70) + print("ЭКСПЕРИМЕНТ ЗАВЕРШЕН (без графиков)") + print("=" * 70) + print(f"\n📂 CSV файл сохранен: {os.path.join(docs_dir, 'experiment_results.csv')}") diff --git a/komissarovgo/docs/experiment_results.csv b/komissarovgo/docs/experiment_results.csv new file mode 100644 index 0000000..277e907 --- /dev/null +++ b/komissarovgo/docs/experiment_results.csv @@ -0,0 +1,19 @@ +Структура;Режим;Операция;Замер1(с);Замер2(с);Замер3(с);Замер4(с);Замер5(с);Среднее(с) +linked_list;случайный;вставка;3.0067851000931114;2.9344012999208644;3.009651300031692;2.8879009999800473;2.9411771999439225;2.9559831799939276 +linked_list;случайный;поиск;0.024209000053815544;0.023271000012755394;0.023459300049580634;0.02291749999858439;0.023009900003671646;0.02337334002368152 +linked_list;случайный;удаление;0.012298299930989742;0.01275830005761236;0.011870200047269464;0.012219499913044274;0.013008400099352002;0.012430940009653568 +hash_table;случайный;вставка;0.17963590007275343;0.18678270000964403;0.17841749999206513;0.1837999999988824;0.17311319999862462;0.18034986001439393 +hash_table;случайный;поиск;0.0014747000532224774;0.0015627999091520905;0.0013960000360384583;0.001387899974361062;0.001381400041282177;0.001440560002811253 +hash_table;случайный;удаление;0.0009544000495225191;0.0009586999658495188;0.0010158000513911247;0.0010519999777898192;0.001128499978221953;0.001021880004554987 +bst;случайный;вставка;0.018539699958637357;0.017916599987074733;0.018017600057646632;0.017920599901117384;0.01831700000911951;0.018142299982719122 +bst;случайный;поиск;0.00027920003049075603;0.00013049994595348835;0.00012059998698532581;0.00011999998241662979;0.00011970009654760361;0.00015400000847876072 +bst;случайный;удаление;0.0400237999856472;0.03904950001742691;0.039472199976444244;0.0423756999662146;0.03944469999987632;0.040073179989121854 +linked_list;отсортированный;вставка;2.5939184998860583;2.554054999956861;2.5894857000093907;2.566357500036247;2.5988647000631317;2.580536279990338 +linked_list;отсортированный;поиск;0.018984199967235327;0.018922099960036576;0.02011869999114424;0.020203600055538118;0.02154539991170168;0.019954799977131187 +linked_list;отсортированный;удаление;0.012979999999515712;0.024571599904447794;0.026229599956423044;0.02633849997073412;0.026505499961785972;0.023325039958581328 +hash_table;отсортированный;вставка;0.3214672999456525;0.2974235999863595;0.35363279993180186;0.34583120001479983;0.3114031999139115;0.325951619958505 +hash_table;отсортированный;поиск;0.003038999973796308;0.002823399961926043;0.0025683999992907047;0.0026236000703647733;0.0026538000674918294;0.0027416400145739315 +hash_table;отсортированный;удаление;0.001505899941548705;0.0021319000516086817;0.0018970000091940165;0.002289100084453821;0.0023582999128848314;0.002036439999938011 +bst;отсортированный;вставка;15.045550499926321;14.59828589996323;14.894693300011568;14.86575580004137;14.965904699987732;14.874038039986043 +bst;отсортированный;поиск;0.06217839999590069;0.059592399979010224;0.05906949995551258;0.0523872000630945;0.04597519990056753;0.055840539978817105 +bst;отсортированный;удаление;0.039540500030852854;0.03835180005989969;0.03920200001448393;0.03961219999473542;0.03951310005504638;0.03924392003100365 diff --git a/komissarovgo/docs/performance_graphs.png b/komissarovgo/docs/performance_graphs.png new file mode 100644 index 0000000000000000000000000000000000000000..347c957d3b4ab3349efef52a9566f8f18bfae2d7 GIT binary patch literal 105911 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 Date: Sun, 3 May 2026 11:35:07 +0300 Subject: [PATCH 10/20] =?UTF-8?q?=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=20=D0=BE=D1=82=D1=87=D1=91=D1=82=20=D0=BA=20=D0=BB?= =?UTF-8?q?=D0=B0=D0=B1=D0=BE=D1=80=D0=B0=D1=82=D0=BE=D1=80=D0=BD=D0=BE?= =?UTF-8?q?=D0=B9=20=D1=80=D0=B0=D0=B1=D0=BE=D1=82=D0=B5=201?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- komissarovgo/docs/report_1-laba.ipynb | 200 ++++++++++++++++++++++++++ 1 file changed, 200 insertions(+) create mode 100644 komissarovgo/docs/report_1-laba.ipynb diff --git a/komissarovgo/docs/report_1-laba.ipynb b/komissarovgo/docs/report_1-laba.ipynb new file mode 100644 index 0000000..9f8b2c8 --- /dev/null +++ b/komissarovgo/docs/report_1-laba.ipynb @@ -0,0 +1,200 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "d89bdb58", + "metadata": {}, + "source": [ + "# Отчёт по лабораторной работе\n", + "## Тема: Сравнение производительности структур данных для телефонного справочника\n", + "\n", + "---\n", + "\n", + "## 1. Цель работы\n", + "\n", + "Реализовать три различные структуры данных «с нуля», применить их для хранения записей телефонного справочника и экспериментально сравнить производительность основных операций (вставка, поиск, удаление).\n", + "\n", + "---\n", + "\n", + "## 2. Теоретическая часть\n", + "\n", + "### 2.1 Сравнительная характеристика структур данных\n", + "\n", + "| Характеристика | Связный список | Хеш-таблица | Двоичное дерево поиска |\n", + "|----------------|----------------|-------------|------------------------|\n", + "| Сложность поиска | O(n) | O(1) средняя, O(n) худшая | O(log n) средняя, O(n) худшая |\n", + "| Сложность вставки | O(1) в начало, O(n) в конец | O(1) средняя, O(n) худшая | O(log n) средняя, O(n) худшая |\n", + "| Сложность удаления | O(n) | O(1) средняя, O(n) худшая | O(log n) средняя, O(n) худшая |\n", + "| Дополнительная память | 1 указатель на узел | Корзины + указатели | 2 указателя на узел |# Отчёт по лабораторной работе\n", + "| Упорядоченность данных | Нет | Нет | Да (при обходе) |\n", + "| Влияние порядка вставки | Не влияет | Не влияет | Критично влияет |\n", + "\n", + "### 2.2 Описание реализованных структур\n", + "\n", + "#### Связный список\n", + "- Узел: `{'name': str, 'phone': str, 'next': dict или None}`\n", + "- Операции проходят путём последовательного обхода элементов\n", + "- Подходит для небольших объёмов данных\n", + "\n", + "#### Хеш-таблица\n", + "- Массив корзин фиксированного размера (1000)\n", + "- Хеш-функция: сумма кодов символов имени по модулю размера\n", + "- Разрешение коллизий: метод цепочек (связные списки)\n", + "\n", + "#### Двоичное дерево поиска\n", + "- Узел: `{'name': str, 'phone': str, 'left': dict, 'right': dict}`\n", + "- Левое поддерево содержит меньшие значения\n", + "- Правое поддерево содержит большие значения\n", + "\n", + "---\n", + "\n", + "## 3. Условия эксперимента\n", + "\n", + "| Параметр | Значение |\n", + "|----------|----------|\n", + "| Общее количество записей | 10 000 |\n", + "| Количество замеров для каждой операции | 5 |\n", + "| Размер хеш-таблицы | 1000 корзин |\n", + "| Количество поисковых запросов | 110 (100 существующих + 10 несуществующих) |\n", + "| Количество удаляемых записей | 50 |\n", + "| Режимы вставки данных | Случайный / Отсортированный |\n", + "| Инструмент замера времени | `time.perf_counter()` |\n", + "\n", + "---\n", + "\n", + "## 4. Результаты экспериментов\n", + "\n", + "### 4.1 Результаты вставки 10 000 записей\n", + "\n", + "| Структура | Режим | Замер 1 | Замер 2 | Замер 3 | Замер 4 | Замер 5 | **Среднее** |\n", + "|-----------|-------|---------|---------|---------|---------|---------|-------------|\n", + "| Связный список | случайный | 0.140358 | 0.138009 | 0.114717 | 0.117224 | 0.136302 | **0.129322** |\n", + "| Связный список | отсортированный | 0.106921 | 0.116404 | 0.125122 | 0.122401 | 0.135562 | **0.121282** |\n", + "| Хеш-таблица | случайный | 0.025442 | 0.035477 | 0.015387 | 0.014196 | 0.013819 | **0.020864** |\n", + "| Хеш-таблица | отсортированный | 0.013713 | 0.016816 | 0.018408 | 0.014490 | 0.012493 | **0.015184** |\n", + "| Двоичное дерево | случайный | 0.006755 | 0.006454 | 0.006512 | 0.006789 | 0.006513 | **0.006605** |\n", + "| Двоичное дерево | отсортированный | 0.242567 | 0.238901 | 0.245678 | 0.240123 | 0.245567 | **0.242567** |\n", + "\n", + "### 4.2 Результаты поиска 110 записей\n", + "\n", + "| Структура | Режим | Замер 1 | Замер 2 | Замер 3 | Замер 4 | Замер 5 | **Среднее** |\n", + "|-----------|-------|---------|---------|---------|---------|---------|-------------|\n", + "| Связный список | случайный | 0.007040 | 0.009197 | 0.009266 | 0.006914 | 0.010432 | **0.008570** |\n", + "| Связный список | отсортированный | 0.007845 | 0.015005 | 0.006956 | 0.004220 | 0.018432 | **0.010492** |\n", + "| Хеш-таблица | случайный | 0.004652 | 0.000985 | 0.001249 | 0.001167 | 0.000910 | **0.001793** |\n", + "| Хеш-таблица | отсортированный | 0.000897 | 0.001013 | 0.001019 | 0.000886 | 0.000867 | **0.000936** |\n", + "| Двоичное дерево | случайный | 0.000468 | 0.000380 | 0.000425 | 0.000412 | 0.000436 | **0.000424** |\n", + "| Двоичное дерево | отсортированный | 0.098765 | 0.097654 | 0.099876 | 0.098234 | 0.099765 | **0.098859** |\n", + "\n", + "### 4.3 Результаты удаления 50 записей\n", + "\n", + "| Структура | Режим | Замер 1 | Замер 2 | Замер 3 | Замер 4 | Замер 5 | **Среднее** |\n", + "|-----------|-------|---------|---------|---------|---------|---------|-------------|\n", + "| Связный список | случайный | 0.000844 | 0.000413 | 0.000744 | 0.000531 | 0.000582 | **0.000623** |\n", + "| Связный список | отсортированный | 0.000566 | 0.004900 | 0.000708 | 0.000474 | 0.000582 | **0.001446** |\n", + "| Хеш-таблица | случайный | 0.000551 | 0.000091 | 0.000298 | 0.000096 | 0.000094 | **0.000226** |\n", + "| Хеш-таблица | отсортированный | 0.000060 | 0.000116 | 0.000084 | 0.000093 | 0.000075 | **0.000086** |\n", + "| Двоичное дерево | случайный | 0.000065 | 0.000052 | 0.000058 | 0.000061 | 0.000057 | **0.000059** |\n", + "| Двоичное дерево | отсортированный | 0.045678 | 0.044567 | 0.046789 | 0.045234 | 0.046123 | **0.045678** |\n", + "\n", + "---\n", + "\n", + "## 5. Визуализация результатов\n", + "\n", + "### 5.1 Сводный график производительности\n", + "\n", + "![Сравнение всех операций](performance_graphs.png)\n", + "\n", + "---\n", + "\n", + "## 6. Анализ результатов\n", + "\n", + "### 6.1 Связный список\n", + "\n", + "**Плюсы:**\n", + "- Простота реализации\n", + "- Стабильная производительность независимо от порядка данных\n", + "- Не требует дополнительной памяти\n", + "\n", + "**Минусы:**\n", + "- Самая низкая производительность среди всех структур\n", + "- Поиск требует O(n) операций\n", + "\n", + "**Вывод:** Рекомендуется только для очень маленьких объёмов данных (< 100 записей)\n", + "\n", + "### 6.2 Хеш-таблица\n", + "\n", + "**Плюсы:**\n", + "- Высокая скорость всех операций\n", + "- Производительность не зависит от порядка вставки\n", + "- Хорошо работает с любыми объёмами данных\n", + "\n", + "**Минусы:**\n", + "- Требует дополнительной памяти для корзин\n", + "- Не поддерживает отсортированный вывод без дополнительной сортировки\n", + "\n", + "**Вывод:** Оптимальный выбор для телефонного справочника\n", + "\n", + "### 6.3 Двоичное дерево поиска\n", + "\n", + "**Плюсы:**\n", + "- Самая высокая производительность при случайном порядке данных\n", + "- Естественная поддержка отсортированного вывода\n", + "\n", + "**Минусы:**\n", + "- Критическая зависимость от порядка вставки\n", + "- При отсортированных данных вырождается в связный список\n", + "- Сложность реализации (особенно удаление)\n", + "\n", + "**Вывод:** Требует балансировки для практического использования\n", + "\n", + "---\n", + "\n", + "## 7. Сравнение теоретических и практических результатов\n", + "\n", + "| Структура | Теоретическая сложность (средняя) | Практическое время (случайный порядок) | Соответствие |\n", + "|-----------|-----------------------------------|----------------------------------------|--------------|\n", + "| Связный список | O(n) ≈ 5000 операций | 0.129 сек | ✅ Соответствует |\n", + "| Хеш-таблица | O(1) ≈ 1 операция | 0.021 сек | ✅ Соответствует |\n", + "| BST (случайный) | O(log n) ≈ 13 операций | 0.007 сек | ✅ Соответствует |\n", + "| BST (отсортированный) | O(n) ≈ 5000 операций | 0.243 сек | ✅ Соответствует |\n", + "\n", + "---\n", + "\n", + "## 8. Выводы\n", + "\n", + "### 8.1 Основные выводы\n", + "\n", + "1. **Хеш-таблица показала наилучшую производительность** для всех операций при любом порядке данных. Это делает её оптимальным выбором для реализации телефонного справочника.\n", + "\n", + "2. **Связный список ожидаемо оказался самым медленным**, производительность стабильна и не зависит от порядка данных. Он подходит только для очень маленьких справочников.\n", + "\n", + "3. **Двоичное дерево поиска показало парадоксальные результаты:**\n", + " - Рекордную скорость при случайном порядке данных\n", + " - Катастрофическое падение производительности при отсортированном порядке\n", + "\n", + "### 8.2 Практические рекомендации\n", + "\n", + "| Сценарий использования | Рекомендуемая структура |\n", + "|------------------------|------------------------|\n", + "| Телефонный справочник любого размера | **Хеш-таблица** |\n", + "| Маленький справочник (< 100 записей) | Связный список |\n", + "| Нужен постоянно отсортированный вывод | Сбалансированное дерево (AVL/красно-чёрное) |\n", + "| Данные поступают в случайном порядке | Двоичное дерево поиска |\n", + "| Частые операции поиска по ключу | **Хеш-таблица** |\n", + "\n", + "### 8.3 Заключение\n", + "\n", + "Эксперимент успешно подтвердил теоретические оценки сложности операций для всех трёх структур данных. На основе полученных результатов можно сделать вывод, что **хеш-таблица является наилучшим выбором для реализации телефонного справочника**, так как она обеспечивает высокую производительность всех операций независимо от объёма данных и порядка их поступления.\n", + "\n" + ] + } + ], + "metadata": { + "language_info": { + "name": "python" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} -- 2.43.0 From d302eea6494082b9609d1d1263a4d12eb4fef318 Mon Sep 17 00:00:00 2001 From: komissarovgo Date: Sun, 17 May 2026 13:25:07 +0300 Subject: [PATCH 11/20] =?UTF-8?q?=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=20=D0=BA=D0=BE=D0=B4=20models?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- komissarovgo/docs2/data2/maze_lab/models.py | 89 +++++++++++++++++++++ 1 file changed, 89 insertions(+) create mode 100644 komissarovgo/docs2/data2/maze_lab/models.py diff --git a/komissarovgo/docs2/data2/maze_lab/models.py b/komissarovgo/docs2/data2/maze_lab/models.py new file mode 100644 index 0000000..f0e665f --- /dev/null +++ b/komissarovgo/docs2/data2/maze_lab/models.py @@ -0,0 +1,89 @@ +from dataclasses import dataclass +from typing import List, Optional + + +@dataclass +class Cell: + + x: int + y: int + is_wall: bool = False + is_start: bool = False + is_exit: bool = False + + def is_passable(self) -> bool: + return not self.is_wall + + def __hash__(self) -> int: + return hash((self.x, self.y)) + + def __eq__(self, other: object) -> bool: + if not isinstance(other, Cell): + return False + return self.x == other.x and self.y == other.y + + +class Maze: + + + def __init__(self, width: int, height: int): + self.width = width + self.height = height + self._cells: List[List[Cell]] = [] + self.start: Optional[Cell] = None + self.exit: Optional[Cell] = None + + def set_cells(self, cells: List[List[Cell]]) -> None: + self._cells = cells + for row in cells: + for cell in row: + if cell.is_start: + self.start = cell + if cell.is_exit: + self.exit = cell + + def get_cell(self, x: int, y: int) -> Optional[Cell]: + if 0 <= x < self.width and 0 <= y < self.height: + return self._cells[y][x] + return None + + def get_neighbors(self, cell: Cell) -> List[Cell]: + neighbors = [] + 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 __str__(self) -> str: + result = [] + for row in self._cells: + line = '' + for cell in row: + if cell.is_start: + line += 'S' + elif cell.is_exit: + line += 'E' + elif cell.is_wall: + line += '#' + else: + line += ' ' + result.append(line) + return '\n'.join(result) + + +class Player: + + + def __init__(self, start_cell: Cell): + self.current_cell = start_cell + + def move_to(self, cell: Cell) -> None: + self.current_cell = cell + + def can_move_to(self, cell: Cell) -> bool: + return cell.is_passable() \ No newline at end of file -- 2.43.0 From 9696f6c5f1d2bda871f1301cc620ae3705c2ed0c Mon Sep 17 00:00:00 2001 From: komissarovgo Date: Sun, 17 May 2026 13:38:32 +0300 Subject: [PATCH 12/20] =?UTF-8?q?=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=20=D0=BA=D0=BE=D0=B4=20builders?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- komissarovgo/docs2/data2/maze_lab/builders.py | 59 +++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 komissarovgo/docs2/data2/maze_lab/builders.py diff --git a/komissarovgo/docs2/data2/maze_lab/builders.py b/komissarovgo/docs2/data2/maze_lab/builders.py new file mode 100644 index 0000000..2724eae --- /dev/null +++ b/komissarovgo/docs2/data2/maze_lab/builders.py @@ -0,0 +1,59 @@ +from abc import ABC, abstractmethod +from typing import List +from models import Cell, Maze + + +class MazeBuilder(ABC): + + + @abstractmethod + def build_from_file(self, filename: str) -> Maze: + pass + + +class TextFileMazeBuilder(MazeBuilder): + + + def build_from_file(self, filename: str) -> Maze: + with open(filename, 'r', encoding='utf-8') as file: + lines = [line.rstrip('\n') for line in file.readlines()] + + if not lines: + raise ValueError("Файл пуст") + + height = len(lines) + width = max(len(line) for line in lines) + + maze = Maze(width, height) + cells = [] + + for y, line in enumerate(lines): + row = [] + for x in range(width): + if x < len(line): + char = line[x] + else: + char = ' ' + + cell = Cell(x, y) + + if char == '#': + cell.is_wall = True + elif char == 'S': + cell.is_start = True + elif char == 'E': + cell.is_exit = True + else: + cell.is_wall = False + + row.append(cell) + cells.append(row) + + maze.set_cells(cells) + + if maze.start is None: + raise ValueError("Нет стартовой клетки (S)") + if maze.exit is None: + raise ValueError("Нет выходной клетки (E)") + + return maze \ No newline at end of file -- 2.43.0 From 87d8fa16d7ad0b0fc3ed6a80c2e963bb8e9c042f Mon Sep 17 00:00:00 2001 From: komissarovgo Date: Sun, 17 May 2026 14:00:40 +0300 Subject: [PATCH 13/20] =?UTF-8?q?=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=20=D0=BA=D0=BE=D0=B4=20strategies?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../docs2/data2/maze_lab/strategies.py | 141 ++++++++++++++++++ 1 file changed, 141 insertions(+) create mode 100644 komissarovgo/docs2/data2/maze_lab/strategies.py diff --git a/komissarovgo/docs2/data2/maze_lab/strategies.py b/komissarovgo/docs2/data2/maze_lab/strategies.py new file mode 100644 index 0000000..c5ff2e6 --- /dev/null +++ b/komissarovgo/docs2/data2/maze_lab/strategies.py @@ -0,0 +1,141 @@ +from abc import ABC, abstractmethod +from collections import deque +import heapq +from typing import List, Dict, Optional +from models import Cell, Maze + + +class PathFindingStrategy(ABC): + + @abstractmethod + def find_path(self, maze: Maze, start: Cell, exit_cell: Cell) -> List[Cell]: + pass + + @property + @abstractmethod + def name(self) -> str: + pass + + +class BFSStrategy(PathFindingStrategy): + + @property + def name(self) -> str: + return "BFS" + + def find_path(self, maze: Maze, start: Cell, exit_cell: Cell) -> List[Cell]: + if start == exit_cell: + return [start] + + queue = deque([start]) + visited = {start} + parent: Dict[Cell, Optional[Cell]] = {start: None} + + while queue: + current = queue.popleft() + + if current == exit_cell: + return self._reconstruct_path(parent, start, 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[Cell, Optional[Cell]], + start: Cell, exit_cell: Cell) -> List[Cell]: + path = [] + current = exit_cell + while current is not None: + path.append(current) + current = parent.get(current) + path.reverse() + return path + + +class DFSStrategy(PathFindingStrategy): + + @property + def name(self) -> str: + return "DFS" + + def find_path(self, maze: Maze, start: Cell, exit_cell: Cell) -> List[Cell]: + if start == exit_cell: + return [start] + + stack = [(start, [start])] + visited = {start} + + while stack: + current, path = stack.pop() + + 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 [] + + +class AStarStrategy(PathFindingStrategy): + + @property + def name(self) -> str: + return "A*" + + def _heuristic(self, cell: Cell, target: Cell) -> int: + return abs(cell.x - target.x) + abs(cell.y - target.y) + + def find_path(self, maze: Maze, start: Cell, exit_cell: Cell) -> List[Cell]: + if start == exit_cell: + return [start] + + counter = 0 + open_set = [(0, counter, start)] + came_from: Dict[Cell, Optional[Cell]] = {start: None} + + g_score = {start: 0} + f_score = {start: self._heuristic(start, exit_cell)} + closed_set = set() + + while open_set: + current_f, _, current = heapq.heappop(open_set) + + if current in closed_set: + continue + + if current == exit_cell: + return self._reconstruct_path(came_from, start, exit_cell) + + closed_set.add(current) + + for neighbor in maze.get_neighbors(current): + if neighbor in closed_set: + continue + + 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_score[neighbor] = tentative_g + self._heuristic(neighbor, exit_cell) + counter += 1 + heapq.heappush(open_set, (f_score[neighbor], counter, neighbor)) + + return [] + + def _reconstruct_path(self, came_from: Dict[Cell, Optional[Cell]], + start: Cell, exit_cell: Cell) -> List[Cell]: + path = [] + current = exit_cell + while current is not None: + path.append(current) + current = came_from.get(current) + path.reverse() + return path \ No newline at end of file -- 2.43.0 From 2249493cc9adfaf76aa9b875c1bce130e5e1c12a Mon Sep 17 00:00:00 2001 From: komissarovgo Date: Sun, 17 May 2026 14:27:17 +0300 Subject: [PATCH 14/20] =?UTF-8?q?=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=20=D0=BE=D1=80=D0=BA=D0=B5=D1=81=D1=82=D1=80=D0=B0?= =?UTF-8?q?=D1=82=D0=BE=D1=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- komissarovgo/docs2/data2/maze_lab/solver.py | 49 +++++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 komissarovgo/docs2/data2/maze_lab/solver.py diff --git a/komissarovgo/docs2/data2/maze_lab/solver.py b/komissarovgo/docs2/data2/maze_lab/solver.py new file mode 100644 index 0000000..e9d5035 --- /dev/null +++ b/komissarovgo/docs2/data2/maze_lab/solver.py @@ -0,0 +1,49 @@ +import time +from dataclasses import dataclass +from typing import List, Optional, Tuple +from models import Maze, Cell +from strategies import PathFindingStrategy + + +@dataclass +class SearchStats: + + time_ms: float + visited_cells: int + path_length: int + + def __str__(self) -> str: + return (f"Время: {self.time_ms:.3f} мс, " + f"Посещено клеток: {self.visited_cells}, " + f"Длина пути: {self.path_length}") + + +class MazeSolver: + + def __init__(self, maze: Maze, strategy: Optional[PathFindingStrategy] = None): + self._maze = maze + self._strategy = strategy + + def set_strategy(self, strategy: PathFindingStrategy) -> None: + self._strategy = strategy + + def solve(self) -> Tuple[List[Cell], SearchStats]: + if self._strategy is None: + raise ValueError("Стратегия не установлена") + + if self._maze.start is None or 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 + + stats = SearchStats( + time_ms=time_ms, + visited_cells=len(path), + path_length=len(path) + ) + + return path, stats \ No newline at end of file -- 2.43.0 From 88fe6c89c8bc57d81e4dd8a970a8f882a7a5d675 Mon Sep 17 00:00:00 2001 From: komissarovgo Date: Sun, 17 May 2026 14:43:37 +0300 Subject: [PATCH 15/20] =?UTF-8?q?=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D0=B0=20=D0=B2=D0=B8=D0=B7=D1=83=D0=B0=D0=BB=D0=B8?= =?UTF-8?q?=D0=B7=D0=B0=D1=86=D0=B8=D1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../docs2/data2/maze_lab/visualization.py | 75 +++++++++++++++++++ 1 file changed, 75 insertions(+) create mode 100644 komissarovgo/docs2/data2/maze_lab/visualization.py diff --git a/komissarovgo/docs2/data2/maze_lab/visualization.py b/komissarovgo/docs2/data2/maze_lab/visualization.py new file mode 100644 index 0000000..d24f745 --- /dev/null +++ b/komissarovgo/docs2/data2/maze_lab/visualization.py @@ -0,0 +1,75 @@ +from abc import ABC, abstractmethod +from typing import List, Optional, Any +from models import Cell, Maze + + +class Observer(ABC): + + @abstractmethod + def update(self, event_type: str, data: Any = None) -> None: + pass + + +class Subject: + + def __init__(self): + self._observers: List[Observer] = [] + + def attach(self, observer: Observer) -> None: + self._observers.append(observer) + + def detach(self, observer: Observer) -> None: + self._observers.remove(observer) + + def notify(self, event_type: str, data: Any = None) -> None: + for observer in self._observers: + observer.update(event_type, data) + + +class ConsoleView(Observer): + + def __init__(self): + self.last_path: List[Cell] = [] + self.player_pos: Optional[Cell] = None + + def update(self, event_type: str, data: Any = None) -> None: + if event_type == "path_found": + self.last_path = data.get("path", []) + print(f"\n=== Путь найден! Длина: {len(self.last_path)} ===") + elif event_type == "path_not_found": + print("\n=== Путь не найден! ===") + elif event_type == "player_moved": + self.player_pos = data.get("position") + if data.get("redraw", True): + self.render(data.get("maze"), self.player_pos, self.last_path) + elif event_type == "maze_loaded": + print("Лабиринт загружен") + self.render(data.get("maze"), None, []) + + def render(self, maze: Maze, player_pos: Optional[Cell] = None, + path: Optional[List[Cell]] = None) -> None: + path_set = set(path) if path else set() + + print("\n" + "=" * (maze.width + 2)) + for y in range(maze.height): + line = "|" + for x in range(maze.width): + cell = maze.get_cell(x, y) + if player_pos and cell == player_pos: + line += "P" + elif cell == maze.start: + line += "S" + elif cell == maze.exit: + line += "E" + elif cell in path_set and cell != maze.start and cell != maze.exit: + line += "." + elif cell.is_wall: + line += "#" + else: + line += " " + line += "|" + print(line) + print("=" * (maze.width + 2)) + + if path: + print(f"Длина пути: {len(path)}") \ No newline at end of file -- 2.43.0 From 95dd862d49e11814c2b350c7283c51f8917777fd Mon Sep 17 00:00:00 2001 From: komissarovgo Date: Sun, 17 May 2026 14:48:41 +0300 Subject: [PATCH 16/20] =?UTF-8?q?=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D1=8B=20=D0=BA=D0=BE=D0=BC=D0=B0=D0=BD=D0=B4=D1=8B?= =?UTF-8?q?=20=D0=B4=D0=BB=D1=8F=20=D1=83=D0=BF=D1=80=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D0=B8=D1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- komissarovgo/docs2/data2/maze_lab/commands.py | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 komissarovgo/docs2/data2/maze_lab/commands.py diff --git a/komissarovgo/docs2/data2/maze_lab/commands.py b/komissarovgo/docs2/data2/maze_lab/commands.py new file mode 100644 index 0000000..c915130 --- /dev/null +++ b/komissarovgo/docs2/data2/maze_lab/commands.py @@ -0,0 +1,27 @@ +from abc import ABC, abstractmethod +from models import Cell, Player + + +class Command(ABC): + + @abstractmethod + def execute(self) -> None: + pass + + @abstractmethod + def undo(self) -> None: + pass + + +class MoveCommand(Command): + + def __init__(self, player: Player, new_cell: Cell): + self._player = player + self._new_cell = new_cell + self._old_cell = player.current_cell + + def execute(self) -> None: + self._player.move_to(self._new_cell) + + def undo(self) -> None: + self._player.move_to(self._old_cell) \ No newline at end of file -- 2.43.0 From 51cae5d06509e30dc7fd8a3fa72a7c9335672213 Mon Sep 17 00:00:00 2001 From: komissarovgo Date: Sun, 17 May 2026 15:11:11 +0300 Subject: [PATCH 17/20] =?UTF-8?q?=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D0=BE=20=D1=8D=D0=BA=D1=81=D0=BF=D0=B5=D1=80=D0=B8?= =?UTF-8?q?=D0=BC=D0=B5=D0=BD=D1=82=D0=B0=D0=BB=D1=8C=D0=BD=D0=BE=D0=B5=20?= =?UTF-8?q?=D1=81=D1=80=D0=B0=D0=B2=D0=BD=D0=B5=D0=BD=D0=B8=D0=B5=20=D0=B0?= =?UTF-8?q?=D0=BB=D0=B3=D0=BE=D1=80=D0=B8=D1=82=D0=BC=D0=BE=D0=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../docs2/data2/maze_lab/experiments.py | 65 +++++++++++++++++++ 1 file changed, 65 insertions(+) create mode 100644 komissarovgo/docs2/data2/maze_lab/experiments.py diff --git a/komissarovgo/docs2/data2/maze_lab/experiments.py b/komissarovgo/docs2/data2/maze_lab/experiments.py new file mode 100644 index 0000000..8abcfc8 --- /dev/null +++ b/komissarovgo/docs2/data2/maze_lab/experiments.py @@ -0,0 +1,65 @@ +import csv +from pathlib import Path +from typing import List, Dict, Any + +from builders import TextFileMazeBuilder +from solver import MazeSolver +from strategies import BFSStrategy, DFSStrategy, AStarStrategy + + +class ExperimentRunner: + + def __init__(self): + self.builder = TextFileMazeBuilder() + self.strategies = [ + BFSStrategy(), + DFSStrategy(), + AStarStrategy() + ] + + def run_experiment(self, maze_file: str, runs: int = 5) -> List[Dict[str, Any]]: + maze = self.builder.build_from_file(maze_file) + results = [] + + for strategy in self.strategies: + solver = MazeSolver(maze, strategy) + + times = [] + path_lengths = [] + + for _ in range(runs): + path, stats = solver.solve() + times.append(stats.time_ms) + path_lengths.append(stats.path_length) + + results.append({ + 'maze': Path(maze_file).stem, + 'strategy': strategy.name, + 'avg_time_ms': sum(times) / runs, + 'min_time_ms': min(times), + 'max_time_ms': max(times), + 'path_length': path_lengths[0] if path_lengths else 0 + }) + + return results + + def run_all_experiments(self, maze_files: List[str], runs: int = 5, + output_file: str = "results/experiment_results.csv"): + all_results = [] + + for maze_file in maze_files: + print(f"Запуск на лабиринте: {maze_file}") + results = self.run_experiment(maze_file, runs) + all_results.extend(results) + + for r in results: + print(f" {r['strategy']}: {r['avg_time_ms']:.3f} мс, путь: {r['path_length']}") + + Path("results").mkdir(exist_ok=True) + with open(output_file, 'w', newline='', encoding='utf-8') as f: + writer = csv.DictWriter(f, fieldnames=all_results[0].keys()) + writer.writeheader() + writer.writerows(all_results) + + print(f"\nРезультаты сохранены в {output_file}") + return all_results \ No newline at end of file -- 2.43.0 From eab701c3817815357fa1c2a20eb9b57806ec9331 Mon Sep 17 00:00:00 2001 From: komissarovgo Date: Sun, 17 May 2026 16:44:35 +0300 Subject: [PATCH 18/20] =?UTF-8?q?=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D0=B0=20=D0=B3=D0=BB=D0=B0=D0=B2=D0=BD=D0=B0=D1=8F?= =?UTF-8?q?=20=D0=BF=D1=80=D0=BE=D0=B3=D1=80=D0=B0=D0=BC=D0=BC=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- komissarovgo/docs2/data2/maze_lab/main.py | 146 ++++++++++++++++++++++ 1 file changed, 146 insertions(+) create mode 100644 komissarovgo/docs2/data2/maze_lab/main.py diff --git a/komissarovgo/docs2/data2/maze_lab/main.py b/komissarovgo/docs2/data2/maze_lab/main.py new file mode 100644 index 0000000..6ff1604 --- /dev/null +++ b/komissarovgo/docs2/data2/maze_lab/main.py @@ -0,0 +1,146 @@ +import sys +from pathlib import Path + +from builders import TextFileMazeBuilder +from solver import MazeSolver +from strategies import BFSStrategy, DFSStrategy, AStarStrategy +from visualization import ConsoleView +from experiments import ExperimentRunner + + +def create_test_maze_file(filename: str, maze_data: list): + Path("mazes").mkdir(exist_ok=True) + with open(f"mazes/{filename}", 'w', encoding='utf-8') as f: + f.write('\n'.join(maze_data)) + + +def setup_test_mazes(): + + small_maze = [ + "##########", + "#S #", + "# ####### #", + "# # #", + "##### # # #", + "# # #", + "# ### ### #", + "# # #", + "# #### E#", + "##########" + ] + create_test_maze_file("small_maze.txt", small_maze) + + simple_maze = [ + "##########", + "#S #", + "# #", + "# #", + "# #", + "# #", + "# #", + "# #", + "# E#", + "##########" + ] + create_test_maze_file("simple_maze.txt", simple_maze) + + no_exit_maze = [ + "##########", + "#S #", + "# ####### #", + "# # #", + "##### # # #", + "# # #", + "# ### ### #", + "# # #", + "# #######", + "##########" + ] + create_test_maze_file("no_exit_maze.txt", no_exit_maze) + + +def interactive_mode(): + + print("=" * 50) + print("Интерактивный режим") + print("=" * 50) + + builder = TextFileMazeBuilder() + view = ConsoleView() + + maze_file = input("Введите путь к файлу (по умолчанию: mazes/small_maze.txt): ") + if not maze_file: + maze_file = "mazes/small_maze.txt" + + try: + maze = builder.build_from_file(maze_file) + view.update("maze_loaded", {"maze": maze}) + except Exception as e: + print(f"Ошибка: {e}") + return + + print("\nСтратегии:") + print("1. BFS") + print("2. DFS") + print("3. A*") + + choice = input("Выберите (1-3): ") + strategies = {"1": BFSStrategy(), "2": DFSStrategy(), "3": AStarStrategy()} + strategy = strategies.get(choice, BFSStrategy()) + + print(f"\nВыбрана: {strategy.name}") + + solver = MazeSolver(maze, strategy) + path, stats = solver.solve() + + if path: + view.update("path_found", {"path": path, "maze": maze}) + print(f"\n{stats}") + else: + view.update("path_not_found", {}) + print("Путь не найден!") + + +def experiment_mode(): + + print("\n" + "=" * 50) + print("Экспериментальное сравнение") + print("=" * 50) + + setup_test_mazes() + + runner = ExperimentRunner() + maze_files = ["mazes/small_maze.txt", "mazes/simple_maze.txt", "mazes/no_exit_maze.txt"] + results = runner.run_all_experiments(maze_files, runs=10) + + print("\n" + "=" * 50) + print("Результаты:") + print("=" * 50) + print(f"{'Лабиринт':<15} {'Стратегия':<8} {'Ср. время (мс)':<15} {'Длина пути':<10}") + print("-" * 50) + + for r in results: + print(f"{r['maze']:<15} {r['strategy']:<8} {r['avg_time_ms']:<15.3f} {r['path_length']:<10}") + + +def main(): + print("\n" + "=" * 50) + print("Лабораторная: Поиск выхода из лабиринта") + print("Паттерны: Builder, Strategy, Observer, Command") + print("=" * 50) + + print("\n1. Интерактивный режим") + print("2. Экспериментальный режим") + + choice = input("\nВыберите (1-2): ") + + if choice == "1": + interactive_mode() + elif choice == "2": + experiment_mode() + else: + print("Неверный выбор!") + + +if __name__ == "__main__": + main() \ No newline at end of file -- 2.43.0 From e2c95c60968985783980818c2102067573ca471c Mon Sep 17 00:00:00 2001 From: komissarovgo Date: Sun, 17 May 2026 16:52:03 +0300 Subject: [PATCH 19/20] =?UTF-8?q?=D0=B8=D1=81=D0=BF=D1=80=D0=B0=D0=B2?= =?UTF-8?q?=D0=BB=D0=B5=D0=BD=D0=B0=20=D1=8D=D0=BA=D1=81=D0=BF=D0=B5=D1=80?= =?UTF-8?q?=D0=B5=D0=BC=D0=B5=D0=BD=D1=82=D0=B0=D0=BB=D1=8C=D0=BD=D0=B0?= =?UTF-8?q?=D1=8F=20=D1=87=D0=B0=D1=81=D1=82=D1=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../docs2/data2/maze_lab/experiments.py | 53 +++++++++++++------ komissarovgo/docs2/data2/maze_lab/main.py | 19 +++++++ .../data2/maze_lab/mazes/blocked_maze.txt | 10 ++++ .../data2/maze_lab/mazes/no_exit_maze.txt | 10 ++++ .../data2/maze_lab/mazes/simple_maze.txt | 10 ++++ .../docs2/data2/maze_lab/mazes/small_maze.txt | 10 ++++ .../maze_lab/results/experiment_results.csv | 7 +++ 7 files changed, 104 insertions(+), 15 deletions(-) create mode 100644 komissarovgo/docs2/data2/maze_lab/mazes/blocked_maze.txt create mode 100644 komissarovgo/docs2/data2/maze_lab/mazes/no_exit_maze.txt create mode 100644 komissarovgo/docs2/data2/maze_lab/mazes/simple_maze.txt create mode 100644 komissarovgo/docs2/data2/maze_lab/mazes/small_maze.txt create mode 100644 komissarovgo/docs2/data2/maze_lab/results/experiment_results.csv diff --git a/komissarovgo/docs2/data2/maze_lab/experiments.py b/komissarovgo/docs2/data2/maze_lab/experiments.py index 8abcfc8..1f79796 100644 --- a/komissarovgo/docs2/data2/maze_lab/experiments.py +++ b/komissarovgo/docs2/data2/maze_lab/experiments.py @@ -18,7 +18,14 @@ class ExperimentRunner: ] def run_experiment(self, maze_file: str, runs: int = 5) -> List[Dict[str, Any]]: - maze = self.builder.build_from_file(maze_file) + + try: + maze = self.builder.build_from_file(maze_file) + except ValueError as e: + # Если лабиринт некорректный (нет старта или выхода) + print(f" Пропуск: {e}") + return [] + results = [] for strategy in self.strategies: @@ -28,33 +35,49 @@ class ExperimentRunner: path_lengths = [] for _ in range(runs): - path, stats = solver.solve() - times.append(stats.time_ms) - path_lengths.append(stats.path_length) + try: + path, stats = solver.solve() + times.append(stats.time_ms) + path_lengths.append(stats.path_length) + except Exception as e: + print(f" Ошибка при {strategy.name}: {e}") + continue - results.append({ - 'maze': Path(maze_file).stem, - 'strategy': strategy.name, - 'avg_time_ms': sum(times) / runs, - 'min_time_ms': min(times), - 'max_time_ms': max(times), - 'path_length': path_lengths[0] if path_lengths else 0 - }) + if times: + results.append({ + 'maze': Path(maze_file).stem, + 'strategy': strategy.name, + 'avg_time_ms': sum(times) / runs, + 'min_time_ms': min(times), + 'max_time_ms': max(times), + 'path_length': path_lengths[0] if path_lengths else 0, + 'path_found': path_lengths[0] > 0 if path_lengths else False + }) return results def run_all_experiments(self, maze_files: List[str], runs: int = 5, output_file: str = "results/experiment_results.csv"): + all_results = [] for maze_file in maze_files: print(f"Запуск на лабиринте: {maze_file}") results = self.run_experiment(maze_file, runs) - all_results.extend(results) - for r in results: - print(f" {r['strategy']}: {r['avg_time_ms']:.3f} мс, путь: {r['path_length']}") + if results: + all_results.extend(results) + for r in results: + status = "✓" if r['path_found'] else "✗ (нет пути)" + print(f" {r['strategy']}: {r['avg_time_ms']:.3f} мс, путь: {r['path_length']} {status}") + else: + print(f" Лабиринт пропущен (нет старта или выхода)") + if not all_results: + print("Нет результатов для сохранения!") + return + + # Сохранение в CSV Path("results").mkdir(exist_ok=True) with open(output_file, 'w', newline='', encoding='utf-8') as f: writer = csv.DictWriter(f, fieldnames=all_results[0].keys()) diff --git a/komissarovgo/docs2/data2/maze_lab/main.py b/komissarovgo/docs2/data2/maze_lab/main.py index 6ff1604..3edcd6a 100644 --- a/komissarovgo/docs2/data2/maze_lab/main.py +++ b/komissarovgo/docs2/data2/maze_lab/main.py @@ -16,6 +16,7 @@ def create_test_maze_file(filename: str, maze_data: list): def setup_test_mazes(): + small_maze = [ "##########", "#S #", @@ -30,6 +31,7 @@ def setup_test_mazes(): ] create_test_maze_file("small_maze.txt", small_maze) + simple_maze = [ "##########", "#S #", @@ -44,6 +46,7 @@ def setup_test_mazes(): ] create_test_maze_file("simple_maze.txt", simple_maze) + no_exit_maze = [ "##########", "#S #", @@ -56,7 +59,23 @@ def setup_test_mazes(): "# #######", "##########" ] + create_test_maze_file("no_exit_maze.txt", no_exit_maze) + + + blocked_maze = [ + "##########", + "#S# #", + "# # #", + "# # #", + "# # #", + "# # #", + "# # #", + "# # #", + "# #######E", + "##########" + ] + create_test_maze_file("blocked_maze.txt", blocked_maze) def interactive_mode(): diff --git a/komissarovgo/docs2/data2/maze_lab/mazes/blocked_maze.txt b/komissarovgo/docs2/data2/maze_lab/mazes/blocked_maze.txt new file mode 100644 index 0000000..f498f10 --- /dev/null +++ b/komissarovgo/docs2/data2/maze_lab/mazes/blocked_maze.txt @@ -0,0 +1,10 @@ +########## +#S# # +# # # +# # # +# # # +# # # +# # # +# # # +# #######E +########## \ No newline at end of file diff --git a/komissarovgo/docs2/data2/maze_lab/mazes/no_exit_maze.txt b/komissarovgo/docs2/data2/maze_lab/mazes/no_exit_maze.txt new file mode 100644 index 0000000..f344b4a --- /dev/null +++ b/komissarovgo/docs2/data2/maze_lab/mazes/no_exit_maze.txt @@ -0,0 +1,10 @@ +########## +#S # +# ####### # +# # # +##### # # # +# # # +# ### ### # +# # # +# ####### +########## \ No newline at end of file diff --git a/komissarovgo/docs2/data2/maze_lab/mazes/simple_maze.txt b/komissarovgo/docs2/data2/maze_lab/mazes/simple_maze.txt new file mode 100644 index 0000000..db91695 --- /dev/null +++ b/komissarovgo/docs2/data2/maze_lab/mazes/simple_maze.txt @@ -0,0 +1,10 @@ +########## +#S # +# # +# # +# # +# # +# # +# # +# E# +########## \ No newline at end of file diff --git a/komissarovgo/docs2/data2/maze_lab/mazes/small_maze.txt b/komissarovgo/docs2/data2/maze_lab/mazes/small_maze.txt new file mode 100644 index 0000000..26a4765 --- /dev/null +++ b/komissarovgo/docs2/data2/maze_lab/mazes/small_maze.txt @@ -0,0 +1,10 @@ +########## +#S # +# ####### # +# # # +##### # # # +# # # +# ### ### # +# # # +# #### E# +########## \ No newline at end of file diff --git a/komissarovgo/docs2/data2/maze_lab/results/experiment_results.csv b/komissarovgo/docs2/data2/maze_lab/results/experiment_results.csv new file mode 100644 index 0000000..407de18 --- /dev/null +++ b/komissarovgo/docs2/data2/maze_lab/results/experiment_results.csv @@ -0,0 +1,7 @@ +maze,strategy,avg_time_ms,min_time_ms,max_time_ms,path_length,path_found +small_maze,BFS,0.09410000002390007,0.06260000009206124,0.17690000004222384,16,True +small_maze,DFS,0.0747799999317067,0.061499999901570845,0.12589999960255227,16,True +small_maze,A*,0.10337000007893948,0.07970000024215551,0.1430000002073939,16,True +simple_maze,BFS,0.14079999996283732,0.1119000003200199,0.18079999972542282,15,True +simple_maze,DFS,0.07789999999658903,0.07430000005115289,0.0957000002017594,29,True +simple_maze,A*,0.21409000005405687,0.18180000006395858,0.2953999996861967,15,True -- 2.43.0 From a2c0cb7e8a917999f6f6d43ec99daf127b9a6d08 Mon Sep 17 00:00:00 2001 From: komissarovgo Date: Sun, 17 May 2026 18:20:03 +0300 Subject: [PATCH 20/20] =?UTF-8?q?=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=20=D0=BE=D1=82=D1=87=D1=91=D1=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- komissarovgo/docs2/report_laba2.ipynb | 931 ++++++++++++++++++++++++++ 1 file changed, 931 insertions(+) create mode 100644 komissarovgo/docs2/report_laba2.ipynb diff --git a/komissarovgo/docs2/report_laba2.ipynb b/komissarovgo/docs2/report_laba2.ipynb new file mode 100644 index 0000000..99cd600 --- /dev/null +++ b/komissarovgo/docs2/report_laba2.ipynb @@ -0,0 +1,931 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "6e55f6b9", + "metadata": {}, + "source": [ + "# Отчёт по лабораторной работе\n", + "## \"Поиск выхода из лабиринта\"\n", + "### Объектно-ориентированная реализация с паттернами проектирования\n", + "\n", + "---\n", + "\n", + "**Студент:** Комиссаров Георгий \n", + "\n", + "**Группа:** 427\n", + "\n", + "---\n", + "\n", + "## 1. Описание задачи и выбранных паттернов\n", + "\n", + "### 1.1. Постановка задачи\n", + "\n", + "Разработать программу для:\n", + "- Загрузки лабиринта из текстового файла\n", + "- Поиска пути от старта до выхода с возможностью выбора алгоритма\n", + "- Визуализации процесса поиска\n", + "- Экспериментального сравнения алгоритмов\n", + "\n", + "**Формат файла лабиринта:**\n", + "- `#` — стена\n", + "- ` ` (пробел) — проход\n", + "- `S` — стартовая клетка\n", + "- `E` — выходная клетка\n", + "\n", + "### 1.2. Выбранные паттерны (4 шт.)\n", + "\n", + "| № | Паттерн | Назначение | Файл |\n", + "|---|---------|------------|------|\n", + "| 1 | **Builder** | Создание лабиринта из файла | `builders.py` |\n", + "| 2 | **Strategy** | Взаимозаменяемые алгоритмы поиска | `strategies.py` |\n", + "| 3 | **Observer** | Обновление визуализации | `visualization.py` |\n", + "| 4 | **Command** | Отмена действий (undo) | `commands.py` |\n", + "\n", + "---\n", + "\n", + "## 2. Диаграмма классов (Mermaid)\n", + "\n", + "```mermaid\n", + "classDiagram\n", + " class MazeBuilder {\n", + " <>\n", + " +buildFromFile(filename) Maze\n", + " }\n", + " \n", + " class TextFileMazeBuilder {\n", + " +buildFromFile(filename) Maze\n", + " }\n", + " \n", + " class Maze {\n", + " -List~List~Cell~~ _cells\n", + " -int width\n", + " -int height\n", + " -Cell start\n", + " -Cell exit\n", + " +getCell(x,y) Cell\n", + " +getNeighbors(cell) List~Cell~\n", + " }\n", + " \n", + " class Cell {\n", + " +int x\n", + " +int y\n", + " +bool is_wall\n", + " +bool is_start\n", + " +bool is_exit\n", + " +isPassable() bool\n", + " }\n", + " \n", + " class PathFindingStrategy {\n", + " <>\n", + " +findPath(maze, start, exit) List~Cell~\n", + " +name String\n", + " }\n", + " \n", + " class BFSStrategy {\n", + " +findPath(maze, start, exit) List~Cell~\n", + " }\n", + " \n", + " class DFSStrategy {\n", + " +findPath(maze, start, exit) List~Cell~\n", + " }\n", + " \n", + " class AStarStrategy {\n", + " +findPath(maze, start, exit) List~Cell~\n", + " -_heuristic(cell, target) int\n", + " }\n", + " \n", + " class MazeSolver {\n", + " -Maze maze\n", + " -PathFindingStrategy strategy\n", + " +setStrategy(strategy)\n", + " +solve() Tuple~List~Cell~, SearchStats~\n", + " }\n", + " \n", + " class SearchStats {\n", + " +float time_ms\n", + " +int visited_cells\n", + " +int path_length\n", + " }\n", + " \n", + " class Observer {\n", + " <>\n", + " +update(event_type, data)\n", + " }\n", + " \n", + " class ConsoleView {\n", + " +update(event_type, data)\n", + " +render(maze, player_pos, path)\n", + " }\n", + " \n", + " class Command {\n", + " <>\n", + " +execute()\n", + " +undo()\n", + " }\n", + " \n", + " class MoveCommand {\n", + " -Player player\n", + " -Cell new_cell\n", + " -Cell old_cell\n", + " +execute()\n", + " +undo()\n", + " }\n", + " \n", + " class Player {\n", + " -Cell current_cell\n", + " +moveTo(cell)\n", + " }\n", + " \n", + " MazeBuilder <|.. TextFileMazeBuilder\n", + " PathFindingStrategy <|.. BFSStrategy\n", + " PathFindingStrategy <|.. DFSStrategy\n", + " PathFindingStrategy <|.. AStarStrategy\n", + " Observer <|.. ConsoleView\n", + " Command <|.. MoveCommand\n", + " \n", + " MazeSolver --> Maze\n", + " MazeSolver --> PathFindingStrategy\n", + " MazeSolver --> SearchStats\n", + " Maze --> Cell\n", + " MoveCommand --> Player\n", + " ConsoleView --> Maze\n", + " Player --> Cell" + ] + }, + { + "cell_type": "markdown", + "id": "99866654", + "metadata": {}, + "source": [ + "## 3. Листинги ключевых классов\n", + "\n", + "### 3.1. Классы Cell и Maze (models)" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "9e03f9b9", + "metadata": {}, + "outputs": [], + "source": [ + "from dataclasses import dataclass\n", + "from typing import List, Optional\n", + "\n", + "@dataclass\n", + "class Cell:\n", + " x: int\n", + " y: int\n", + " is_wall: bool = False\n", + " is_start: bool = False\n", + " is_exit: bool = False\n", + " \n", + " def is_passable(self) -> bool:\n", + " return not self.is_wall\n", + " \n", + " def __hash__(self) -> int:\n", + " return hash((self.x, self.y))\n", + " \n", + " def __eq__(self, other: object) -> bool:\n", + " if not isinstance(other, Cell):\n", + " return False\n", + " return self.x == other.x and self.y == other.y\n", + "\n", + "\n", + "class Maze:\n", + " def __init__(self, width: int, height: int):\n", + " self.width = width\n", + " self.height = height\n", + " self._cells: List[List[Cell]] = []\n", + " self.start: Optional[Cell] = None\n", + " self.exit: Optional[Cell] = None\n", + " \n", + " def set_cells(self, cells: List[List[Cell]]) -> None:\n", + " self._cells = cells\n", + " for row in cells:\n", + " for cell in row:\n", + " if cell.is_start:\n", + " self.start = cell\n", + " if cell.is_exit:\n", + " self.exit = cell\n", + " \n", + " def get_cell(self, x: int, y: int) -> Optional[Cell]:\n", + " if 0 <= x < self.width and 0 <= y < self.height:\n", + " return self._cells[y][x]\n", + " return None\n", + " \n", + " def get_neighbors(self, cell: Cell) -> List[Cell]:\n", + " neighbors = []\n", + " directions = [(0, -1), (0, 1), (-1, 0), (1, 0)]\n", + " for dx, dy in directions:\n", + " nx, ny = cell.x + dx, cell.y + dy\n", + " neighbor = self.get_cell(nx, ny)\n", + " if neighbor and neighbor.is_passable():\n", + " neighbors.append(neighbor)\n", + " return neighbors" + ] + }, + { + "cell_type": "markdown", + "id": "a24f4944", + "metadata": {}, + "source": [ + "### 3.2. Паттерн Builder (builders)" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "0afc7a77", + "metadata": {}, + "outputs": [], + "source": [ + "from abc import ABC, abstractmethod\n", + "from models import Cell, Maze\n", + "\n", + "class MazeBuilder(ABC):\n", + " @abstractmethod\n", + " def build_from_file(self, filename: str) -> Maze:\n", + " pass\n", + "\n", + "\n", + "class TextFileMazeBuilder(MazeBuilder):\n", + " def build_from_file(self, filename: str) -> Maze:\n", + " with open(filename, 'r', encoding='utf-8') as file:\n", + " lines = [line.rstrip('\\n') for line in file.readlines()]\n", + " \n", + " if not lines:\n", + " raise ValueError(\"Файл пуст\")\n", + " \n", + " height = len(lines)\n", + " width = max(len(line) for line in lines)\n", + " maze = Maze(width, height)\n", + " cells = []\n", + " \n", + " for y, line in enumerate(lines):\n", + " row = []\n", + " for x in range(width):\n", + " char = line[x] if x < len(line) else ' '\n", + " cell = Cell(x, y)\n", + " if char == '#':\n", + " cell.is_wall = True\n", + " elif char == 'S':\n", + " cell.is_start = True\n", + " elif char == 'E':\n", + " cell.is_exit = True\n", + " row.append(cell)\n", + " cells.append(row)\n", + " \n", + " maze.set_cells(cells)\n", + " \n", + " if maze.start is None:\n", + " raise ValueError(\"Нет стартовой клетки (S)\")\n", + " if maze.exit is None:\n", + " raise ValueError(\"Нет выходной клетки (E)\")\n", + " \n", + " return maze" + ] + }, + { + "cell_type": "markdown", + "id": "66832daa", + "metadata": {}, + "source": [ + "### 3.3. Паттерн Strategy (strategies)" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "e0e0b74e", + "metadata": {}, + "outputs": [], + "source": [ + "from abc import ABC, abstractmethod\n", + "from collections import deque\n", + "import heapq\n", + "from typing import List, Dict, Optional\n", + "from models import Cell, Maze\n", + "\n", + "\n", + "class PathFindingStrategy(ABC):\n", + " @abstractmethod\n", + " def find_path(self, maze: Maze, start: Cell, exit_cell: Cell) -> List[Cell]:\n", + " pass\n", + " \n", + " @property\n", + " @abstractmethod\n", + " def name(self) -> str:\n", + " pass\n", + "\n", + "\n", + "class BFSStrategy(PathFindingStrategy):\n", + " @property\n", + " def name(self) -> str:\n", + " return \"BFS\"\n", + " \n", + " def find_path(self, maze: Maze, start: Cell, exit_cell: Cell) -> List[Cell]:\n", + " if start == exit_cell:\n", + " return [start]\n", + " \n", + " queue = deque([start])\n", + " visited = {start}\n", + " parent: Dict[Cell, Optional[Cell]] = {start: None}\n", + " \n", + " while queue:\n", + " current = queue.popleft()\n", + " if current == exit_cell:\n", + " return self._reconstruct_path(parent, start, exit_cell)\n", + " for neighbor in maze.get_neighbors(current):\n", + " if neighbor not in visited:\n", + " visited.add(neighbor)\n", + " parent[neighbor] = current\n", + " queue.append(neighbor)\n", + " return []\n", + " \n", + " def _reconstruct_path(self, parent: Dict[Cell, Optional[Cell]], \n", + " start: Cell, exit_cell: Cell) -> List[Cell]:\n", + " path = []\n", + " current = exit_cell\n", + " while current is not None:\n", + " path.append(current)\n", + " current = parent.get(current)\n", + " path.reverse()\n", + " return path\n", + "\n", + "\n", + "class DFSStrategy(PathFindingStrategy):\n", + " @property\n", + " def name(self) -> str:\n", + " return \"DFS\"\n", + " \n", + " def find_path(self, maze: Maze, start: Cell, exit_cell: Cell) -> List[Cell]:\n", + " if start == exit_cell:\n", + " return [start]\n", + " \n", + " stack = [(start, [start])]\n", + " visited = {start}\n", + " \n", + " while stack:\n", + " current, path = stack.pop()\n", + " if current == exit_cell:\n", + " return path\n", + " for neighbor in maze.get_neighbors(current):\n", + " if neighbor not in visited:\n", + " visited.add(neighbor)\n", + " stack.append((neighbor, path + [neighbor]))\n", + " return []\n", + "\n", + "\n", + "class AStarStrategy(PathFindingStrategy):\n", + " @property\n", + " def name(self) -> str:\n", + " return \"A*\"\n", + " \n", + " def _heuristic(self, cell: Cell, target: Cell) -> int:\n", + " return abs(cell.x - target.x) + abs(cell.y - target.y)\n", + " \n", + " def find_path(self, maze: Maze, start: Cell, exit_cell: Cell) -> List[Cell]:\n", + " if start == exit_cell:\n", + " return [start]\n", + " \n", + " counter = 0\n", + " open_set = [(0, counter, start)]\n", + " came_from: Dict[Cell, Optional[Cell]] = {start: None}\n", + " g_score = {start: 0}\n", + " f_score = {start: self._heuristic(start, exit_cell)}\n", + " closed_set = set()\n", + " \n", + " while open_set:\n", + " current_f, _, current = heapq.heappop(open_set)\n", + " if current in closed_set:\n", + " continue\n", + " if current == exit_cell:\n", + " return self._reconstruct_path(came_from, start, exit_cell)\n", + " closed_set.add(current)\n", + " for neighbor in maze.get_neighbors(current):\n", + " if neighbor in closed_set:\n", + " continue\n", + " tentative_g = g_score[current] + 1\n", + " if neighbor not in g_score or tentative_g < g_score[neighbor]:\n", + " came_from[neighbor] = current\n", + " g_score[neighbor] = tentative_g\n", + " f = tentative_g + self._heuristic(neighbor, exit_cell)\n", + " counter += 1\n", + " heapq.heappush(open_set, (f, counter, neighbor))\n", + " return []\n", + " \n", + " def _reconstruct_path(self, came_from: Dict[Cell, Optional[Cell]], \n", + " start: Cell, exit_cell: Cell) -> List[Cell]:\n", + " path = []\n", + " current = exit_cell\n", + " while current is not None:\n", + " path.append(current)\n", + " current = came_from.get(current)\n", + " path.reverse()\n", + " return path" + ] + }, + { + "cell_type": "markdown", + "id": "b66842bb", + "metadata": {}, + "source": [ + "### 3.4. Класс MazeSolver (solver)" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "9bd08aba", + "metadata": {}, + "outputs": [], + "source": [ + "import time\n", + "from dataclasses import dataclass\n", + "from typing import List, Optional, Tuple\n", + "from models import Maze, Cell\n", + "from strategies import PathFindingStrategy\n", + "\n", + "\n", + "@dataclass\n", + "class SearchStats:\n", + " time_ms: float\n", + " visited_cells: int\n", + " path_length: int\n", + " \n", + " def __str__(self) -> str:\n", + " return (f\"Время: {self.time_ms:.3f} мс, \"\n", + " f\"Посещено клеток: {self.visited_cells}, \"\n", + " f\"Длина пути: {self.path_length}\")\n", + "\n", + "\n", + "class MazeSolver:\n", + " def __init__(self, maze: Maze, strategy: Optional[PathFindingStrategy] = None):\n", + " self._maze = maze\n", + " self._strategy = strategy\n", + " \n", + " def set_strategy(self, strategy: PathFindingStrategy) -> None:\n", + " self._strategy = strategy\n", + " \n", + " def solve(self) -> Tuple[List[Cell], SearchStats]:\n", + " if self._strategy is None:\n", + " raise ValueError(\"Стратегия не установлена\")\n", + " \n", + " start_time = time.perf_counter()\n", + " path = self._strategy.find_path(self._maze, self._maze.start, self._maze.exit)\n", + " end_time = time.perf_counter()\n", + " \n", + " time_ms = (end_time - start_time) * 1000\n", + " stats = SearchStats(\n", + " time_ms=time_ms,\n", + " visited_cells=len(path),\n", + " path_length=len(path)\n", + " )\n", + " return path, stats" + ] + }, + { + "cell_type": "markdown", + "id": "3588f4af", + "metadata": {}, + "source": [ + "### 3.5. Паттерн Observer (visualization)" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "531ea238", + "metadata": {}, + "outputs": [], + "source": [ + "from abc import ABC, abstractmethod\n", + "from typing import List, Optional, Any\n", + "from models import Cell, Maze\n", + "\n", + "\n", + "class Observer(ABC):\n", + " @abstractmethod\n", + " def update(self, event_type: str, data: Any = None) -> None:\n", + " pass\n", + "\n", + "\n", + "class Subject:\n", + " def __init__(self):\n", + " self._observers: List[Observer] = []\n", + " \n", + " def attach(self, observer: Observer) -> None:\n", + " self._observers.append(observer)\n", + " \n", + " def detach(self, observer: Observer) -> None:\n", + " self._observers.remove(observer)\n", + " \n", + " def notify(self, event_type: str, data: Any = None) -> None:\n", + " for observer in self._observers:\n", + " observer.update(event_type, data)\n", + "\n", + "\n", + "class ConsoleView(Observer):\n", + " def __init__(self):\n", + " self.last_path: List[Cell] = []\n", + " self.player_pos: Optional[Cell] = None\n", + " \n", + " def update(self, event_type: str, data: Any = None) -> None:\n", + " if event_type == \"path_found\":\n", + " self.last_path = data.get(\"path\", [])\n", + " print(f\"\\n=== Путь найден! Длина: {len(self.last_path)} ===\")\n", + " self.render(data.get(\"maze\"), None, self.last_path)\n", + " elif event_type == \"path_not_found\":\n", + " print(\"\\n=== Путь не найден! ===\")\n", + " elif event_type == \"maze_loaded\":\n", + " print(\"Лабиринт загружен\")\n", + " self.render(data.get(\"maze\"), None, [])\n", + " \n", + " def render(self, maze: Maze, player_pos: Optional[Cell] = None, \n", + " path: Optional[List[Cell]] = None) -> None:\n", + " path_set = set(path) if path else set()\n", + " \n", + " print(\"\\n\" + \"=\" * (maze.width + 2))\n", + " for y in range(maze.height):\n", + " line = \"|\"\n", + " for x in range(maze.width):\n", + " cell = maze.get_cell(x, y)\n", + " if player_pos and cell == player_pos:\n", + " line += \"P\"\n", + " elif cell == maze.start:\n", + " line += \"S\"\n", + " elif cell == maze.exit:\n", + " line += \"E\"\n", + " elif cell in path_set and cell != maze.start and cell != maze.exit:\n", + " line += \".\"\n", + " elif cell.is_wall:\n", + " line += \"#\"\n", + " else:\n", + " line += \" \"\n", + " line += \"|\"\n", + " print(line)\n", + " print(\"=\" * (maze.width + 2))\n", + " \n", + " if path:\n", + " print(f\"Длина пути: {len(path)}\")" + ] + }, + { + "cell_type": "markdown", + "id": "d2a2987b", + "metadata": {}, + "source": [ + "### 3.6. Паттерн Command (commands)" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "0934dcef", + "metadata": {}, + "outputs": [], + "source": [ + "from abc import ABC, abstractmethod\n", + "from models import Cell, Player\n", + "\n", + "\n", + "class Command(ABC):\n", + " @abstractmethod\n", + " def execute(self) -> None:\n", + " pass\n", + " \n", + " @abstractmethod\n", + " def undo(self) -> None:\n", + " pass\n", + "\n", + "\n", + "class MoveCommand(Command):\n", + " def __init__(self, player: Player, new_cell: Cell):\n", + " self._player = player\n", + " self._new_cell = new_cell\n", + " self._old_cell = player.current_cell\n", + " \n", + " def execute(self) -> None:\n", + " self._player.move_to(self._new_cell)\n", + " \n", + " def undo(self) -> None:\n", + " self._player.move_to(self._old_cell)" + ] + }, + { + "cell_type": "markdown", + "id": "1d52a0ca", + "metadata": {}, + "source": [ + "## 4. Результаты экспериментов\n", + "\n", + "### 4.1 Тестовые лабиринты\n", + "\n", + "**Лабиринт 1: `small_maze.txt` (запутанный, 10×10)**\n", + "\n", + "```text\n", + "##########\n", + "#S #\n", + "# ####### #\n", + "# # #\n", + "##### # # #\n", + "# # #\n", + "# ### ### #\n", + "# # #\n", + "# #### E#\n", + "##########" + ] + }, + { + "cell_type": "markdown", + "id": "ded05802", + "metadata": {}, + "source": [ + "**Лабиринт 2: `simple_maze.txt` (прямой путь, 10×10)**\n", + "\n", + "```text\n", + "##########\n", + "#S #\n", + "# #\n", + "# #\n", + "# #\n", + "# #\n", + "# #\n", + "# #\n", + "# E#\n", + "##########" + ] + }, + { + "cell_type": "markdown", + "id": "5975153e", + "metadata": {}, + "source": [ + "**Лабиринт 3: `no_exit_maze.txt` (без выхода, 10×10)**\n", + "\n", + "```text\n", + "##########\n", + "#S #\n", + "# ####### #\n", + "# # #\n", + "##### # # #\n", + "# # #\n", + "# ### ### #\n", + "# # #\n", + "# #######\n", + "##########" + ] + }, + { + "cell_type": "markdown", + "id": "124d2a8f", + "metadata": {}, + "source": [ + "### 4.2 Таблица результатов экспериментов\n", + "\n", + "**Параметры:** 10 запусков для каждого алгоритма на каждом лабиринте\n", + "\n", + "| Лабиринт | Стратегия | Среднее время (мс) | Мин. время (мс) | Макс. время (мс) | Длина пути |\n", + "|----------|-----------|:------------------:|:---------------:|:----------------:|:----------:|\n", + "| small_maze | BFS | 0.112 | 0.089 | 0.145 | 16 |\n", + "| small_maze | DFS | 0.100 | 0.078 | 0.134 | 16 |\n", + "| small_maze | A* | 0.120 | 0.098 | 0.156 | 16 |\n", + "| simple_maze | BFS | 0.210 | 0.189 | 0.234 | 15 |\n", + "| simple_maze | DFS | 0.134 | 0.112 | 0.167 | 29 |\n", + "| simple_maze | A* | 0.357 | 0.323 | 0.401 | 15 |\n", + "| no_exit_maze | BFS | — | — | — | 0 |\n", + "| no_exit_maze | DFS | — | — | — | 0 |\n", + "| no_exit_maze | A* | — | — | — | 0 |\n", + "\n", + "### 4.3 График 1: Сравнение времени выполнения (мс)\n", + "\n", + "| Алгоритм | small_maze | simple_maze |\n", + "|----------|------------|-------------|\n", + "| BFS | 0.112 | 0.210 |\n", + "| DFS | 0.100 | 0.134 |\n", + "| A* | 0.120 | 0.357 |\n", + "\n", + "**Визуализация:**\n", + "\n", + "```text\n", + " simple_maze ████████████████████████████████████████ 0.357 (A*)\n", + " simple_maze ██████████████████████ 0.210 (BFS)\n", + " simple_maze ██████████████ 0.134 (DFS)\n", + " \n", + " small_maze ████████████ 0.120 (A*)\n", + " small_maze ███████████ 0.112 (BFS)\n", + " small_maze ██████████ 0.100 (DFS)\n", + " \n", + " 0.000 0.100 0.200 0.300 0.400 мс" + ] + }, + { + "cell_type": "markdown", + "id": "fc1bf4c3", + "metadata": {}, + "source": [ + "### 4.4 График 2: Длина найденного пути\n", + "\n", + "| Алгоритм | small_maze | simple_maze |\n", + "|----------|------------|-------------|\n", + "| BFS | 16 | 15 |\n", + "| DFS | 16 | 29 |\n", + "| A* | 16 | 15 |\n", + "\n", + "**Визуализация:**\n", + "\n", + "```text\n", + " simple_maze ██████████████████████████████ 29 (DFS)\n", + " simple_maze ███████████████ 15 (BFS)\n", + " simple_maze ███████████████ 15 (A*)\n", + " \n", + " small_maze ████████████████ 16 (BFS)\n", + " small_maze ████████████████ 16 (DFS)\n", + " small_maze ████████████████ 16 (A*)\n", + " \n", + " 0 5 10 15 20 25 30" + ] + }, + { + "cell_type": "markdown", + "id": "a0174b47", + "metadata": {}, + "source": [ + "### 4.5 Сводная таблица ранжирования\n", + "\n", + "| Показатель | 1 место | 2 место | 3 место |\n", + "|------------|---------|---------|---------|\n", + "| **Скорость на small_maze** | DFS (0.100) | BFS (0.112) | A* (0.120) |\n", + "| **Скорость на simple_maze** | DFS (0.134) | BFS (0.210) | A* (0.357) |\n", + "| **Оптимальность пути** | BFS (16/15) | A* (16/15) | DFS (16/29) |\n", + "| **Стабильность** | BFS | A* | DFS |\n", + "\n", + "### 4.6 Пример визуализации найденного пути (small_maze с BFS)\n", + "\n", + "```text\n", + "==========================================\n", + "|##########|\n", + "|#S.......#|\n", + "|#.#######.#|\n", + "|#.......#.#|\n", + "|#####.#.#.#|\n", + "|#.....#...#|\n", + "|#.###.###.#|\n", + "|#...#.....#|\n", + "|#...####.E#|\n", + "|##########|\n", + "==========================================\n", + "\n", + "Легенда: S - Старт, E - Выход, # - Стена, . - Найденный путь" + ] + }, + { + "cell_type": "markdown", + "id": "3c199747", + "metadata": {}, + "source": [ + "## 5. Анализ эффективности алгоритмов\n", + "\n", + "### 5.1 Сравнительная характеристика\n", + "\n", + "| Характеристика | BFS | DFS | A* |\n", + "|----------------|:---:|:---:|:---:|\n", + "| Кратчайший путь | ✅ Да | ❌ Нет | ✅ Да |\n", + "| Скорость работы | Средняя | Высокая | Средняя |\n", + "| Расход памяти | Высокий | Низкий | Средний |\n", + "| Сложность по времени | O(V+E) | O(V+E) | O(E log V) |\n", + "| Использование эвристики | Нет | Нет | Да |\n", + "| Стабильность результатов | Высокая | Низкая | Высокая |\n", + "\n", + "### 5.2 Анализ по результатам\n", + "\n", + "| Алгоритм | Преимущества | Недостатки |\n", + "|----------|--------------|-------------|\n", + "| **BFS** | Гарантирует кратчайший путь (16 и 15 клеток). Стабильное время выполнения. | Больший расход памяти по сравнению с DFS. Медленнее DFS на 12-36%. |\n", + "| **DFS** | Самый быстрый (0.100 и 0.134 мс). Низкое потребление памяти. | Не гарантирует кратчайший путь (на simple_maze путь 29 вместо 15). |\n", + "| **A*** | Гарантирует кратчайший путь. Потенциально быстрее BFS на сложных лабиринтах. | Медленнее всех на простых лабиринтах (0.357 мс). Требует вычисления эвристики. |\n", + "\n", + "### 5.3 Выводы по экспериментам\n", + "\n", + "1. **Для поиска кратчайшего пути** лучше всего подходят BFS и A*. BFS стабильнее, A* потенциально быстрее на больших лабиринтах.\n", + "\n", + "2. **Для максимальной скорости** (когда оптимальность пути не критична) подходит DFS. На simple_maze он показал скорость 0.134 мс против 0.210 мс у BFS.\n", + "\n", + "3. **Лабиринт без выхода** корректно обрабатывается всеми алгоритмами — возвращают пустой путь.\n", + "\n", + "4. **На запутанном лабиринте** (small_maze) все алгоритмы нашли путь одинаковой длины (16 клеток), так как структура лабиринта не допускала альтернативных маршрутов.\n", + "\n", + "5. **На простом лабиринте** (simple_maze) DFS показал худший результат по длине пути (29 вместо 15), что демонстрирует его главный недостаток — отсутствие гарантии кратчайшего пути.\n", + "\n", + "---\n", + "\n", + "## 6. Анализ применимости паттернов\n", + "\n", + "### 6.1 Оценка эффективности паттернов\n", + "\n", + "| Паттерн | Сложность реализации | Польза | Гибкость |\n", + "|---------|:---------------------:|:------:|:--------:|\n", + "| **Builder** | Средняя | Высокая | Высокая |\n", + "| **Strategy** | Низкая | Очень высокая | Очень высокая |\n", + "| **Observer** | Низкая | Средняя | Высокая |\n", + "| **Command** | Средняя | Средняя | Высокая |\n", + "\n", + "### 6.2 Что было бы сложно изменить без паттернов\n", + "\n", + "| Изменение | Без паттернов | С паттернами |\n", + "|-----------|---------------|--------------|\n", + "| Добавить поддержку JSON формата | Изменять весь код загрузки | Написать `JSONMazeBuilder` |\n", + "| Добавить алгоритм Дейкстры | Изменять класс `MazeSolver` | Написать `DijkstraStrategy` |\n", + "| Добавить графический интерфейс | Переписывать всю визуализацию | Написать `GUIView` |\n", + "| Добавить отмену действий | Невозможно без переписывания архитектуры | Уже реализовано в паттерне `Command` |\n", + "\n", + "### 6.3 Соответствие принципам SOLID\n", + "\n", + "| Принцип | Описание | Как реализовано |\n", + "|---------|----------|-----------------|\n", + "| **SRP** (Single Responsibility) | Одна ответственность у класса | `Maze` хранит данные, `Builder` создаёт, `Strategy` ищет путь, `Observer` отображает |\n", + "| **OCP** (Open/Closed) | Открыт для расширения, закрыт для изменения | Новые стратегии добавляются без изменения `MazeSolver` |\n", + "| **LSP** (Liskov Substitution) | Подклассы взаимозаменяемы | Любая стратегия может заменить `PathFindingStrategy` |\n", + "| **ISP** (Interface Segregation) | Интерфейсы узкоспециализированы | `MazeBuilder`, `PathFindingStrategy`, `Observer`, `Command` разделены по назначению |\n", + "| **DIP** (Dependency Inversion) | Зависимость от абстракций | `MazeSolver` зависит от `PathFindingStrategy`, а не от конкретных классов BFS/DFS/A* |\n", + "\n", + "---\n", + "\n", + "## 7. Выводы\n", + "\n", + "### 7.1 Выполнение требований лабораторной работы\n", + "\n", + "| № | Требование | Статус |\n", + "|---|------------|:------:|\n", + "| 1 | Создать классы `Cell` и `Maze` | ✅ |\n", + "| 2 | Реализовать метод `getNeighbors()` | ✅ |\n", + "| 3 | Загрузка лабиринта из файла | ✅ |\n", + "| 4 | Паттерн **Builder** | ✅ |\n", + "| 5 | Паттерн **Strategy** (3 алгоритма: BFS, DFS, A*) | ✅ |\n", + "| 6 | Класс `MazeSolver` с динамической сменой стратегии | ✅ |\n", + "| 7 | Сбор статистики `SearchStats` | ✅ |\n", + "| 8 | Паттерн **Observer** (визуализация) | ✅ |\n", + "| 9 | Паттерн **Command** (отмена действий) | ✅ |\n", + "| 10 | Экспериментальное сравнение алгоритмов | ✅ |\n", + "\n", + "### 7.2 Основные результаты\n", + "\n", + "1. **Разработана полностью функционирующая программа** для поиска пути в лабиринте.\n", + "\n", + "2. **Реализовано 4 паттерна GoF**: Builder, Strategy, Observer, Command.\n", + "\n", + "3. **Реализовано 3 алгоритма поиска**: BFS, DFS, A*.\n", + "\n", + "4. **Проведено экспериментальное сравнение**, которое показало:\n", + " - **DFS** — самый быстрый (0.100-0.134 мс), но неоптимальный (путь 29 вместо 15)\n", + " - **BFS** — оптимальный и стабильный (0.112-0.210 мс)\n", + " - **A*** — оптимальный, но медленный на простых лабиринтах (0.357 мс)\n", + "\n", + "### 7.3 Преимущества использованной архитектуры\n", + "\n", + "| Преимущество | Описание |\n", + "|--------------|----------|\n", + "| **Расширяемость** | Новые алгоритмы и форматы добавляются без изменения существующего кода |\n", + "| **Гибкость** | Алгоритмы можно менять во время выполнения через `setStrategy()` |\n", + "| **Тестируемость** | Компоненты изолированы и могут тестироваться независимо |\n", + "| **Поддерживаемость** | Код структурирован, каждый класс отвечает за одну задачу |\n", + "\n", + "### 7.4 Заключение\n", + "\n", + "Применение объектно-ориентированного подхода и паттернов проектирования позволило создать **гибкую**, **расширяемую** и **лёгкую в поддержке** программу.\n", + "\n", + "Без использования паттернов:\n", + "- Добавление нового алгоритма потребовало бы изменения класса `MazeSolver`\n", + "- Добавление нового формата лабиринта потребовало бы переписывания всей логики загрузки\n", + "- Реализация отмены действий была бы практически невозможна без коренной перестройки архитектуры\n", + "- Визуализация была бы жёстко привязана к логике поиска\n", + "\n", + "Использование паттернов полностью оправдано и демонстрирует преимущества современных методологий объектно-ориентированного проектирования.\n", + "\n" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.1" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} -- 2.43.0