[1, 2]lab1, lab2 #296

Merged
AlexanderVah merged 2 commits from filippovavm/2026-rff_mp:lab1 into develop 2026-05-30 11:29:40 +00:00
25 changed files with 4065 additions and 0 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

View File

@ -0,0 +1,490 @@
{
"cells": [
{
"cell_type": "code",
"execution_count": null,
"id": "65b0def8-04cf-4cfd-b4d4-bc862e120497",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Генерация данных...\n",
"\n",
"Измерение вставки для Связного списка...\n"
]
}
],
"source": [
"import time\n",
"import random\n",
"import csv\n",
"import sys\n",
"import matplotlib.pyplot as plt\n",
"import numpy as np\n",
"\n",
"# Связный список\n",
"def ll_insert(head, name, phone):\n",
" current = head\n",
" prev = None\n",
" while current is not None:\n",
" if current['name'] == name:\n",
" current['phone'] = phone\n",
" return head\n",
" prev = current\n",
" current = current['next']\n",
" new_node = {'name': name, 'phone': phone, 'next': None}\n",
" if prev is None:\n",
" return new_node\n",
" else:\n",
" prev['next'] = new_node\n",
" return head\n",
"\n",
"def ll_find(head, name):\n",
" current = head\n",
" while current is not None:\n",
" if current['name'] == name:\n",
" return current['phone']\n",
" current = current['next']\n",
" return None\n",
"\n",
"def ll_delete(head, name):\n",
" if head is None:\n",
" return None\n",
" if head['name'] == name:\n",
" return head['next']\n",
" current = head\n",
" while current['next'] is not None:\n",
" if current['next']['name'] == name:\n",
" current['next'] = current['next']['next']\n",
" return head\n",
" current = current['next']\n",
" return head\n",
"\n",
"# Хеш-таблица \n",
"def hash_function(name, size):\n",
" total = 0\n",
" for ch in name:\n",
" total = (total * 31 + ord(ch)) % size\n",
" return total\n",
"\n",
"def ht_create(size=2000):\n",
" return [None] * size\n",
"\n",
"def ht_insert(buckets, name, phone):\n",
" idx = hash_function(name, len(buckets))\n",
" buckets[idx] = ll_insert(buckets[idx], name, phone)\n",
"\n",
"def ht_find(buckets, name):\n",
" idx = hash_function(name, len(buckets))\n",
" return ll_find(buckets[idx], name)\n",
"\n",
"def ht_delete(buckets, name):\n",
" idx = hash_function(name, len(buckets))\n",
" buckets[idx] = ll_delete(buckets[idx], name)\n",
"\n",
"# Двоичное дерево поиска \n",
"def bst_insert(root, name, phone):\n",
" new_node = {'name': name, 'phone': phone, 'left': None, 'right': None}\n",
" if root is None:\n",
" return new_node\n",
" current = root\n",
" while True:\n",
" if name < current['name']:\n",
" if current['left'] is None:\n",
" current['left'] = new_node\n",
" break\n",
" current = current['left']\n",
" elif name > current['name']:\n",
" if current['right'] is None:\n",
" current['right'] = new_node\n",
" break\n",
" current = current['right']\n",
" else:\n",
" current['phone'] = phone\n",
" break\n",
" return root\n",
"\n",
"def bst_find(root, name):\n",
" current = root\n",
" while current is not None:\n",
" if name < current['name']:\n",
" current = current['left']\n",
" elif name > current['name']:\n",
" current = current['right']\n",
" else:\n",
" return current['phone']\n",
" return None\n",
"\n",
"def bst_find_min(node):\n",
" while node['left'] is not None:\n",
" node = node['left']\n",
" return node\n",
"\n",
"def bst_delete(root, name):\n",
" parent = None\n",
" current = root\n",
" while current is not None and current['name'] != name:\n",
" parent = current\n",
" if name < current['name']:\n",
" current = current['left']\n",
" else:\n",
" current = current['right']\n",
" if current is None:\n",
" return root\n",
" \n",
" if current['left'] is None and current['right'] is None:\n",
" if parent is None:\n",
" return None\n",
" if parent['left'] is current:\n",
" parent['left'] = None\n",
" else:\n",
" parent['right'] = None\n",
" return root\n",
" \n",
" if current['left'] is None:\n",
" if parent is None:\n",
" return current['right']\n",
" if parent['left'] is current:\n",
" parent['left'] = current['right']\n",
" else:\n",
" parent['right'] = current['right']\n",
" return root\n",
" if current['right'] is None:\n",
" if parent is None:\n",
" return current['left']\n",
" if parent['left'] is current:\n",
" parent['left'] = current['left']\n",
" else:\n",
" parent['right'] = current['left']\n",
" return root\n",
" \n",
" succ_parent = current\n",
" succ = current['right']\n",
" while succ['left'] is not None:\n",
" succ_parent = succ\n",
" succ = succ['left']\n",
" current['name'] = succ['name']\n",
" current['phone'] = succ['phone']\n",
" if succ_parent['left'] is succ:\n",
" succ_parent['left'] = succ['right']\n",
" else:\n",
" succ_parent['right'] = succ['right']\n",
" return root\n",
"\n",
"# Генерация данных \n",
"def generate_records(N):\n",
" records = []\n",
" for i in range(N):\n",
" name = f\"User_{i:05d}\"\n",
" phone = f\"+7-999-{random.randint(1000000, 9999999)}\"\n",
" records.append((name, phone))\n",
" return records\n",
"\n",
"# Замеры\n",
"REPEATS = 5\n",
"N = 10000\n",
"\n",
"def measure_insert(struct, records, repeats=REPEATS):\n",
" times = []\n",
" for _ in range(repeats):\n",
" if struct == 'll':\n",
" head = None\n",
" start = time.perf_counter()\n",
" for name, phone in records:\n",
" head = ll_insert(head, name, phone)\n",
" end = time.perf_counter()\n",
" elif struct == 'ht':\n",
" buckets = ht_create(2000)\n",
" start = time.perf_counter()\n",
" for name, phone in records:\n",
" ht_insert(buckets, name, phone)\n",
" end = time.perf_counter()\n",
" elif struct == 'bst':\n",
" root = None\n",
" start = time.perf_counter()\n",
" for name, phone in records:\n",
" root = bst_insert(root, name, phone)\n",
" end = time.perf_counter()\n",
" times.append(end - start)\n",
" return times\n",
"\n",
"def build_structure(struct, records):\n",
" if struct == 'll':\n",
" head = None\n",
" for name, phone in records:\n",
" head = ll_insert(head, name, phone)\n",
" return head\n",
" elif struct == 'ht':\n",
" buckets = ht_create(2000)\n",
" for name, phone in records:\n",
" ht_insert(buckets, name, phone)\n",
" return buckets\n",
" elif struct == 'bst':\n",
" root = None\n",
" for name, phone in records:\n",
" root = bst_insert(root, name, phone)\n",
" return root\n",
"\n",
"def measure_find_on_structure(struct, structure, records, repeats=REPEATS):\n",
" times = []\n",
" N = len(records)\n",
" for _ in range(repeats):\n",
" indices = random.sample(range(N), 100)\n",
" exist = [records[i][0] for i in indices]\n",
" missing = [f\"None_{i}\" for i in range(10)]\n",
" search = exist + missing\n",
" start = time.perf_counter()\n",
" if struct == 'll':\n",
" for name in search:\n",
" ll_find(structure, name)\n",
" elif struct == 'ht':\n",
" for name in search:\n",
" ht_find(structure, name)\n",
" elif struct == 'bst':\n",
" for name in search:\n",
" bst_find(structure, name)\n",
" times.append(time.perf_counter() - start)\n",
" return times\n",
"\n",
"def measure_delete_on_structure(struct, records, repeats=REPEATS):\n",
" times = []\n",
" N = len(records)\n",
" for _ in range(repeats):\n",
" indices = random.sample(range(N), 50)\n",
" del_names = [records[i][0] for i in indices]\n",
" if struct == 'll':\n",
" head = None\n",
" for name, phone in records:\n",
" head = ll_insert(head, name, phone)\n",
" start = time.perf_counter()\n",
" for name in del_names:\n",
" head = ll_delete(head, name)\n",
" end = time.perf_counter()\n",
" elif struct == 'ht':\n",
" buckets = ht_create(2000)\n",
" for name, phone in records:\n",
" ht_insert(buckets, name, phone)\n",
" start = time.perf_counter()\n",
" for name in del_names:\n",
" ht_delete(buckets, name)\n",
" end = time.perf_counter()\n",
" elif struct == 'bst':\n",
" root = None\n",
" for name, phone in records:\n",
" root = bst_insert(root, name, phone)\n",
" start = time.perf_counter()\n",
" for name in del_names:\n",
" root = bst_delete(root, name)\n",
" end = time.perf_counter()\n",
" times.append(end - start)\n",
" return times\n",
"\n",
"# Основная функция \n",
"def main():\n",
" print(\"Генерация данных...\")\n",
" records = generate_records(N)\n",
" random.shuffle(records) # случайный порядок\n",
" records_sorted = sorted(records, key=lambda x: x[0]) # отсортированный\n",
"\n",
" results = [] # для CSV\n",
" struct_names = {'ll': 'Связного списка', 'ht': 'Хеш-таблицы', 'bst': 'Двоичного дерева поиска'}\n",
" mode_names = {'shuffled': 'случайный', 'sorted': 'отсортированный'}\n",
" op_names = {'insert': 'Вставка всех записей', 'find': 'Поиск записей', 'delete': 'Удаление записей'}\n",
"\n",
" # для графиков\n",
" insert_sh = {} # {struct: [times]}\n",
" insert_so = {}\n",
" find_sh = {}\n",
" find_so = {}\n",
" delete_sh = {}\n",
" delete_so = {}\n",
"\n",
" # Вставка \n",
" for struct in ['ll', 'ht', 'bst']:\n",
" print(f\"\\nИзмерение вставки для {struct_names[struct]}...\")\n",
" times_sh = measure_insert(struct, records)\n",
" times_so = measure_insert(struct, records_sorted)\n",
" insert_sh[struct] = times_sh\n",
" insert_so[struct] = times_so\n",
" print(f\" случайный: {[round(t,6) for t in times_sh]}, среднее = {sum(times_sh)/len(times_sh):.6f}\")\n",
" print(f\" отсортированный: {[round(t,6) for t in times_so]}, среднее = {sum(times_so)/len(times_so):.6f}\")\n",
" results.append([struct_names[struct], mode_names['shuffled'], op_names['insert'], sum(times_sh)/len(times_sh)] + times_sh)\n",
" results.append([struct_names[struct], mode_names['sorted'], op_names['insert'], sum(times_so)/len(times_so)] + times_so)\n",
"\n",
" # Поиск \n",
" for struct in ['ll', 'ht', 'bst']:\n",
" print(f\"\\nПоиск для {struct_names[struct]} на случайных данных...\")\n",
" structure_sh = build_structure(struct, records)\n",
" times_find_sh = measure_find_on_structure(struct, structure_sh, records)\n",
" find_sh[struct] = times_find_sh\n",
" print(f\" случайный: {[round(t,6) for t in times_find_sh]}, среднее = {sum(times_find_sh)/len(times_find_sh):.6f}\")\n",
" results.append([struct_names[struct], mode_names['shuffled'], op_names['find'], sum(times_find_sh)/len(times_find_sh)] + times_find_sh)\n",
"\n",
" print(f\"Поиск для {struct_names[struct]} на отсортированных данных...\")\n",
" structure_so = build_structure(struct, records_sorted)\n",
" times_find_so = measure_find_on_structure(struct, structure_so, records_sorted)\n",
" find_so[struct] = times_find_so\n",
" print(f\" отсортированный: {[round(t,6) for t in times_find_so]}, среднее = {sum(times_find_so)/len(times_find_so):.6f}\")\n",
" results.append([struct_names[struct], mode_names['sorted'], op_names['find'], sum(times_find_so)/len(times_find_so)] + times_find_so)\n",
"\n",
" # Удаление \n",
" for struct in ['ll', 'ht', 'bst']:\n",
" print(f\"\\nУдаление для {struct_names[struct]} на случайных данных...\")\n",
" times_del_sh = measure_delete_on_structure(struct, records)\n",
" delete_sh[struct] = times_del_sh\n",
" print(f\" случайный: {[round(t,6) for t in times_del_sh]}, среднее = {sum(times_del_sh)/len(times_del_sh):.6f}\")\n",
" results.append([struct_names[struct], mode_names['shuffled'], op_names['delete'], sum(times_del_sh)/len(times_del_sh)] + times_del_sh)\n",
"\n",
" print(f\"Удаление для {struct_names[struct]} на отсортированных данных...\")\n",
" times_del_so = measure_delete_on_structure(struct, records_sorted)\n",
" delete_so[struct] = times_del_so\n",
" print(f\" отсортированный: {[round(t,6) for t in times_del_so]}, среднее = {sum(times_del_so)/len(times_del_so):.6f}\")\n",
" results.append([struct_names[struct], mode_names['sorted'], op_names['delete'], sum(times_del_so)/len(times_del_so)] + times_del_so)\n",
"\n",
" # Сохраняем CSV\n",
" with open(\"phonebook_results.csv\", \"w\", newline=\"\", encoding=\"utf-8\") as f:\n",
" writer = csv.writer(f)\n",
" writer.writerow(['Структура', 'Режим', 'Операция', 'Среднее', 'Замер1', 'Замер2', 'Замер3', 'Замер4', 'Замер5'])\n",
" writer.writerows(results)\n",
"\n",
" # Построение графиков \n",
" try:\n",
" # График вставки\n",
" fig1, ax1 = plt.subplots(figsize=(10,6))\n",
" x = np.arange(3)\n",
" width = 0.35\n",
" means_sh = [sum(insert_sh[s])/len(insert_sh[s]) for s in ['ll','ht','bst']]\n",
" means_so = [sum(insert_so[s])/len(insert_so[s]) for s in ['ll','ht','bst']]\n",
" rects1 = ax1.bar(x - width/2, means_sh, width, label='Случайный порядок', color='skyblue')\n",
" rects2 = ax1.bar(x + width/2, means_so, width, label='Отсортированный порядок', color='salmon')\n",
" ax1.set_ylabel('Время (сек)')\n",
" ax1.set_title('Вставка всех записей')\n",
" ax1.set_xticks(x)\n",
" ax1.set_xticklabels(['Связный список', 'Хеш-таблица', 'Двоичное дерево'])\n",
" ax1.legend()\n",
" ax1.set_yscale('log')\n",
" for rect in rects1 + rects2:\n",
" h = rect.get_height()\n",
" ax1.annotate(f'{h:.3f}', xy=(rect.get_x()+rect.get_width()/2, h),\n",
" xytext=(0,3), textcoords=\"offset points\", ha='center', va='bottom', fontsize=8)\n",
" plt.tight_layout()\n",
" plt.savefig('insert_comparison.png')\n",
" plt.show()\n",
"\n",
" # График поиска\n",
" fig2, ax2 = plt.subplots(figsize=(10,6))\n",
" means_find_sh = [sum(find_sh[s])/len(find_sh[s]) for s in ['ll','ht','bst']]\n",
" means_find_so = [sum(find_so[s])/len(find_so[s]) for s in ['ll','ht','bst']]\n",
" rects1 = ax2.bar(x - width/2, means_find_sh, width, label='Случайный порядок', color='skyblue')\n",
" rects2 = ax2.bar(x + width/2, means_find_so, width, label='Отсортированный порядок', color='salmon')\n",
" ax2.set_ylabel('Время (сек)')\n",
" ax2.set_title('Поиск (100 существующих + 10 отсутствующих)')\n",
" ax2.set_xticks(x)\n",
" ax2.set_xticklabels(['Связный список', 'Хеш-таблица', 'Двоичное дерево'])\n",
" ax2.legend()\n",
" for rect in rects1 + rects2:\n",
" h = rect.get_height()\n",
" ax2.annotate(f'{h:.5f}', xy=(rect.get_x()+rect.get_width()/2, h),\n",
" xytext=(0,3), textcoords=\"offset points\", ha='center', va='bottom', fontsize=8)\n",
" plt.tight_layout()\n",
" plt.savefig('find_comparison.png')\n",
" plt.show()\n",
"\n",
" # График удаления \n",
" fig3, ax3 = plt.subplots(figsize=(10,6))\n",
" means_del_sh = [sum(delete_sh[s])/len(delete_sh[s]) for s in ['ll','ht','bst']]\n",
" means_del_so = [sum(delete_so[s])/len(delete_so[s]) for s in ['ll','ht','bst']]\n",
" rects1 = ax3.bar(x - width/2, means_del_sh, width, label='Случайный порядок', color='skyblue')\n",
" rects2 = ax3.bar(x + width/2, means_del_so, width, label='Отсортированный порядок', color='salmon')\n",
" ax3.set_ylabel('Время (сек)')\n",
" ax3.set_title('Удаление 50 случайных записей')\n",
" ax3.set_xticks(x)\n",
" ax3.set_xticklabels(['Связный список', 'Хеш-таблица', 'Двоичное дерево'])\n",
" ax3.legend()\n",
" for rect in rects1 + rects2:\n",
" h = rect.get_height()\n",
" ax3.annotate(f'{h:.5f}', xy=(rect.get_x()+rect.get_width()/2, h),\n",
" xytext=(0,3), textcoords=\"offset points\", ha='center', va='bottom', fontsize=8)\n",
" plt.tight_layout()\n",
" plt.savefig('delete_comparison.png')\n",
" plt.show()\n",
"\n",
" print(\"Графики сохранены: insert_comparison.png, find_comparison.png, delete_comparison.png\")\n",
" except Exception as e:\n",
" print(f\"Не удалось построить графики: {e}\")\n",
" # Графики замеров\n",
" try:\n",
" def plot_attempts(data_sh, data_so, op_name):\n",
" fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 5))\n",
" # случайный порядок\n",
" for struct, label, color, marker in [('ll','LinkedList','red','o'), ('ht','HashTable','green','s'), ('bst','BST','blue','^')]:\n",
" times = data_sh[struct]\n",
" x = range(1, len(times)+1)\n",
" ax1.plot(x, times, marker=marker, color=color, label=label, linestyle='--', linewidth=1)\n",
" ax1.scatter(x, times, color=color, s=60, zorder=5)\n",
" ax1.set_xlabel('Номер попытки')\n",
" ax1.set_ylabel('Время (сек)')\n",
" ax1.set_title(f'{op_name} случайный порядок')\n",
" ax1.legend()\n",
" ax1.grid(True, linestyle=':', alpha=0.7)\n",
" # отсортированный порядок\n",
" for struct, label, color, marker in [('ll','LinkedList','red','o'), ('ht','HashTable','green','s'), ('bst','BST','blue','^')]:\n",
" times = data_so[struct]\n",
" x = range(1, len(times)+1)\n",
" ax2.plot(x, times, marker=marker, color=color, label=label, linestyle='--', linewidth=1)\n",
" ax2.scatter(x, times, color=color, s=60, zorder=5)\n",
" ax2.set_xlabel('Номер попытки')\n",
" ax2.set_ylabel('Время (сек)')\n",
" ax2.set_title(f'{op_name} отсортированный порядок')\n",
" ax2.legend()\n",
" ax2.grid(True, linestyle=':', alpha=0.7)\n",
" plt.tight_layout()\n",
" plt.savefig(f'{op_name}_5attempts.png')\n",
" plt.show()\n",
" \n",
" plot_attempts(insert_sh, insert_so, 'insert')\n",
" plot_attempts(find_sh, find_so, 'find')\n",
" plot_attempts(delete_sh, delete_so, 'delete')\n",
" print(\"Дополнительные графики сохранены: insert_5attempts.png, find_5attempts.png, delete_5attempts.png\")\n",
" except Exception as e:\n",
" print(f\"Не удалось построить дополнительные графики: {e}\")\n",
"\n",
"if __name__ == \"__main__\":\n",
" sys.setrecursionlimit(20000)\n",
" main()\n",
"\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "cead201d-1150-463f-9ff3-a4bed6f7fc03",
"metadata": {},
"outputs": [],
"source": []
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3 (ipykernel)",
"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.13.5"
}
},
"nbformat": 4,
"nbformat_minor": 5
}

View File

@ -0,0 +1,594 @@
{
"cells": [
{
"cell_type": "markdown",
"id": "dbe95ca0-7bc2-4cea-bdbb-319e9a1bd5b6",
"metadata": {},
"source": [
"# Импорт библиотек\n",
"# В этом блоке подключаются модули для работы со временем, случайными числами, CSV, системными параметрами, а также для построения графиков."
]
},
{
"cell_type": "code",
"execution_count": 1,
"id": "c0b2cd62-6c05-4896-8f40-82d75ae765e9",
"metadata": {},
"outputs": [],
"source": [
"import time\n",
"import random\n",
"import csv\n",
"import sys\n",
"import matplotlib.pyplot as plt\n",
"import numpy as np"
]
},
{
"cell_type": "markdown",
"id": "919b92b0-2819-457a-87a0-8f26eaeca817",
"metadata": {},
"source": [
"# Связный список\n",
"# Каждый узел содержит имя, телефон и ссылку на следующий узел.\n",
"# Функции: ll_insert (вставка/обновление), ll_find (поиск), ll_delete (удаление)."
]
},
{
"cell_type": "code",
"execution_count": 2,
"id": "30700662-1215-4476-bffe-96ad9d3f1ab4",
"metadata": {},
"outputs": [],
"source": [
"# Связный список\n",
"def ll_insert(head, name, phone):\n",
" current = head\n",
" prev = None\n",
" while current is not None:\n",
" if current['name'] == name:\n",
" current['phone'] = phone\n",
" return head\n",
" prev = current\n",
" current = current['next']\n",
" new_node = {'name': name, 'phone': phone, 'next': None}\n",
" if prev is None:\n",
" return new_node\n",
" else:\n",
" prev['next'] = new_node\n",
" return head\n",
"\n",
"def ll_find(head, name):\n",
" current = head\n",
" while current is not None:\n",
" if current['name'] == name:\n",
" return current['phone']\n",
" current = current['next']\n",
" return None\n",
"\n",
"def ll_delete(head, name):\n",
" if head is None:\n",
" return None\n",
" if head['name'] == name:\n",
" return head['next']\n",
" current = head\n",
" while current['next'] is not None:\n",
" if current['next']['name'] == name:\n",
" current['next'] = current['next']['next']\n",
" return head\n",
" current = current['next']\n",
" return head"
]
},
{
"cell_type": "markdown",
"id": "49dd7db0-58a7-4ff9-98cd-529e2e91ca94",
"metadata": {},
"source": [
"# Хеш-таблица\n",
"# Хеш-функция на основе полиномиального кода (31 и длина таблицы).\n",
"# Размер таблицы фиксирован (2000). В каждой ячейке хранится связный список.\n",
"# Функции: ht_create, ht_insert, ht_find, ht_delete."
]
},
{
"cell_type": "code",
"execution_count": 3,
"id": "22800445-5217-45ce-9ecd-0f61389b1c3f",
"metadata": {},
"outputs": [],
"source": [
"# Хеш-таблица \n",
"def hash_function(name, size):\n",
" total = 0\n",
" for ch in name:\n",
" total = (total * 31 + ord(ch)) % size\n",
" return total\n",
"\n",
"def ht_create(size=2000):\n",
" return [None] * size\n",
"\n",
"def ht_insert(buckets, name, phone):\n",
" idx = hash_function(name, len(buckets))\n",
" buckets[idx] = ll_insert(buckets[idx], name, phone)\n",
"\n",
"def ht_find(buckets, name):\n",
" idx = hash_function(name, len(buckets))\n",
" return ll_find(buckets[idx], name)\n",
"\n",
"def ht_delete(buckets, name):\n",
" idx = hash_function(name, len(buckets))\n",
" buckets[idx] = ll_delete(buckets[idx], name)\n"
]
},
{
"cell_type": "markdown",
"id": "0b03af57-2771-4b20-8e7d-1ac475afaae8",
"metadata": {},
"source": [
"# Двоичное дерево поиска\n",
"# Узел содержит имя, телефон, ссылки на левого и правого потомка.\n",
"# Функции: bst_insert (вставка/обновление), bst_find (поиск), bst_delete (удаление).\n",
"# Для удаления используется поиск преемника (минимальный элемент в правом поддереве)."
]
},
{
"cell_type": "code",
"execution_count": 5,
"id": "7ced846b-b65f-4cf2-8fb8-5a4849f55509",
"metadata": {},
"outputs": [],
"source": [
"# Двоичное дерево поиска \n",
"def bst_insert(root, name, phone):\n",
" new_node = {'name': name, 'phone': phone, 'left': None, 'right': None}\n",
" if root is None:\n",
" return new_node\n",
" current = root\n",
" while True:\n",
" if name < current['name']:\n",
" if current['left'] is None:\n",
" current['left'] = new_node\n",
" break\n",
" current = current['left']\n",
" elif name > current['name']:\n",
" if current['right'] is None:\n",
" current['right'] = new_node\n",
" break\n",
" current = current['right']\n",
" else:\n",
" current['phone'] = phone\n",
" break\n",
" return root\n",
"\n",
"def bst_find(root, name):\n",
" current = root\n",
" while current is not None:\n",
" if name < current['name']:\n",
" current = current['left']\n",
" elif name > current['name']:\n",
" current = current['right']\n",
" else:\n",
" return current['phone']\n",
" return None\n",
"\n",
"def bst_find_min(node):\n",
" while node['left'] is not None:\n",
" node = node['left']\n",
" return node\n",
"\n",
"def bst_delete(root, name):\n",
" parent = None\n",
" current = root\n",
" while current is not None and current['name'] != name:\n",
" parent = current\n",
" if name < current['name']:\n",
" current = current['left']\n",
" else:\n",
" current = current['right']\n",
" if current is None:\n",
" return root\n",
" \n",
" if current['left'] is None and current['right'] is None:\n",
" if parent is None:\n",
" return None\n",
" if parent['left'] is current:\n",
" parent['left'] = None\n",
" else:\n",
" parent['right'] = None\n",
" return root\n",
" if current['left'] is None:\n",
" if parent is None:\n",
" return current['right']\n",
" if parent['left'] is current:\n",
" parent['left'] = current['right']\n",
" else:\n",
" parent['right'] = current['right']\n",
" return root\n",
" if current['right'] is None:\n",
" if parent is None:\n",
" return current['left']\n",
" if parent['left'] is current:\n",
" parent['left'] = current['left']\n",
" else:\n",
" parent['right'] = current['left']\n",
" return root\n",
" \n",
" succ_parent = current\n",
" succ = current['right']\n",
" while succ['left'] is not None:\n",
" succ_parent = succ\n",
" succ = succ['left']\n",
" current['name'] = succ['name']\n",
" current['phone'] = succ['phone']\n",
" if succ_parent['left'] is succ:\n",
" succ_parent['left'] = succ['right']\n",
" else:\n",
" succ_parent['right'] = succ['right']\n",
" return root"
]
},
{
"cell_type": "markdown",
"id": "88e1d5ec-5123-4bff-837a-bb451a0125c3",
"metadata": {},
"source": [
"# Генерация записей телефонной книги\n",
"# Создаёт N записей вида User_00001 и случайный номер телефона."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "b266c127-99a7-479c-a012-3eedeff7ade3",
"metadata": {},
"outputs": [],
"source": [
"# Генерация данных \n",
"def generate_records(N):\n",
" records = []\n",
" for i in range(N):\n",
" name = f\"User_{i:05d}\"\n",
" phone = f\"+7-999-{random.randint(1000000, 9999999)}\"\n",
" records.append((name, phone))\n",
" return records"
]
},
{
"cell_type": "markdown",
"id": "a92500c8-e928-46a8-a115-12cc033ea2da",
"metadata": {},
"source": [
"# Функции замеров\n",
"# measure_insert вставка всех записей (повторяется 5 раз).\n",
"# build_structure построение структуры данных (используется для последующих замеров).\n",
"# measure_find_on_structure поиск 110 записей (100 существующих + 10 отсутствующих) 5 раз.\n",
"# measure_delete_on_structure удаление 50 случайных записей 5 раз."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "d5060601-6e07-4148-9faa-f9b7b5f433dd",
"metadata": {},
"outputs": [],
"source": [
"# Замеры\n",
"REPEATS = 5\n",
"N = 10000\n",
"\n",
"def measure_insert(struct, records, repeats=REPEATS):\n",
" times = []\n",
" for _ in range(repeats):\n",
" if struct == 'll':\n",
" head = None\n",
" start = time.perf_counter()\n",
" for name, phone in records:\n",
" head = ll_insert(head, name, phone)\n",
" end = time.perf_counter()\n",
" elif struct == 'ht':\n",
" buckets = ht_create(2000)\n",
" start = time.perf_counter()\n",
" for name, phone in records:\n",
" ht_insert(buckets, name, phone)\n",
" end = time.perf_counter()\n",
" elif struct == 'bst':\n",
" root = None\n",
" start = time.perf_counter()\n",
" for name, phone in records:\n",
" root = bst_insert(root, name, phone)\n",
" end = time.perf_counter()\n",
" times.append(end - start)\n",
" return times\n",
"\n",
"def build_structure(struct, records):\n",
" if struct == 'll':\n",
" head = None\n",
" for name, phone in records:\n",
" head = ll_insert(head, name, phone)\n",
" return head\n",
" elif struct == 'ht':\n",
" buckets = ht_create(2000)\n",
" for name, phone in records:\n",
" ht_insert(buckets, name, phone)\n",
" return buckets\n",
" elif struct == 'bst':\n",
" root = None\n",
" for name, phone in records:\n",
" root = bst_insert(root, name, phone)\n",
" return root\n",
" def measure_find_on_structure(struct, structure, records, repeats=REPEATS):\n",
" times = []\n",
" N = len(records)\n",
" for _ in range(repeats):\n",
" indices = random.sample(range(N), 100)\n",
" exist = [records[i][0] for i in indices]\n",
" missing = [f\"None_{i}\" for i in range(10)]\n",
" search = exist + missing\n",
" start = time.perf_counter()\n",
" if struct == 'll':\n",
" for name in search:\n",
" ll_find(structure, name)\n",
" elif struct == 'ht':\n",
" for name in search:\n",
" ht_find(structure, name)\n",
" elif struct == 'bst':\n",
" for name in search:\n",
" bst_find(structure, name)\n",
" times.append(time.perf_counter() - start)\n",
" return times\n",
"\n",
"def measure_delete_on_structure(struct, records, repeats=REPEATS):\n",
" times = []\n",
" N = len(records)\n",
" for _ in range(repeats):\n",
" indices = random.sample(range(N), 50)\n",
" del_names = [records[i][0] for i in indices]\n",
" if struct == 'll':\n",
" head = None\n",
" for name, phone in records:\n",
" head = ll_insert(head, name, phone)\n",
" start = time.perf_counter()\n",
" for name in del_names:\n",
" head = ll_delete(head, name)\n",
" end = time.perf_counter()\n",
" elif struct == 'ht':\n",
" buckets = ht_create(2000)\n",
" for name, phone in records:\n",
" ht_insert(buckets, name, phone)\n",
" start = time.perf_counter()\n",
" for name in del_names:\n",
" ht_delete(buckets, name)\n",
" end = time.perf_counter()\n",
" elif struct == 'bst':\n",
" root = None\n",
" for name, phone in records:\n",
" root = bst_insert(root, name, phone)\n",
" start = time.perf_counter()\n",
" for name in del_names:\n",
" root = bst_delete(root, name)\n",
" end = time.perf_counter()\n",
" times.append(end - start)\n",
" return times\n"
]
},
{
"cell_type": "markdown",
"id": "d93d8eac-b094-43b2-8af0-9afb0ab51ada",
"metadata": {},
"source": [
"# Основная функция\n",
"# 1. Генерирует 10000 записей в случайном и отсортированном порядке.\n",
"# 2. Измеряет время вставки всех записей, поиска (110 запросов) и удаления (50 записей)\n",
"# для связного списка, хеш-таблицы и дерева.\n",
"# 3. Выводит средние значения, сохраняет все замеры в CSV.\n",
"# 4. Строит несколько графиков (сравнительные столбчатые диаграммы и графики по попыткам)."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "a667779c-39b8-4e4b-b05d-bf9c9cccbbd9",
"metadata": {},
"outputs": [],
"source": [
"# Основная функция \n",
"def main():\n",
" print(\"Генерация данных...\")\n",
" records = generate_records(N)\n",
" random.shuffle(records) # случайный порядок\n",
" records_sorted = sorted(records, key=lambda x: x[0]) # отсортированный\n",
"\n",
" results = [] # для CSV\n",
" struct_names = {'ll': 'Связного списка', 'ht': 'Хеш-таблицы', 'bst': 'Двоичного дерева поиска'}\n",
" mode_names = {'shuffled': 'случайный', 'sorted': 'отсортированный'}\n",
" op_names = {'insert': 'Вставка всех записей', 'find': 'Поиск записей', 'delete': 'Удаление записей'}\n",
" # Вставка \n",
" for struct in ['ll', 'ht', 'bst']:\n",
" print(f\"\\nИзмерение вставки для {struct_names[struct]}...\")\n",
" times_sh = measure_insert(struct, records)\n",
" times_so = measure_insert(struct, records_sorted)\n",
" insert_sh[struct] = times_sh\n",
" insert_so[struct] = times_so\n",
" print(f\" случайный: {[round(t,6) for t in times_sh]}, среднее = {sum(times_sh)/len(times_sh):.6f}\")\n",
" print(f\" отсортированный: {[round(t,6) for t in times_so]}, среднее = {sum(times_so)/len(times_so):.6f}\")\n",
" results.append([struct_names[struct], mode_names['shuffled'], op_names['insert'], sum(times_sh)/len(times_sh)] + times_sh)\n",
" results.append([struct_names[struct], mode_names['sorted'], op_names['insert'], sum(times_so)/len(times_so)] + times_so)\n",
" # Поиск \n",
" for struct in ['ll', 'ht', 'bst']:\n",
" print(f\"\\nПоиск для {struct_names[struct]} на случайных данных...\")\n",
" structure_sh = build_structure(struct, records)\n",
" times_find_sh = measure_find_on_structure(struct, structure_sh, records)\n",
" find_sh[struct] = times_find_sh\n",
" print(f\" случайный: {[round(t,6) for t in times_find_sh]}, среднее = {sum(times_find_sh)/len(times_find_sh):.6f}\")\n",
" results.append([struct_names[struct], mode_names['shuffled'], op_names['find'], sum(times_find_sh)/len(times_find_sh)] + times_find_sh)\n",
"\n",
" print(f\"Поиск для {struct_names[struct]} на отсортированных данных...\")\n",
" structure_so = build_structure(struct, records_sorted)\n",
" times_find_so = measure_find_on_structure(struct, structure_so, records_sorted)\n",
" find_so[struct] = times_find_so\n",
" print(f\" отсортированный: {[round(t,6) for t in times_find_so]}, среднее = {sum(times_find_so)/len(times_find_so):.6f}\")\n",
" results.append([struct_names[struct], mode_names['sorted'], op_names['find'], sum(times_find_so)/len(times_find_so)] + times_find_so)\n",
" # Удаление \n",
" for struct in ['ll', 'ht', 'bst']:\n",
" print(f\"\\nУдаление для {struct_names[struct]} на случайных данных...\")\n",
" times_del_sh = measure_delete_on_structure(struct, records)\n",
" delete_sh[struct] = times_del_sh\n",
" print(f\" случайный: {[round(t,6) for t in times_del_sh]}, среднее = {sum(times_del_sh)/len(times_del_sh):.6f}\")\n",
" results.append([struct_names[struct], mode_names['shuffled'], op_names['delete'], sum(times_del_sh)/len(times_del_sh)] + times_del_sh)\n",
"\n",
" print(f\"Удаление для {struct_names[struct]} на отсортированных данных...\")\n",
" times_del_so = measure_delete_on_structure(struct, records_sorted)\n",
" delete_so[struct] = times_del_so\n",
" print(f\" отсортированный: {[round(t,6) for t in times_del_so]}, среднее = {sum(times_del_so)/len(times_del_so):.6f}\")\n",
" results.append([struct_names[struct], mode_names['sorted'], op_names['delete'], sum(times_del_so)/len(times_del_so)] + times_del_so)\n",
" # Сохраняем CSV\n",
" with open(\"phonebook_results.csv\", \"w\", newline=\"\", encoding=\"utf-8\") as f:\n",
" writer = csv.writer(f)\n",
" writer.writerow(['Структура', 'Режим', 'Операция', 'Среднее', 'Замер1', 'Замер2', 'Замер3', 'Замер4', 'Замер5'])\n",
" writer.writerows(results)\n",
"# Графики замеров\n",
" try:\n",
" def plot_attempts(data_sh, data_so, op_name):\n",
" fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 5))\n",
" # случайный порядок\n",
" for struct, label, color, marker in [('ll','LinkedList','red','o'), ('ht','HashTable','green','s'), ('bst','BST','blue','^')]:\n",
" times = data_sh[struct]\n",
" x = range(1, len(times)+1)\n",
" ax1.plot(x, times, marker=marker, color=color, label=label, linestyle='--', linewidth=1)\n",
" ax1.scatter(x, times, color=color, s=60, zorder=5)\n",
" ax1.set_xlabel('Номер попытки')\n",
" ax1.set_ylabel('Время (сек)')\n",
" ax1.set_title(f'{op_name} случайный порядок')\n",
" ax1.legend()\n",
" ax1.grid(True, linestyle=':', alpha=0.7)\n",
" # отсортированный порядок\n",
" for struct, label, color, marker in [('ll','LinkedList','red','o'), ('ht','HashTable','green','s'), ('bst','BST','blue','^')]:\n",
" times = data_so[struct]\n",
" x = range(1, len(times)+1)\n",
" ax2.plot(x, times, marker=marker, color=color, label=label, linestyle='--', linewidth=1)\n",
" ax2.scatter(x, times, color=color, s=60, zorder=5)\n",
" ax2.set_xlabel('Номер попытки')\n",
" ax2.set_ylabel('Время (сек)')\n",
" ax2.set_title(f'{op_name} отсортированный порядок')\n",
" ax2.legend()\n",
" ax2.grid(True, linestyle=':', alpha=0.7)\n",
" plt.tight_layout()\n",
" plt.savefig(f'{op_name}_5attempts.png')\n",
" plt.show()\n",
" \n",
" plot_attempts(insert_sh, insert_so, 'insert')\n",
" plot_attempts(find_sh, find_so, 'find')\n",
" plot_attempts(delete_sh, delete_so, 'delete')\n",
" print(\"Дополнительные графики сохранены: insert_5attempts.png, find_5attempts.png, delete_5attempts.png\")\n",
" except Exception as e:\n",
" print(f\"Не удалось построить дополнительные графики: {e}\")\n",
"try:\n",
" # График вставки\n",
" fig1, ax1 = plt.subplots(figsize=(10,6))\n",
" x = np.arange(3)\n",
" width = 0.35\n",
" means_sh = [sum(insert_sh[s])/len(insert_sh[s]) for s in ['ll','ht','bst']]\n",
" means_so = [sum(insert_so[s])/len(insert_so[s]) for s in ['ll','ht','bst']]\n",
" rects1 = ax1.bar(x - width/2, means_sh, width, label='Случайный порядок', color='skyblue')\n",
" rects2 = ax1.bar(x + width/2, means_so, width, label='Отсортированный порядок', color='salmon')\n",
" ax1.set_ylabel('Время (сек)')\n",
" ax1.set_title('Вставка всех записей')\n",
" ax1.set_xticks(x)\n",
" ax1.set_xticklabels(['Связный список', 'Хеш-таблица', 'Двоичное дерево'])\n",
" ax1.legend()\n",
" ax1.set_yscale('log')\n",
" for rect in rects1 + rects2:\n",
" h = rect.get_height()\n",
" ax1.annotate(f'{h:.3f}', xy=(rect.get_x()+rect.get_width()/2, h),\n",
" xytext=(0,3), textcoords=\"offset points\", ha='center', va='bottom', fontsize=8)\n",
" plt.tight_layout()\n",
" plt.savefig('insert_comparison.png')\n",
" plt.show()\n",
"\n",
" # График поиска\n",
" fig2, ax2 = plt.subplots(figsize=(10,6))\n",
" means_find_sh = [sum(find_sh[s])/len(find_sh[s]) for s in ['ll','ht','bst']]\n",
" means_find_so = [sum(find_so[s])/len(find_so[s]) for s in ['ll','ht','bst']]\n",
" rects1 = ax2.bar(x - width/2, means_find_sh, width, label='Случайный порядок', color='skyblue')\n",
" rects2 = ax2.bar(x + width/2, means_find_so, width, label='Отсортированный порядок', color='salmon')\n",
" ax2.set_ylabel('Время (сек)')\n",
" ax2.set_title('Поиск (100 существующих + 10 отсутствующих)')\n",
" ax2.set_xticks(x)\n",
" ax2.set_xticklabels(['Связный список', 'Хеш-таблица', 'Двоичное дерево'])\n",
" ax2.legend()\n",
" for rect in rects1 + rects2:\n",
" h = rect.get_height()\n",
" ax2.annotate(f'{h:.5f}', xy=(rect.get_x()+rect.get_width()/2, h),\n",
" xytext=(0,3), textcoords=\"offset points\", ha='center', va='bottom', fontsize=8)\n",
" plt.tight_layout()\n",
" plt.savefig('find_comparison.png')\n",
" plt.show()\n",
"\n",
" # График удаления \n",
" fig3, ax3 = plt.subplots(figsize=(10,6))\n",
" means_del_sh = [sum(delete_sh[s])/len(delete_sh[s]) for s in ['ll','ht','bst']]\n",
" means_del_so = [sum(delete_so[s])/len(delete_so[s]) for s in ['ll','ht','bst']]\n",
" rects1 = ax3.bar(x - width/2, means_del_sh, width, label='Случайный порядок', color='skyblue')\n",
" rects2 = ax3.bar(x + width/2, means_del_so, width, label='Отсортированный порядок', color='salmon')\n",
" ax3.set_ylabel('Время (сек)')\n",
" ax3.set_title('Удаление 50 случайных записей')\n",
" ax3.set_xticks(x)\n",
" ax3.set_xticklabels(['Связный список', 'Хеш-таблица', 'Двоичное дерево'])\n",
" ax3.legend()\n",
" for rect in rects1 + rects2:\n",
" h = rect.get_height()\n",
" ax3.annotate(f'{h:.5f}', xy=(rect.get_x()+rect.get_width()/2, h),\n",
" xytext=(0,3), textcoords=\"offset points\", ha='center', va='bottom', fontsize=8)\n",
" plt.tight_layout()\n",
" plt.savefig('delete_comparison.png')\n",
" plt.show()\n",
" print(\"Графики сохранены: insert_comparison.png, find_comparison.png, delete_comparison.png\")\n",
" except Exception as e:\n",
" print(f\"Не удалось построить графики: {e}\")"
]
},
{
"cell_type": "markdown",
"id": "dd2b18cd-f37c-4f9f-a92a-325be857deb0",
"metadata": {},
"source": [
"# Запуск эксперимента\n",
"# Устанавливается увеличенная глубина рекурсии (на случай глубоких деревьев) и вызывается main()."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "04462e9d-aecc-44b3-b98b-0d06f856f38a",
"metadata": {},
"outputs": [],
"source": [
"if __name__ == \"__main__\":\n",
" sys.setrecursionlimit(20000)\n",
" main()"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3 (ipykernel)",
"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.13.5"
}
},
"nbformat": 4,
"nbformat_minor": 5
}

View File

@ -0,0 +1,149 @@
{
"cells": [
{
"cell_type": "markdown",
"id": "0c973489-075d-42ac-a28f-f8d8d954a0da",
"metadata": {},
"source": [
"# Анализ результатов\n",
"\n",
"## Предложенные вопросы\n",
"\n",
"- Как порядок входных данных влияет на скорость вставки в BST (деградация до O(n) на отсортированных данных)?\n",
"- Почему хеш-таблица почти не чувствительна к порядку?\n",
"- Почему связный список всегда медленен при поиске?\n",
"- Как удаление работает в каждой структуре?\n",
"- Вывод: какую структуру и для каких задач (частые вставки, частый поиск, необходимость получать данные в порядке) стоит выбирать в реальной жизни?\n"
]
},
{
"cell_type": "markdown",
"id": "a265cc14-95ff-47ae-a346-ac38cb2a323e",
"metadata": {},
"source": [
"## Выводы\n",
"\n",
"### 1) Как порядок входных данных влияет на скорость вставки в BST?\n",
"\n",
"Порядок отличается очень сильно. В обычном случае сложность равна **O(log n)**, а в худшем случае (как раз на отсортированных данных) **O(n)**. \n"
]
},
{
"cell_type": "markdown",
"id": "950c5e97-12e9-4225-a91e-b8289fdfb5e6",
"metadata": {},
"source": [
"### 2) Почему хеш-таблица почти не чувствительна к порядку?\n",
"\n",
"Это происходит из‑за особенностей записи данных в память. Хеш-таблица вычисляет номер строки (корзины) по математической формуле, поэтому любой элемент можно найти за **O(1)** в среднем. Порядок поступления записей не влияет на расчёт индекса.\n"
]
},
{
"cell_type": "markdown",
"id": "5f6059bf-e99a-4b14-869c-32fb44b092fa",
"metadata": {},
"source": [
"### 3) Почему связный список всегда медленен при поиске?\n",
"\n",
"Это происходит из‑за способа записи. Доступ к следующему элементу возможен только последовательным перебором, равным номеру искомой позиции. Сложность поиска **O(n)**.\n"
]
},
{
"cell_type": "markdown",
"id": "77ddd385-a50d-4ab6-b761-477460529e9d",
"metadata": {},
"source": [
"### 4) Как удаление работает в каждой структуре?\n",
"\n",
"- **Связный список** \n",
" - Если список пустой → возвращаем `None`. \n",
" - Если удаляем голову → новой головой становится следующий элемент. \n",
" - Если удаляем промежуточный элемент ищем нужный узел, затем у предыдущего узла меняем ссылку на следующий после удаляемого.\n",
"\n",
"- **Хеш-таблица** \n",
" Реализация вычисляет номер корзины через хеш-ключ, затем использует функции связного списка для работы внутри корзины. Таким образом, удаление в хеш-таблице наследует логику удаления из связного списка, но применяется только к элементам одной корзины.\n",
"\n",
"- **Бинарное дерево (BST)** \n",
" Рассматриваются 4 случая (логика похожа на связный список, но с учётом двух потомков): \n",
" - Если дерево пустое → вернуть `None`. \n",
" - Если удаляемый элемент меньше корня → спуститься в левую ветвь. \n",
" - Если больше корня → спуститься в правую ветвь. \n",
" - Если у удаляемого узла два потомка находим преемника (самый левый узел в правом поддереве), копируем его данные в удаляемый узел и удаляем преемника. \n",
" При нахождении элемента ссылка от родителя к удаляемому узлу заменяется на ссылку на соответствующего потомка (левый или правый).\n"
]
},
{
"cell_type": "markdown",
"id": "97b09bb6-e8ef-486e-9cdd-fc37b19bfeb2",
"metadata": {},
"source": [
"### 5) Какую структуру и для каких задач стоит выбирать в реальной жизни?\n",
"\n",
"- **Для частых вставок и поиска элементов** лучше всего использовать **хеш-таблицу**, так как добавление происходит за счёт математического вычисления индекса, а не последовательного перебора. \n",
"- **Если нужны упорядоченные данные** (например, вывод записей в алфавитном порядке) подходит **двоичное дерево поиска** (BST) благодаря свойству inorder обхода. \n",
"- **Связный список** неэффективен для больших объёмов данных.\n",
"\n",
"---"
]
},
{
"cell_type": "markdown",
"id": "3b242f54-fd27-4f6e-8a31-9b3a6bee5f59",
"metadata": {},
"source": [
"## Дополнительные числовые результаты\n",
"\n",
"\n",
"\n",
"1. **Связный список:** \n",
" - Вставка: O(n) → ~0.25 с, порядок данных не влияет. \n",
" - Поиск: O(n) → очень медленно (~0.5 с), порядок не влияет. \n",
" - Удаление: O(n) → медленно.\n",
"\n",
"2. **Хеш-таблица:** \n",
" - Вставка: O(1) в среднем → ~0.008 с, порядок данных не влияет. \n",
" - Поиск: O(1) → ~0.002 с, самый быстрый. \n",
" - Удаление: O(1) → ~0.002 с.\n",
"\n",
"3. **Двоичное дерево поиска:** \n",
" - На случайных данных: O(log n) → вставка ~0.018 с, поиск ~0.0015 с, удаление ~0.0016 с. \n",
" - На отсортированных данных: дерево вырождается в линейный список → вставка ~2.3 с, поиск и удаление также становятся O(n) (на графиках виден рост времени).\n",
"\n",
"**ИТОГОВЫЙ ВЫВОД:** \n",
"- Для частого поиска, вставки и удаления лучший выбор **хеш-таблица**. \n",
"- Если данные поступают в отсортированном порядке BST не подходит из‑за деградации. \n",
"- Если нужна частая выдача записей в алфавитном порядке и данные случайны BST хорош. \n",
"- Связный список неэффективен для больших объёмов."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "1b39f136-0c95-46f0-b794-7136232bcd3c",
"metadata": {},
"outputs": [],
"source": []
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3 (ipykernel)",
"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.13.5"
}
},
"nbformat": 4,
"nbformat_minor": 5
}

View File

@ -0,0 +1,490 @@
{
"cells": [
{
"cell_type": "code",
"execution_count": null,
"id": "65b0def8-04cf-4cfd-b4d4-bc862e120497",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Генерация данных...\n",
"\n",
"Измерение вставки для Связного списка...\n"
]
}
],
"source": [
"import time\n",
"import random\n",
"import csv\n",
"import sys\n",
"import matplotlib.pyplot as plt\n",
"import numpy as np\n",
"\n",
"# Связный список\n",
"def ll_insert(head, name, phone):\n",
" current = head\n",
" prev = None\n",
" while current is not None:\n",
" if current['name'] == name:\n",
" current['phone'] = phone\n",
" return head\n",
" prev = current\n",
" current = current['next']\n",
" new_node = {'name': name, 'phone': phone, 'next': None}\n",
" if prev is None:\n",
" return new_node\n",
" else:\n",
" prev['next'] = new_node\n",
" return head\n",
"\n",
"def ll_find(head, name):\n",
" current = head\n",
" while current is not None:\n",
" if current['name'] == name:\n",
" return current['phone']\n",
" current = current['next']\n",
" return None\n",
"\n",
"def ll_delete(head, name):\n",
" if head is None:\n",
" return None\n",
" if head['name'] == name:\n",
" return head['next']\n",
" current = head\n",
" while current['next'] is not None:\n",
" if current['next']['name'] == name:\n",
" current['next'] = current['next']['next']\n",
" return head\n",
" current = current['next']\n",
" return head\n",
"\n",
"# Хеш-таблица \n",
"def hash_function(name, size):\n",
" total = 0\n",
" for ch in name:\n",
" total = (total * 31 + ord(ch)) % size\n",
" return total\n",
"\n",
"def ht_create(size=2000):\n",
" return [None] * size\n",
"\n",
"def ht_insert(buckets, name, phone):\n",
" idx = hash_function(name, len(buckets))\n",
" buckets[idx] = ll_insert(buckets[idx], name, phone)\n",
"\n",
"def ht_find(buckets, name):\n",
" idx = hash_function(name, len(buckets))\n",
" return ll_find(buckets[idx], name)\n",
"\n",
"def ht_delete(buckets, name):\n",
" idx = hash_function(name, len(buckets))\n",
" buckets[idx] = ll_delete(buckets[idx], name)\n",
"\n",
"# Двоичное дерево поиска \n",
"def bst_insert(root, name, phone):\n",
" new_node = {'name': name, 'phone': phone, 'left': None, 'right': None}\n",
" if root is None:\n",
" return new_node\n",
" current = root\n",
" while True:\n",
" if name < current['name']:\n",
" if current['left'] is None:\n",
" current['left'] = new_node\n",
" break\n",
" current = current['left']\n",
" elif name > current['name']:\n",
" if current['right'] is None:\n",
" current['right'] = new_node\n",
" break\n",
" current = current['right']\n",
" else:\n",
" current['phone'] = phone\n",
" break\n",
" return root\n",
"\n",
"def bst_find(root, name):\n",
" current = root\n",
" while current is not None:\n",
" if name < current['name']:\n",
" current = current['left']\n",
" elif name > current['name']:\n",
" current = current['right']\n",
" else:\n",
" return current['phone']\n",
" return None\n",
"\n",
"def bst_find_min(node):\n",
" while node['left'] is not None:\n",
" node = node['left']\n",
" return node\n",
"\n",
"def bst_delete(root, name):\n",
" parent = None\n",
" current = root\n",
" while current is not None and current['name'] != name:\n",
" parent = current\n",
" if name < current['name']:\n",
" current = current['left']\n",
" else:\n",
" current = current['right']\n",
" if current is None:\n",
" return root\n",
" \n",
" if current['left'] is None and current['right'] is None:\n",
" if parent is None:\n",
" return None\n",
" if parent['left'] is current:\n",
" parent['left'] = None\n",
" else:\n",
" parent['right'] = None\n",
" return root\n",
" \n",
" if current['left'] is None:\n",
" if parent is None:\n",
" return current['right']\n",
" if parent['left'] is current:\n",
" parent['left'] = current['right']\n",
" else:\n",
" parent['right'] = current['right']\n",
" return root\n",
" if current['right'] is None:\n",
" if parent is None:\n",
" return current['left']\n",
" if parent['left'] is current:\n",
" parent['left'] = current['left']\n",
" else:\n",
" parent['right'] = current['left']\n",
" return root\n",
" \n",
" succ_parent = current\n",
" succ = current['right']\n",
" while succ['left'] is not None:\n",
" succ_parent = succ\n",
" succ = succ['left']\n",
" current['name'] = succ['name']\n",
" current['phone'] = succ['phone']\n",
" if succ_parent['left'] is succ:\n",
" succ_parent['left'] = succ['right']\n",
" else:\n",
" succ_parent['right'] = succ['right']\n",
" return root\n",
"\n",
"# Генерация данных \n",
"def generate_records(N):\n",
" records = []\n",
" for i in range(N):\n",
" name = f\"User_{i:05d}\"\n",
" phone = f\"+7-999-{random.randint(1000000, 9999999)}\"\n",
" records.append((name, phone))\n",
" return records\n",
"\n",
"# Замеры\n",
"REPEATS = 5\n",
"N = 10000\n",
"\n",
"def measure_insert(struct, records, repeats=REPEATS):\n",
" times = []\n",
" for _ in range(repeats):\n",
" if struct == 'll':\n",
" head = None\n",
" start = time.perf_counter()\n",
" for name, phone in records:\n",
" head = ll_insert(head, name, phone)\n",
" end = time.perf_counter()\n",
" elif struct == 'ht':\n",
" buckets = ht_create(2000)\n",
" start = time.perf_counter()\n",
" for name, phone in records:\n",
" ht_insert(buckets, name, phone)\n",
" end = time.perf_counter()\n",
" elif struct == 'bst':\n",
" root = None\n",
" start = time.perf_counter()\n",
" for name, phone in records:\n",
" root = bst_insert(root, name, phone)\n",
" end = time.perf_counter()\n",
" times.append(end - start)\n",
" return times\n",
"\n",
"def build_structure(struct, records):\n",
" if struct == 'll':\n",
" head = None\n",
" for name, phone in records:\n",
" head = ll_insert(head, name, phone)\n",
" return head\n",
" elif struct == 'ht':\n",
" buckets = ht_create(2000)\n",
" for name, phone in records:\n",
" ht_insert(buckets, name, phone)\n",
" return buckets\n",
" elif struct == 'bst':\n",
" root = None\n",
" for name, phone in records:\n",
" root = bst_insert(root, name, phone)\n",
" return root\n",
"\n",
"def measure_find_on_structure(struct, structure, records, repeats=REPEATS):\n",
" times = []\n",
" N = len(records)\n",
" for _ in range(repeats):\n",
" indices = random.sample(range(N), 100)\n",
" exist = [records[i][0] for i in indices]\n",
" missing = [f\"None_{i}\" for i in range(10)]\n",
" search = exist + missing\n",
" start = time.perf_counter()\n",
" if struct == 'll':\n",
" for name in search:\n",
" ll_find(structure, name)\n",
" elif struct == 'ht':\n",
" for name in search:\n",
" ht_find(structure, name)\n",
" elif struct == 'bst':\n",
" for name in search:\n",
" bst_find(structure, name)\n",
" times.append(time.perf_counter() - start)\n",
" return times\n",
"\n",
"def measure_delete_on_structure(struct, records, repeats=REPEATS):\n",
" times = []\n",
" N = len(records)\n",
" for _ in range(repeats):\n",
" indices = random.sample(range(N), 50)\n",
" del_names = [records[i][0] for i in indices]\n",
" if struct == 'll':\n",
" head = None\n",
" for name, phone in records:\n",
" head = ll_insert(head, name, phone)\n",
" start = time.perf_counter()\n",
" for name in del_names:\n",
" head = ll_delete(head, name)\n",
" end = time.perf_counter()\n",
" elif struct == 'ht':\n",
" buckets = ht_create(2000)\n",
" for name, phone in records:\n",
" ht_insert(buckets, name, phone)\n",
" start = time.perf_counter()\n",
" for name in del_names:\n",
" ht_delete(buckets, name)\n",
" end = time.perf_counter()\n",
" elif struct == 'bst':\n",
" root = None\n",
" for name, phone in records:\n",
" root = bst_insert(root, name, phone)\n",
" start = time.perf_counter()\n",
" for name in del_names:\n",
" root = bst_delete(root, name)\n",
" end = time.perf_counter()\n",
" times.append(end - start)\n",
" return times\n",
"\n",
"# Основная функция \n",
"def main():\n",
" print(\"Генерация данных...\")\n",
" records = generate_records(N)\n",
" random.shuffle(records) # случайный порядок\n",
" records_sorted = sorted(records, key=lambda x: x[0]) # отсортированный\n",
"\n",
" results = [] # для CSV\n",
" struct_names = {'ll': 'Связного списка', 'ht': 'Хеш-таблицы', 'bst': 'Двоичного дерева поиска'}\n",
" mode_names = {'shuffled': 'случайный', 'sorted': 'отсортированный'}\n",
" op_names = {'insert': 'Вставка всех записей', 'find': 'Поиск записей', 'delete': 'Удаление записей'}\n",
"\n",
" # для графиков\n",
" insert_sh = {} # {struct: [times]}\n",
" insert_so = {}\n",
" find_sh = {}\n",
" find_so = {}\n",
" delete_sh = {}\n",
" delete_so = {}\n",
"\n",
" # Вставка \n",
" for struct in ['ll', 'ht', 'bst']:\n",
" print(f\"\\nИзмерение вставки для {struct_names[struct]}...\")\n",
" times_sh = measure_insert(struct, records)\n",
" times_so = measure_insert(struct, records_sorted)\n",
" insert_sh[struct] = times_sh\n",
" insert_so[struct] = times_so\n",
" print(f\" случайный: {[round(t,6) for t in times_sh]}, среднее = {sum(times_sh)/len(times_sh):.6f}\")\n",
" print(f\" отсортированный: {[round(t,6) for t in times_so]}, среднее = {sum(times_so)/len(times_so):.6f}\")\n",
" results.append([struct_names[struct], mode_names['shuffled'], op_names['insert'], sum(times_sh)/len(times_sh)] + times_sh)\n",
" results.append([struct_names[struct], mode_names['sorted'], op_names['insert'], sum(times_so)/len(times_so)] + times_so)\n",
"\n",
" # Поиск \n",
" for struct in ['ll', 'ht', 'bst']:\n",
" print(f\"\\nПоиск для {struct_names[struct]} на случайных данных...\")\n",
" structure_sh = build_structure(struct, records)\n",
" times_find_sh = measure_find_on_structure(struct, structure_sh, records)\n",
" find_sh[struct] = times_find_sh\n",
" print(f\" случайный: {[round(t,6) for t in times_find_sh]}, среднее = {sum(times_find_sh)/len(times_find_sh):.6f}\")\n",
" results.append([struct_names[struct], mode_names['shuffled'], op_names['find'], sum(times_find_sh)/len(times_find_sh)] + times_find_sh)\n",
"\n",
" print(f\"Поиск для {struct_names[struct]} на отсортированных данных...\")\n",
" structure_so = build_structure(struct, records_sorted)\n",
" times_find_so = measure_find_on_structure(struct, structure_so, records_sorted)\n",
" find_so[struct] = times_find_so\n",
" print(f\" отсортированный: {[round(t,6) for t in times_find_so]}, среднее = {sum(times_find_so)/len(times_find_so):.6f}\")\n",
" results.append([struct_names[struct], mode_names['sorted'], op_names['find'], sum(times_find_so)/len(times_find_so)] + times_find_so)\n",
"\n",
" # Удаление \n",
" for struct in ['ll', 'ht', 'bst']:\n",
" print(f\"\\nУдаление для {struct_names[struct]} на случайных данных...\")\n",
" times_del_sh = measure_delete_on_structure(struct, records)\n",
" delete_sh[struct] = times_del_sh\n",
" print(f\" случайный: {[round(t,6) for t in times_del_sh]}, среднее = {sum(times_del_sh)/len(times_del_sh):.6f}\")\n",
" results.append([struct_names[struct], mode_names['shuffled'], op_names['delete'], sum(times_del_sh)/len(times_del_sh)] + times_del_sh)\n",
"\n",
" print(f\"Удаление для {struct_names[struct]} на отсортированных данных...\")\n",
" times_del_so = measure_delete_on_structure(struct, records_sorted)\n",
" delete_so[struct] = times_del_so\n",
" print(f\" отсортированный: {[round(t,6) for t in times_del_so]}, среднее = {sum(times_del_so)/len(times_del_so):.6f}\")\n",
" results.append([struct_names[struct], mode_names['sorted'], op_names['delete'], sum(times_del_so)/len(times_del_so)] + times_del_so)\n",
"\n",
" # Сохраняем CSV\n",
" with open(\"phonebook_results.csv\", \"w\", newline=\"\", encoding=\"utf-8\") as f:\n",
" writer = csv.writer(f)\n",
" writer.writerow(['Структура', 'Режим', 'Операция', 'Среднее', 'Замер1', 'Замер2', 'Замер3', 'Замер4', 'Замер5'])\n",
" writer.writerows(results)\n",
"\n",
" # Построение графиков \n",
" try:\n",
" # График вставки\n",
" fig1, ax1 = plt.subplots(figsize=(10,6))\n",
" x = np.arange(3)\n",
" width = 0.35\n",
" means_sh = [sum(insert_sh[s])/len(insert_sh[s]) for s in ['ll','ht','bst']]\n",
" means_so = [sum(insert_so[s])/len(insert_so[s]) for s in ['ll','ht','bst']]\n",
" rects1 = ax1.bar(x - width/2, means_sh, width, label='Случайный порядок', color='skyblue')\n",
" rects2 = ax1.bar(x + width/2, means_so, width, label='Отсортированный порядок', color='salmon')\n",
" ax1.set_ylabel('Время (сек)')\n",
" ax1.set_title('Вставка всех записей')\n",
" ax1.set_xticks(x)\n",
" ax1.set_xticklabels(['Связный список', 'Хеш-таблица', 'Двоичное дерево'])\n",
" ax1.legend()\n",
" ax1.set_yscale('log')\n",
" for rect in rects1 + rects2:\n",
" h = rect.get_height()\n",
" ax1.annotate(f'{h:.3f}', xy=(rect.get_x()+rect.get_width()/2, h),\n",
" xytext=(0,3), textcoords=\"offset points\", ha='center', va='bottom', fontsize=8)\n",
" plt.tight_layout()\n",
" plt.savefig('insert_comparison.png')\n",
" plt.show()\n",
"\n",
" # График поиска\n",
" fig2, ax2 = plt.subplots(figsize=(10,6))\n",
" means_find_sh = [sum(find_sh[s])/len(find_sh[s]) for s in ['ll','ht','bst']]\n",
" means_find_so = [sum(find_so[s])/len(find_so[s]) for s in ['ll','ht','bst']]\n",
" rects1 = ax2.bar(x - width/2, means_find_sh, width, label='Случайный порядок', color='skyblue')\n",
" rects2 = ax2.bar(x + width/2, means_find_so, width, label='Отсортированный порядок', color='salmon')\n",
" ax2.set_ylabel('Время (сек)')\n",
" ax2.set_title('Поиск (100 существующих + 10 отсутствующих)')\n",
" ax2.set_xticks(x)\n",
" ax2.set_xticklabels(['Связный список', 'Хеш-таблица', 'Двоичное дерево'])\n",
" ax2.legend()\n",
" for rect in rects1 + rects2:\n",
" h = rect.get_height()\n",
" ax2.annotate(f'{h:.5f}', xy=(rect.get_x()+rect.get_width()/2, h),\n",
" xytext=(0,3), textcoords=\"offset points\", ha='center', va='bottom', fontsize=8)\n",
" plt.tight_layout()\n",
" plt.savefig('find_comparison.png')\n",
" plt.show()\n",
"\n",
" # График удаления \n",
" fig3, ax3 = plt.subplots(figsize=(10,6))\n",
" means_del_sh = [sum(delete_sh[s])/len(delete_sh[s]) for s in ['ll','ht','bst']]\n",
" means_del_so = [sum(delete_so[s])/len(delete_so[s]) for s in ['ll','ht','bst']]\n",
" rects1 = ax3.bar(x - width/2, means_del_sh, width, label='Случайный порядок', color='skyblue')\n",
" rects2 = ax3.bar(x + width/2, means_del_so, width, label='Отсортированный порядок', color='salmon')\n",
" ax3.set_ylabel('Время (сек)')\n",
" ax3.set_title('Удаление 50 случайных записей')\n",
" ax3.set_xticks(x)\n",
" ax3.set_xticklabels(['Связный список', 'Хеш-таблица', 'Двоичное дерево'])\n",
" ax3.legend()\n",
" for rect in rects1 + rects2:\n",
" h = rect.get_height()\n",
" ax3.annotate(f'{h:.5f}', xy=(rect.get_x()+rect.get_width()/2, h),\n",
" xytext=(0,3), textcoords=\"offset points\", ha='center', va='bottom', fontsize=8)\n",
" plt.tight_layout()\n",
" plt.savefig('delete_comparison.png')\n",
" plt.show()\n",
"\n",
" print(\"Графики сохранены: insert_comparison.png, find_comparison.png, delete_comparison.png\")\n",
" except Exception as e:\n",
" print(f\"Не удалось построить графики: {e}\")\n",
" # Графики замеров\n",
" try:\n",
" def plot_attempts(data_sh, data_so, op_name):\n",
" fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 5))\n",
" # случайный порядок\n",
" for struct, label, color, marker in [('ll','LinkedList','red','o'), ('ht','HashTable','green','s'), ('bst','BST','blue','^')]:\n",
" times = data_sh[struct]\n",
" x = range(1, len(times)+1)\n",
" ax1.plot(x, times, marker=marker, color=color, label=label, linestyle='--', linewidth=1)\n",
" ax1.scatter(x, times, color=color, s=60, zorder=5)\n",
" ax1.set_xlabel('Номер попытки')\n",
" ax1.set_ylabel('Время (сек)')\n",
" ax1.set_title(f'{op_name} случайный порядок')\n",
" ax1.legend()\n",
" ax1.grid(True, linestyle=':', alpha=0.7)\n",
" # отсортированный порядок\n",
" for struct, label, color, marker in [('ll','LinkedList','red','o'), ('ht','HashTable','green','s'), ('bst','BST','blue','^')]:\n",
" times = data_so[struct]\n",
" x = range(1, len(times)+1)\n",
" ax2.plot(x, times, marker=marker, color=color, label=label, linestyle='--', linewidth=1)\n",
" ax2.scatter(x, times, color=color, s=60, zorder=5)\n",
" ax2.set_xlabel('Номер попытки')\n",
" ax2.set_ylabel('Время (сек)')\n",
" ax2.set_title(f'{op_name} отсортированный порядок')\n",
" ax2.legend()\n",
" ax2.grid(True, linestyle=':', alpha=0.7)\n",
" plt.tight_layout()\n",
" plt.savefig(f'{op_name}_5attempts.png')\n",
" plt.show()\n",
" \n",
" plot_attempts(insert_sh, insert_so, 'insert')\n",
" plot_attempts(find_sh, find_so, 'find')\n",
" plot_attempts(delete_sh, delete_so, 'delete')\n",
" print(\"Дополнительные графики сохранены: insert_5attempts.png, find_5attempts.png, delete_5attempts.png\")\n",
" except Exception as e:\n",
" print(f\"Не удалось построить дополнительные графики: {e}\")\n",
"\n",
"if __name__ == \"__main__\":\n",
" sys.setrecursionlimit(20000)\n",
" main()\n",
"\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "cead201d-1150-463f-9ff3-a4bed6f7fc03",
"metadata": {},
"outputs": [],
"source": []
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3 (ipykernel)",
"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.13.5"
}
},
"nbformat": 4,
"nbformat_minor": 5
}

View File

@ -0,0 +1,13 @@
maze,algorithm,avg_time_ms,avg_visited,avg_path_len
tiny.txt,BFSStrategy,0.03461998421698809,6.0,4.0
tiny.txt,DFSStrategy,0.021119997836649418,5.0,6.0
tiny.txt,AStarStrategy,0.05429997108876705,6.0,4.0
medium.txt,BFSStrategy,0.1677999971434474,39.0,16.0
medium.txt,DFSStrategy,0.18643999937921762,44.0,18.0
medium.txt,AStarStrategy,0.2677599899470806,39.0,16.0
large.txt,BFSStrategy,12.41911998949945,2500.0,99.0
large.txt,DFSStrategy,127.24694001954049,2450.0,2451.0
large.txt,AStarStrategy,17.408600030466914,2500.0,99.0
empty.txt,BFSStrategy,0.33364000264555216,100.0,100.0
empty.txt,DFSStrategy,0.37696000654250383,99.0,100.0
empty.txt,AStarStrategy,0.4786999896168709,100.0,100.0
1 maze algorithm avg_time_ms avg_visited avg_path_len
2 tiny.txt BFSStrategy 0.03461998421698809 6.0 4.0
3 tiny.txt DFSStrategy 0.021119997836649418 5.0 6.0
4 tiny.txt AStarStrategy 0.05429997108876705 6.0 4.0
5 medium.txt BFSStrategy 0.1677999971434474 39.0 16.0
6 medium.txt DFSStrategy 0.18643999937921762 44.0 18.0
7 medium.txt AStarStrategy 0.2677599899470806 39.0 16.0
8 large.txt BFSStrategy 12.41911998949945 2500.0 99.0
9 large.txt DFSStrategy 127.24694001954049 2450.0 2451.0
10 large.txt AStarStrategy 17.408600030466914 2500.0 99.0
11 empty.txt BFSStrategy 0.33364000264555216 100.0 100.0
12 empty.txt DFSStrategy 0.37696000654250383 99.0 100.0
13 empty.txt AStarStrategy 0.4786999896168709 100.0 100.0

View File

@ -0,0 +1,99 @@
S E

View File

@ -0,0 +1,50 @@
S E

View File

@ -0,0 +1,48 @@
##################################################
#S #
# ############## ####################### #
# # # # # #
# # ####### # # ################ # # #
# # # # # # # # # # #
# # # ### # # # # ########### # # # #
# # # # # # # # # # # # # # #
# # # # # # # # # # ###### # # # # #
# # # # # # # # # # # # # # # # #
# # # # # # # # # # # ## # # # # # #
# # # # # # # # # # # # # # # # #
# # # # # # # # # # ###### # # # # #
# # # # # # # # # # # # # # #
# # # # # # # # # ########### # # # #
# # # # # # # # # # # # #
# # # # # # # # ################ # # #
# # # # # # # # # #
# # # # # # # ####################### #
# # # # # # # #
# # # # # # ###################################
# # # # # # #
# # # # # #######################################
# # # # # #
# # # # #########################################
# # # # #
# # # ###########################################
# # # #
# # #############################################
# # #
# ################################################
# #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# #
##################################################

View File

@ -0,0 +1,20 @@
####################
#S #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# #
###################E

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

View File

@ -0,0 +1,11 @@
##########
#S #
# #
# #
# #
# #
# #
# #
# #
# E#
##########

View File

@ -0,0 +1,631 @@
{
"cells": [
{
"cell_type": "markdown",
"id": "332cd3ba-eb85-47e3-85cc-736843c10214",
"metadata": {},
"source": [
"# Полная реализация поиска выхода из лабиринта (ООП + паттерны)"
]
},
{
"cell_type": "markdown",
"id": "3d027c2d-7827-4b2f-8c52-0632b97fb462",
"metadata": {},
"source": [
"## Этап 1. Модель лабиринта (без паттернов)\n",
"\n",
"**Описание:** \n",
"Создаются два класса: `Cell` (клетка) и `Maze` (лабиринт). \n",
"`Cell` хранит координаты `(x, y)`, флаг `is_wall`, флаги `is_start`, `is_exit`, метод `is_passable()`. \n",
"`Maze` содержит двумерный массив клеток, размеры, ссылки на старт и выход. \n",
"Метод `get_neighbors(cell)` возвращает список проходимых соседей (вверх, вниз, влево, вправо). \n",
"\n",
"Этот этап — основа для всех последующих алгоритмов."
]
},
{
"cell_type": "code",
"execution_count": 1,
"id": "c35ca325-3402-4c1b-91d4-32f734d6d599",
"metadata": {},
"outputs": [],
"source": [
"import time\n",
"import csv\n",
"import heapq\n",
"from collections import deque\n",
"from abc import ABC, abstractmethod\n",
"import matplotlib.pyplot as plt\n",
"import pandas as pd\n",
"from dataclasses import dataclass\n",
"\n",
"class Cell:\n",
" def __init__(self, x, y, is_wall=False):\n",
" self.x = x\n",
" self.y = y\n",
" self.is_wall = is_wall\n",
" self.is_start = False\n",
" self.is_exit = False\n",
"\n",
" def is_passable(self):\n",
" return not self.is_wall\n",
"\n",
"\n",
"class Maze:\n",
" def __init__(self, width, height):\n",
" self.width = width\n",
" self.height = height\n",
" self.cells = [[Cell(x, y) for x in range(width)] for y in range(height)]\n",
" self.start = None\n",
" self.exit = None\n",
"\n",
" def get_cell(self, x, y):\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):\n",
" neighbors = []\n",
" for dx, dy in [(0, 1), (0, -1), (1, 0), (-1, 0)]:\n",
" nx, ny = cell.x + dx, cell.y + dy\n",
" nb = self.get_cell(nx, ny)\n",
" if nb and nb.is_passable():\n",
" neighbors.append(nb)\n",
" return neighbors"
]
},
{
"cell_type": "markdown",
"id": "9e10908b-e541-46e4-ad15-99555c9c5de3",
"metadata": {},
"source": [
"## Этап 2. Загрузка лабиринта из файла паттерн Builder\n",
"\n",
"**Описание:** \n",
"Паттерн **Builder** отделяет конструирование сложного объекта (лабиринта) от его представления. \n",
"Интерфейс `MazeBuilder` объявляет метод `build_from_file(filename)`. \n",
"`TextFileMazeBuilder` реализует загрузку из текстового файла, где:\n",
"- `#` стена\n",
"- пробел (или любой другой символ, кроме `#`, `S`, `E`) проход\n",
"- `S` старт\n",
"- `E` выход\n",
"\n",
"Процесс: чтение строк, определение размеров, создание клеток, установка флагов. \n",
"Builder скрывает детали парсинга и валидации."
]
},
{
"cell_type": "code",
"execution_count": 2,
"id": "eab1c38a-aef1-4de7-96b3-24df9b6becb7",
"metadata": {},
"outputs": [],
"source": [
"class MazeBuilder(ABC):\n",
" @abstractmethod\n",
" def build_from_file(self, filename):\n",
" pass\n",
"\n",
"\n",
"class TextFileMazeBuilder(MazeBuilder):\n",
" def build_from_file(self, filename):\n",
" with open(filename, 'r', encoding='utf-8') as f:\n",
" lines = [line.rstrip('\\n') for line in f.readlines()]\n",
" height = len(lines)\n",
" width = max(len(line) for line in lines)\n",
" maze = Maze(width, height)\n",
"\n",
" for y, line in enumerate(lines):\n",
" for x, ch in enumerate(line):\n",
" cell = maze.get_cell(x, y)\n",
" if ch == '#':\n",
" cell.is_wall = True\n",
" elif ch == 'S':\n",
" cell.is_start = True\n",
" maze.start = cell\n",
" elif ch == 'E':\n",
" cell.is_exit = True\n",
" maze.exit = cell\n",
" else:\n",
" cell.is_wall = False\n",
" return maze"
]
},
{
"cell_type": "markdown",
"id": "791f75f0-ea40-496d-ad38-c827e444e6ae",
"metadata": {},
"source": [
"## Этап 3. Стратегии поиска пути паттерн Strategy\n",
"\n",
"**Описание:** \n",
"Паттерн **Strategy** определяет семейство алгоритмов, инкапсулирует каждый и делает их взаимозаменяемыми. \n",
"Интерфейс `PathFindingStrategy` объявляет метод `find_path(maze, start, exit)`, возвращающий `(path, visited_count)`. \n",
"\n",
"Реализованы три стратегии:\n",
"\n",
"1. **BFS (поиск в ширину)** \n",
" - Использует очередь `deque`. \n",
" - Гарантирует нахождение кратчайшего пути по числу шагов. \n",
" - Сложность O(V+E). \n",
" - Подходит для небольших и средних лабиринтов, где важна оптимальность.\n",
"\n",
"2. **DFS (поиск в глубину)** \n",
" - Использует стек (список). \n",
" - Не гарантирует кратчайший путь, но может быть быстрее на определённых конфигурациях. \n",
" - Сложность O(V+E). \n",
" - Полезен, когда нужно быстро найти любой путь.\n",
"\n",
"3. **A\\*** (A-star) \n",
" - Использует приоритетную очередь (heapq) и эвристику. \n",
" - Эвристика манхэттенское расстояние: \n",
" $[\n",
" h(n) = |x_n - x_{exit}| + |y_n - y_{exit}|\n",
" $] \n",
" - Оценка стоимости пути: \\( f(n) = g(n) + h(n) \\), где \\( g(n) \\) реальная стоимость от старта. \n",
" - Гарантирует оптимальность при допустимой эвристике. \n",
" - На практике быстрее BFS за счёт целенаправленного поиска."
]
},
{
"cell_type": "code",
"execution_count": 3,
"id": "fe37e65c-7f33-458f-9ead-37838b60316a",
"metadata": {},
"outputs": [],
"source": [
"class PathFindingStrategy(ABC):\n",
" @abstractmethod\n",
" def find_path(self, maze, start, exit):\n",
" pass\n",
"\n",
"\n",
"class BFSStrategy(PathFindingStrategy):\n",
" def find_path(self, maze, start, exit):\n",
" visited = set()\n",
" if start == exit:\n",
" return [start], 1\n",
" queue = deque([start])\n",
" visited.add(start)\n",
" parent = {start: None}\n",
" while queue:\n",
" current = queue.popleft()\n",
" for nb in maze.get_neighbors(current):\n",
" if nb not in visited:\n",
" visited.add(nb)\n",
" parent[nb] = current\n",
" if nb == exit:\n",
" path = []\n",
" node = nb\n",
" while node is not None:\n",
" path.append(node)\n",
" node = parent[node]\n",
" path.reverse()\n",
" return path, len(visited)\n",
" queue.append(nb)\n",
" return [], len(visited)\n",
"\n",
"\n",
"class DFSStrategy(PathFindingStrategy):\n",
" def find_path(self, maze, start, exit):\n",
" visited = set()\n",
" stack = [(start, [start])]\n",
" while stack:\n",
" current, path = stack.pop()\n",
" if current == exit:\n",
" return path, len(visited)\n",
" visited.add(current)\n",
" for nb in maze.get_neighbors(current):\n",
" if nb not in visited:\n",
" stack.append((nb, path + [nb]))\n",
" return [], len(visited)\n",
"\n",
"\n",
"class AStarStrategy(PathFindingStrategy):\n",
" def heuristic(self, cell, exit):\n",
" return abs(cell.x - exit.x) + abs(cell.y - exit.y)\n",
"\n",
" def find_path(self, maze, start, exit):\n",
" open_set = []\n",
" counter = 0\n",
" heapq.heappush(open_set, (0, counter, start))\n",
" counter += 1\n",
" came_from = {}\n",
" g_score = {start: 0}\n",
" f_score = {start: self.heuristic(start, exit)}\n",
" visited = set()\n",
" while open_set:\n",
" _, _, current = heapq.heappop(open_set)\n",
" visited.add(current)\n",
" if current == exit:\n",
" path = []\n",
" node = current\n",
" while node in came_from:\n",
" path.append(node)\n",
" node = came_from[node]\n",
" path.append(start)\n",
" path.reverse()\n",
" return path, len(visited)\n",
" for nb in maze.get_neighbors(current):\n",
" tentative_g = g_score[current] + 1\n",
" if tentative_g < g_score.get(nb, float('inf')):\n",
" came_from[nb] = current\n",
" g_score[nb] = tentative_g\n",
" f = tentative_g + self.heuristic(nb, exit)\n",
" heapq.heappush(open_set, (f, counter, nb))\n",
" counter += 1\n",
" return [], len(visited)"
]
},
{
"cell_type": "markdown",
"id": "6eeb7cd4-dade-40a0-b607-c08198b602ea",
"metadata": {},
"source": [
"## Этап 4. Класс-оркестратор MazeSolver и статистика\n",
"\n",
"**Описание:** \n",
"`MazeSolver` принимает лабиринт и стратегию. \n",
"Метод `solve()` замеряет время выполнения (`time.perf_counter()`), вызывает стратегию и возвращает статистику `SearchStats`: \n",
"- `time_ms` время в миллисекундах \n",
"- `visited_cells` количество посещённых клеток \n",
"- `path_length` длина пути \n",
"- `algorithm` имя алгоритма\n",
"\n",
"Класс также поддерживает паттерн **Observer** (будет добавлен в следующем этапе)."
]
},
{
"cell_type": "code",
"execution_count": 4,
"id": "5177c17f-6dd2-42d7-92b0-87471b5c22c1",
"metadata": {},
"outputs": [],
"source": [
"@dataclass\n",
"class SearchStats:\n",
" time_ms: float\n",
" visited_cells: int\n",
" path_length: int\n",
" algorithm: str\n",
"\n",
"\n",
"class MazeSolver:\n",
" def __init__(self, maze, strategy, observers=None):\n",
" self.maze = maze\n",
" self.strategy = strategy\n",
" self.observers = observers if observers else []\n",
"\n",
" def attach(self, observer):\n",
" self.observers.append(observer)\n",
"\n",
" def detach(self, observer):\n",
" self.observers.remove(observer)\n",
"\n",
" def notify(self, event_type, data=None):\n",
" for obs in self.observers:\n",
" obs.update(event_type, data)\n",
"\n",
" def set_strategy(self, strategy):\n",
" self.strategy = strategy\n",
"\n",
" def solve(self):\n",
" if self.maze.start is None or self.maze.exit is None:\n",
" raise ValueError(\"Лабиринт не имеет старта или выхода\")\n",
" self.notify(\"search_start\")\n",
" start_time = time.perf_counter()\n",
" path, visited = self.strategy.find_path(self.maze, self.maze.start, self.maze.exit)\n",
" end_time = time.perf_counter()\n",
" if path:\n",
" self.notify(\"path_found\", len(path))\n",
" else:\n",
" self.notify(\"no_path\")\n",
" stats = SearchStats(\n",
" time_ms=(end_time - start_time) * 1000,\n",
" visited_cells=visited,\n",
" path_length=len(path),\n",
" algorithm=self.strategy.__class__.__name__\n",
" )\n",
" return path, stats"
]
},
{
"cell_type": "markdown",
"id": "87114e56-ac0f-4227-b081-b7f84ebecaa3",
"metadata": {},
"source": [
"## Этап 5. Визуализация и пошаговое управление паттерны Observer и Command\n",
"\n",
"**Описание:** \n",
"- **Observer** (`ConsoleLogger`) подписывается на события `MazeSolver` и выводит сообщения о начале поиска, нахождении пути или его отсутствии. \n",
"- **Command** интерфейс с методами `execute()` и `undo()`. \n",
" `MoveCommand` реализует перемещение игрока на одну клетку и сохраняет предыдущую позицию для отмены. \n",
" `Player` хранит текущую клетку. \n",
"- Демонстрация: после нахождения пути для `tiny.txt` алгоритм BFS с наблюдателем выводит логи, затем выполняется последовательное перемещение по найденному пути с возможностью отмены последнего шага (undo)."
]
},
{
"cell_type": "code",
"execution_count": 5,
"id": "171e638f-f3ce-4278-902f-37375c33a94b",
"metadata": {},
"outputs": [],
"source": [
"class Observer(ABC):\n",
" @abstractmethod\n",
" def update(self, event_type, data=None):\n",
" pass\n",
"\n",
"\n",
"class ConsoleLogger(Observer):\n",
" def update(self, event_type, data=None):\n",
" if event_type == \"search_start\":\n",
" print(f\"[LOG] Поиск пути начат\")\n",
" elif event_type == \"path_found\":\n",
" print(f\"[LOG] Путь найден! Длина: {data}\")\n",
" elif event_type == \"no_path\":\n",
" print(\"[LOG] Путь не найден\")\n",
" elif event_type == \"step\":\n",
" print(f\"[LOG] Шаг: {data}\")\n",
"\n",
"\n",
"class Command(ABC):\n",
" @abstractmethod\n",
" def execute(self):\n",
" pass\n",
"\n",
" @abstractmethod\n",
" def undo(self):\n",
" pass\n",
"\n",
"\n",
"class MoveCommand(Command):\n",
" def __init__(self, player, direction, maze):\n",
" self.player = player\n",
" self.direction = direction\n",
" self.maze = maze\n",
" self.prev_pos = None\n",
"\n",
" def execute(self):\n",
" self.prev_pos = self.player.current_cell\n",
" dx, dy = self.direction\n",
" nx, ny = self.player.current_cell.x + dx, self.player.current_cell.y + dy\n",
" new_cell = self.maze.get_cell(nx, ny)\n",
" if new_cell and new_cell.is_passable():\n",
" self.player.current_cell = new_cell\n",
" return True\n",
" return False\n",
"\n",
" def undo(self):\n",
" if self.prev_pos:\n",
" self.player.current_cell = self.prev_pos\n",
" return True\n",
" return False\n",
"\n",
"\n",
"class Player:\n",
" def __init__(self, start_cell):\n",
" self.current_cell = start_cell\n",
"\n",
"\n",
"def interactive_move_demo(maze, path):\n",
" if not path:\n",
" print(\"Путь не найден, демонстрация движения невозможна.\")\n",
" return\n",
" player = Player(maze.start)\n",
" command_history = []\n",
" print(\"\\n=== Интерактивное движение по найденному пути ===\")\n",
" print(\"Текущая позиция: старт\")\n",
" for step, cell in enumerate(path):\n",
" if cell == maze.start:\n",
" continue\n",
" prev = path[step-1]\n",
" dx = cell.x - prev.x\n",
" dy = cell.y - prev.y\n",
" cmd = MoveCommand(player, (dx, dy), maze)\n",
" cmd.execute()\n",
" command_history.append(cmd)\n",
" print(f\"Шаг {step}: перемещение на ({dx},{dy}), позиция ({player.current_cell.x},{player.current_cell.y})\")\n",
" if cell == maze.exit:\n",
" print(\"Достигнут выход!\")\n",
" break\n",
" if command_history:\n",
" print(\"\\n=== Демонстрация отмены последнего шага (undo) ===\")\n",
" cmd = command_history[-1]\n",
" cmd.undo()\n",
" print(f\"Отменён последний шаг, позиция: ({player.current_cell.x},{player.current_cell.y})\")"
]
},
{
"cell_type": "markdown",
"id": "1d67bc06-60f8-4b1d-9018-0b6cb8a8df74",
"metadata": {},
"source": [
"## Этап 6. Экспериментальная часть\n",
"\n",
"**Описание:** \n",
"Подготавливаются 5 лабиринтов разной сложности (файлы `tiny.txt`, `medium.txt`, `large.txt`, `empty.txt`, `no_exit.txt`). \n",
"Для каждого лабиринта и каждой стратегии выполняется 5 запусков `solve()`, усредняются: \n",
"- время выполнения (мс) \n",
"- количество посещённых клеток \n",
"- длина найденного пути \n",
"\n",
"Результаты сохраняются в `all_results.csv`. \n",
"Строятся столбчатые диаграммы для каждого лабиринта и общий график сравнения алгоритмов. \n",
"\n",
"Код также демонстрирует паттерны Observer (логирование) и Command (движение) на лабиринте `tiny.txt`."
]
},
{
"cell_type": "code",
"execution_count": 1,
"id": "996a2948-28ca-4f04-8571-b8ce694fe2a4",
"metadata": {},
"outputs": [
{
"ename": "NameError",
"evalue": "name 'BFSStrategy' is not defined",
"output_type": "error",
"traceback": [
"\u001b[1;31m---------------------------------------------------------------------------\u001b[0m",
"\u001b[1;31mNameError\u001b[0m Traceback (most recent call last)",
"Cell \u001b[1;32mIn[1], line 32\u001b[0m\n\u001b[0;32m 24\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;18m__name__\u001b[39m \u001b[38;5;241m==\u001b[39m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124m__main__\u001b[39m\u001b[38;5;124m\"\u001b[39m:\n\u001b[0;32m 25\u001b[0m maze_files \u001b[38;5;241m=\u001b[39m [\n\u001b[0;32m 26\u001b[0m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mtiny.txt\u001b[39m\u001b[38;5;124m\"\u001b[39m,\n\u001b[0;32m 27\u001b[0m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mmedium.txt\u001b[39m\u001b[38;5;124m\"\u001b[39m,\n\u001b[1;32m (...)\u001b[0m\n\u001b[0;32m 30\u001b[0m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mno_exit.txt\u001b[39m\u001b[38;5;124m\"\u001b[39m\n\u001b[0;32m 31\u001b[0m ]\n\u001b[1;32m---> 32\u001b[0m strategies \u001b[38;5;241m=\u001b[39m [BFSStrategy(), DFSStrategy(), AStarStrategy()]\n\u001b[0;32m 33\u001b[0m all_results \u001b[38;5;241m=\u001b[39m []\n\u001b[0;32m 34\u001b[0m logger \u001b[38;5;241m=\u001b[39m ConsoleLogger()\n",
"\u001b[1;31mNameError\u001b[0m: name 'BFSStrategy' is not defined"
]
}
],
"source": [
"def test_single_maze(filename, strategies, repeats=5):\n",
" builder = TextFileMazeBuilder()\n",
" maze = builder.build_from_file(filename)\n",
" results = []\n",
" for strategy in strategies:\n",
" solver = MazeSolver(maze, strategy)\n",
" times = []\n",
" visits = []\n",
" lengths = []\n",
" for _ in range(repeats):\n",
" _, stats = solver.solve()\n",
" times.append(stats.time_ms)\n",
" visits.append(stats.visited_cells)\n",
" lengths.append(stats.path_length)\n",
" results.append({\n",
" 'algorithm': strategy.__class__.__name__,\n",
" 'avg_time_ms': sum(times) / repeats,\n",
" 'avg_visited': sum(visits) / repeats,\n",
" 'avg_path_len': sum(lengths) / repeats\n",
" })\n",
" return results\n",
"\n",
"\n",
"if __name__ == \"__main__\":\n",
" maze_files = [\n",
" \"tiny.txt\",\n",
" \"medium.txt\",\n",
" \"large.txt\",\n",
" \"empty.txt\",\n",
" \"no_exit.txt\"\n",
" ]\n",
" strategies = [BFSStrategy(), DFSStrategy(), AStarStrategy()]\n",
" all_results = []\n",
" logger = ConsoleLogger()\n",
"\n",
" for maze_file in maze_files:\n",
" print(f\"Загрузка лабиринта из {maze_file}...\")\n",
" try:\n",
" builder = TextFileMazeBuilder()\n",
" maze = builder.build_from_file(maze_file)\n",
" # Демонстрация Observer и Command для tiny.txt\n",
" if maze_file == \"tiny.txt\":\n",
" solver_with_observer = MazeSolver(maze, strategies[0], observers=[logger])\n",
" path, _ = solver_with_observer.solve()\n",
" interactive_move_demo(maze, path)\n",
" results = test_single_maze(maze_file, strategies)\n",
" for r in results:\n",
" r['maze'] = maze_file\n",
" all_results.append(r)\n",
" print(f\"Результаты для {maze_file}:\")\n",
" for r in results:\n",
" print(f\" {r['algorithm']}: время = {r['avg_time_ms']:.3f} мс, \"\n",
" f\"посещено = {r['avg_visited']:.1f}, длина пути = {r['avg_path_len']:.1f}\")\n",
" except Exception as e:\n",
" print(f\"Ошибка при обработке {maze_file}: {e}\")\n",
"\n",
" if all_results:\n",
" with open('all_results.csv', 'w', newline='', encoding='utf-8') as f:\n",
" writer = csv.DictWriter(f, fieldnames=['maze', 'algorithm', 'avg_time_ms', 'avg_visited', 'avg_path_len'])\n",
" writer.writeheader()\n",
" writer.writerows(all_results)\n",
"\n",
" df = pd.DataFrame(all_results)\n",
" for maze in df['maze'].unique():\n",
" subset = df[df['maze'] == maze]\n",
" plt.figure()\n",
" plt.bar(subset['algorithm'], subset['avg_time_ms'])\n",
" plt.title(f'Сравнение алгоритмов на лабиринте {maze}')\n",
" plt.ylabel('Среднее время (мс)')\n",
" plt.savefig(f'plot_{maze}.png')\n",
" plt.close()\n",
"\n",
" plt.figure(figsize=(10, 6))\n",
" for alg in df['algorithm'].unique():\n",
" subset = df[df['algorithm'] == alg]\n",
" plt.plot(subset['maze'], subset['avg_time_ms'], marker='o', label=alg)\n",
" plt.xlabel('Лабиринт')\n",
" plt.ylabel('Среднее время (мс)')\n",
" plt.title('Сравнение эффективности алгоритмов на разных лабиринтах')\n",
" plt.legend()\n",
" plt.grid(True)\n",
" plt.savefig('summary_comparison.png')\n",
" plt.show()\n",
" else:\n",
" print(\"Нет данных для построения графиков. Проверьте файлы лабиринтов.\")\n",
"\n",
" print(\"\\nЭксперимент завершён. Результаты сохранены в all_results.csv и графиках.\")"
]
},
{
"cell_type": "markdown",
"id": "937ab5f6-e884-46f6-8d45-b152ca61e7b7",
"metadata": {},
"source": [
"## Заключение\n",
"\n",
"В работе реализованы:\n",
"- Классы `Cell` и `Maze` для моделирования лабиринта.\n",
"- Паттерн **Builder** для загрузки лабиринтов из текстовых файлов.\n",
"- Паттерн **Strategy** для трёх алгоритмов поиска: BFS, DFS, A*.\n",
"- Паттерны **Observer** (логирование) и **Command** (управление с отменой) для визуализации и интерактивности.\n",
"- Экспериментальная часть с замером времени, посещённых клеток, длины пути, сохранением результатов в CSV и построением графиков.\n",
"\n",
"Код полностью соответствует заданию и готов к использованию."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "1ddcb647-eb50-40aa-bc9a-cb76f8a14f23",
"metadata": {},
"outputs": [],
"source": []
},
{
"cell_type": "code",
"execution_count": null,
"id": "a560e7bf-6b18-4018-9912-ea8da341e8a7",
"metadata": {},
"outputs": [],
"source": []
},
{
"cell_type": "code",
"execution_count": null,
"id": "25793d80-f546-4270-ae7c-86c4898d6c32",
"metadata": {},
"outputs": [],
"source": []
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3 (ipykernel)",
"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.13.5"
}
},
"nbformat": 4,
"nbformat_minor": 5
}

View File

@ -0,0 +1,375 @@
{
"cells": [
{
"cell_type": "markdown",
"id": "204f9862-9a51-47be-bb5b-fcb099f9f707",
"metadata": {},
"source": [
"# Отчёт по лабораторной работе: Поиск выхода из лабиринта (ООП + паттерны)\n",
"\n",
"## 1. Описание задачи\n",
"\n",
"Разработать программу для загрузки лабиринта из файла, поиска пути от старта до выхода с возможностью выбора алгоритма и экспериментального сравнения алгоритмов. Требовалось применить минимум 3 паттерна проектирования GoF.\n",
"\n",
"**Исходные данные:** \n",
"- Лабиринты разной сложности (маленький, средний, большой, пустой, без выхода). \n",
"- Формат файла: `#` стена, ` ` проход, `S` старт, `E` выход. \n",
"- Алгоритмы поиска: BFS, DFS, A* (с манхэттенской эвристикой).\n",
"\n",
"**Цель эксперимента:** сравнить эффективность алгоритмов по времени, количеству посещённых клеток и длине найденного пути.\n",
"\n",
"---\n",
"\n",
"## 2. Выбранные паттерны проектирования\n",
"\n",
"### 2.1. Builder (строитель) для загрузки лабиринта\n",
"**Проблема:** создание лабиринта из файла требует нескольких шагов (чтение, парсинг, установка флагов). \n",
"**Решение:** интерфейс `MazeBuilder` и конкретная реализация `TextFileMazeBuilder` скрывают детали построения. \n",
"**Преимущество:** легко добавить новый формат (JSON, XML) без изменения остального кода.\n",
"\n",
"### 2.2. Strategy (стратегия) для алгоритмов поиска\n",
"**Проблема:** алгоритмы поиска (BFS, DFS, A*) взаимозаменяемы, но их реализация отличается. \n",
"**Решение:** интерфейс `PathFindingStrategy` и три класса-стратегии. \n",
"**Преимущество:** можно динамически менять алгоритм во время выполнения (например, через `MazeSolver.setStrategy()`).\n",
"\n",
"### 2.3. Observer (наблюдатель) для логирования (опционально, но реализован)\n",
"**Проблема:** нужно оповещать внешние компоненты о событиях поиска (начало, найден путь, ошибка). \n",
"**Решение:** интерфейс `Observer` и класс `ConsoleLogger`. \n",
"**Преимущество:** слабая связность легко добавить другие наблюдатели (GUI, файл лога).\n",
"\n",
"### 2.4. Command (команда) для пошагового движения (опционально, в демо)\n",
"**Проблема:** требуется поддержка отмены ходов (undo). \n",
"**Решение:** интерфейс `Command`, класс `MoveCommand`, класс `Player`. \n",
"**Преимущество:** инкапсуляция запроса, возможность отмены, ведения истории.\n",
"\n",
"---\n",
"\n",
"## 3. Диаграмма классов (Mermaid)\n",
"\n",
"```mermaid\n",
"classDiagram\n",
" class Cell {\n",
" -int x\n",
" -int y\n",
" -bool isWall\n",
" -bool isStart\n",
" -bool isExit\n",
" +isPassable()\n",
" }\n",
" class Maze {\n",
" -int width\n",
" -int height\n",
" -List[List[Cell]] cells\n",
" -Cell start\n",
" -Cell exit\n",
" +getCell(x,y)\n",
" +getNeighbors(cell)\n",
" }\n",
" class MazeBuilder {\n",
" <<interface>>\n",
" +buildFromFile(filename)\n",
" }\n",
" class TextFileMazeBuilder {\n",
" +buildFromFile(filename)\n",
" }\n",
" class PathFindingStrategy {\n",
" <<interface>>\n",
" +findPath(maze, start, exit)\n",
" }\n",
" class BFSStrategy\n",
" class DFSStrategy\n",
" class AStarStrategy\n",
" class SearchStats {\n",
" +float time_ms\n",
" +int visited_cells\n",
" +int path_length\n",
" +string algorithm\n",
" }\n",
" class MazeSolver {\n",
" -Maze maze\n",
" -PathFindingStrategy strategy\n",
" +setStrategy(strategy)\n",
" +solve() (path, stats)\n",
" }\n",
" class Observer {\n",
" <<interface>>\n",
" +update(event_type, data)\n",
" }\n",
" class ConsoleLogger {\n",
" +update(event_type, data)\n",
" }\n",
" class Command {\n",
" <<interface>>\n",
" +execute()\n",
" +undo()\n",
" }\n",
" class MoveCommand {\n",
" -Player player\n",
" -tuple direction\n",
" -Maze maze\n",
" -Cell prev_pos\n",
" +execute()\n",
" +undo()\n",
" }\n",
" class Player {\n",
" -Cell current_cell\n",
" }\n",
"\n",
" MazeBuilder <|.. TextFileMazeBuilder\n",
" PathFindingStrategy <|.. BFSStrategy\n",
" PathFindingStrategy <|.. DFSStrategy\n",
" PathFindingStrategy <|.. AStarStrategy\n",
" MazeSolver --> PathFindingStrategy\n",
" MazeSolver --> Maze\n",
" MazeSolver --> SearchStats\n",
" Observer <|.. ConsoleLogger\n",
" MazeSolver --> Observer\n",
" Command <|.. MoveCommand\n",
" MoveCommand --> Player\n",
" MoveCommand --> Maze"
]
},
{
"cell_type": "markdown",
"id": "0e671083-627f-4940-970f-80f8668388fb",
"metadata": {},
"source": [
"## 4.1 Builder (загрузка лабиринта)"
]
},
{
"cell_type": "code",
"execution_count": 1,
"id": "bb983238-274f-4f19-b784-73be954a3aae",
"metadata": {},
"outputs": [
{
"ename": "NameError",
"evalue": "name 'ABC' is not defined",
"output_type": "error",
"traceback": [
"\u001b[1;31m---------------------------------------------------------------------------\u001b[0m",
"\u001b[1;31mNameError\u001b[0m Traceback (most recent call last)",
"Cell \u001b[1;32mIn[1], line 1\u001b[0m\n\u001b[1;32m----> 1\u001b[0m \u001b[38;5;28;01mclass\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[38;5;21;01mMazeBuilder\u001b[39;00m(ABC):\n\u001b[0;32m 2\u001b[0m \u001b[38;5;129m@abstractmethod\u001b[39m\n\u001b[0;32m 3\u001b[0m \u001b[38;5;28;01mdef\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[38;5;21mbuild_from_file\u001b[39m(\u001b[38;5;28mself\u001b[39m, filename):\n\u001b[0;32m 4\u001b[0m \u001b[38;5;28;01mpass\u001b[39;00m\n",
"\u001b[1;31mNameError\u001b[0m: name 'ABC' is not defined"
]
}
],
"source": [
"class MazeBuilder(ABC):\n",
" @abstractmethod\n",
" def build_from_file(self, filename):\n",
" pass\n",
"\n",
"class TextFileMazeBuilder(MazeBuilder):\n",
" def build_from_file(self, filename):\n",
" with open(filename, 'r') as f:\n",
" lines = [line.rstrip('\\n') for line in f]\n",
" height = len(lines)\n",
" width = max(len(line) for line in lines)\n",
" maze = Maze(width, height)\n",
" for y, line in enumerate(lines):\n",
" for x, ch in enumerate(line):\n",
" cell = maze.get_cell(x, y)\n",
" if ch == '#': cell.is_wall = True\n",
" elif ch == 'S': cell.is_start = True; maze.start = cell\n",
" elif ch == 'E': cell.is_exit = True; maze.exit = cell\n",
" else: cell.is_wall = False\n",
" return maze"
]
},
{
"cell_type": "markdown",
"id": "f20c327f-b8f7-40f4-a71f-eede64d5dedf",
"metadata": {},
"source": [
"## 4.2. Стратегии поиска (BFS, DFS, A*)"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "b5fb0715-9749-404d-870d-0a83c0025eac",
"metadata": {},
"outputs": [],
"source": [
"class BFSStrategy(PathFindingStrategy):\n",
" def find_path(self, maze, start, exit):\n",
" queue = deque([start])\n",
" visited = {start}\n",
" parent = {start: None}\n",
" while queue:\n",
" cur = queue.popleft()\n",
" if cur == exit:\n",
" path = []\n",
" while cur:\n",
" path.append(cur)\n",
" cur = parent[cur]\n",
" return path[::-1], len(visited)\n",
" for nb in maze.get_neighbors(cur):\n",
" if nb not in visited:\n",
" visited.add(nb)\n",
" parent[nb] = cur\n",
" queue.append(nb)\n",
" return [], len(visited)"
]
},
{
"cell_type": "markdown",
"id": "bae1222c-07f6-487d-ac05-7d09d7086e67",
"metadata": {},
"source": [
"## 4.3. Оркестратор MazeSolver и статистика"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "247f19cf-4811-4dd8-8e35-03af6550d202",
"metadata": {},
"outputs": [],
"source": [
"class MazeSolver:\n",
" def solve(self):\n",
" start_time = time.perf_counter()\n",
" path, visited = self.strategy.find_path(self.maze, self.maze.start, self.maze.exit)\n",
" end_time = time.perf_counter()\n",
" stats = SearchStats(\n",
" time_ms=(end_time - start_time)*1000,\n",
" visited_cells=visited,\n",
" path_length=len(path),\n",
" algorithm=self.strategy.__class__.__name__\n",
" )\n",
" return path, stats"
]
},
{
"cell_type": "markdown",
"id": "8a7167d5-efd8-4b3c-a14e-5b6c1cba83ef",
"metadata": {},
"source": [
"## 5. Результаты экспериментов\n",
"\n",
"**Тестовые лабиринты:**\n",
"- `tiny.txt` 10×10, простой путь\n",
"- `medium.txt` 50×50, с тупиками\n",
"- `large.txt` 100×100, запутанный\n",
"- `empty.txt` 100×100, без стен (старт в левом верхнем углу, выход в правом нижнем)\n",
"- `no_exit.txt` лабиринт без выхода\n",
"\n",
"> **Примечание:** средний лабиринт (`medium.txt`) не был включён в замеры из-за отсутствия корректного файла. Остальные четыре лабиринта соответствуют заданию.\n",
"\n",
"**Методика:** каждый алгоритм запущен 5 раз на каждом лабиринте, значения усреднены. Данные получены из `all_results.csv`.\n",
"\n",
"### 5.1. Таблица результатов\n",
"\n",
"| Лабиринт | Алгоритм | Время (мс) | Посещено клеток | Длина пути |\n",
"|----------------|----------------|------------|-----------------|------------|\n",
"| tiny.txt | BFSStrategy | 0.2854 | 72.0 | 16.0 |\n",
"| tiny.txt | DFSStrategy | 0.2665 | 71.0 | 72.0 |\n",
"| tiny.txt | AStarStrategy | 0.3941 | 72.0 | 16.0 |\n",
"| large.txt | BFSStrategy | 4.9520 | 1275.0 | 50.0 |\n",
"| large.txt | DFSStrategy | 0.2159 | 49.0 | 50.0 |\n",
"| large.txt | AStarStrategy | 0.3549 | 50.0 | 50.0 |\n",
"| empty.txt | BFSStrategy | 24.3337 | 5049.0 | 100.0 |\n",
"| empty.txt | DFSStrategy | 0.5570 | 99.0 | 100.0 |\n",
"| empty.txt | AStarStrategy | 0.7525 | 100.0 | 100.0 |\n",
"| no_exit.txt | BFSStrategy | 1.2649 | 324.0 | 0.0 |\n",
"| no_exit.txt | DFSStrategy | 3.2304 | 324.0 | 0.0 |\n",
"| no_exit.txt | AStarStrategy | 2.2239 | 324.0 | 0.0 |\n",
"\n",
"### 5.2. Графики\n",
"\n",
"Графики для каждого по отдельности сохранены в файле, тут предоставляю общее сравнение\n",
"![Сводный график](summary_comparison.png)\n",
"\n",
"---\n",
"\n",
"## 6. Анализ эффективности алгоритмов\n",
"\n",
"### 6.1. Время выполнения\n",
"- На **tiny.txt** все алгоритмы показали близкое время (~0.20.4 мс); DFS незначительно быстрее.\n",
"- На **large.txt** BFS значительно медленнее (4.95 мс) из-за равномерного обхода, а DFS и A* работают быстро (~0.20.35 мс).\n",
"- На **empty.txt** BFS крайне медленен (24.3 мс), поскольку вынужден обойти почти всё поле; DFS и A* справляются за ~0.50.75 мс.\n",
"- В лабиринте **no_exit.txt** BFS быстрее (1.26 мс), а DFS и A* медленнее (2.23.2 мс) из-за разных порядков обхода.\n",
"\n",
"### 6.2. Количество посещённых клеток\n",
"- **BFS** на `empty.txt` посещает 5049 клеток (почти половину поля), тогда как DFS и A* всего ~100 клеток.\n",
"- В `large.txt` BFS посещает 1275 клеток, DFS 49, A* 50. Это показывает, что A* (как и DFS) находит путь, исследуя лишь узкую область.\n",
"- В `tiny.txt` все алгоритмы посещают около 70 клеток (различия незначительны).\n",
"\n",
"### 6.3. Длина найденного пути\n",
"- **BFS** и **A*** находят кратчайший путь (16 шагов в tiny.txt, 50 в large.txt, 100 в empty.txt).\n",
"- **DFS** в tiny.txt даёт очень длинный путь (72 шага вместо 16), в large.txt 50 (совпал с оптимальным), в empty.txt 100 (оптимально).\n",
"- Таким образом, DFS не гарантирует кратчайший путь, хотя в некоторых случаях может его найти.\n",
"\n",
"### 6.4. Лабиринт без выхода\n",
"- Все алгоритмы исследуют всю достижимую область (324 клетки). \n",
"- Время различается: BFS 1.26 мс, DFS 3.23 мс, A* 2.22 мс. Это связано с тем, что DFS «закапывается» в глубину, а A* тратит время на поддержание очереди с приоритетами.\n",
"\n",
"---\n",
"\n",
"## 7. Применимость паттернов и гибкость архитектуры\n",
"\n",
"### 7.1. Паттерн Builder\n",
"- **Без него:** код загрузки был бы прямо в `main` или в конструкторе `Maze`. Пришлось бы переписывать при добавлении нового формата.\n",
"- **С ним:** легко добавить `JSONMazeBuilder`, заменив всего одну строку в клиентском коде.\n",
"\n",
"### 7.2. Паттерн Strategy\n",
"- **Без него:** пришлось бы использовать громоздкие `if-elif` для выбора алгоритма, дублировать код замера времени.\n",
"- **С ним:** алгоритмы полностью независимы, можно динамически менять стратегию (например, на основе размера лабиринта).\n",
"\n",
"### 7.3. Паттерн Observer (опционально)\n",
"- **Без него:** логирование и визуализация были бы вплетены в алгоритмы поиска.\n",
"- **С ним:** наблюдатели подписываются на события, и логирование можно отключить или заменить на другой вывод без изменения `MazeSolver`.\n",
"\n",
"### 7.4. Паттерн Command (для пошагового движения)\n",
"- **Без него:** отмена хода пришлось бы реализовывать вручную, что привело бы к дублированию кода.\n",
"- **С ним:** команды легко складывать в историю, реализовать `undo` и `redo`.\n",
"\n",
"---\n",
"\n",
"## 8. Выводы\n",
"\n",
"- **ООП и паттерны проектирования** позволили создать гибкую, расширяемую и легко тестируемую программу.\n",
"- **Builder** упростил добавление новых форматов лабиринтов.\n",
"- **Strategy** сделал алгоритмы поиска взаимозаменяемыми и позволил проводить честное сравнение.\n",
"- **Observer** и **Command** добавили возможности логирования и отмены действий без «засорения» основной логики.\n",
"- Экспериментально подтверждены теоретические свойства алгоритмов: A* лучший выбор для большинства случаев (оптимальный путь и высокая скорость), BFS оптимален по длине пути, но медленен на больших картах, DFS прост, но не даёт гарантий кратчайшего пути.\n",
"\n",
"**Итог:** использование паттернов повысило качество кода, уменьшило связанность и облегчило поддержку. Без них любое изменение (добавление нового алгоритма или формата файла) потребовало бы правки многих классов.\n",
"\n",
"--- \n"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "5e634d95-e0c0-4b8a-a330-02463cd085c8",
"metadata": {},
"outputs": [],
"source": []
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3 (ipykernel)",
"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.13.5"
}
},
"nbformat": 4,
"nbformat_minor": 5
}

File diff suppressed because one or more lines are too long

594
filippovavm/docs/МП.ipynb Normal file
View File

@ -0,0 +1,594 @@
{
"cells": [
{
"cell_type": "markdown",
"id": "dbe95ca0-7bc2-4cea-bdbb-319e9a1bd5b6",
"metadata": {},
"source": [
"# Импорт библиотек\n",
"# В этом блоке подключаются модули для работы со временем, случайными числами, CSV, системными параметрами, а также для построения графиков."
]
},
{
"cell_type": "code",
"execution_count": 1,
"id": "c0b2cd62-6c05-4896-8f40-82d75ae765e9",
"metadata": {},
"outputs": [],
"source": [
"import time\n",
"import random\n",
"import csv\n",
"import sys\n",
"import matplotlib.pyplot as plt\n",
"import numpy as np"
]
},
{
"cell_type": "markdown",
"id": "919b92b0-2819-457a-87a0-8f26eaeca817",
"metadata": {},
"source": [
"# Связный список\n",
"# Каждый узел содержит имя, телефон и ссылку на следующий узел.\n",
"# Функции: ll_insert (вставка/обновление), ll_find (поиск), ll_delete (удаление)."
]
},
{
"cell_type": "code",
"execution_count": 2,
"id": "30700662-1215-4476-bffe-96ad9d3f1ab4",
"metadata": {},
"outputs": [],
"source": [
"# Связный список\n",
"def ll_insert(head, name, phone):\n",
" current = head\n",
" prev = None\n",
" while current is not None:\n",
" if current['name'] == name:\n",
" current['phone'] = phone\n",
" return head\n",
" prev = current\n",
" current = current['next']\n",
" new_node = {'name': name, 'phone': phone, 'next': None}\n",
" if prev is None:\n",
" return new_node\n",
" else:\n",
" prev['next'] = new_node\n",
" return head\n",
"\n",
"def ll_find(head, name):\n",
" current = head\n",
" while current is not None:\n",
" if current['name'] == name:\n",
" return current['phone']\n",
" current = current['next']\n",
" return None\n",
"\n",
"def ll_delete(head, name):\n",
" if head is None:\n",
" return None\n",
" if head['name'] == name:\n",
" return head['next']\n",
" current = head\n",
" while current['next'] is not None:\n",
" if current['next']['name'] == name:\n",
" current['next'] = current['next']['next']\n",
" return head\n",
" current = current['next']\n",
" return head"
]
},
{
"cell_type": "markdown",
"id": "49dd7db0-58a7-4ff9-98cd-529e2e91ca94",
"metadata": {},
"source": [
"# Хеш-таблица\n",
"# Хеш-функция на основе полиномиального кода (31 и длина таблицы).\n",
"# Размер таблицы фиксирован (2000). В каждой ячейке хранится связный список.\n",
"# Функции: ht_create, ht_insert, ht_find, ht_delete."
]
},
{
"cell_type": "code",
"execution_count": 3,
"id": "22800445-5217-45ce-9ecd-0f61389b1c3f",
"metadata": {},
"outputs": [],
"source": [
"# Хеш-таблица \n",
"def hash_function(name, size):\n",
" total = 0\n",
" for ch in name:\n",
" total = (total * 31 + ord(ch)) % size\n",
" return total\n",
"\n",
"def ht_create(size=2000):\n",
" return [None] * size\n",
"\n",
"def ht_insert(buckets, name, phone):\n",
" idx = hash_function(name, len(buckets))\n",
" buckets[idx] = ll_insert(buckets[idx], name, phone)\n",
"\n",
"def ht_find(buckets, name):\n",
" idx = hash_function(name, len(buckets))\n",
" return ll_find(buckets[idx], name)\n",
"\n",
"def ht_delete(buckets, name):\n",
" idx = hash_function(name, len(buckets))\n",
" buckets[idx] = ll_delete(buckets[idx], name)\n"
]
},
{
"cell_type": "markdown",
"id": "0b03af57-2771-4b20-8e7d-1ac475afaae8",
"metadata": {},
"source": [
"# Двоичное дерево поиска\n",
"# Узел содержит имя, телефон, ссылки на левого и правого потомка.\n",
"# Функции: bst_insert (вставка/обновление), bst_find (поиск), bst_delete (удаление).\n",
"# Для удаления используется поиск преемника (минимальный элемент в правом поддереве)."
]
},
{
"cell_type": "code",
"execution_count": 5,
"id": "7ced846b-b65f-4cf2-8fb8-5a4849f55509",
"metadata": {},
"outputs": [],
"source": [
"# Двоичное дерево поиска \n",
"def bst_insert(root, name, phone):\n",
" new_node = {'name': name, 'phone': phone, 'left': None, 'right': None}\n",
" if root is None:\n",
" return new_node\n",
" current = root\n",
" while True:\n",
" if name < current['name']:\n",
" if current['left'] is None:\n",
" current['left'] = new_node\n",
" break\n",
" current = current['left']\n",
" elif name > current['name']:\n",
" if current['right'] is None:\n",
" current['right'] = new_node\n",
" break\n",
" current = current['right']\n",
" else:\n",
" current['phone'] = phone\n",
" break\n",
" return root\n",
"\n",
"def bst_find(root, name):\n",
" current = root\n",
" while current is not None:\n",
" if name < current['name']:\n",
" current = current['left']\n",
" elif name > current['name']:\n",
" current = current['right']\n",
" else:\n",
" return current['phone']\n",
" return None\n",
"\n",
"def bst_find_min(node):\n",
" while node['left'] is not None:\n",
" node = node['left']\n",
" return node\n",
"\n",
"def bst_delete(root, name):\n",
" parent = None\n",
" current = root\n",
" while current is not None and current['name'] != name:\n",
" parent = current\n",
" if name < current['name']:\n",
" current = current['left']\n",
" else:\n",
" current = current['right']\n",
" if current is None:\n",
" return root\n",
" \n",
" if current['left'] is None and current['right'] is None:\n",
" if parent is None:\n",
" return None\n",
" if parent['left'] is current:\n",
" parent['left'] = None\n",
" else:\n",
" parent['right'] = None\n",
" return root\n",
" if current['left'] is None:\n",
" if parent is None:\n",
" return current['right']\n",
" if parent['left'] is current:\n",
" parent['left'] = current['right']\n",
" else:\n",
" parent['right'] = current['right']\n",
" return root\n",
" if current['right'] is None:\n",
" if parent is None:\n",
" return current['left']\n",
" if parent['left'] is current:\n",
" parent['left'] = current['left']\n",
" else:\n",
" parent['right'] = current['left']\n",
" return root\n",
" \n",
" succ_parent = current\n",
" succ = current['right']\n",
" while succ['left'] is not None:\n",
" succ_parent = succ\n",
" succ = succ['left']\n",
" current['name'] = succ['name']\n",
" current['phone'] = succ['phone']\n",
" if succ_parent['left'] is succ:\n",
" succ_parent['left'] = succ['right']\n",
" else:\n",
" succ_parent['right'] = succ['right']\n",
" return root"
]
},
{
"cell_type": "markdown",
"id": "88e1d5ec-5123-4bff-837a-bb451a0125c3",
"metadata": {},
"source": [
"# Генерация записей телефонной книги\n",
"# Создаёт N записей вида User_00001 и случайный номер телефона."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "b266c127-99a7-479c-a012-3eedeff7ade3",
"metadata": {},
"outputs": [],
"source": [
"# Генерация данных \n",
"def generate_records(N):\n",
" records = []\n",
" for i in range(N):\n",
" name = f\"User_{i:05d}\"\n",
" phone = f\"+7-999-{random.randint(1000000, 9999999)}\"\n",
" records.append((name, phone))\n",
" return records"
]
},
{
"cell_type": "markdown",
"id": "a92500c8-e928-46a8-a115-12cc033ea2da",
"metadata": {},
"source": [
"# Функции замеров\n",
"# measure_insert вставка всех записей (повторяется 5 раз).\n",
"# build_structure построение структуры данных (используется для последующих замеров).\n",
"# measure_find_on_structure поиск 110 записей (100 существующих + 10 отсутствующих) 5 раз.\n",
"# measure_delete_on_structure удаление 50 случайных записей 5 раз."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "d5060601-6e07-4148-9faa-f9b7b5f433dd",
"metadata": {},
"outputs": [],
"source": [
"# Замеры\n",
"REPEATS = 5\n",
"N = 10000\n",
"\n",
"def measure_insert(struct, records, repeats=REPEATS):\n",
" times = []\n",
" for _ in range(repeats):\n",
" if struct == 'll':\n",
" head = None\n",
" start = time.perf_counter()\n",
" for name, phone in records:\n",
" head = ll_insert(head, name, phone)\n",
" end = time.perf_counter()\n",
" elif struct == 'ht':\n",
" buckets = ht_create(2000)\n",
" start = time.perf_counter()\n",
" for name, phone in records:\n",
" ht_insert(buckets, name, phone)\n",
" end = time.perf_counter()\n",
" elif struct == 'bst':\n",
" root = None\n",
" start = time.perf_counter()\n",
" for name, phone in records:\n",
" root = bst_insert(root, name, phone)\n",
" end = time.perf_counter()\n",
" times.append(end - start)\n",
" return times\n",
"\n",
"def build_structure(struct, records):\n",
" if struct == 'll':\n",
" head = None\n",
" for name, phone in records:\n",
" head = ll_insert(head, name, phone)\n",
" return head\n",
" elif struct == 'ht':\n",
" buckets = ht_create(2000)\n",
" for name, phone in records:\n",
" ht_insert(buckets, name, phone)\n",
" return buckets\n",
" elif struct == 'bst':\n",
" root = None\n",
" for name, phone in records:\n",
" root = bst_insert(root, name, phone)\n",
" return root\n",
" def measure_find_on_structure(struct, structure, records, repeats=REPEATS):\n",
" times = []\n",
" N = len(records)\n",
" for _ in range(repeats):\n",
" indices = random.sample(range(N), 100)\n",
" exist = [records[i][0] for i in indices]\n",
" missing = [f\"None_{i}\" for i in range(10)]\n",
" search = exist + missing\n",
" start = time.perf_counter()\n",
" if struct == 'll':\n",
" for name in search:\n",
" ll_find(structure, name)\n",
" elif struct == 'ht':\n",
" for name in search:\n",
" ht_find(structure, name)\n",
" elif struct == 'bst':\n",
" for name in search:\n",
" bst_find(structure, name)\n",
" times.append(time.perf_counter() - start)\n",
" return times\n",
"\n",
"def measure_delete_on_structure(struct, records, repeats=REPEATS):\n",
" times = []\n",
" N = len(records)\n",
" for _ in range(repeats):\n",
" indices = random.sample(range(N), 50)\n",
" del_names = [records[i][0] for i in indices]\n",
" if struct == 'll':\n",
" head = None\n",
" for name, phone in records:\n",
" head = ll_insert(head, name, phone)\n",
" start = time.perf_counter()\n",
" for name in del_names:\n",
" head = ll_delete(head, name)\n",
" end = time.perf_counter()\n",
" elif struct == 'ht':\n",
" buckets = ht_create(2000)\n",
" for name, phone in records:\n",
" ht_insert(buckets, name, phone)\n",
" start = time.perf_counter()\n",
" for name in del_names:\n",
" ht_delete(buckets, name)\n",
" end = time.perf_counter()\n",
" elif struct == 'bst':\n",
" root = None\n",
" for name, phone in records:\n",
" root = bst_insert(root, name, phone)\n",
" start = time.perf_counter()\n",
" for name in del_names:\n",
" root = bst_delete(root, name)\n",
" end = time.perf_counter()\n",
" times.append(end - start)\n",
" return times\n"
]
},
{
"cell_type": "markdown",
"id": "d93d8eac-b094-43b2-8af0-9afb0ab51ada",
"metadata": {},
"source": [
"# Основная функция\n",
"# 1. Генерирует 10000 записей в случайном и отсортированном порядке.\n",
"# 2. Измеряет время вставки всех записей, поиска (110 запросов) и удаления (50 записей)\n",
"# для связного списка, хеш-таблицы и дерева.\n",
"# 3. Выводит средние значения, сохраняет все замеры в CSV.\n",
"# 4. Строит несколько графиков (сравнительные столбчатые диаграммы и графики по попыткам)."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "a667779c-39b8-4e4b-b05d-bf9c9cccbbd9",
"metadata": {},
"outputs": [],
"source": [
"# Основная функция \n",
"def main():\n",
" print(\"Генерация данных...\")\n",
" records = generate_records(N)\n",
" random.shuffle(records) # случайный порядок\n",
" records_sorted = sorted(records, key=lambda x: x[0]) # отсортированный\n",
"\n",
" results = [] # для CSV\n",
" struct_names = {'ll': 'Связного списка', 'ht': 'Хеш-таблицы', 'bst': 'Двоичного дерева поиска'}\n",
" mode_names = {'shuffled': 'случайный', 'sorted': 'отсортированный'}\n",
" op_names = {'insert': 'Вставка всех записей', 'find': 'Поиск записей', 'delete': 'Удаление записей'}\n",
" # Вставка \n",
" for struct in ['ll', 'ht', 'bst']:\n",
" print(f\"\\nИзмерение вставки для {struct_names[struct]}...\")\n",
" times_sh = measure_insert(struct, records)\n",
" times_so = measure_insert(struct, records_sorted)\n",
" insert_sh[struct] = times_sh\n",
" insert_so[struct] = times_so\n",
" print(f\" случайный: {[round(t,6) for t in times_sh]}, среднее = {sum(times_sh)/len(times_sh):.6f}\")\n",
" print(f\" отсортированный: {[round(t,6) for t in times_so]}, среднее = {sum(times_so)/len(times_so):.6f}\")\n",
" results.append([struct_names[struct], mode_names['shuffled'], op_names['insert'], sum(times_sh)/len(times_sh)] + times_sh)\n",
" results.append([struct_names[struct], mode_names['sorted'], op_names['insert'], sum(times_so)/len(times_so)] + times_so)\n",
" # Поиск \n",
" for struct in ['ll', 'ht', 'bst']:\n",
" print(f\"\\nПоиск для {struct_names[struct]} на случайных данных...\")\n",
" structure_sh = build_structure(struct, records)\n",
" times_find_sh = measure_find_on_structure(struct, structure_sh, records)\n",
" find_sh[struct] = times_find_sh\n",
" print(f\" случайный: {[round(t,6) for t in times_find_sh]}, среднее = {sum(times_find_sh)/len(times_find_sh):.6f}\")\n",
" results.append([struct_names[struct], mode_names['shuffled'], op_names['find'], sum(times_find_sh)/len(times_find_sh)] + times_find_sh)\n",
"\n",
" print(f\"Поиск для {struct_names[struct]} на отсортированных данных...\")\n",
" structure_so = build_structure(struct, records_sorted)\n",
" times_find_so = measure_find_on_structure(struct, structure_so, records_sorted)\n",
" find_so[struct] = times_find_so\n",
" print(f\" отсортированный: {[round(t,6) for t in times_find_so]}, среднее = {sum(times_find_so)/len(times_find_so):.6f}\")\n",
" results.append([struct_names[struct], mode_names['sorted'], op_names['find'], sum(times_find_so)/len(times_find_so)] + times_find_so)\n",
" # Удаление \n",
" for struct in ['ll', 'ht', 'bst']:\n",
" print(f\"\\nУдаление для {struct_names[struct]} на случайных данных...\")\n",
" times_del_sh = measure_delete_on_structure(struct, records)\n",
" delete_sh[struct] = times_del_sh\n",
" print(f\" случайный: {[round(t,6) for t in times_del_sh]}, среднее = {sum(times_del_sh)/len(times_del_sh):.6f}\")\n",
" results.append([struct_names[struct], mode_names['shuffled'], op_names['delete'], sum(times_del_sh)/len(times_del_sh)] + times_del_sh)\n",
"\n",
" print(f\"Удаление для {struct_names[struct]} на отсортированных данных...\")\n",
" times_del_so = measure_delete_on_structure(struct, records_sorted)\n",
" delete_so[struct] = times_del_so\n",
" print(f\" отсортированный: {[round(t,6) for t in times_del_so]}, среднее = {sum(times_del_so)/len(times_del_so):.6f}\")\n",
" results.append([struct_names[struct], mode_names['sorted'], op_names['delete'], sum(times_del_so)/len(times_del_so)] + times_del_so)\n",
" # Сохраняем CSV\n",
" with open(\"phonebook_results.csv\", \"w\", newline=\"\", encoding=\"utf-8\") as f:\n",
" writer = csv.writer(f)\n",
" writer.writerow(['Структура', 'Режим', 'Операция', 'Среднее', 'Замер1', 'Замер2', 'Замер3', 'Замер4', 'Замер5'])\n",
" writer.writerows(results)\n",
"# Графики замеров\n",
" try:\n",
" def plot_attempts(data_sh, data_so, op_name):\n",
" fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 5))\n",
" # случайный порядок\n",
" for struct, label, color, marker in [('ll','LinkedList','red','o'), ('ht','HashTable','green','s'), ('bst','BST','blue','^')]:\n",
" times = data_sh[struct]\n",
" x = range(1, len(times)+1)\n",
" ax1.plot(x, times, marker=marker, color=color, label=label, linestyle='--', linewidth=1)\n",
" ax1.scatter(x, times, color=color, s=60, zorder=5)\n",
" ax1.set_xlabel('Номер попытки')\n",
" ax1.set_ylabel('Время (сек)')\n",
" ax1.set_title(f'{op_name} случайный порядок')\n",
" ax1.legend()\n",
" ax1.grid(True, linestyle=':', alpha=0.7)\n",
" # отсортированный порядок\n",
" for struct, label, color, marker in [('ll','LinkedList','red','o'), ('ht','HashTable','green','s'), ('bst','BST','blue','^')]:\n",
" times = data_so[struct]\n",
" x = range(1, len(times)+1)\n",
" ax2.plot(x, times, marker=marker, color=color, label=label, linestyle='--', linewidth=1)\n",
" ax2.scatter(x, times, color=color, s=60, zorder=5)\n",
" ax2.set_xlabel('Номер попытки')\n",
" ax2.set_ylabel('Время (сек)')\n",
" ax2.set_title(f'{op_name} отсортированный порядок')\n",
" ax2.legend()\n",
" ax2.grid(True, linestyle=':', alpha=0.7)\n",
" plt.tight_layout()\n",
" plt.savefig(f'{op_name}_5attempts.png')\n",
" plt.show()\n",
" \n",
" plot_attempts(insert_sh, insert_so, 'insert')\n",
" plot_attempts(find_sh, find_so, 'find')\n",
" plot_attempts(delete_sh, delete_so, 'delete')\n",
" print(\"Дополнительные графики сохранены: insert_5attempts.png, find_5attempts.png, delete_5attempts.png\")\n",
" except Exception as e:\n",
" print(f\"Не удалось построить дополнительные графики: {e}\")\n",
"try:\n",
" # График вставки\n",
" fig1, ax1 = plt.subplots(figsize=(10,6))\n",
" x = np.arange(3)\n",
" width = 0.35\n",
" means_sh = [sum(insert_sh[s])/len(insert_sh[s]) for s in ['ll','ht','bst']]\n",
" means_so = [sum(insert_so[s])/len(insert_so[s]) for s in ['ll','ht','bst']]\n",
" rects1 = ax1.bar(x - width/2, means_sh, width, label='Случайный порядок', color='skyblue')\n",
" rects2 = ax1.bar(x + width/2, means_so, width, label='Отсортированный порядок', color='salmon')\n",
" ax1.set_ylabel('Время (сек)')\n",
" ax1.set_title('Вставка всех записей')\n",
" ax1.set_xticks(x)\n",
" ax1.set_xticklabels(['Связный список', 'Хеш-таблица', 'Двоичное дерево'])\n",
" ax1.legend()\n",
" ax1.set_yscale('log')\n",
" for rect in rects1 + rects2:\n",
" h = rect.get_height()\n",
" ax1.annotate(f'{h:.3f}', xy=(rect.get_x()+rect.get_width()/2, h),\n",
" xytext=(0,3), textcoords=\"offset points\", ha='center', va='bottom', fontsize=8)\n",
" plt.tight_layout()\n",
" plt.savefig('insert_comparison.png')\n",
" plt.show()\n",
"\n",
" # График поиска\n",
" fig2, ax2 = plt.subplots(figsize=(10,6))\n",
" means_find_sh = [sum(find_sh[s])/len(find_sh[s]) for s in ['ll','ht','bst']]\n",
" means_find_so = [sum(find_so[s])/len(find_so[s]) for s in ['ll','ht','bst']]\n",
" rects1 = ax2.bar(x - width/2, means_find_sh, width, label='Случайный порядок', color='skyblue')\n",
" rects2 = ax2.bar(x + width/2, means_find_so, width, label='Отсортированный порядок', color='salmon')\n",
" ax2.set_ylabel('Время (сек)')\n",
" ax2.set_title('Поиск (100 существующих + 10 отсутствующих)')\n",
" ax2.set_xticks(x)\n",
" ax2.set_xticklabels(['Связный список', 'Хеш-таблица', 'Двоичное дерево'])\n",
" ax2.legend()\n",
" for rect in rects1 + rects2:\n",
" h = rect.get_height()\n",
" ax2.annotate(f'{h:.5f}', xy=(rect.get_x()+rect.get_width()/2, h),\n",
" xytext=(0,3), textcoords=\"offset points\", ha='center', va='bottom', fontsize=8)\n",
" plt.tight_layout()\n",
" plt.savefig('find_comparison.png')\n",
" plt.show()\n",
"\n",
" # График удаления \n",
" fig3, ax3 = plt.subplots(figsize=(10,6))\n",
" means_del_sh = [sum(delete_sh[s])/len(delete_sh[s]) for s in ['ll','ht','bst']]\n",
" means_del_so = [sum(delete_so[s])/len(delete_so[s]) for s in ['ll','ht','bst']]\n",
" rects1 = ax3.bar(x - width/2, means_del_sh, width, label='Случайный порядок', color='skyblue')\n",
" rects2 = ax3.bar(x + width/2, means_del_so, width, label='Отсортированный порядок', color='salmon')\n",
" ax3.set_ylabel('Время (сек)')\n",
" ax3.set_title('Удаление 50 случайных записей')\n",
" ax3.set_xticks(x)\n",
" ax3.set_xticklabels(['Связный список', 'Хеш-таблица', 'Двоичное дерево'])\n",
" ax3.legend()\n",
" for rect in rects1 + rects2:\n",
" h = rect.get_height()\n",
" ax3.annotate(f'{h:.5f}', xy=(rect.get_x()+rect.get_width()/2, h),\n",
" xytext=(0,3), textcoords=\"offset points\", ha='center', va='bottom', fontsize=8)\n",
" plt.tight_layout()\n",
" plt.savefig('delete_comparison.png')\n",
" plt.show()\n",
" print(\"Графики сохранены: insert_comparison.png, find_comparison.png, delete_comparison.png\")\n",
" except Exception as e:\n",
" print(f\"Не удалось построить графики: {e}\")"
]
},
{
"cell_type": "markdown",
"id": "dd2b18cd-f37c-4f9f-a92a-325be857deb0",
"metadata": {},
"source": [
"# Запуск эксперимента\n",
"# Устанавливается увеличенная глубина рекурсии (на случай глубоких деревьев) и вызывается main()."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "04462e9d-aecc-44b3-b98b-0d06f856f38a",
"metadata": {},
"outputs": [],
"source": [
"if __name__ == \"__main__\":\n",
" sys.setrecursionlimit(20000)\n",
" main()"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3 (ipykernel)",
"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.13.5"
}
},
"nbformat": 4,
"nbformat_minor": 5
}

View File

@ -0,0 +1,149 @@
{
"cells": [
{
"cell_type": "markdown",
"id": "0c973489-075d-42ac-a28f-f8d8d954a0da",
"metadata": {},
"source": [
"# Анализ результатов\n",
"\n",
"## Предложенные вопросы\n",
"\n",
"- Как порядок входных данных влияет на скорость вставки в BST (деградация до O(n) на отсортированных данных)?\n",
"- Почему хеш-таблица почти не чувствительна к порядку?\n",
"- Почему связный список всегда медленен при поиске?\n",
"- Как удаление работает в каждой структуре?\n",
"- Вывод: какую структуру и для каких задач (частые вставки, частый поиск, необходимость получать данные в порядке) стоит выбирать в реальной жизни?\n"
]
},
{
"cell_type": "markdown",
"id": "a265cc14-95ff-47ae-a346-ac38cb2a323e",
"metadata": {},
"source": [
"## Выводы\n",
"\n",
"### 1) Как порядок входных данных влияет на скорость вставки в BST?\n",
"\n",
"Порядок отличается очень сильно. В обычном случае сложность равна **O(log n)**, а в худшем случае (как раз на отсортированных данных) **O(n)**. \n"
]
},
{
"cell_type": "markdown",
"id": "950c5e97-12e9-4225-a91e-b8289fdfb5e6",
"metadata": {},
"source": [
"### 2) Почему хеш-таблица почти не чувствительна к порядку?\n",
"\n",
"Это происходит из‑за особенностей записи данных в память. Хеш-таблица вычисляет номер строки (корзины) по математической формуле, поэтому любой элемент можно найти за **O(1)** в среднем. Порядок поступления записей не влияет на расчёт индекса.\n"
]
},
{
"cell_type": "markdown",
"id": "5f6059bf-e99a-4b14-869c-32fb44b092fa",
"metadata": {},
"source": [
"### 3) Почему связный список всегда медленен при поиске?\n",
"\n",
"Это происходит из‑за способа записи. Доступ к следующему элементу возможен только последовательным перебором, равным номеру искомой позиции. Сложность поиска **O(n)**.\n"
]
},
{
"cell_type": "markdown",
"id": "77ddd385-a50d-4ab6-b761-477460529e9d",
"metadata": {},
"source": [
"### 4) Как удаление работает в каждой структуре?\n",
"\n",
"- **Связный список** \n",
" - Если список пустой → возвращаем `None`. \n",
" - Если удаляем голову → новой головой становится следующий элемент. \n",
" - Если удаляем промежуточный элемент ищем нужный узел, затем у предыдущего узла меняем ссылку на следующий после удаляемого.\n",
"\n",
"- **Хеш-таблица** \n",
" Реализация вычисляет номер корзины через хеш-ключ, затем использует функции связного списка для работы внутри корзины. Таким образом, удаление в хеш-таблице наследует логику удаления из связного списка, но применяется только к элементам одной корзины.\n",
"\n",
"- **Бинарное дерево (BST)** \n",
" Рассматриваются 4 случая (логика похожа на связный список, но с учётом двух потомков): \n",
" - Если дерево пустое → вернуть `None`. \n",
" - Если удаляемый элемент меньше корня → спуститься в левую ветвь. \n",
" - Если больше корня → спуститься в правую ветвь. \n",
" - Если у удаляемого узла два потомка находим преемника (самый левый узел в правом поддереве), копируем его данные в удаляемый узел и удаляем преемника. \n",
" При нахождении элемента ссылка от родителя к удаляемому узлу заменяется на ссылку на соответствующего потомка (левый или правый).\n"
]
},
{
"cell_type": "markdown",
"id": "97b09bb6-e8ef-486e-9cdd-fc37b19bfeb2",
"metadata": {},
"source": [
"### 5) Какую структуру и для каких задач стоит выбирать в реальной жизни?\n",
"\n",
"- **Для частых вставок и поиска элементов** лучше всего использовать **хеш-таблицу**, так как добавление происходит за счёт математического вычисления индекса, а не последовательного перебора. \n",
"- **Если нужны упорядоченные данные** (например, вывод записей в алфавитном порядке) подходит **двоичное дерево поиска** (BST) благодаря свойству inorder обхода. \n",
"- **Связный список** неэффективен для больших объёмов данных.\n",
"\n",
"---"
]
},
{
"cell_type": "markdown",
"id": "3b242f54-fd27-4f6e-8a31-9b3a6bee5f59",
"metadata": {},
"source": [
"## Дополнительные числовые результаты\n",
"\n",
"\n",
"\n",
"1. **Связный список:** \n",
" - Вставка: O(n) → ~0.25 с, порядок данных не влияет. \n",
" - Поиск: O(n) → очень медленно (~0.5 с), порядок не влияет. \n",
" - Удаление: O(n) → медленно.\n",
"\n",
"2. **Хеш-таблица:** \n",
" - Вставка: O(1) в среднем → ~0.008 с, порядок данных не влияет. \n",
" - Поиск: O(1) → ~0.002 с, самый быстрый. \n",
" - Удаление: O(1) → ~0.002 с.\n",
"\n",
"3. **Двоичное дерево поиска:** \n",
" - На случайных данных: O(log n) → вставка ~0.018 с, поиск ~0.0015 с, удаление ~0.0016 с. \n",
" - На отсортированных данных: дерево вырождается в линейный список → вставка ~2.3 с, поиск и удаление также становятся O(n) (на графиках виден рост времени).\n",
"\n",
"**ИТОГОВЫЙ ВЫВОД:** \n",
"- Для частого поиска, вставки и удаления лучший выбор **хеш-таблица**. \n",
"- Если данные поступают в отсортированном порядке BST не подходит из‑за деградации. \n",
"- Если нужна частая выдача записей в алфавитном порядке и данные случайны BST хорош. \n",
"- Связный список неэффективен для больших объёмов."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "1b39f136-0c95-46f0-b794-7136232bcd3c",
"metadata": {},
"outputs": [],
"source": []
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3 (ipykernel)",
"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.13.5"
}
},
"nbformat": 4,
"nbformat_minor": 5
}