# Структуры данных Цель работы: Реализовать три различные структуры данных «с нуля», применить их для хранения записей телефонного справочника и экспериментально сравнить производительность основных операций. Вы должны собственными руками написать код, чтобы понять внутреннее устройство связного списка, хеш-таблицы и двоичного дерева поиска, а также осознать их сильные и слабые стороны на практике. ## Подготовка среды ```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). Таким образом, для реальных задач наиболее подходят хеш-таблицы или сбалансированные деревья, если требуется получить данные в отсортированном виде.