forked from UNN/2026-rff_mp
253 lines
8.9 KiB
Python
253 lines
8.9 KiB
Python
|
|
import random
|
||
|
|
import pandas as pd
|
||
|
|
import time
|
||
|
|
import sys
|
||
|
|
import os
|
||
|
|
import matplotlib.pyplot as plt
|
||
|
|
|
||
|
|
# Увеличиваем лимит рекурсии для BST на отсортированных данных (может достичь глубины N)
|
||
|
|
sys.setrecursionlimit(20000)
|
||
|
|
|
||
|
|
# =========================================================
|
||
|
|
# 1. СВЯЗНЫЙ СПИСОК (LinkedListPhoneBook)
|
||
|
|
# =========================================================
|
||
|
|
def ll_insert(head, name, phone):
|
||
|
|
if head is None:
|
||
|
|
return {'name': name, 'phone': phone, 'next': None}
|
||
|
|
|
||
|
|
curr = head
|
||
|
|
while True:
|
||
|
|
if curr['name'] == name:
|
||
|
|
curr['phone'] = phone # Обновление существующей записи
|
||
|
|
break
|
||
|
|
if curr['next'] is None:
|
||
|
|
curr['next'] = {'name': name, 'phone': phone, 'next': None}
|
||
|
|
break
|
||
|
|
curr = curr['next']
|
||
|
|
return head
|
||
|
|
|
||
|
|
def ll_find(head, name):
|
||
|
|
curr = head
|
||
|
|
while curr:
|
||
|
|
if curr['name'] == name:
|
||
|
|
return curr['phone']
|
||
|
|
curr = curr['next']
|
||
|
|
return None
|
||
|
|
|
||
|
|
def ll_delete(head, name):
|
||
|
|
if head is None:
|
||
|
|
return None
|
||
|
|
if head['name'] == name:
|
||
|
|
return head['next']
|
||
|
|
|
||
|
|
curr = head
|
||
|
|
while curr['next']:
|
||
|
|
if curr['next']['name'] == name:
|
||
|
|
curr['next'] = curr['next']['next']
|
||
|
|
break
|
||
|
|
curr = curr['next']
|
||
|
|
return head
|
||
|
|
|
||
|
|
def ll_list_all(head):
|
||
|
|
res = []
|
||
|
|
curr = head
|
||
|
|
while curr:
|
||
|
|
res.append((curr['name'], curr['phone']))
|
||
|
|
curr = curr['next']
|
||
|
|
res.sort(key=lambda x: x[0])
|
||
|
|
return res
|
||
|
|
|
||
|
|
# =========================================================
|
||
|
|
# 2. ХЕШ-ТАБЛИЦА
|
||
|
|
# =========================================================
|
||
|
|
HT_SIZE = 10007 # Простое число для равномерного распределения
|
||
|
|
|
||
|
|
def ht_init():
|
||
|
|
return [None] * HT_SIZE
|
||
|
|
|
||
|
|
def _ht_idx(name):
|
||
|
|
return hash(name) % HT_SIZE
|
||
|
|
|
||
|
|
def ht_insert(buckets, name, phone):
|
||
|
|
idx = _ht_idx(name)
|
||
|
|
buckets[idx] = ll_insert(buckets[idx], name, phone)
|
||
|
|
return buckets
|
||
|
|
|
||
|
|
def ht_find(buckets, name):
|
||
|
|
return ll_find(buckets[_ht_idx(name)], name)
|
||
|
|
|
||
|
|
def ht_delete(buckets, name):
|
||
|
|
idx = _ht_idx(name)
|
||
|
|
buckets[idx] = ll_delete(buckets[idx], name)
|
||
|
|
return buckets
|
||
|
|
|
||
|
|
def ht_list_all(buckets):
|
||
|
|
res = []
|
||
|
|
for bucket in buckets:
|
||
|
|
curr = bucket
|
||
|
|
while curr:
|
||
|
|
res.append((curr['name'], curr['phone']))
|
||
|
|
curr = curr['next']
|
||
|
|
res.sort(key=lambda x: x[0])
|
||
|
|
return res
|
||
|
|
|
||
|
|
# =========================================================
|
||
|
|
# 3. ДВОИЧНОЕ ДЕРЕВО ПОИСКА (BST)
|
||
|
|
# =========================================================
|
||
|
|
def bst_insert(root, name, phone):
|
||
|
|
if root is None:
|
||
|
|
return {'name': name, 'phone': phone, 'left': None, 'right': None}
|
||
|
|
|
||
|
|
if name < root['name']:
|
||
|
|
root['left'] = bst_insert(root['left'], name, phone)
|
||
|
|
elif name > root['name']:
|
||
|
|
root['right'] = bst_insert(root['right'], name, phone)
|
||
|
|
else:
|
||
|
|
root['phone'] = phone
|
||
|
|
return root
|
||
|
|
|
||
|
|
def bst_find(root, name):
|
||
|
|
curr = root
|
||
|
|
while curr:
|
||
|
|
if name == curr['name']:
|
||
|
|
return curr['phone']
|
||
|
|
elif name < curr['name']:
|
||
|
|
curr = curr['left']
|
||
|
|
else:
|
||
|
|
curr = curr['right']
|
||
|
|
return None
|
||
|
|
|
||
|
|
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']
|
||
|
|
if root['right'] is None:
|
||
|
|
return root['left']
|
||
|
|
|
||
|
|
# Два потомка: находим минимальный в правом поддереве
|
||
|
|
min_node = root['right']
|
||
|
|
while min_node['left'] is not None:
|
||
|
|
min_node = min_node['left']
|
||
|
|
|
||
|
|
root['name'] = min_node['name']
|
||
|
|
root['phone'] = min_node['phone']
|
||
|
|
root['right'] = bst_delete(root['right'], min_node['name'])
|
||
|
|
return root
|
||
|
|
|
||
|
|
def bst_list_all(root):
|
||
|
|
if root is None:
|
||
|
|
return []
|
||
|
|
return bst_list_all(root['left']) + [(root['name'], root['phone'])] + bst_list_all(root['right'])
|
||
|
|
|
||
|
|
# =========================================================
|
||
|
|
# ЭКСПЕРИМЕНТАЛЬНАЯ ЧАСТЬ
|
||
|
|
# =========================================================
|
||
|
|
def run_experiments():
|
||
|
|
N = 10000
|
||
|
|
RECORDS = [(f"User_{i:05d}", f"+7900{i:04d}{i%100:02d}") for i in range(N)]
|
||
|
|
|
||
|
|
records_shuffled = RECORDS[:]
|
||
|
|
random.shuffle(records_shuffled)
|
||
|
|
|
||
|
|
records_sorted = sorted(RECORDS, key=lambda x: x[0])
|
||
|
|
|
||
|
|
# Наборы для поиска и удаления
|
||
|
|
existing_names = [r[0] for r in random.sample(RECORDS, 100)]
|
||
|
|
non_existing_names = [f"None_{i}" for i in range(10)]
|
||
|
|
find_names = existing_names + non_existing_names
|
||
|
|
delete_names = [r[0] for r in random.sample(RECORDS, 50)]
|
||
|
|
|
||
|
|
structures = {
|
||
|
|
"LinkedList": (lambda: None, ll_insert, ll_find, ll_delete),
|
||
|
|
"HashTable": (ht_init, ht_insert, ht_find, ht_delete),
|
||
|
|
"BST": (lambda: None, bst_insert, bst_find, bst_delete)
|
||
|
|
}
|
||
|
|
|
||
|
|
modes = {"случайный": records_shuffled, "отсортированный": records_sorted}
|
||
|
|
results = []
|
||
|
|
|
||
|
|
print("Запуск экспериментов...")
|
||
|
|
trials = 5
|
||
|
|
for struct_name, (init_f, ins_f, find_f, del_f) in structures.items():
|
||
|
|
for mode_name, data in modes.items():
|
||
|
|
print(f" {struct_name} | {mode_name}")
|
||
|
|
for t in range(1, trials + 1):
|
||
|
|
# Инициализация
|
||
|
|
ds = init_f()
|
||
|
|
|
||
|
|
# A. Вставка
|
||
|
|
t0 = time.perf_counter()
|
||
|
|
for name, phone in data:
|
||
|
|
ds = ins_f(ds, name, phone)
|
||
|
|
t_ins = time.perf_counter() - t0
|
||
|
|
|
||
|
|
# B. Поиск
|
||
|
|
t0 = time.perf_counter()
|
||
|
|
for name in find_names:
|
||
|
|
find_f(ds, name)
|
||
|
|
t_find = time.perf_counter() - t0
|
||
|
|
|
||
|
|
# C. Удаление
|
||
|
|
t0 = time.perf_counter()
|
||
|
|
for name in delete_names:
|
||
|
|
ds = del_f(ds, name)
|
||
|
|
t_del = time.perf_counter() - t0
|
||
|
|
|
||
|
|
results.append([struct_name, mode_name, "вставка", t, t_ins])
|
||
|
|
results.append([struct_name, mode_name, "поиск", t, t_find])
|
||
|
|
results.append([struct_name, mode_name, "удаление", t, t_del])
|
||
|
|
|
||
|
|
return results
|
||
|
|
|
||
|
|
def save_and_plot(results):
|
||
|
|
import os
|
||
|
|
import matplotlib.pyplot as plt
|
||
|
|
import pandas as pd
|
||
|
|
|
||
|
|
os.makedirs("docs/data", exist_ok=True)
|
||
|
|
|
||
|
|
# 1. Сохранение CSV (как было)
|
||
|
|
df = pd.DataFrame(results, columns=["Структура", "Режим", "Операция", "Повторение", "Время (сек)"])
|
||
|
|
avg = df.groupby(["Структура", "Режим", "Операция"])["Время (сек)"].mean().reset_index()
|
||
|
|
avg["Повторение"] = "СРЕДНЕЕ"
|
||
|
|
df_full = pd.concat([df, avg], ignore_index=True)
|
||
|
|
df_full.to_csv("docs/data/results.csv", index=False, encoding="utf-8-sig")
|
||
|
|
|
||
|
|
# 2. Улучшенный график: 3 отдельных подграфика + логарифмическая шкала
|
||
|
|
fig, axes = plt.subplots(1, 3, figsize=(18, 6))
|
||
|
|
operations = ["вставка", "поиск", "удаление"]
|
||
|
|
structures_order = ["HashTable", "BST", "LinkedList"] # Фиксируем порядок для удобства чтения
|
||
|
|
colors = {"случайный": "#6C157F", "отсортированный": "#1E299F"}
|
||
|
|
|
||
|
|
for ax, op in zip(axes, operations):
|
||
|
|
op_data = avg[avg["Операция"] == op]
|
||
|
|
pivot = op_data.pivot(index="Структура", columns="Режим", values="Время (сек)")
|
||
|
|
pivot = pivot.reindex(structures_order) # Ставим структуры в удобном порядке
|
||
|
|
|
||
|
|
pivot.plot(kind="bar", ax=ax, color=[colors["случайный"], colors["отсортированный"]], width=0.75)
|
||
|
|
ax.set_title(f"Операция: {op.capitalize()}")
|
||
|
|
ax.set_ylabel("Время (сек)")
|
||
|
|
ax.set_xticklabels(ax.get_xticklabels(), rotation=0)
|
||
|
|
ax.grid(axis="y", alpha=0.3, linestyle="--")
|
||
|
|
|
||
|
|
# ЛОГАРИФМИЧЕСКАЯ ШКАЛА: обязательна при разбросе от 0.0001 до 30 сек
|
||
|
|
ax.set_yscale("log")
|
||
|
|
ax.legend(title="Режим", loc="upper right")
|
||
|
|
|
||
|
|
fig.suptitle("Сравнение производительности структур данных", fontsize=16, y=1.05)
|
||
|
|
plt.tight_layout()
|
||
|
|
plt.savefig("docs/data/plot.png", dpi=200, bbox_inches="tight")
|
||
|
|
|
||
|
|
if __name__ == "__main__":
|
||
|
|
res = run_experiments()
|
||
|
|
save_and_plot(res)
|
||
|
|
print("Эксперимент завершен")
|