forked from UNN/2026-rff_mp
377 lines
14 KiB
Python
377 lines
14 KiB
Python
|
|
import sys
|
||
|
|
|
||
|
|
sys.setrecursionlimit(10000)
|
||
|
|
|
||
|
|
import time
|
||
|
|
import random
|
||
|
|
import csv
|
||
|
|
import os
|
||
|
|
import matplotlib.pyplot as plt
|
||
|
|
import numpy as np
|
||
|
|
|
||
|
|
|
||
|
|
def ll_insert(head, name, phone):
|
||
|
|
new_node = {'name': name, 'phone': phone, 'next': None}
|
||
|
|
if head is None:
|
||
|
|
return new_node
|
||
|
|
if head['name'] > name:
|
||
|
|
new_node['next'] = head
|
||
|
|
return new_node
|
||
|
|
current = head
|
||
|
|
while current['next'] is not None and current['next']['name'] < name:
|
||
|
|
current = current['next']
|
||
|
|
if current['name'] == name:
|
||
|
|
current['phone'] = phone
|
||
|
|
return head
|
||
|
|
new_node['next'] = current['next']
|
||
|
|
current['next'] = new_node
|
||
|
|
return head
|
||
|
|
|
||
|
|
|
||
|
|
def ll_find(head, name):
|
||
|
|
current = head
|
||
|
|
while current is not None:
|
||
|
|
if current['name'] == name:
|
||
|
|
return current['phone']
|
||
|
|
current = current['next']
|
||
|
|
return None
|
||
|
|
|
||
|
|
|
||
|
|
def ll_delete(head, name):
|
||
|
|
if head is None:
|
||
|
|
return None
|
||
|
|
if head['name'] == name:
|
||
|
|
return head['next']
|
||
|
|
current = head
|
||
|
|
while current['next'] is not None:
|
||
|
|
if current['next']['name'] == name:
|
||
|
|
current['next'] = current['next']['next']
|
||
|
|
return head
|
||
|
|
current = current['next']
|
||
|
|
return head
|
||
|
|
|
||
|
|
|
||
|
|
def ll_list_all(head):
|
||
|
|
result = []
|
||
|
|
current = head
|
||
|
|
while current is not None:
|
||
|
|
result.append((current['name'], current['phone']))
|
||
|
|
current = current['next']
|
||
|
|
result.sort(key=lambda x: x[0])
|
||
|
|
return result
|
||
|
|
|
||
|
|
|
||
|
|
def ht_insert(buckets, name, phone):
|
||
|
|
index = hash(name) % len(buckets)
|
||
|
|
buckets[index] = ll_insert(buckets[index], name, phone)
|
||
|
|
|
||
|
|
|
||
|
|
def ht_find(buckets, name):
|
||
|
|
index = hash(name) % len(buckets)
|
||
|
|
return ll_find(buckets[index], name)
|
||
|
|
|
||
|
|
|
||
|
|
def ht_delete(buckets, name):
|
||
|
|
index = hash(name) % len(buckets)
|
||
|
|
buckets[index] = ll_delete(buckets[index], name)
|
||
|
|
|
||
|
|
|
||
|
|
def ht_list_all(buckets):
|
||
|
|
result = []
|
||
|
|
for bucket in buckets:
|
||
|
|
if bucket is not None:
|
||
|
|
current = bucket
|
||
|
|
while current is not None:
|
||
|
|
result.append((current['name'], current['phone']))
|
||
|
|
current = current['next']
|
||
|
|
result.sort(key=lambda x: x[0])
|
||
|
|
return result
|
||
|
|
|
||
|
|
|
||
|
|
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):
|
||
|
|
current = root
|
||
|
|
while current is not None:
|
||
|
|
if name == current['name']:
|
||
|
|
return current['phone']
|
||
|
|
elif name < current['name']:
|
||
|
|
current = current['left']
|
||
|
|
else:
|
||
|
|
current = current['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):
|
||
|
|
result = []
|
||
|
|
|
||
|
|
def inorder(node):
|
||
|
|
if node is not None:
|
||
|
|
inorder(node['left'])
|
||
|
|
result.append((node['name'], node['phone']))
|
||
|
|
inorder(node['right'])
|
||
|
|
|
||
|
|
inorder(root)
|
||
|
|
return result
|
||
|
|
|
||
|
|
|
||
|
|
def generate_records(n, mode='shuffled'):
|
||
|
|
names = [f"User_{i:05d}" for i in range(n)]
|
||
|
|
if mode == 'shuffled':
|
||
|
|
random.shuffle(names)
|
||
|
|
records = [(name, f"Phone_{i}") for i, name in enumerate(names)]
|
||
|
|
return records
|
||
|
|
|
||
|
|
|
||
|
|
def benchmark_insert(structure_type, records, buckets_size=512):
|
||
|
|
if structure_type == 'LinkedList':
|
||
|
|
head = None
|
||
|
|
start = time.perf_counter()
|
||
|
|
for name, phone in records:
|
||
|
|
head = ll_insert(head, name, phone)
|
||
|
|
end = time.perf_counter()
|
||
|
|
return end - start, head
|
||
|
|
elif structure_type == 'HashTable':
|
||
|
|
buckets = [None] * buckets_size
|
||
|
|
start = time.perf_counter()
|
||
|
|
for name, phone in records:
|
||
|
|
ht_insert(buckets, name, phone)
|
||
|
|
end = time.perf_counter()
|
||
|
|
return end - start, buckets
|
||
|
|
elif structure_type == 'BST':
|
||
|
|
root = None
|
||
|
|
start = time.perf_counter()
|
||
|
|
for name, phone in records:
|
||
|
|
root = bst_insert(root, name, phone)
|
||
|
|
end = time.perf_counter()
|
||
|
|
return end - start, root
|
||
|
|
|
||
|
|
|
||
|
|
def benchmark_find(structure_type, structure, names):
|
||
|
|
start = time.perf_counter()
|
||
|
|
if structure_type == 'LinkedList':
|
||
|
|
for name in names:
|
||
|
|
ll_find(structure, name)
|
||
|
|
elif structure_type == 'HashTable':
|
||
|
|
for name in names:
|
||
|
|
ht_find(structure, name)
|
||
|
|
elif structure_type == 'BST':
|
||
|
|
for name in names:
|
||
|
|
bst_find(structure, name)
|
||
|
|
end = time.perf_counter()
|
||
|
|
return end - start
|
||
|
|
|
||
|
|
|
||
|
|
def benchmark_delete(structure_type, structure, names):
|
||
|
|
start = time.perf_counter()
|
||
|
|
if structure_type == 'LinkedList':
|
||
|
|
head = structure
|
||
|
|
for name in names:
|
||
|
|
head = ll_delete(head, name)
|
||
|
|
end = time.perf_counter()
|
||
|
|
return end - start, head
|
||
|
|
elif structure_type == 'HashTable':
|
||
|
|
buckets = structure
|
||
|
|
for name in names:
|
||
|
|
ht_delete(buckets, name)
|
||
|
|
end = time.perf_counter()
|
||
|
|
return end - start, buckets
|
||
|
|
elif structure_type == 'BST':
|
||
|
|
root = structure
|
||
|
|
for name in names:
|
||
|
|
root = bst_delete(root, name)
|
||
|
|
end = time.perf_counter()
|
||
|
|
return end - start, root
|
||
|
|
|
||
|
|
|
||
|
|
def benchmark_list_all(structure_type, structure):
|
||
|
|
start = time.perf_counter()
|
||
|
|
if structure_type == 'LinkedList':
|
||
|
|
ll_list_all(structure)
|
||
|
|
elif structure_type == 'HashTable':
|
||
|
|
ht_list_all(structure)
|
||
|
|
elif structure_type == 'BST':
|
||
|
|
bst_list_all(structure)
|
||
|
|
end = time.perf_counter()
|
||
|
|
return end - start
|
||
|
|
|
||
|
|
|
||
|
|
def main():
|
||
|
|
random.seed(42)
|
||
|
|
N = 1000
|
||
|
|
REPETITIONS = 5
|
||
|
|
|
||
|
|
all_results = []
|
||
|
|
structures = ['LinkedList', 'HashTable', 'BST']
|
||
|
|
modes = ['shuffled', 'sorted']
|
||
|
|
|
||
|
|
os.makedirs('docs/data', exist_ok=True)
|
||
|
|
|
||
|
|
for mode in modes:
|
||
|
|
records = generate_records(N, mode)
|
||
|
|
find_names = [random.choice(records)[0] for _ in range(100)] + [f"None_{i}" for i in range(10)]
|
||
|
|
delete_names = [random.choice(records)[0] for _ in range(50)]
|
||
|
|
|
||
|
|
for struct in structures:
|
||
|
|
print(f"Тестирую {struct} на {mode} данных...")
|
||
|
|
for rep in range(REPETITIONS):
|
||
|
|
insert_time, structure = benchmark_insert(struct, records)
|
||
|
|
all_results.append([struct, mode, 'insert', insert_time, rep + 1])
|
||
|
|
|
||
|
|
find_time = benchmark_find(struct, structure, find_names)
|
||
|
|
all_results.append([struct, mode, 'find', find_time, rep + 1])
|
||
|
|
|
||
|
|
delete_time, _ = benchmark_delete(struct, structure, delete_names)
|
||
|
|
all_results.append([struct, mode, 'delete', delete_time, rep + 1])
|
||
|
|
|
||
|
|
with open('docs/data/results.csv', 'w', newline='') as f:
|
||
|
|
writer = csv.writer(f)
|
||
|
|
writer.writerow(['structure', 'order', 'operation', 'time', 'run'])
|
||
|
|
writer.writerows(all_results)
|
||
|
|
|
||
|
|
print("\nCSV сохранён в docs/data/results.csv")
|
||
|
|
|
||
|
|
data = {}
|
||
|
|
for row in all_results:
|
||
|
|
key = (row[0], row[1], row[2])
|
||
|
|
if key not in data:
|
||
|
|
data[key] = []
|
||
|
|
data[key].append(row[3])
|
||
|
|
|
||
|
|
averaged = []
|
||
|
|
for key, times in data.items():
|
||
|
|
avg_time = sum(times) / len(times)
|
||
|
|
averaged.append([key[0], key[1], key[2], avg_time])
|
||
|
|
|
||
|
|
print("\nУСРЕДНЁННЫЕ РЕЗУЛЬТАТЫ (сек):")
|
||
|
|
print(f"{'Структура':<15} {'Режим':<15} {'Операция':<10} {'Среднее время':<15}")
|
||
|
|
print("-" * 55)
|
||
|
|
for row in sorted(averaged, key=lambda x: (x[0], x[1], x[2])):
|
||
|
|
print(f"{row[0]:<15} {row[1]:<15} {row[2]:<10} {row[3]:<15.6f}")
|
||
|
|
|
||
|
|
structures_list = ['LinkedList', 'HashTable', 'BST']
|
||
|
|
operations_list = ['insert', 'find', 'delete']
|
||
|
|
|
||
|
|
fig, axes = plt.subplots(1, 3, figsize=(18, 6))
|
||
|
|
|
||
|
|
for i, op in enumerate(operations_list):
|
||
|
|
ax = axes[i]
|
||
|
|
x = np.arange(len(structures_list))
|
||
|
|
width = 0.35
|
||
|
|
|
||
|
|
shuffled_times = []
|
||
|
|
sorted_times = []
|
||
|
|
|
||
|
|
for struct in structures_list:
|
||
|
|
shuffled_times.append(
|
||
|
|
next(row[3] for row in averaged if row[0] == struct and row[1] == 'shuffled' and row[2] == op))
|
||
|
|
sorted_times.append(
|
||
|
|
next(row[3] for row in averaged if row[0] == struct and row[1] == 'sorted' and row[2] == op))
|
||
|
|
|
||
|
|
bars1 = ax.bar(x - width / 2, shuffled_times, width, label='Случайный', color='#3498db')
|
||
|
|
bars2 = ax.bar(x + width / 2, sorted_times, width, label='Отсортированный', color='#e74c3c')
|
||
|
|
|
||
|
|
for bar in bars1:
|
||
|
|
height = bar.get_height()
|
||
|
|
ax.text(bar.get_x() + bar.get_width() / 2., height + max(shuffled_times) * 0.01,
|
||
|
|
f'{height:.6f}', ha='center', va='bottom', fontsize=7)
|
||
|
|
for bar in bars2:
|
||
|
|
height = bar.get_height()
|
||
|
|
ax.text(bar.get_x() + bar.get_width() / 2., height + max(sorted_times) * 0.01,
|
||
|
|
f'{height:.6f}', ha='center', va='bottom', fontsize=7)
|
||
|
|
|
||
|
|
ax.set_xlabel('Структура данных')
|
||
|
|
ax.set_ylabel('Время (сек)')
|
||
|
|
ax.set_title(f'Операция: {op}')
|
||
|
|
ax.set_xticks(x)
|
||
|
|
ax.set_xticklabels(structures_list)
|
||
|
|
ax.legend()
|
||
|
|
ax.grid(axis='y', alpha=0.3)
|
||
|
|
|
||
|
|
if op == 'find':
|
||
|
|
ax.set_yscale('log')
|
||
|
|
|
||
|
|
plt.suptitle(f'Сравнение производительности структур данных (N = {N} записей)',
|
||
|
|
fontsize=14, fontweight='bold')
|
||
|
|
plt.tight_layout()
|
||
|
|
|
||
|
|
os.makedirs('docs', exist_ok=True)
|
||
|
|
plt.savefig('docs/benchmark_results.png', dpi=150, bbox_inches='tight')
|
||
|
|
plt.show()
|
||
|
|
print(f"График сохранён в docs/benchmark_results.png")
|
||
|
|
|
||
|
|
print("\nАНАЛИЗ РЕЗУЛЬТАТОВ")
|
||
|
|
|
||
|
|
print("\n1. Влияние порядка данных на BST:")
|
||
|
|
bst_shuffled_insert = next(
|
||
|
|
row[3] for row in averaged if row[0] == 'BST' and row[1] == 'shuffled' and row[2] == 'insert')
|
||
|
|
bst_sorted_insert = next(
|
||
|
|
row[3] for row in averaged if row[0] == 'BST' and row[1] == 'sorted' and row[2] == 'insert')
|
||
|
|
print(f" - Случайные данные: {bst_shuffled_insert:.6f} сек")
|
||
|
|
print(f" - Отсортированные данные: {bst_sorted_insert:.6f} сек")
|
||
|
|
if bst_shuffled_insert > 0:
|
||
|
|
print(f" - Замедление в {bst_sorted_insert / bst_shuffled_insert:.1f} раз")
|
||
|
|
print(" Причина: на отсортированных данных BST вырождается в связный список (глубина = N)")
|
||
|
|
|
||
|
|
print("\n2. Стабильность хеш-таблицы:")
|
||
|
|
ht_shuffled = next(
|
||
|
|
row[3] for row in averaged if row[0] == 'HashTable' and row[1] == 'shuffled' and row[2] == 'insert')
|
||
|
|
ht_sorted = next(row[3] for row in averaged if row[0] == 'HashTable' and row[1] == 'sorted' and row[2] == 'insert')
|
||
|
|
print(f" - Случайные: {ht_shuffled:.6f} сек")
|
||
|
|
print(f" - Отсортированные: {ht_sorted:.6f} сек")
|
||
|
|
print(" Причина: хеш-функция равномерно распределяет ключи независимо от порядка")
|
||
|
|
|
||
|
|
print("\n3. Медленный поиск в связном списке:")
|
||
|
|
ll_search = next(row[3] for row in averaged if row[0] == 'LinkedList' and row[1] == 'shuffled' and row[2] == 'find')
|
||
|
|
ht_search = next(row[3] for row in averaged if row[0] == 'HashTable' and row[1] == 'shuffled' and row[2] == 'find')
|
||
|
|
print(f" - LinkedList: {ll_search:.6f} сек")
|
||
|
|
print(f" - HashTable: {ht_search:.6f} сек")
|
||
|
|
if ht_search > 0:
|
||
|
|
print(f" - Хеш-таблица быстрее в {ll_search / ht_search:.1f} раз")
|
||
|
|
print(" Причина: поиск в списке всегда O(n), в хеш-таблице ~O(1)")
|
||
|
|
|
||
|
|
print("\n4. Удаление:")
|
||
|
|
for label in ['LinkedList', 'HashTable', 'BST']:
|
||
|
|
del_shuff = next(row[3] for row in averaged if row[0] == label and row[1] == 'shuffled' and row[2] == 'delete')
|
||
|
|
del_sort = next(row[3] for row in averaged if row[0] == label and row[1] == 'sorted' and row[2] == 'delete')
|
||
|
|
print(f" - {label:15}: случ.={del_shuff:.6f} сек, отсорт.={del_sort:.6f} сек")
|
||
|
|
|
||
|
|
print("\n5. Рекомендации:")
|
||
|
|
print(" - Частый поиск + вставки → Хеш-таблица")
|
||
|
|
print(" - Нужна сортировка «из коробки» → Сбалансированное BST (AVL/Красно-чёрное)")
|
||
|
|
print(" - Только добавление в конец → Связный список")
|
||
|
|
print(" - Обычный BST опасен на реальных частично упорядоченных данных!")
|
||
|
|
|
||
|
|
if __name__ == '__main__':
|
||
|
|
main()
|