Добавлен отчет

This commit is contained in:
yanyaevaa 2026-05-01 01:13:46 +03:00
parent 3f9c9f0ca8
commit ce2b43a84e

317
YanyaevAA/docs/Report_1.md Normal file
View File

@ -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']:
return bst_find(root['left'], name)
else:
return bst_find(root['right'], name)
def minimum(node):
current = node
while current['left'] is not None:
current = current['left']
return current
def bst_delete(root, name):
if root is None:
return None
if name < root['name']:
root['left'] = bst_delete(root['left'], name)
elif name > root['name']:
root['right'] = bst_delete(root['right'], name)
else:
if root['left'] is None:
return root['right']
elif root['right'] is None:
return root['left']
min=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).
Таким образом, для реальных задач наиболее подходят хеш-таблицы или сбалансированные деревья, если требуется получить данные в отсортированном виде.