From ce2b43a84ee0f5e0e64e1f782a367f214219a865 Mon Sep 17 00:00:00 2001 From: yanyaevaa Date: Fri, 1 May 2026 01:13:46 +0300 Subject: [PATCH] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=20=D0=BE=D1=82=D1=87=D0=B5=D1=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- YanyaevAA/docs/Report_1.md | 317 +++++++++++++++++++++++++++++++++++++ 1 file changed, 317 insertions(+) create mode 100644 YanyaevAA/docs/Report_1.md diff --git a/YanyaevAA/docs/Report_1.md b/YanyaevAA/docs/Report_1.md new file mode 100644 index 0000000..ad02b00 --- /dev/null +++ b/YanyaevAA/docs/Report_1.md @@ -0,0 +1,317 @@ +# Структуры данных +Цель работы: Реализовать три различные структуры данных «с нуля», применить их для хранения записей телефонного справочника и экспериментально сравнить производительность основных операций. Вы должны собственными руками написать код, чтобы понять внутреннее устройство связного списка, хеш-таблицы и двоичного дерева поиска, а также осознать их сильные и слабые стороны на практике. + +## Подготовка среды +```Python +import time +from pathlib import Path +import random +import csv +import sys +import pandas as pd +import matplotlib.pyplot as plt +import seaborn as sns +sys.setrecursionlimit(12000) #увеличивает глубину рекурсии +``` + +# Базовые операции +```Python +#Связный список +def ll_insert(head, name, phone): + current = head + while current: + if current['name'] == name: + current['phone'] = phone + return head + current = current['next'] + new_node = {'name': name, 'phone': phone, 'next': None} + new_node['next'] = head + return new_node + +def ll_find(head, name): + current = head + while current: + if current['name'] == name: + return current['phone'] + current = current['next'] + return None + +def ll_delete(head, name): + if head['name'] == name: + return head['next'] + current = head + while current['next']: + if current['next']['name'] == name: + current['next'] = current['next']['next'] + break + current = current['next'] + return head + +def ll_list_all(head): + data= [] + current = head + while current: + data.append((current['name'], current['phone'])) + current = current['next'] + return sorted(data) + +#хеш-таблица +def ht_insert(buckets, name, phone): + id=hash(name)%len(buckets) + buckets[id] = ll_insert(buckets[id], name, phone) + +def ht_find(buckets, name): + id= hash(name)%len(buckets) + return ll_find(buckets[id], name) + +def ht_delete(buckets, name): + id= hash(name)%len(buckets) + buckets[id] = ll_delete(buckets[id], name) + +def ht_list_all(buckets): + data = [] + for head in buckets: + current = head + while current: + data.append((current['name'], current['phone'])) + current = current['next'] + return sorted(data) + + + +#Двоичное дерево поиска +def bst_insert(root, name, phone): + if root is None: + return {'name': name, 'phone': phone, 'left': None, 'right': None} + if name == root['name']: + root['phone'] = phone + elif name < root['name']: + root['left'] = bst_insert(root['left'], name, phone) + else: + root['right'] = bst_insert(root['right'], name, phone) + return root + +def bst_find(root, name): + if root is None: + return None + if root['name'] == name: + return root['phone'] + elif name root['name']: + root['right'] = bst_delete(root['right'], name) + else: + if root['left'] is None: + return root['right'] + elif root['right'] is None: + return root['left'] + min=minimum(root['right']) + root['name']=min['name'] + root['phone']=min['phone'] + root['right']=bst_delete(root['right'], min['name']) + return root + +def bst_list_all(root): + result=[] + if root: + result.extend(bst_list_all(root['left'])) + result.append((root['name'], root['phone'])) + result.extend(bst_list_all(root['right'])) + return result +``` + +# Экспериментальная часть + +## Генерация +Создаем список records из N=10000 элементов. Каждый элемент — кортеж (name, phone). +Имена генерируются как f"User_{i:05d}" (равномерное распределение). Для проверки влияния порядка подготовим два варианта одного и того же набора: + +records_shuffled — случайный порядок. + +records_sorted — отсортированный по имени (по алфавиту). +```Python +def generate(n=10000): + records = [(f"User_{i:05d}", f"+7 ({random.randint(100, 999)}) {random.randint(100, 999)}-{random.randint(00, 99):02}-{random.randint(00, 99):02}") for i in range(n)] + records_sorted =records.copy() + records_shuffled=records.copy() + random.shuffle(records_shuffled) + return records_sorted, records_shuffled +``` +## Проведение замеров + +**А. Вставка всех записей** +Создаем пустую структуру. +Засекаем время, выполняем insert для каждой записи из входного списка. +Фиксируем общее время вставки. +```Python +def task_A(structure_name, data): + start =time.perf_counter() + if structure_name=="LinkedList": + head=None + for name, phone in data: + head = ll_insert(head, name, phone) + container=head + elif structure_name=="HashTable": + buckets=[None]*1000 + for name, phone in data: + ht_insert(buckets, name, phone) + container=buckets + elif structure_name=="BinarySearchTree": + root=None + for name, phone in data: + root = bst_insert(root, name, phone) + container=root + end = time.perf_counter() + elapsed = end - start + return elapsed, container +``` + +**Б. Поиск 100 случайных записей** +Берем 100 случайных имён из того же набора (гарантированно существующих) и 10 имён, которых нет ("None_{i}"). +Засекаем время на выполнение всех 110 вызовов find. +```Python +def task_B(structure_name,container, data): + start=time.perf_counter() + if structure_name=="LinkedList": + for name in data: + ll_find(container, name) + elif structure_name=="HashTable": + for name in data: + ht_find(container, name) + elif structure_name=="BinarySearchTree": + for name in data: + bst_find(container, name) + end=time.perf_counter() + elapsed = end - start + return elapsed +``` + +**В. Удаление 50 случайных записей** +Берем 50 случайных имён из набора. +Засекаем время на выполнение delete для каждого. +```Python +def task_C(structure_name,container, data): + start=time.perf_counter() + if structure_name=="LinkedList": + for name in data: + container=ll_delete(container, name) + elif structure_name=="HashTable": + for name in data: + ht_delete(container, name) + elif structure_name=="BinarySearchTree": + for name in data: + container = bst_delete(container, name) + end=time.perf_counter() + elapsed = end - start + return elapsed +``` + +### Реализация замеров +```Python +results=[["Структура", "Режим", "Операция", "Время (сек)"]] +structures_name=["LinkedList", "HashTable", "BinarySearchTree"] +experiment_name=["Вставка", "Поиск", "Удаление"] +mode_of_data=["Случайный", "Отсортированный"] + +records_sorted, records_shuffled = generate() +container_shuffled=[]#хранилище структур со случайными данными +container_sorted=[]#хранилище структур с отсортированными данными +names=[record[0] for record in records_shuffled] +#Данные для задания Б +random_names=random.sample(names, 100) +missing_names=[f"None_{i}" for i in range(10)] +names_for_test=random_names+missing_names +#Данные для задания В +names_to_delete=random.sample(names,50) + +for i in range(3): + container_shuffled.append(task_A(structures_name[i], records_shuffled)[1]) + container_sorted.append(task_A(structures_name[i], records_sorted)[1]) + for j in range(5): + # Реализация задания А + result_shuffled = task_A(structures_name[i], records_shuffled)[0] + results.append([structures_name[i], mode_of_data[0], experiment_name[0], result_shuffled]) + + result_sorted= task_A(structures_name[i], records_sorted)[0] + results.append([structures_name[i], mode_of_data[1], experiment_name[0], result_sorted]) + print(f"{structures_name[i]}: Время вставки всех записей {mode_of_data[0]}: {result_shuffled} {mode_of_data[1]}: {result_sorted}") + # Реализация задания Б + result_shuffled = task_B(structures_name[i], container_shuffled[i], names_for_test) + results.append([structures_name[i], mode_of_data[0], experiment_name[1], result_shuffled]) + + result_sorted = task_B(structures_name[i], container_sorted[i], names_for_test) + results.append([structures_name[i], mode_of_data[1], experiment_name[1], result_sorted]) + print(f"{structures_name[i]}: Время нахождения 110 записей для {mode_of_data[0]}: {result_shuffled} {mode_of_data[1]}: {result_sorted} ") + #Реализация задания В + shuffled = container_shuffled[i] + sorted = container_sorted[i] + result_shuffled = task_C(structures_name[i], shuffled, names_to_delete) + results.append([structures_name[i], mode_of_data[0], experiment_name[2], result_shuffled]) + + result_sorted = task_C(structures_name[i], sorted, names_to_delete) + results.append([structures_name[i], mode_of_data[1], experiment_name[2], result_sorted]) + print(f"{structures_name[i]}: Время удаления 50 записей для {mode_of_data[0]}: {result_shuffled} {mode_of_data[1]}: {result_sorted}") +``` + +## Сохранение результатов +```Python +current_dir=Path.cwd() +target=current_dir.parent/"docs"/"data" +csv_file=target /"results.csv" +with open(csv_file, "w", newline="",encoding="utf-8-sig") as f: + writer = csv.writer(f) + writer.writerows(results) +``` + +# Анализ результатов + +## Построение графиков +```Python +df = pd.read_csv(csv_file) +df_avg = df.groupby(["Структура", "Режим", "Операция"])["Время (сек)"].mean().reset_index() +fig, axes = plt.subplots(1, 3, figsize=(18, 6)) +for i, experiment in enumerate(experiment_name): + data_experiment = df_avg[df_avg["Операция"] == experiment] + sns.barplot(ax=axes[i],data=data_experiment, x="Структура",y="Время (сек)",hue="Режим") + axes[i].set_title(experiment) + axes[i].set_ylabel("Среднее время (сек)") + axes[i].set_yscale("log") +plt.tight_layout() +png_file= target/"graphics.png" +plt.savefig(png_file, dpi=300, bbox_inches='tight') +plt.show() +``` +![](data/graphics.png) + +### Как порядок входных данных влияет на скорость вставки в BST +Если подать на вход отсортированные данные, дерево превращается в связный список: каждый новый узел становится правым потомком предыдущего. И сложность меняется с логарифмической O(log n) на линейную O(n). Вставка для неотсортированных данных заняла 0.016531 с, а для отсортированных: 7.112118 с, разница в 430 раз. Получается, что BST сильно зависит от входных данных. +### Почему хеш-таблица почти не чувствительна к порядку +Хеш-таблица имеет низкую чувствительность к порядку входных данных, поскольку хеш-функция вычисляет индекс в массиве на основе значения ключа, обеспечивая равномерное распределение элементов по бакетам независимо от их исходной последовательности. По графикам видно, что разница между случайными и отсортированными данными минимальна. И для всех операций сложность составляет O(1). +### Почему связный список всегда медленен при поиске +Связный список всегда медленен при поиске, потому что у него отсутствует прямой доступ к элементам, и нужно перебирать все элементы по порядку. И из-за этого связный список имееет сложность O(n). +### Как удаление работает в каждой структуре +- **Связный список:** Сначала программа ищет нужный элемент, перебирая их по порядку от головы, что занимает время O(n). Как только элемент найден, то у предыдущего обновляется ссылка на элемент, который шел после удаляемого, что занимает время O(1). По графикам видно, что время удаления близко ко времени поиска. Время удаления для отсортированных данных: 0.017500 с, а для случайных: 0.018947 с. +- **Хеш-таблица:** Программа определяет нужный бакет и удаляет элемент из короткого связного списка внутри этого бакета за O(1). Время удаления для отсортированных данных: 0.000036 с, а для случайных: 0.000043 с. +- **Двоичное дерево поиска:** Нет потомков: Узел просто стирается. Один потомок: Потомок занимает место удаленного родителя. Два потомка: На место удаленного узла ставится самый минимальный элемент из его правого поддерева. Для случайных данных занимает O(log n), а для отсортированных данных занимает O(n). Время удаления для отсортированных данных: 0.039463 с, а для случайных: 0.000153 с. + +# Вывод +На основе полученных результатов можно сделать вывод: +- **Связный список:** всегда имеет линейную сложность O(n), что делает его неподходящим для задач частых вставок, частого поиска и получения данных в порядке. Но подходит только в узких случаях: максимально быстрая вставка и удаление элементов в начало или конец структуры(очереди, стеки). +- **Хеш-таблица:** является лучшим выбором для максимально задач частого поиска, добавления и удаления элементов, которые имеют сложность O(1), при этом порядок входных данных не имеет значение. Она идеально подходит для словарей и кэшей. +- **Двоичное дерево поиска:** Необходимо использовать в тех случаях, когда необходимо получать данные в отсортированном состоянии и выполнять поиск в заданном диапазоне значений. При случайных входных данных имеет хорошую сложность O(log n), но при получении отсортированных входных данных сложность возрастает до линейной O(n). + +Таким образом, для реальных задач наиболее подходят хеш-таблицы или сбалансированные деревья, если требуется получить данные в отсортированном виде. \ No newline at end of file