2026-05-16 17:01:46 +00:00
|
|
|
|
import random
|
|
|
|
|
|
import time
|
|
|
|
|
|
import csv
|
|
|
|
|
|
import os
|
|
|
|
|
|
import traceback
|
|
|
|
|
|
import sys
|
|
|
|
|
|
|
|
|
|
|
|
sys.setrecursionlimit(30000)
|
|
|
|
|
|
|
2026-05-03 16:07:02 +00:00
|
|
|
|
def ll_insert(head, name, phone):
|
|
|
|
|
|
|
2026-05-10 15:20:20 +00:00
|
|
|
|
"""
|
|
|
|
|
|
проходит до конца (или сразу добавляет в конец)
|
|
|
|
|
|
возвращает новую голову (если вставка в начало) или изменяет список по ссылке.
|
|
|
|
|
|
"""
|
2026-05-03 16:07:02 +00:00
|
|
|
|
new_node = {'name': name, 'phone': phone, 'next': None}
|
2026-05-10 15:20:20 +00:00
|
|
|
|
|
|
|
|
|
|
# в случае пустого списка, новый узел становится головой
|
|
|
|
|
|
if head is None:
|
|
|
|
|
|
return new_node
|
|
|
|
|
|
|
|
|
|
|
|
# в противном случае проходим в конец списка
|
|
|
|
|
|
current = head
|
|
|
|
|
|
while current['next'] is not None:
|
|
|
|
|
|
if current['name'] == name:
|
|
|
|
|
|
current['phone'] = phone
|
|
|
|
|
|
return head
|
|
|
|
|
|
current = current['next']
|
|
|
|
|
|
|
|
|
|
|
|
# проверяем последний узел
|
|
|
|
|
|
if current['name'] == name:
|
|
|
|
|
|
current['phone'] = phone
|
|
|
|
|
|
else:
|
|
|
|
|
|
current['next'] = new_node
|
|
|
|
|
|
|
|
|
|
|
|
return head
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def ll_find(head, name):
|
|
|
|
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
|
ищет узел по имени.
|
|
|
|
|
|
возвращает телефон или None
|
|
|
|
|
|
"""
|
|
|
|
|
|
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:
|
|
|
|
|
|
new_head = head['next']
|
|
|
|
|
|
head['next'] = None
|
|
|
|
|
|
return new_head
|
|
|
|
|
|
|
|
|
|
|
|
# ищем узел для удаления
|
|
|
|
|
|
current = head
|
|
|
|
|
|
while current['next'] is not None:
|
|
|
|
|
|
if current['next']['name'] == name:
|
|
|
|
|
|
target = current['next']
|
|
|
|
|
|
current['next'] = target['next']
|
|
|
|
|
|
target['next'] = None # разрываем связь у удаляемого узла (иными словами, обнуление ссылки)
|
|
|
|
|
|
return head
|
|
|
|
|
|
current = current['next']
|
|
|
|
|
|
|
|
|
|
|
|
return head
|
|
|
|
|
|
|
2026-05-10 18:03:20 +00:00
|
|
|
|
def ll_collect(head, result_list):
|
|
|
|
|
|
"""собирает все данные из связного списка в result_list"""
|
|
|
|
|
|
current = head
|
|
|
|
|
|
while current is not None:
|
|
|
|
|
|
result_list.append((current['name'], current['phone']))
|
|
|
|
|
|
current = current['next']
|
|
|
|
|
|
|
2026-05-10 15:20:20 +00:00
|
|
|
|
|
|
|
|
|
|
def ll_list_all(head):
|
|
|
|
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
|
собирает все записи в список и сортирует
|
|
|
|
|
|
"""
|
|
|
|
|
|
result = []
|
|
|
|
|
|
current = head
|
|
|
|
|
|
|
|
|
|
|
|
while current is not None:
|
|
|
|
|
|
result.append((current['name'], current['phone']))
|
|
|
|
|
|
current = current['next']
|
|
|
|
|
|
|
|
|
|
|
|
# ручная сортировка пузырьком
|
|
|
|
|
|
n = len(result)
|
|
|
|
|
|
for i in range(n):
|
|
|
|
|
|
for j in range(0, n - i - 1):
|
|
|
|
|
|
if result[j][0] > result[j + 1][0]:
|
|
|
|
|
|
result[j], result[j + 1] = result[j + 1], result[j]
|
|
|
|
|
|
|
2026-05-10 17:51:53 +00:00
|
|
|
|
return result
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def hash_table(size):
|
|
|
|
|
|
"""создание хеш-таблицы"""
|
|
|
|
|
|
return [None] * size
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def hash_func(name, buckets_count):
|
|
|
|
|
|
"""
|
|
|
|
|
|
использует умножение на простое число для лучшего распределения
|
|
|
|
|
|
"""
|
|
|
|
|
|
h = 0
|
|
|
|
|
|
multiplier = 1
|
|
|
|
|
|
for char in name:
|
|
|
|
|
|
h = (h + ord(char) * multiplier) % buckets_count
|
|
|
|
|
|
multiplier = (multiplier * 31) % buckets_count
|
|
|
|
|
|
return h
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def ht_insert(buckets, name, phone):
|
|
|
|
|
|
"""добавить или обновить запись"""
|
|
|
|
|
|
if buckets is None:
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
index = hash_func(name, len(buckets))
|
|
|
|
|
|
buckets[index] = ll_insert(buckets[index], name, phone)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def ht_find(buckets, name):
|
|
|
|
|
|
"""найти телефон по имени"""
|
|
|
|
|
|
idx = hash_func(name, len(buckets))
|
|
|
|
|
|
return ll_find(buckets[idx], name)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def ht_delete(buckets, name):
|
|
|
|
|
|
"""
|
|
|
|
|
|
удалить запись
|
|
|
|
|
|
"""
|
|
|
|
|
|
idx = hash_func(name, len(buckets))
|
|
|
|
|
|
buckets[idx] = ll_delete(buckets[idx], name)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def bubble_sort(records):
|
|
|
|
|
|
"""пузырьковая сортировка"""
|
|
|
|
|
|
n = len(records)
|
|
|
|
|
|
for i in range(n - 1):
|
|
|
|
|
|
swapped = False
|
|
|
|
|
|
for j in range(n - 1 - i):
|
|
|
|
|
|
if records[j][0] > records[j + 1][0]:
|
|
|
|
|
|
records[j], records[j + 1] = records[j + 1], records[j]
|
|
|
|
|
|
swapped = True
|
|
|
|
|
|
if not swapped:
|
|
|
|
|
|
break
|
|
|
|
|
|
return records
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def ht_list_all(buckets):
|
|
|
|
|
|
"""
|
|
|
|
|
|
собрание всех записей и сортировка
|
|
|
|
|
|
"""
|
|
|
|
|
|
# Собираем все записи
|
|
|
|
|
|
full_data = []
|
|
|
|
|
|
for head in buckets:
|
|
|
|
|
|
ll_collect(head, full_data)
|
|
|
|
|
|
|
|
|
|
|
|
# Сортируем пузырьком
|
|
|
|
|
|
bubble_sort(full_data)
|
|
|
|
|
|
return full_data
|
2026-05-10 18:03:20 +00:00
|
|
|
|
|
2026-05-10 18:16:48 +00:00
|
|
|
|
#Hash_table1 = hash_table(3)
|
2026-05-10 18:03:20 +00:00
|
|
|
|
|
2026-05-10 18:16:48 +00:00
|
|
|
|
#ht_insert(Hash_table1, 'Alena', '010')
|
|
|
|
|
|
#ht_insert(Hash_table1, 'Helena', '111')
|
|
|
|
|
|
#ht_insert(Hash_table1, 'Gena', '222')
|
2026-05-10 18:03:20 +00:00
|
|
|
|
|
|
|
|
|
|
|
2026-05-10 18:16:48 +00:00
|
|
|
|
#print(ht_list_all(Hash_table1))
|
|
|
|
|
|
|
|
|
|
|
|
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):
|
|
|
|
|
|
"""
|
|
|
|
|
|
рекурсивный поиск телефона по имени
|
|
|
|
|
|
возвращает phone или None
|
|
|
|
|
|
"""
|
|
|
|
|
|
# не нашли
|
|
|
|
|
|
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 bst_find_min(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 and root['right'] is None:
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
elif root['left'] is None:
|
|
|
|
|
|
return root['right']
|
|
|
|
|
|
elif root['right'] is None:
|
|
|
|
|
|
return root['left']
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# находим минимальный элемент в правом поддереве
|
|
|
|
|
|
successor = bst_find_min(root['right'])
|
|
|
|
|
|
|
|
|
|
|
|
# копируем данные из преемника в текущий узел
|
|
|
|
|
|
root['name'] = successor['name']
|
|
|
|
|
|
root['phone'] = successor['phone']
|
|
|
|
|
|
|
|
|
|
|
|
# удаляем преемника
|
|
|
|
|
|
root['right'] = bst_delete(root['right'], successor['name'])
|
|
|
|
|
|
|
|
|
|
|
|
return root
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def bst_list_all(root, result=None):
|
|
|
|
|
|
"""
|
|
|
|
|
|
центрированный (in-order) обход дерева
|
|
|
|
|
|
рекурсивно собирает записи в отсортированном порядке
|
|
|
|
|
|
"""
|
|
|
|
|
|
if result is None:
|
|
|
|
|
|
result = []
|
|
|
|
|
|
|
|
|
|
|
|
if root is not None:
|
|
|
|
|
|
# сначала обходим левое поддерево (все меньшие ключи)
|
|
|
|
|
|
bst_list_all(root['left'], result)
|
|
|
|
|
|
|
|
|
|
|
|
# затем текущий узел
|
|
|
|
|
|
result.append((root['name'], root['phone']))
|
|
|
|
|
|
|
|
|
|
|
|
# затем правое поддерево (все большие ключи)
|
|
|
|
|
|
bst_list_all(root['right'], result)
|
|
|
|
|
|
|
2026-05-16 17:01:46 +00:00
|
|
|
|
return result
|
|
|
|
|
|
|
|
|
|
|
|
def generate_records(count=10000):
|
|
|
|
|
|
"""
|
|
|
|
|
|
генерация тестовых данных
|
|
|
|
|
|
70% уникальных имён, 30% повторяющихся (для коллизий)
|
|
|
|
|
|
"""
|
|
|
|
|
|
records = []
|
|
|
|
|
|
base_names = ["Алексей", "Борис", "Владимир", "Дмитрий", "Елена",
|
|
|
|
|
|
"Иван", "Мария", "Николай", "Ольга", "Павел"]
|
|
|
|
|
|
|
|
|
|
|
|
for i in range(count):
|
|
|
|
|
|
if random.random() < 0.7:
|
|
|
|
|
|
name = f"User_{i:05d}"
|
|
|
|
|
|
else:
|
|
|
|
|
|
name = random.choice(base_names) + f"_{random.randint(1, 100)}"
|
|
|
|
|
|
|
|
|
|
|
|
phone = f"+7-{random.randint(100,999)}-{random.randint(100,999)}-{random.randint(1000,9999)}"
|
|
|
|
|
|
records.append((name, phone))
|
|
|
|
|
|
|
|
|
|
|
|
shuffled = records.copy()
|
|
|
|
|
|
random.shuffle(shuffled)
|
|
|
|
|
|
sorted_records = sorted(records, key=lambda x: x[0])
|
|
|
|
|
|
|
|
|
|
|
|
return shuffled, sorted_records
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def measure_insertion(structure_name, records):
|
|
|
|
|
|
"""
|
|
|
|
|
|
замер времени вставки
|
|
|
|
|
|
возвращает список замеров и заполненную структуру
|
|
|
|
|
|
"""
|
|
|
|
|
|
times = []
|
|
|
|
|
|
filled_structure = None
|
|
|
|
|
|
|
|
|
|
|
|
for run in range(5):
|
|
|
|
|
|
if structure_name == "linked_list":
|
|
|
|
|
|
structure = None
|
|
|
|
|
|
elif structure_name == "hash_table":
|
|
|
|
|
|
structure = hash_table(2003)
|
|
|
|
|
|
elif structure_name == "bst":
|
|
|
|
|
|
structure = None
|
|
|
|
|
|
|
|
|
|
|
|
start = time.perf_counter()
|
|
|
|
|
|
|
|
|
|
|
|
for name, phone in records:
|
|
|
|
|
|
if structure_name == "linked_list":
|
|
|
|
|
|
structure = ll_insert(structure, name, phone)
|
|
|
|
|
|
elif structure_name == "hash_table":
|
|
|
|
|
|
ht_insert(structure, name, phone)
|
|
|
|
|
|
elif structure_name == "bst":
|
|
|
|
|
|
structure = bst_insert(structure, name, phone)
|
|
|
|
|
|
|
|
|
|
|
|
end = time.perf_counter()
|
|
|
|
|
|
times.append(end - start)
|
|
|
|
|
|
|
|
|
|
|
|
if run == 4: # Сохраняем после последнего замера
|
|
|
|
|
|
filled_structure = structure
|
|
|
|
|
|
|
|
|
|
|
|
return times, filled_structure
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def measure_search(structure_name, structure, search_names):
|
|
|
|
|
|
"""
|
|
|
|
|
|
замер времени поиска
|
|
|
|
|
|
возвращает список замеров
|
|
|
|
|
|
"""
|
|
|
|
|
|
times = []
|
|
|
|
|
|
|
|
|
|
|
|
for run in range(5):
|
|
|
|
|
|
start = time.perf_counter()
|
|
|
|
|
|
|
|
|
|
|
|
for name in search_names:
|
|
|
|
|
|
if structure_name == "linked_list":
|
|
|
|
|
|
ll_find(structure, name)
|
|
|
|
|
|
elif structure_name == "hash_table":
|
|
|
|
|
|
ht_find(structure, name)
|
|
|
|
|
|
elif structure_name == "bst":
|
|
|
|
|
|
bst_find(structure, name)
|
|
|
|
|
|
|
|
|
|
|
|
end = time.perf_counter()
|
|
|
|
|
|
times.append(end - start)
|
|
|
|
|
|
|
|
|
|
|
|
return times
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def measure_deletion(structure_name, original_structure, delete_names):
|
|
|
|
|
|
"""
|
|
|
|
|
|
замер времени удаления
|
|
|
|
|
|
возвращает список замеров
|
|
|
|
|
|
"""
|
|
|
|
|
|
times = []
|
|
|
|
|
|
|
|
|
|
|
|
for run in range(5):
|
|
|
|
|
|
# создаём копию структуры
|
|
|
|
|
|
if structure_name == "linked_list":
|
|
|
|
|
|
all_records = ll_list_all(original_structure)
|
|
|
|
|
|
test_structure = None
|
|
|
|
|
|
for name, phone in all_records:
|
|
|
|
|
|
test_structure = ll_insert(test_structure, name, phone)
|
|
|
|
|
|
|
|
|
|
|
|
elif structure_name == "hash_table":
|
|
|
|
|
|
all_records = ht_list_all(original_structure)
|
|
|
|
|
|
test_structure = hash_table(2003)
|
|
|
|
|
|
for name, phone in all_records:
|
|
|
|
|
|
ht_insert(test_structure, name, phone)
|
|
|
|
|
|
|
|
|
|
|
|
elif structure_name == "bst":
|
|
|
|
|
|
all_records = bst_list_all(original_structure)
|
|
|
|
|
|
test_structure = None
|
|
|
|
|
|
for name, phone in all_records:
|
|
|
|
|
|
test_structure = bst_insert(test_structure, name, phone)
|
|
|
|
|
|
|
|
|
|
|
|
start = time.perf_counter()
|
|
|
|
|
|
|
|
|
|
|
|
for name in delete_names:
|
|
|
|
|
|
if structure_name == "linked_list":
|
|
|
|
|
|
test_structure = ll_delete(test_structure, name)
|
|
|
|
|
|
elif structure_name == "hash_table":
|
|
|
|
|
|
ht_delete(test_structure, name)
|
|
|
|
|
|
elif structure_name == "bst":
|
|
|
|
|
|
test_structure = bst_delete(test_structure, name)
|
|
|
|
|
|
|
|
|
|
|
|
end = time.perf_counter()
|
|
|
|
|
|
times.append(end - start)
|
|
|
|
|
|
|
|
|
|
|
|
return times
|
|
|
|
|
|
|
|
|
|
|
|
print(f"Текущая рабочая директория: {os.getcwd()}")
|
|
|
|
|
|
print(f"Путь к файлу: {os.path.abspath(__file__)}")
|
|
|
|
|
|
|
|
|
|
|
|
def run_experiment():
|
|
|
|
|
|
"""
|
|
|
|
|
|
запуск всех экспериментов и сохранение результатов
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
current_dir = os.path.dirname(__file__)
|
|
|
|
|
|
docs_dir = os.path.dirname(current_dir)
|
|
|
|
|
|
csv_file = os.path.join(docs_dir, "experiment_results.csv")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
os.makedirs(docs_dir, exist_ok=True)
|
|
|
|
|
|
|
|
|
|
|
|
print("ЭКСПЕРИМЕНТАЛЬНОЕ СРАВНЕНИЕ СТРУКТУР ДАННЫХ")
|
|
|
|
|
|
print("Телефонный справочник - 10000 записей")
|
|
|
|
|
|
print(f"\nРезультаты будут сохранены в: {csv_file}")
|
|
|
|
|
|
|
|
|
|
|
|
# генерация данных
|
|
|
|
|
|
print("\n1. Генерация тестовых данных...")
|
|
|
|
|
|
shuffled_records, sorted_records = generate_records(10000)
|
|
|
|
|
|
print(f"Сгенерировано 10000 записей")
|
|
|
|
|
|
print(f"Уникальных имён: {len(set([r[0] for r in shuffled_records]))}")
|
|
|
|
|
|
|
|
|
|
|
|
# подготовка имён для поиска и удаления
|
|
|
|
|
|
random.seed(42)
|
|
|
|
|
|
existing_names = [shuffled_records[i][0] for i in random.sample(range(10000), 100)]
|
|
|
|
|
|
nonexisting_names = [f"NotExist_{i}" for i in range(10)]
|
|
|
|
|
|
search_names = existing_names + nonexisting_names
|
|
|
|
|
|
delete_names = [shuffled_records[i][0] for i in random.sample(range(10000), 50)]
|
|
|
|
|
|
|
|
|
|
|
|
results = [["Структура", "Режим", "Операция",
|
|
|
|
|
|
"Замер1(с)", "Замер2(с)", "Замер3(с)", "Замер4(с)", "Замер5(с)",
|
|
|
|
|
|
"Среднее(с)"]]
|
|
|
|
|
|
|
|
|
|
|
|
# тестирование для каждого режима
|
|
|
|
|
|
for mode_name, records in [("случайный", shuffled_records),
|
|
|
|
|
|
("отсортированный", sorted_records)]:
|
|
|
|
|
|
|
|
|
|
|
|
print(f"\n2. Тестирование режима: {mode_name}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
for struct_name in ["linked_list", "hash_table", "bst"]:
|
|
|
|
|
|
print(f"\n {struct_name.upper()}:")
|
|
|
|
|
|
|
|
|
|
|
|
# вставка
|
|
|
|
|
|
print("Вставка 10000 записей...")
|
|
|
|
|
|
insert_times, filled_struct = measure_insertion(struct_name, records)
|
|
|
|
|
|
avg_insert = sum(insert_times) / 5
|
|
|
|
|
|
print(f"Время: {avg_insert:.4f} сек (среднее)")
|
|
|
|
|
|
|
|
|
|
|
|
# поиск
|
|
|
|
|
|
print("Поиск 110 записей (100 существующих + 10 которых нет)...")
|
|
|
|
|
|
search_times = measure_search(struct_name, filled_struct, search_names)
|
|
|
|
|
|
avg_search = sum(search_times) / 5
|
|
|
|
|
|
print(f"Время: {avg_search:.4f} сек (среднее)")
|
|
|
|
|
|
|
|
|
|
|
|
# удаление
|
|
|
|
|
|
print("Удаление 50 случайных записей...")
|
|
|
|
|
|
delete_times = measure_deletion(struct_name, filled_struct, delete_names)
|
|
|
|
|
|
avg_delete = sum(delete_times) / 5
|
|
|
|
|
|
print(f"Время: {avg_delete:.4f} сек (среднее)")
|
|
|
|
|
|
|
|
|
|
|
|
# сохраняем результаты
|
|
|
|
|
|
results.append([struct_name, mode_name, "вставка"] + insert_times + [avg_insert])
|
|
|
|
|
|
results.append([struct_name, mode_name, "поиск"] + search_times + [avg_search])
|
|
|
|
|
|
results.append([struct_name, mode_name, "удаление"] + delete_times + [avg_delete])
|
|
|
|
|
|
|
|
|
|
|
|
# сохранение CSV
|
|
|
|
|
|
print("\n3. Сохранение результатов...")
|
|
|
|
|
|
try:
|
|
|
|
|
|
with open(csv_file, "w", newline="", encoding="utf-8-sig") as f:
|
|
|
|
|
|
writer = csv.writer(f, delimiter=';')
|
|
|
|
|
|
writer.writerows(results)
|
|
|
|
|
|
print(f"Результаты сохранены в: {csv_file}")
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
print(f"Ошибка сохранения: {e}")
|
|
|
|
|
|
|
|
|
|
|
|
# вывод табл.
|
|
|
|
|
|
print("СВОДНАЯ ТАБЛИЦА РЕЗУЛЬТАТОВ")
|
|
|
|
|
|
print(f"{'Структура':<15} {'Режим':<12} {'Операция':<10} {'Среднее время (сек)':<20}")
|
|
|
|
|
|
|
|
|
|
|
|
for row in results[1:]:
|
|
|
|
|
|
struct, mode, op, t1, t2, t3, t4, t5, avg = row
|
|
|
|
|
|
print(f"{struct:<15} {mode:<12} {op:<10} {avg:<20.6f}")
|
|
|
|
|
|
|
|
|
|
|
|
return results
|
|
|
|
|
|
|
|
|
|
|
|
def create_report_table(results):
|
|
|
|
|
|
"""Создание сводной таблицы"""
|
|
|
|
|
|
|
|
|
|
|
|
print("СВОДНАЯ ТАБЛИЦА (среднее время в секундах)")
|
|
|
|
|
|
|
|
|
|
|
|
print(f"{'Структура':<12} {'Режим':<12} {'Вставка':<12} {'Поиск':<12} {'Удаление':<12}")
|
|
|
|
|
|
|
|
|
|
|
|
summary = {}
|
|
|
|
|
|
for row in results[1:]:
|
|
|
|
|
|
struct, mode, op, _, _, _, _, _, avg = row
|
|
|
|
|
|
key = (struct, mode)
|
|
|
|
|
|
if key not in summary:
|
|
|
|
|
|
summary[key] = {}
|
|
|
|
|
|
summary[key][op] = avg
|
|
|
|
|
|
|
|
|
|
|
|
names = {'linked_list': 'LinkedList', 'hash_table': 'HashTable', 'bst': 'BST'}
|
|
|
|
|
|
for (struct, mode), ops in summary.items():
|
|
|
|
|
|
print(f"{names[struct]:<12} {mode:<12} {ops.get('вставка', 0):<12.6f} {ops.get('поиск', 0):<12.6f} {ops.get('удаление', 0):<12.6f}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def print_analysis():
|
|
|
|
|
|
"""вывод краткого анализа"""
|
|
|
|
|
|
|
|
|
|
|
|
print("АНАЛИЗ РЕЗУЛЬТАТОВ")
|
|
|
|
|
|
|
|
|
|
|
|
print("""
|
|
|
|
|
|
1. Влияние порядка данных на BST:
|
|
|
|
|
|
- На случайных данных: быстро O(log n)
|
|
|
|
|
|
- На отсортированных: деградация до O(n) (дерево вырождается в список)
|
|
|
|
|
|
|
|
|
|
|
|
2. Хеш-таблица не чувствительна к порядку:
|
|
|
|
|
|
- Хеш-функция случайно распределяет данные по bucket'ам
|
|
|
|
|
|
- Порядок вставки не влияет на время операций
|
|
|
|
|
|
|
|
|
|
|
|
3. Связный список всегда медленен при поиске:
|
|
|
|
|
|
- Поиск требует последовательного прохода O(n)
|
|
|
|
|
|
- Нет индексов или сортировки для ускорения
|
|
|
|
|
|
|
|
|
|
|
|
4. Сравнение удаления:
|
|
|
|
|
|
- Связный список: O(n) — нужен поиск элемента
|
|
|
|
|
|
- Хеш-таблица: O(1) — прямой доступ по индексу
|
|
|
|
|
|
- BST: O(log n) в среднем, O(n) на отсортированных
|
|
|
|
|
|
|
|
|
|
|
|
5. Рекомендация для реальных задач:
|
|
|
|
|
|
- Хеш-таблица: частый поиск, словари, кэши
|
|
|
|
|
|
- BST (сбалансированный): нужны отсортированные данные
|
|
|
|
|
|
- Связный список: маленькие объёмы, очереди/стеки
|
|
|
|
|
|
- Для телефонного справочника ЛУЧШЕ: ХЕШ-ТАБЛИЦА
|
|
|
|
|
|
""")
|
|
|
|
|
|
|
|
|
|
|
|
def create_graphs(results):
|
|
|
|
|
|
"""Построение столбчатых диаграмм"""
|
|
|
|
|
|
import matplotlib.pyplot as plt
|
|
|
|
|
|
import numpy as np
|
|
|
|
|
|
|
|
|
|
|
|
data = {}
|
|
|
|
|
|
for row in results[1:]:
|
|
|
|
|
|
struct = row[0]
|
|
|
|
|
|
mode = row[1]
|
|
|
|
|
|
op = row[2]
|
|
|
|
|
|
avg = row[8]
|
|
|
|
|
|
|
|
|
|
|
|
if struct not in data:
|
|
|
|
|
|
data[struct] = {}
|
|
|
|
|
|
if mode not in data[struct]:
|
|
|
|
|
|
data[struct][mode] = {}
|
|
|
|
|
|
data[struct][mode][op] = avg
|
|
|
|
|
|
|
|
|
|
|
|
# Настройки
|
|
|
|
|
|
struct_names = {'linked_list': 'LinkedList', 'hash_table': 'HashTable', 'bst': 'BST'}
|
|
|
|
|
|
colors = {'linked_list': '#3498db', 'hash_table': '#2ecc71', 'bst': '#e74c3c'}
|
|
|
|
|
|
modes = ['случайный', 'отсортированный']
|
|
|
|
|
|
operations = ['вставка', 'поиск', 'удаление']
|
|
|
|
|
|
op_titles = ['Вставка (10000 записей)', 'Поиск (110 запросов)', 'Удаление (50 записей)']
|
|
|
|
|
|
|
|
|
|
|
|
fig, axes = plt.subplots(1, 3, figsize=(14, 5))
|
|
|
|
|
|
fig.suptitle('Сравнение производительности структур данных', fontsize=14, fontweight='bold')
|
|
|
|
|
|
|
|
|
|
|
|
for idx, (op, title) in enumerate(zip(operations, op_titles)):
|
|
|
|
|
|
ax = axes[idx]
|
|
|
|
|
|
x = np.arange(len(modes))
|
|
|
|
|
|
width = 0.25
|
|
|
|
|
|
multiplier = 0
|
|
|
|
|
|
|
|
|
|
|
|
for struct in ['linked_list', 'hash_table', 'bst']:
|
|
|
|
|
|
values = [data[struct][mode][op] for mode in modes]
|
|
|
|
|
|
bars = ax.bar(x + multiplier * width, values, width,
|
|
|
|
|
|
label=struct_names[struct], color=colors[struct])
|
|
|
|
|
|
|
|
|
|
|
|
for bar, val in zip(bars, values):
|
|
|
|
|
|
if val < 0.001:
|
|
|
|
|
|
ax.text(bar.get_x() + bar.get_width()/2, bar.get_height(),
|
|
|
|
|
|
f'{val:.6f}', ha='center', va='bottom', fontsize=7)
|
|
|
|
|
|
elif val < 0.01:
|
|
|
|
|
|
ax.text(bar.get_x() + bar.get_width()/2, bar.get_height(),
|
|
|
|
|
|
f'{val:.5f}', ha='center', va='bottom', fontsize=7)
|
|
|
|
|
|
else:
|
|
|
|
|
|
ax.text(bar.get_x() + bar.get_width()/2, bar.get_height(),
|
|
|
|
|
|
f'{val:.3f}', ha='center', va='bottom', fontsize=8)
|
|
|
|
|
|
multiplier += 1
|
|
|
|
|
|
|
|
|
|
|
|
ax.set_title(title)
|
|
|
|
|
|
ax.set_ylabel('Время (сек)')
|
|
|
|
|
|
ax.set_yscale('log')
|
|
|
|
|
|
ax.set_ylim(1e-5, 10)
|
|
|
|
|
|
ax.set_xticks(x + width)
|
|
|
|
|
|
ax.set_xticklabels(['Случайный', 'Отсортированный'])
|
|
|
|
|
|
ax.legend()
|
|
|
|
|
|
ax.grid(True, alpha=0.3, axis='y')
|
|
|
|
|
|
|
|
|
|
|
|
plt.tight_layout()
|
|
|
|
|
|
|
|
|
|
|
|
current_dir = os.path.dirname(__file__)
|
|
|
|
|
|
docs_dir = os.path.dirname(current_dir)
|
|
|
|
|
|
path = os.path.join(docs_dir, 'graphs.png')
|
|
|
|
|
|
plt.savefig(path, dpi=150)
|
|
|
|
|
|
plt.close()
|
|
|
|
|
|
print(f"\nГрафики сохранены: {path}")
|
|
|
|
|
|
return path
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
|
|
results = run_experiment()
|
|
|
|
|
|
create_report_table(results)
|
|
|
|
|
|
create_graphs(results)
|
|
|
|
|
|
print_analysis()
|
|
|
|
|
|
|
|
|
|
|
|
print("ЭКСПЕРИМЕНТ ВЫПОЛНЕН ПОЛНОСТЬЮ!")
|