From eb6587c537c23bea7cd6cc94a68ce90bf77cbe49 Mon Sep 17 00:00:00 2001 From: KuznetsovYuM Date: Fri, 22 May 2026 17:11:40 +0000 Subject: [PATCH 1/7] [1] Implement linked list phonebook --- .../1-st-exercise/phonebook_structures.py | 67 +++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 KuznetsovYuM/docs/data/1-st-exercise/phonebook_structures.py diff --git a/KuznetsovYuM/docs/data/1-st-exercise/phonebook_structures.py b/KuznetsovYuM/docs/data/1-st-exercise/phonebook_structures.py new file mode 100644 index 0000000..25b9bf4 --- /dev/null +++ b/KuznetsovYuM/docs/data/1-st-exercise/phonebook_structures.py @@ -0,0 +1,67 @@ + +def linked_list_add(head, name, phone): + curr = head + while curr is not None: + if curr['name'] == name: + curr['phone'] = phone + return head + curr = curr['next'] + + new_node = {'name': name, 'phone': phone, 'next': None} + if head is None: + return new_node + + curr = head + while curr['next'] is not None: + curr = curr['next'] + curr['next'] = new_node + return head + + +def linked_list_find(head, name): + curr = head + while curr is not None: + if curr['name'] == name: + return curr['phone'] + curr = curr['next'] + return None + + +def linked_list_remove(head, name): + if head is None: + return None + + if head['name'] == name: + return head['next'] + + prev = head + curr = head['next'] + while curr is not None: + if curr['name'] == name: + prev['next'] = curr['next'] + return head + prev = curr + curr = curr['next'] + return head + + +def linked_list_collect_all(head): + records = [] + curr = head + while curr is not None: + records.append((curr['name'], curr['phone'])) + curr = curr['next'] + records.sort(key=lambda pair: pair[0]) + return records + + +# Quick test +if __name__ == '__main__': + lst = None + lst = linked_list_add(lst, 'Alice', '111-222') + lst = linked_list_add(lst, 'Bob', '333-444') + lst = linked_list_add(lst, 'Alice', '555-666') + print(linked_list_find(lst, 'Alice')) # 555-666 + print(linked_list_collect_all(lst)) # [('Alice','555-666'), ('Bob','333-444')] + lst = linked_list_remove(lst, 'Bob') + print(linked_list_collect_all(lst)) # [('Alice','555-666')] \ No newline at end of file From d2001eaf53a75faaf6d80f357231277350e6fdb0 Mon Sep 17 00:00:00 2001 From: KuznetsovYuM Date: Fri, 22 May 2026 17:13:03 +0000 Subject: [PATCH 2/7] [1] Implement hash table based on linked list buckets --- .../1-st-exercise/phonebook_structures.py | 57 +++++++++++++++---- 1 file changed, 45 insertions(+), 12 deletions(-) diff --git a/KuznetsovYuM/docs/data/1-st-exercise/phonebook_structures.py b/KuznetsovYuM/docs/data/1-st-exercise/phonebook_structures.py index 25b9bf4..e16a070 100644 --- a/KuznetsovYuM/docs/data/1-st-exercise/phonebook_structures.py +++ b/KuznetsovYuM/docs/data/1-st-exercise/phonebook_structures.py @@ -1,4 +1,3 @@ - def linked_list_add(head, name, phone): curr = head while curr is not None: @@ -30,10 +29,8 @@ def linked_list_find(head, name): def linked_list_remove(head, name): if head is None: return None - if head['name'] == name: return head['next'] - prev = head curr = head['next'] while curr is not None: @@ -55,13 +52,49 @@ def linked_list_collect_all(head): return records -# Quick test +def _hash_bucket_index(key, table_size): + return hash(key) % table_size + + +def hash_table_create(bucket_count=10): + return [None] * bucket_count + + +def hash_table_put(table, name, phone): + idx = _hash_bucket_index(name, len(table)) + table[idx] = linked_list_add(table[idx], name, phone) + return table + + +def hash_table_get(table, name): + idx = _hash_bucket_index(name, len(table)) + return linked_list_find(table[idx], name) + + +def hash_table_remove(table, name): + idx = _hash_bucket_index(name, len(table)) + table[idx] = linked_list_remove(table[idx], name) + return table + + +def hash_table_collect_all(table): + all_records = [] + for head in table: + curr = head + while curr is not None: + all_records.append((curr['name'], curr['phone'])) + curr = curr['next'] + all_records.sort(key=lambda pair: pair[0]) + return all_records + + +# Quick test if __name__ == '__main__': - lst = None - lst = linked_list_add(lst, 'Alice', '111-222') - lst = linked_list_add(lst, 'Bob', '333-444') - lst = linked_list_add(lst, 'Alice', '555-666') - print(linked_list_find(lst, 'Alice')) # 555-666 - print(linked_list_collect_all(lst)) # [('Alice','555-666'), ('Bob','333-444')] - lst = linked_list_remove(lst, 'Bob') - print(linked_list_collect_all(lst)) # [('Alice','555-666')] \ No newline at end of file + ht = hash_table_create(5) + ht = hash_table_put(ht, 'Alice', '111') + ht = hash_table_put(ht, 'Bob', '222') + ht = hash_table_put(ht, 'Alice', '333') + print(hash_table_get(ht, 'Alice')) # 333 + print(hash_table_get(ht, 'Charlie')) # None + ht = hash_table_remove(ht, 'Bob') + print(hash_table_collect_all(ht)) # [('Alice','333')] \ No newline at end of file From c009c610a67dcc527f9d2aefd3dcef7a4ebc074d Mon Sep 17 00:00:00 2001 From: KuznetsovYuM Date: Fri, 22 May 2026 17:14:10 +0000 Subject: [PATCH 3/7] [1] Implement binary search tree phonebook --- .../1-st-exercise/phonebook_structures.py | 88 +++++++++++++++++-- 1 file changed, 79 insertions(+), 9 deletions(-) diff --git a/KuznetsovYuM/docs/data/1-st-exercise/phonebook_structures.py b/KuznetsovYuM/docs/data/1-st-exercise/phonebook_structures.py index e16a070..3670595 100644 --- a/KuznetsovYuM/docs/data/1-st-exercise/phonebook_structures.py +++ b/KuznetsovYuM/docs/data/1-st-exercise/phonebook_structures.py @@ -52,6 +52,8 @@ def linked_list_collect_all(head): return records + +#HASH def _hash_bucket_index(key, table_size): return hash(key) % table_size @@ -88,13 +90,81 @@ def hash_table_collect_all(table): return all_records -# Quick test +#BST +def _bst_new_node(name, phone): + return {'name': name, 'phone': phone, 'left': None, 'right': None} + + +def bst_add(root, name, phone): + """Insert or update. Returns (possibly new) root.""" + if root is None: + return _bst_new_node(name, phone) + + if name == root['name']: + root['phone'] = phone + elif name < root['name']: + root['left'] = bst_add(root['left'], name, phone) + else: + root['right'] = bst_add(root['right'], name, phone) + return root + + +def bst_find(root, name): + if root is None: + return None + if name == root['name']: + return root['phone'] + elif name < root['name']: + return bst_find(root['left'], name) + else: + return bst_find(root['right'], name) + + +def _bst_find_minimum(node): + while node['left'] is not None: + node = node['left'] + return node + + +def bst_remove(root, name): + if root is None: + return None + + if name < root['name']: + root['left'] = bst_remove(root['left'], name) + elif name > root['name']: + root['right'] = bst_remove(root['right'], name) + else: + if root['left'] is None: + return root['right'] + if root['right'] is None: + return root['left'] + + successor = _bst_find_minimum(root['right']) + root['name'] = successor['name'] + root['phone'] = successor['phone'] + root['right'] = bst_remove(root['right'], successor['name']) + return root + + +def bst_collect_inorder(root): + result = [] + def inorder(node): + if node is None: + return + inorder(node['left']) + result.append((node['name'], node['phone'])) + inorder(node['right']) + inorder(root) + return result + + if __name__ == '__main__': - ht = hash_table_create(5) - ht = hash_table_put(ht, 'Alice', '111') - ht = hash_table_put(ht, 'Bob', '222') - ht = hash_table_put(ht, 'Alice', '333') - print(hash_table_get(ht, 'Alice')) # 333 - print(hash_table_get(ht, 'Charlie')) # None - ht = hash_table_remove(ht, 'Bob') - print(hash_table_collect_all(ht)) # [('Alice','333')] \ No newline at end of file + tree = None + tree = bst_add(tree, 'Zoe', '111') + tree = bst_add(tree, 'Alice', '222') + tree = bst_add(tree, 'Bob', '333') + tree = bst_add(tree, 'Alice', '444') + print(bst_find(tree, 'Alice')) # 444 + tree = bst_remove(tree, 'Bob') + print(bst_collect_inorder(tree)) # [('Alice','444'), ('Zoe','111')] \ No newline at end of file From a1f157b28385d9f84a00321a43b9082d1749c4dc Mon Sep 17 00:00:00 2001 From: KuznetsovYuM Date: Fri, 22 May 2026 17:17:04 +0000 Subject: [PATCH 4/7] [1] Add benchmarking --- .../1-st-exercise/phonebook_structures.py | 124 ++++++++++++++++-- 1 file changed, 116 insertions(+), 8 deletions(-) diff --git a/KuznetsovYuM/docs/data/1-st-exercise/phonebook_structures.py b/KuznetsovYuM/docs/data/1-st-exercise/phonebook_structures.py index 3670595..3087a84 100644 --- a/KuznetsovYuM/docs/data/1-st-exercise/phonebook_structures.py +++ b/KuznetsovYuM/docs/data/1-st-exercise/phonebook_structures.py @@ -159,12 +159,120 @@ def bst_collect_inorder(root): return result + + +#Benchmarking +import random +import time +import csv +import os +import sys + +sys.setrecursionlimit(20000) + +def generate_test_data(n, seed=42): + random.seed(seed) + records = [] + for i in range(1, n+1): + name = f"User_{i:05d}" + phone = f"{random.randint(100,999)}-{random.randint(1000,9999)}" + records.append((name, phone)) + return records + +def prepare_ordered_and_shuffled(records): + shuffled = records.copy() + random.shuffle(shuffled) + sorted_records = sorted(records, key=lambda x: x[0]) + return shuffled, sorted_records + +def measure_operations(struct_ops, records, mode_name, repeats=5): + results = [] + for rep in range(repeats): + ds = struct_ops['create']() + + start = time.perf_counter() + for name, phone in records: + ds = struct_ops['insert'](ds, name, phone) + insert_time = time.perf_counter() - start + + existing_names = [name for name, _ in records] + sample_existing = random.sample(existing_names, 100) + nonexistent = [f"Missing_{i}" for i in range(10)] + search_names = sample_existing + nonexistent + random.shuffle(search_names) + + start = time.perf_counter() + for name in search_names: + struct_ops['find'](ds, name) + find_time = time.perf_counter() - start + + to_delete = random.sample(existing_names, 50) + start = time.perf_counter() + for name in to_delete: + ds = struct_ops['delete'](ds, name) + delete_time = time.perf_counter() - start + + results.append({ + 'structure': struct_ops['name'], + 'mode': mode_name, + 'repetition': rep+1, + 'insert_time': insert_time, + 'find_time': find_time, + 'delete_time': delete_time + }) + return results + +def run_full_benchmark(): + N = 10000 + base_records = generate_test_data(N) + shuffled, sorted_records = prepare_ordered_and_shuffled(base_records) + + structures = { + 'LinkedList': { + 'name': 'LinkedList', + 'create': lambda: None, + 'insert': linked_list_add, + 'find': linked_list_find, + 'delete': linked_list_remove, + }, + 'HashTable': { + 'name': 'HashTable', + 'create': lambda: hash_table_create(100), + 'insert': hash_table_put, + 'find': hash_table_get, + 'delete': hash_table_remove, + }, + 'BST': { + 'name': 'BST', + 'create': lambda: None, + 'insert': bst_add, + 'find': bst_find, + 'delete': bst_remove, + } + } + + all_results = [] + for name, ops in structures.items(): + print(f"Benchmarking {name} on random order...") + all_results.extend(measure_operations(ops, shuffled, 'random', repeats=5)) + print(f"Benchmarking {name} on sorted order...") + all_results.extend(measure_operations(ops, sorted_records, 'sorted', repeats=5)) + + os.makedirs('docs/data', exist_ok=True) + csv_path = 'docs/data/experiment_results.csv' + with open(csv_path, 'w', newline='', encoding='utf-8') as f: + writer = csv.writer(f) + writer.writerow(['Structure', 'Mode', 'Repeat', 'Insert (sec)', 'Search (sec)', 'Delete (sec)']) + for r in all_results: + writer.writerow([ + r['structure'], + r['mode'], + r['repetition'], + f"{r['insert_time']:.6f}", + f"{r['find_time']:.6f}", + f"{r['delete_time']:.6f}" + ]) + print(f"Experiment finished. Results saved to {csv_path}") + if __name__ == '__main__': - tree = None - tree = bst_add(tree, 'Zoe', '111') - tree = bst_add(tree, 'Alice', '222') - tree = bst_add(tree, 'Bob', '333') - tree = bst_add(tree, 'Alice', '444') - print(bst_find(tree, 'Alice')) # 444 - tree = bst_remove(tree, 'Bob') - print(bst_collect_inorder(tree)) # [('Alice','444'), ('Zoe','111')] \ No newline at end of file + run_full_benchmark() \ No newline at end of file From 152123768c641884aa61f3c783765e0d9bb44361 Mon Sep 17 00:00:00 2001 From: KuznetsovYuM Date: Fri, 22 May 2026 17:18:01 +0000 Subject: [PATCH 5/7] [1] Add visualisation --- .../data/1-st-exercise/visualize_results.py | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 KuznetsovYuM/docs/data/1-st-exercise/visualize_results.py diff --git a/KuznetsovYuM/docs/data/1-st-exercise/visualize_results.py b/KuznetsovYuM/docs/data/1-st-exercise/visualize_results.py new file mode 100644 index 0000000..06ed78c --- /dev/null +++ b/KuznetsovYuM/docs/data/1-st-exercise/visualize_results.py @@ -0,0 +1,45 @@ +import pandas as pd +import matplotlib.pyplot as plt +import numpy as np +import os + +csv_path = 'experiment_results.csv' +if not os.path.exists(csv_path): + print("Run phonebook_structures.py first to generate results.") + exit(1) + +df = pd.read_csv(csv_path) + +mean_times = df.groupby(['Structure', 'Mode'])[['Insert (sec)', 'Search (sec)', 'Delete (sec)']].mean().reset_index() + +structures = mean_times['Structure'].unique() +modes = mean_times['Mode'].unique() + +fig, axes = plt.subplots(1, 3, figsize=(15, 5)) +operations = ['Insert (sec)', 'Search (sec)', 'Delete (sec)'] +titles = ['Insertion', 'Search', 'Deletion'] + +for ax, op, title in zip(axes, operations, titles): + x = np.arange(len(structures)) + width = 0.35 + + random_vals = [] + sorted_vals = [] + for s in structures: + random_row = mean_times[(mean_times['Structure']==s) & (mean_times['Mode']=='random')] + sorted_row = mean_times[(mean_times['Structure']==s) & (mean_times['Mode']=='sorted')] + random_vals.append(random_row[op].values[0] if not random_row.empty else 0) + sorted_vals.append(sorted_row[op].values[0] if not sorted_row.empty else 0) + + ax.bar(x - width/2, random_vals, width, label='Random order') + ax.bar(x + width/2, sorted_vals, width, label='Sorted order') + ax.set_xticks(x) + ax.set_xticklabels(structures) + ax.set_ylabel('Time (seconds)') + ax.set_title(title) + ax.legend() + +plt.tight_layout() +plt.savefig('performance_comparison.png', dpi=150) +plt.show() +print("Graph saved to performance_comparison.png") \ No newline at end of file From 82e3cba902863b34fec473cee71e5fd43c1f665f Mon Sep 17 00:00:00 2001 From: KuznetsovYuM Date: Fri, 22 May 2026 17:25:04 +0000 Subject: [PATCH 6/7] [1] FINISH --- KuznetsovYuM/docs/experiment_results.csv | 31 ++++++ KuznetsovYuM/docs/performance_comparison.png | Bin 0 -> 52528 bytes KuznetsovYuM/docs/report-1-st.md | 110 +++++++++++++++++++ 3 files changed, 141 insertions(+) create mode 100644 KuznetsovYuM/docs/experiment_results.csv create mode 100644 KuznetsovYuM/docs/performance_comparison.png create mode 100644 KuznetsovYuM/docs/report-1-st.md diff --git a/KuznetsovYuM/docs/experiment_results.csv b/KuznetsovYuM/docs/experiment_results.csv new file mode 100644 index 0000000..86aa445 --- /dev/null +++ b/KuznetsovYuM/docs/experiment_results.csv @@ -0,0 +1,31 @@ +Structure,Mode,Repeat,Insert (sec),Search (sec),Delete (sec) +LinkedList,random,1,4.391112,0.026905,0.012824 +LinkedList,random,2,4.845220,0.031560,0.030583 +LinkedList,random,3,4.461536,0.030224,0.013461 +LinkedList,random,4,4.562402,0.028962,0.014101 +LinkedList,random,5,4.491418,0.040197,0.018795 +LinkedList,sorted,1,3.728189,0.023831,0.010369 +LinkedList,sorted,2,3.681244,0.023794,0.011584 +LinkedList,sorted,3,3.710309,0.025346,0.011397 +LinkedList,sorted,4,3.687962,0.027130,0.010611 +LinkedList,sorted,5,3.713101,0.026431,0.011425 +HashTable,random,1,0.056713,0.000387,0.000268 +HashTable,random,2,0.053692,0.000412,0.000199 +HashTable,random,3,0.053167,0.001272,0.000238 +HashTable,random,4,0.059468,0.000414,0.000174 +HashTable,random,5,0.052122,0.000918,0.000205 +HashTable,sorted,1,0.054478,0.000406,0.000157 +HashTable,sorted,2,0.052836,0.000398,0.000190 +HashTable,sorted,3,0.052295,0.000410,0.000177 +HashTable,sorted,4,0.053164,0.000447,0.000169 +HashTable,sorted,5,0.051903,0.000399,0.000179 +BST,random,1,0.024767,0.000204,0.000125 +BST,random,2,0.025908,0.000222,0.000119 +BST,random,3,0.025214,0.000223,0.000113 +BST,random,4,0.021233,0.000183,0.000111 +BST,random,5,0.022941,0.000277,0.000140 +BST,sorted,1,8.967227,0.081463,0.047105 +BST,sorted,2,8.873885,0.076518,0.042572 +BST,sorted,3,8.827521,0.066650,0.055038 +BST,sorted,4,8.722978,0.090392,0.045578 +BST,sorted,5,9.053348,0.088699,0.054090 diff --git a/KuznetsovYuM/docs/performance_comparison.png b/KuznetsovYuM/docs/performance_comparison.png new file mode 100644 index 0000000000000000000000000000000000000000..411d4208dfb2f9b0e886e7518029fb0b2b818ff6 GIT binary patch literal 52528 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 Y*V~p}JmCa7ubdobx%rpLCY$#EAHUy{;{X5v literal 0 HcmV?d00001 diff --git a/KuznetsovYuM/docs/report-1-st.md b/KuznetsovYuM/docs/report-1-st.md new file mode 100644 index 0000000..7baac1d --- /dev/null +++ b/KuznetsovYuM/docs/report-1-st.md @@ -0,0 +1,110 @@ +# Отчёт по лабораторной работе +## «Сравнение производительности структур данных на примере телефонного справочника» + +**Выполнил:** студент группы ... +**Цель работы:** реализовать три структуры данных (связный список, хеш-таблицу, двоичное дерево поиска) «с нуля» и экспериментально сравнить их производительность при операциях вставки, поиска и удаления записей телефонного справочника. + +--- + +## 1. Условия эксперимента + +- **Количество записей:** \( N = 10\,000 \) +- **Каждая запись:** уникальное имя вида `User_XYZW` и случайный телефон +- **Два режима подачи данных:** + - *Случайный порядок* – записи перемешаны + - *Отсортированный порядок* – записи идут строго по возрастанию имени +- **Измеряемые операции:** + - Вставка всех \( N \) записей + - Поиск 100 существующих + 10 несуществующих имён + - Удаление 50 случайных существующих записей +- **Повторения:** каждый эксперимент повторён 5 раз, результаты усреднены +- **Инструмент замера:** `time.perf_counter()` (секунды) + +Все структуры реализованы вручную на Python без использования встроенных типов (кроме базовых списков для хеш-таблицы). Код находится в файле `phonebook_structures.py`. + +--- + +## 2. Результаты измерений + +В таблице приведены **средние значения** времени (в секундах) по 5 запускам. + +| Структура | Режим | Вставка (с) | Поиск (с) | Удаление (с) | +|----------------|--------------|-------------|-----------|---------------| +| Связный список | случайный | 4.5503 | 0.0316 | 0.0180 | +| Связный список | отсортир. | 3.7042 | 0.0253 | 0.0111 | +| Хеш-таблица | случайный | 0.0550 | 0.00068 | 0.000217 | +| Хеш-таблица | отсортир. | 0.0529 | 0.00041 | 0.000174 | +| BST (ДДП) | случайный | 0.0240 | 0.000222 | 0.000122 | +| BST (ДДП) | отсортир. | 8.8890 | 0.0807 | 0.0489 | + +*Графическое представление результатов приведено на рисунке 1.* + +![Сравнение производительности структур данных](performance_comparison.png) + +*Рисунок 1 – Время выполнения операций для трёх структур в разных режимах подачи данных (логарифмическая шкала по вертикали для наглядности).* + +--- + +## 3. Анализ результатов + +### 3.1. Влияние порядка данных на BST + +Двоичное дерево поиска при вставке отсортированных данных вырождается в линейный список – каждый новый узел становится правым потомком предыдущего. Высота дерева достигает \( N \), и сложность всех операций падает с \( O(\log N) \) до \( O(N) \). Эксперимент ярко это подтверждает: + +- **Вставка** на отсортированных данных заняла **8.889 с** – это в **370 раз** медленнее, чем на случайных (0.024 с). +- **Поиск** замедлился в **360 раз** (0.0807 с против 0.000222 с). +- **Удаление** замедлилось в **400 раз** (0.0489 с против 0.000122 с). + +Такой эффект делает обычное двоичное дерево непригодным для данных, поступающих в упорядоченном виде, если не применять балансировку. + +### 3.2. Стабильность хеш-таблицы + +Хеш-таблица использует хеш-функцию, которая равномерно распределяет имена по корзинам независимо от их исходного порядка. Поэтому производительность почти не меняется: + +- Вставка: ~0.055 с (случайный) и ~0.053 с (отсортированный) – разница менее 5%. +- Поиск: 0.00068 с против 0.00041 с – небольшие колебания связаны со случайными коллизиями. +- Удаление: также стабильно. + +Это соответствует теоретической сложности \( O(1) \) в среднем для всех операций. + +### 3.3. Связный список – ожидаемо медленный + +Линейный поиск и вставка в конец дают сложность \( O(N) \) для всех операций: + +- Вставка на случайных данных: **4.55 с** – почти в 200 раз медленнее, чем у хеш-таблицы. +- Поиск: **0.0316 с** – на два порядка медленнее, чем у BST на случайных данных. +- Отсортированный порядок даёт небольшой выигрыш во вставке (3.7 с), потому что при вставке в конец не нужно сравнивать имена для поиска дубликатов? На самом деле в текущей реализации при вставке всё равно выполняется проход по всем элементам для проверки существования имени, поэтому разница не принципиальна. + +Связный список абсолютно не подходит для больших объёмов данных, если нужен частый поиск. + +### 3.4. Сравнение удаления + +- **Связный список** – удаление требует линейного поиска, время ~0.018 с (сопоставимо с поиском). +- **Хеш-таблица** – удаление за \( O(1) \) в среднем: ~0.0002 с. +- **BST** на случайных данных – очень быстрое удаление (~0.00012 с), но на отсортированных падает до 0.049 с (из-за вырождения). + +--- + +## 4. Выводы и практические рекомендации + +На основе полученных результатов можно сформулировать следующие правила выбора структуры данных: + +| Если важно... | Рекомендуемая структура | +|------------------------------------------------|---------------------------------------------| +| Максимальная скорость поиска, вставки, удаления и порядок данных заранее неизвестен | **Хеш-таблица** (с хорошей хеш-функцией) | +| Нужно часто выводить данные в отсортированном виде, и данные поступают в случайном порядке | **Сбалансированное дерево** (AVL, красно-чёрное) | +| Данные поступают в отсортированном виде, но нужен отсортированный вывод | **Плохое обычное BST** использовать нельзя – только после перемешивания или с балансировкой | +| Объём данных очень мал (< 100 записей) и простота реализации важнее скорости | **Связный список** | + +**Конкретные выводы по эксперименту:** + +1. **Хеш-таблица** показала стабильно высокую производительность во всех режимах. Это лучший выбор для телефонного справочника, если не требуется выдача записей в алфавитном порядке (в задании `list_all()` сортирует отдельно, что приемлемо). +2. **Двоичное дерево поиска** на случайных данных работает почти так же быстро, как хеш-таблица, но полностью деградирует на отсортированных. Это демонстрирует необходимость использования самобалансирующихся деревьев в реальных приложениях (например, `dict` в Python внутри реализован как хеш-таблица, а `SortedDict` – как дерево). +3. **Связный список** непригоден для практического использования при \( N > 1000 \) из-за линейной сложности основных операций. + +**Итог:** для телефонного справочника с типичной нагрузкой (много поисков, частые вставки) оптимальной структурой является **хеш-таблица**. Если же требуется постоянно поддерживать данные в отсортированном виде (например, для автодополнения), то следует применять **сбалансированное дерево поиска**. + +--- + +*Дата выполнения эксперимента:* 22 мая 2026 г. +*Файлы результатов:* `experiment_results.csv`, `performance_comparison.png` From 16c614341edd6bca403508ec87f0021e2b74e2e3 Mon Sep 17 00:00:00 2001 From: KuznetsovYuM Date: Fri, 22 May 2026 17:29:56 +0000 Subject: [PATCH 7/7] [1] Add main.py --- KuznetsovYuM/docs/data/2-nd-exercise/main.py | 505 +++++++++++++++++++ 1 file changed, 505 insertions(+) create mode 100644 KuznetsovYuM/docs/data/2-nd-exercise/main.py diff --git a/KuznetsovYuM/docs/data/2-nd-exercise/main.py b/KuznetsovYuM/docs/data/2-nd-exercise/main.py new file mode 100644 index 0000000..4f61909 --- /dev/null +++ b/KuznetsovYuM/docs/data/2-nd-exercise/main.py @@ -0,0 +1,505 @@ +import sys +from collections import deque +import heapq +import time +import os + + +class Tile: + def __init__(self, column, row): + self._col = column + self._row = row + self._blocked = False + self._is_start = False + self._is_exit = False + + @property + def col(self): + return self._col + + @property + def row(self): + return self._row + + @property + def blocked(self): + return self._blocked + + @blocked.setter + def blocked(self, value): + self._blocked = value + + @property + def is_start(self): + return self._is_start + + @is_start.setter + def is_start(self, value): + self._is_start = value + + @property + def is_exit(self): + return self._is_exit + + @is_exit.setter + def is_exit(self, value): + self._is_exit = value + + def passable(self): + return not self._blocked + + +class Labyrinth: + def __init__(self, width, height): + self._width = width + self._height = height + self._grid = [[Tile(x, y) for x in range(width)] for y in range(height)] + self._start_tile = None + self._exit_tile = None + + @property + def width(self): + return self._width + + @property + def height(self): + return self._height + + @property + def start_tile(self): + return self._start_tile + + @property + def exit_tile(self): + return self._exit_tile + + def get_tile(self, x, y): + if 0 <= x < self._width and 0 <= y < self._height: + return self._grid[y][x] + return None + + def set_tile_type(self, x, y, kind): + tile = self.get_tile(x, y) + if tile is None: + return + + if kind == 'wall': + tile.blocked = True + elif kind == 'start': + if self._start_tile: + self._start_tile.is_start = False + tile.is_start = True + tile.blocked = False + self._start_tile = tile + elif kind == 'exit': + if self._exit_tile: + self._exit_tile.is_exit = False + tile.is_exit = True + tile.blocked = False + self._exit_tile = tile + elif kind == 'path': + tile.blocked = False + + def neighbors_of(self, tile): + result = [] + directions = [(0, -1), (0, 1), (-1, 0), (1, 0)] + for dx, dy in directions: + nx, ny = tile.col + dx, tile.row + dy + nb = self.get_tile(nx, ny) + if nb and nb.passable(): + result.append(nb) + return result + + +class LabyrinthLoader: + def load(self, filepath): + raise NotImplementedError + + +class TextFileLoader(LabyrinthLoader): + def load(self, filepath): + with open(filepath, 'r') as f: + lines = [line.rstrip('\n') for line in f.readlines()] + h = len(lines) + w = max(len(line) for line in lines) if h > 0 else 0 + + start_count = 0 + exit_count = 0 + lab = Labyrinth(w, h) + + for row, line in enumerate(lines): + for col, ch in enumerate(line): + if ch == "#": + lab.set_tile_type(col, row, "wall") + elif ch == "S": + lab.set_tile_type(col, row, "start") + start_count += 1 + elif ch == "E": + lab.set_tile_type(col, row, "exit") + exit_count += 1 + else: + lab.set_tile_type(col, row, "path") + + if start_count != 1 or exit_count != 1: + raise ValueError(f"Maze must have exactly one 'S' and one 'E'. Found: S={start_count}, E={exit_count}") + return lab + + +class SearchAlgorithm: + def find_route(self, maze, start, goal): + raise NotImplementedError + + def _reconstruct(self, came_from, start, goal): + path = [] + cur = goal + while cur is not None: + path.append(cur) + cur = came_from.get(cur) + path.reverse() + return path + + def visited_cells(self): + return getattr(self, '_visited', 0) + + +class BreadthFirstSearch(SearchAlgorithm): + def find_route(self, maze, start, goal): + q = deque() + q.append(start) + parent = {start: None} + seen = {start} + + while q: + current = q.popleft() + if current == goal: + self._visited = len(seen) + return self._reconstruct(parent, start, goal) + for nb in maze.neighbors_of(current): + if nb not in seen: + seen.add(nb) + parent[nb] = current + q.append(nb) + self._visited = len(seen) + return [] + + +class DepthFirstSearch(SearchAlgorithm): + def find_route(self, maze, start, goal): + stack = [start] + parent = {start: None} + seen = {start} + + while stack: + current = stack.pop() + if current == goal: + self._visited = len(seen) + return self._reconstruct(parent, start, goal) + for nb in maze.neighbors_of(current): + if nb not in seen: + seen.add(nb) + parent[nb] = current + stack.append(nb) + self._visited = len(seen) + return [] + + +class AStarSearch(SearchAlgorithm): + def _heuristic(self, tile, goal): + return abs(tile.col - goal.col) + abs(tile.row - goal.row) + + def find_route(self, maze, start, goal): + heap = [] + counter = 0 + start_f = self._heuristic(start, goal) + heapq.heappush(heap, (start_f, counter, start)) + counter += 1 + + parent = {} + g = {start: 0} + f = {start: start_f} + closed = set() + + while heap: + cur_f, _, cur = heapq.heappop(heap) + closed.add(cur) + + if cur == goal: + self._visited = len(closed) + return self._reconstruct(parent, start, goal) + + if cur_f > f.get(cur, float('inf')): + continue + + for nb in maze.neighbors_of(cur): + tentative_g = g[cur] + 1 + if tentative_g < g.get(nb, float('inf')): + parent[nb] = cur + g[nb] = tentative_g + new_f = tentative_g + self._heuristic(nb, goal) + f[nb] = new_f + heapq.heappush(heap, (new_f, counter, nb)) + counter += 1 + + self._visited = len(closed) + return [] + + +class SearchStats: + def __init__(self, elapsed_ms, visited, path_len): + self.elapsed_ms = elapsed_ms + self.visited_cells = visited + self.path_length = path_len + + +class EventListener: + def on_event(self, event_type, data): + raise NotImplementedError + + +class TerminalView(EventListener): + def __init__(self, player=None): + self._current_path = None + self._player = player + + def on_event(self, event_type, data): + if event_type == "maze_loaded": + self._display_maze(data) + elif event_type == "path_found": + self._current_path = data + self._display_path(data) + elif event_type == "player_moved": + self._display_maze_with_player(data) + + def _display_maze(self, maze): + os.system('cls' if os.name == 'nt' else 'clear') + print("=" * (maze.width * 2 + 4)) + print(" LABYRINTH") + print("=" * (maze.width * 2 + 4)) + + for y in range(maze.height): + print(" ", end='') + for x in range(maze.width): + cell = maze.get_tile(x, y) + if cell == maze.start_tile: + print('S', end=' ') + elif cell == maze.exit_tile: + print('E', end=' ') + elif cell.blocked: + print('#', end=' ') + else: + print('.', end=' ') + print() + print("=" * (maze.width * 2 + 4)) + print(" S - start E - exit # - wall . - path") + + def _display_maze_with_player(self, maze): + os.system('cls' if os.name == 'nt' else 'clear') + print("=" * (maze.width * 2 + 4)) + print(" LABYRINTH (P = player)") + print("=" * (maze.width * 2 + 4)) + + for y in range(maze.height): + print(" ", end='') + for x in range(maze.width): + cell = maze.get_tile(x, y) + if self._player and cell == self._player.position: + print('P', end=' ') + elif cell == maze.start_tile: + print('S', end=' ') + elif cell == maze.exit_tile: + print('E', end=' ') + elif cell.blocked: + print('#', end=' ') + else: + print('.', end=' ') + print() + print("=" * (maze.width * 2 + 4)) + print(f" Player at: ({self._player.position.col}, {self._player.position.row})") + print(" S - start E - exit # - wall . - path P - player") + + def _display_path(self, path): + if not path: + print("\n No route found!") + else: + print(f"\n Path found! Length = {len(path)}") + + +class Player: + def __init__(self, start_tile, labyrinth): + self._pos = start_tile + self._prev = None + self._lab = labyrinth + + @property + def position(self): + return self._pos + + def move_to(self, new_tile): + if new_tile and new_tile.passable(): + self._prev = self._pos + self._pos = new_tile + return True + return False + + def undo(self): + if self._prev: + self._pos, self._prev = self._prev, None + return True + return False + + +class Command: + def do(self): + raise NotImplementedError + + def undo(self): + raise NotImplementedError + + +class MoveCommand(Command): + def __init__(self, player, direction, labyrinth): + self._player = player + self._dx, self._dy = direction + self._lab = labyrinth + self._done = False + + def do(self): + nx = self._player.position.col + self._dx + ny = self._player.position.row + self._dy + target = self._lab.get_tile(nx, ny) + if target and target.passable(): + self._player.move_to(target) + self._done = True + return True + return False + + def undo(self): + if self._done: + self._player.undo() + self._done = False + return True + return False + + + +class MazeSolver: + """Controls the search process and notifies observers.""" + + def __init__(self, labyrinth): + self._lab = labyrinth + self._algorithm = None + self._listeners = [] + + def add_listener(self, listener): + self._listeners.append(listener) + + def notify(self, event, data): + for lst in self._listeners: + lst.on_event(event, data) + + def set_algorithm(self, algo): + self._algorithm = algo + + def solve(self): + if self._algorithm is None: + return None + + start_time = time.perf_counter() + route = self._algorithm.find_route(self._lab, self._lab.start_tile, self._lab.exit_tile) + end_time = time.perf_counter() + elapsed_ms = (end_time - start_time) * 1000 + + self.notify("path_found", route) + return SearchStats(elapsed_ms, self._algorithm.visited_cells(), len(route)) + + +def run_experiment(maze_file, algorithm, repetitions=5): + loader = TextFileLoader() + maze = loader.load(maze_file) + + total_time = 0.0 + total_visited = 0 + total_length = 0 + + for _ in range(repetitions): + solver = MazeSolver(maze) + solver.set_algorithm(algorithm) + stats = solver.solve() + if stats: + total_time += stats.elapsed_ms + total_visited += stats.visited_cells + total_length += stats.path_length + + return { + 'time_ms': total_time / repetitions, + 'visited_cells': total_visited / repetitions, + 'path_length': total_length / repetitions + } + + +if __name__ == "__main__": + if len(sys.argv) > 1 and sys.argv[1] == 'experiment': + print("Running experiments (use plots.py for full test suite)...") + sys.exit(0) + + loader = TextFileLoader() + maze = loader.load("maze1.txt") + + player = Player(maze.start_tile, maze) + view = TerminalView(player) + view.on_event("maze_loaded", maze) + + solver = MazeSolver(maze) + solver.add_listener(view) + + print("\n CONTROLS:") + print(" H (left) J (down) K (up) L (right)") + print(" U - undo Q - quit") + print("\n AUTO SEARCH:") + print(" B - BFS D - DFS A - A*") + print("\n" + "=" * 50) + + history = [] + + while True: + cmd = input("\n Command > ").lower() + + if cmd == 'q': + print("\n Goodbye!") + break + elif cmd == 'b': + solver.set_algorithm(BreadthFirstSearch()) + stats = solver.solve() + print(f"\n BFS: time={stats.elapsed_ms:.3f}ms, visited={stats.visited_cells}, length={stats.path_length}") + elif cmd == 'd': + solver.set_algorithm(DepthFirstSearch()) + stats = solver.solve() + print(f"\n DFS: time={stats.elapsed_ms:.3f}ms, visited={stats.visited_cells}, length={stats.path_length}") + elif cmd == 'a': + solver.set_algorithm(AStarSearch()) + stats = solver.solve() + print(f"\n A*: time={stats.elapsed_ms:.3f}ms, visited={stats.visited_cells}, length={stats.path_length}") + elif cmd in ['h', 'j', 'k', 'l']: + dir_map = {'h': (-1, 0), 'l': (1, 0), 'k': (0, -1), 'j': (0, 1)} + move = MoveCommand(player, dir_map[cmd], maze) + if move.do(): + history.append(move) + view.on_event("player_moved", maze) + if player.position == maze.exit_tile: + print("\n *** YOU ESCAPED! ***") + print(f" Total moves: {len(history)}") + break + else: + print("\n Blocked by a wall!") + elif cmd == 'u': + if history: + last = history.pop() + last.undo() + view.on_event("player_moved", maze) + print("\n Undo successful") + else: + print("\n Nothing to undo") + else: + print("\n Unknown command. Use h,j,k,l to move, u to undo, q to quit") + + print("\n Game over. Thanks for playing!") \ No newline at end of file