forked from UNN/2026-rff_mp
376 lines
13 KiB
Python
376 lines
13 KiB
Python
import sys
|
||
|
||
sys.setrecursionlimit(30000) # Увеличиваю лимит рекурсии для BST
|
||
|
||
# Связный список
|
||
def ll_insert(head, name, phone):
|
||
new_node = {'name': name, 'phone': phone, 'next': None} # Создаю новый узел
|
||
if head is None: # Если список пуст
|
||
return new_node # Возвращаю узел как голову
|
||
|
||
curr = head # Указатель для обхода
|
||
prev = None # Храню предыдущий узел
|
||
while curr is not None: # Иду по списку
|
||
if curr['name'] == name: # Если нашел такое же имя
|
||
curr['phone'] = phone # Обновляю телефон
|
||
return head
|
||
prev = curr
|
||
curr = curr['next']
|
||
prev['next'] = new_node # Добавляю в конец
|
||
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'] # Перепрыгиваю через него
|
||
return head
|
||
curr = curr['next']
|
||
return head
|
||
|
||
|
||
def ll_list_all(head):
|
||
result = []
|
||
curr = head
|
||
while curr: # Собираю все элементы
|
||
result.append((curr['name'], curr['phone']))
|
||
curr = curr['next']
|
||
result.sort(key=lambda x: x[0]) # Сортирую по имени
|
||
return result
|
||
|
||
|
||
# Хэш-таблица
|
||
HASH_SIZE = 1009 # Размер таблицы - простое число
|
||
|
||
|
||
def _hash_name(name):
|
||
return hash(name) % HASH_SIZE # Беру остаток от деления - это индекс корзины
|
||
|
||
|
||
def ht_insert(buckets, name, phone):
|
||
idx = _hash_name(name) # Вычисляю индекс корзины
|
||
buckets[idx] = ll_insert(buckets[idx], name, phone) # Метод цепочек - вставляю в список
|
||
|
||
|
||
def ht_find(buckets, name):
|
||
idx = _hash_name(name) # Нахожу корзину
|
||
return ll_find(buckets[idx], name) # Ищу в цепочке
|
||
|
||
|
||
def ht_delete(buckets, name):
|
||
idx = _hash_name(name) # Нахожу корзину
|
||
buckets[idx] = ll_delete(buckets[idx], name) # Удаляю из цепочки
|
||
|
||
|
||
def ht_list_all(buckets):
|
||
all_entries = []
|
||
for bucket in buckets: # Прохожу по всем корзинам
|
||
if bucket is not None:
|
||
curr = bucket
|
||
while curr: # Собираю всю цепочку
|
||
all_entries.append((curr['name'], curr['phone']))
|
||
curr = curr['next']
|
||
all_entries.sort(key=lambda x: x[0])
|
||
return all_entries
|
||
|
||
|
||
# Двоичное дерево поиска
|
||
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']: # Иду до самого левого
|
||
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):
|
||
result = []
|
||
|
||
def inorder(node): # Симметричный обход
|
||
if node:
|
||
inorder(node['left']) # Сначала левое
|
||
result.append((node['name'], node['phone'])) # Потом корень
|
||
inorder(node['right']) # Потом правое
|
||
|
||
inorder(root)
|
||
return result
|
||
|
||
|
||
# ============================================================
|
||
# TECT
|
||
# ============================================================
|
||
|
||
import os
|
||
import random
|
||
import time
|
||
import csv
|
||
import pandas as pd
|
||
import matplotlib.pyplot as plt
|
||
|
||
# ============================================================
|
||
# ПОДГОТОВКА ПАПОК
|
||
# ============================================================
|
||
|
||
DATA_DIR = os.path.join("docs", "data")
|
||
os.makedirs(DATA_DIR, exist_ok=True)
|
||
|
||
csv_path = os.path.join(DATA_DIR, "lab1_results.csv")
|
||
graph_path = os.path.join(DATA_DIR, "lab1_graph.png")
|
||
|
||
# ============================================================
|
||
# ТЕСТОВЫЕ ДАННЫЕ
|
||
# ============================================================
|
||
|
||
random.seed(42) # Фиксирую seed для повторяемости
|
||
|
||
N = 3000 # 3000 записей
|
||
|
||
base_records = [
|
||
(f"User_{i:05d}", f"123-{i:05d}")
|
||
for i in range(N)
|
||
]
|
||
|
||
records_shuffled = base_records.copy()
|
||
random.shuffle(records_shuffled) # Перемешанный порядок
|
||
|
||
records_sorted = sorted(base_records, key=lambda x: x[0]) # Отсортированный порядок
|
||
|
||
# Данные для поиска
|
||
search_existing = [
|
||
name for name, _ in random.sample(base_records, 100) # 100 существующих имен
|
||
]
|
||
|
||
search_nonexist = [
|
||
f"None_{i}"
|
||
for i in range(10) # 10 несуществующих имен
|
||
]
|
||
|
||
# Данные для удаления
|
||
delete_names = [
|
||
name for name, _ in random.sample(base_records, 50) # 50 имен для удаления
|
||
]
|
||
|
||
|
||
# ============================================================
|
||
# СОЗДАНИЕ СТРУКТУР
|
||
# ============================================================
|
||
|
||
def build_structure(records, struct_type):
|
||
if struct_type == "ll":
|
||
structure = None
|
||
for name, phone in records:
|
||
structure = ll_insert(structure, name, phone) # Последовательная вставка
|
||
return structure
|
||
|
||
elif struct_type == "ht":
|
||
structure = [None] * HASH_SIZE
|
||
for name, phone in records:
|
||
ht_insert(structure, name, phone) # Вставка с хэшированием
|
||
return structure
|
||
|
||
elif struct_type == "bst":
|
||
structure = None
|
||
for name, phone in records:
|
||
structure = bst_insert(structure, name, phone) # Вставка с ветвлением
|
||
return structure
|
||
|
||
|
||
# ============================================================
|
||
# INSERT
|
||
# ============================================================
|
||
|
||
def measure_insert(records, struct_type):
|
||
start = time.perf_counter()
|
||
build_structure(records, struct_type) # Замеряю время построения структуры
|
||
end = time.perf_counter()
|
||
return end - start
|
||
|
||
|
||
# ============================================================
|
||
# SEARCH
|
||
# ============================================================
|
||
|
||
def measure_search(records, struct_type):
|
||
structure = build_structure(records, struct_type) # Строю структуру
|
||
start = time.perf_counter()
|
||
|
||
if struct_type == "ll":
|
||
for name in search_existing + search_nonexist:
|
||
ll_find(structure, name) # Поиск перебором
|
||
elif struct_type == "ht":
|
||
for name in search_existing + search_nonexist:
|
||
ht_find(structure, name) # Поиск через хэш
|
||
elif struct_type == "bst":
|
||
for name in search_existing + search_nonexist:
|
||
bst_find(structure, name) # Поиск спуском по дереву
|
||
|
||
end = time.perf_counter()
|
||
return end - start
|
||
|
||
|
||
# ============================================================
|
||
# DELETE
|
||
# ============================================================
|
||
|
||
def measure_delete(records, struct_type):
|
||
structure = build_structure(records, struct_type) # Строю структуру
|
||
start = time.perf_counter()
|
||
|
||
if struct_type == "ll":
|
||
for name in delete_names:
|
||
structure = ll_delete(structure, name) # Удаление со сдвигом
|
||
elif struct_type == "ht":
|
||
for name in delete_names:
|
||
ht_delete(structure, name) # Удаление из цепочки
|
||
elif struct_type == "bst":
|
||
for name in delete_names:
|
||
structure = bst_delete(structure, name) # Удаление с ребалансировкой
|
||
|
||
end = time.perf_counter()
|
||
return end - start
|
||
|
||
|
||
# ============================================================
|
||
# ЗАМЕРЫ
|
||
# ============================================================
|
||
|
||
all_data = []
|
||
|
||
experiments = [
|
||
("LinkedList", "ll"),
|
||
("HashTable", "ht"),
|
||
("BST", "bst")
|
||
]
|
||
|
||
modes = [
|
||
("shuffled", records_shuffled), # Тест на случайных данных
|
||
("sorted", records_sorted) # Тест на отсортированных данных
|
||
]
|
||
|
||
for struct_name, struct_type in experiments:
|
||
for mode_name, records in modes:
|
||
for rep in range(1, 4): # 3 повтора для усреднения
|
||
insert_time = measure_insert(records, struct_type)
|
||
search_time = measure_search(records, struct_type)
|
||
delete_time = measure_delete(records, struct_type)
|
||
|
||
all_data.append([struct_name, mode_name, rep, "insert", insert_time])
|
||
all_data.append([struct_name, mode_name, rep, "search", search_time])
|
||
all_data.append([struct_name, mode_name, rep, "delete", delete_time])
|
||
|
||
# ============================================================
|
||
# CSV
|
||
# ============================================================
|
||
|
||
with open(csv_path, "w", newline="", encoding="utf-8") as f:
|
||
writer = csv.writer(f)
|
||
writer.writerow(["Структура", "Режим", "Повтор", "Операция", "Время (сек)"])
|
||
writer.writerows(all_data)
|
||
|
||
print(f"CSV сохранён: {csv_path}")
|
||
|
||
# ============================================================
|
||
# ГРАФИК
|
||
# ============================================================
|
||
|
||
df = pd.read_csv(csv_path)
|
||
|
||
df_avg = (
|
||
df.groupby(["Структура", "Режим", "Операция"])["Время (сек)"]
|
||
.mean()
|
||
.reset_index()
|
||
)
|
||
|
||
fig, ax = plt.subplots(figsize=(12, 6))
|
||
|
||
ops = ["insert", "search", "delete"]
|
||
x = range(len(ops))
|
||
width = 0.12
|
||
|
||
configs = [
|
||
("LinkedList", "shuffled"),
|
||
("LinkedList", "sorted"),
|
||
("HashTable", "shuffled"),
|
||
("HashTable", "sorted"),
|
||
("BST", "shuffled"),
|
||
("BST", "sorted")
|
||
]
|
||
|
||
for i, (struct, mode) in enumerate(configs):
|
||
subset = df_avg[
|
||
(df_avg["Структура"] == struct) &
|
||
(df_avg["Режим"] == mode)
|
||
]
|
||
times = [
|
||
subset[subset["Операция"] == op]["Время (сек)"].values[0]
|
||
for op in ops
|
||
]
|
||
ax.bar(
|
||
[p + i * width for p in x],
|
||
times,
|
||
width,
|
||
label=f"{struct} ({mode})"
|
||
)
|
||
|
||
ax.set_xticks([p + 2.5 * width for p in x])
|
||
ax.set_xticklabels(ops)
|
||
ax.set_ylabel("Среднее время (сек)")
|
||
ax.set_title("Сравнение структур данных")
|
||
ax.legend(bbox_to_anchor=(1.05, 1), loc="upper left")
|
||
|
||
plt.tight_layout()
|
||
plt.savefig(graph_path)
|
||
print(f"График сохранён: {graph_path}")
|
||
plt.show() |